Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/protocol-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Protocol E2E (REST / GraphQL / gRPC)

# Stands up a Spring Boot server exposing the same logical service over REST
# (OpenAPI), GraphQL and gRPC-Web, then builds a Codename One client whose
# generated-style @RestClient / @GraphQLClient / @GrpcClient code is wired by
# process-annotations and run on the JavaSE simulator (cn1:test). The client
# performs real round-trips against all three endpoints and asserts the
# responses -- an end-to-end check of the full generated-client stack.

on:
pull_request:
paths:
- 'scripts/protocol-e2e/**'
- 'CodenameOne/src/com/codename1/io/graphql/**'
- 'CodenameOne/src/com/codename1/annotations/graphql/**'
- 'CodenameOne/src/com/codename1/io/grpc/**'
- 'CodenameOne/src/com/codename1/io/rest/**'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/**'
- '.github/workflows/protocol-e2e.yml'
push:
branches: [ master ]
paths:
- 'scripts/protocol-e2e/**'
- 'CodenameOne/src/com/codename1/io/graphql/**'
- 'CodenameOne/src/com/codename1/annotations/graphql/**'
- '.github/workflows/protocol-e2e.yml'
workflow_dispatch:

jobs:
protocol-e2e:
runs-on: ubuntu-latest
container: ghcr.io/codenameone/codenameone/pr-ci-container:latest
timeout-minutes: 40
steps:
- uses: actions/checkout@v4

- name: Install Codename One artifacts (core / javase / css-compiler / plugin)
run: |
export JAVA_HOME="${JAVA_HOME_8}"
export PATH="${JAVA_HOME_8}/bin:$PATH"
cd maven
mvn -B -pl core,javase,css-compiler,codenameone-maven-plugin -am install \
-Plocal-dev-javase -DskipTests -Dmaven.javadoc.skip=true

- name: Ensure a virtual display (xvfb) for the simulator
run: |
if ! command -v xvfb-run >/dev/null 2>&1; then
(apt-get update && apt-get install -y xvfb) || true
fi

- name: Run REST / GraphQL / gRPC end-to-end
run: |
export JAVA_HOME="${JAVA_HOME_17}"
export PATH="${JAVA_HOME_17}/bin:$PATH"
scripts/protocol-e2e/run-protocol-e2e.sh

- name: Upload server log
if: always()
uses: actions/upload-artifact@v4
with:
name: protocol-e2e-server-log
path: /tmp/protocol-e2e-server.log
if-no-files-found: ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*/
package com.codename1.annotations.graphql;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/// Marks an interface as a GraphQL client that the build-time
/// annotation processor wires up to a generated implementation. Each
/// abstract method carries one of [Query], [Mutation] or
/// [Subscription] holding the GraphQL operation document, zero or more
/// [Var]-annotated parameters supplying its variables, an optional
/// `@Header("Authorization") String` bearer token, and a trailing
/// callback:
///
/// - `OnComplete<GraphQLResponse<T>>` for [Query] / [Mutation];
/// - `GraphQLSubscription.Handler<T>` for [Subscription], in which case
/// the method returns a `GraphQLSubscription` handle.
///
/// The processor emits a `<SimpleName>Impl` class and registers it with
/// [com.codename1.io.graphql.GraphQLClients] so the interface's
/// `static T of(String endpoint)` factory can return an instance
/// without the project source referencing the impl directly. Mirrors
/// [com.codename1.annotations.rest.RestClient] and
/// [com.codename1.annotations.grpc.GrpcClient].
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface GraphQLClient {
/// Optional default GraphQL endpoint URL baked into the generated
/// `of()` factory's documentation. The effective endpoint is the
/// argument passed to `of(String endpoint)`; this value is purely
/// informational and may be left empty.
String value() default "";
}
33 changes: 33 additions & 0 deletions CodenameOne/src/com/codename1/annotations/graphql/Mutation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*/
package com.codename1.annotations.graphql;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/// Declares a [GraphQLClient] method as a GraphQL **mutation**.
/// Identical in shape to [Query] -- the [#value()] is the operation
/// document and [Var]-annotated parameters supply its `$variables` --
/// but sent as a mutation. The method ends with an
/// `OnComplete<GraphQLResponse<T>>` callback.
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface Mutation {
/// The GraphQL operation document, e.g.
/// `mutation AddReview($ep: Episode!, $review: ReviewInput!) { createReview(episode: $ep, review: $review) { stars } }`.
String value();

/// The operation name to send in the request's `operationName`
/// field. Optional; required only when [#value()] declares more
/// than one operation.
String operationName() default "";
}
33 changes: 33 additions & 0 deletions CodenameOne/src/com/codename1/annotations/graphql/Query.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*/
package com.codename1.annotations.graphql;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/// Declares a [GraphQLClient] method as a GraphQL **query**. The
/// [#value()] is the operation document sent verbatim to the server;
/// the method's [Var]-annotated parameters supply its `$variables`.
/// The method ends with an `OnComplete<GraphQLResponse<T>>` callback
/// whose `T` is the generated `@Mapped` response-data type.
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface Query {
/// The GraphQL operation document, e.g.
/// `query HeroName($episode: Episode) { hero(episode: $episode) { name } }`.
String value();

/// The operation name to send in the request's `operationName`
/// field. Optional; required only when [#value()] declares more
/// than one operation.
String operationName() default "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*/
package com.codename1.annotations.graphql;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/// Declares a [GraphQLClient] method as a GraphQL **subscription**,
/// streamed over a WebSocket using the `graphql-transport-ws`
/// protocol. The [#value()] is the operation document and
/// [Var]-annotated parameters supply its `$variables`. The method takes
/// a trailing `GraphQLSubscription.Handler<T>` and returns a
/// `GraphQLSubscription` handle whose `cancel()` ends the stream.
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface Subscription {
/// The GraphQL operation document, e.g.
/// `subscription OnReview($ep: Episode!) { reviewAdded(episode: $ep) { stars } }`.
String value();

/// The operation name to send in the `subscribe` message's
/// `operationName` field. Optional; required only when [#value()]
/// declares more than one operation.
String operationName() default "";
}
29 changes: 29 additions & 0 deletions CodenameOne/src/com/codename1/annotations/graphql/Var.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*/
package com.codename1.annotations.graphql;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/// Binds a [GraphQLClient] method parameter to a GraphQL operation
/// variable. The [#value()] is the variable name as it appears (without
/// the `$`) in the operation document's variable definitions. The
/// argument is serialised into the request's `variables` object:
/// strings/numbers/booleans/lists/maps pass through directly, enums
/// serialise as their `name()`, and any other object is treated as an
/// `@Mapped` business object. A null argument omits the variable.
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.PARAMETER)
public @interface Var {
/// The GraphQL variable name (without the leading `$`).
String value();
}
32 changes: 32 additions & 0 deletions CodenameOne/src/com/codename1/impl/WebSocketImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
public abstract class WebSocketImpl {
private final String url;
private WebSocketEventSink sink;
private String[] requestedSubprotocols;
// Written by the port's connect/handshake thread, read by the user's
// connect handler -- volatile to publish the value across threads.
@SuppressWarnings("PMD.AvoidUsingVolatile")
private volatile String selectedSubprotocol;

protected WebSocketImpl(String url) {
this.url = url;
Expand All @@ -52,6 +57,33 @@ protected final WebSocketEventSink sink() {
return sink;
}

/// Sets the subprotocols (RFC 6455 `Sec-WebSocket-Protocol`) the client
/// offers, in preference order. Called by the public facade before
/// `connect(int)`; ports include them in the handshake. Null or empty
/// means "no subprotocol negotiation".
public final void setRequestedSubprotocols(String[] protocols) {
this.requestedSubprotocols = protocols;
}

/// The subprotocols the client offered, or null when none were set.
/// Ports read this while building the handshake.
protected final String[] requestedSubprotocols() {
return requestedSubprotocols;
}

/// Records the subprotocol the server selected. Ports call this once
/// the handshake completes, before firing `sink().onConnect()`, so the
/// value is visible to the user's connect handler.
protected final void setSelectedSubprotocol(String protocol) {
this.selectedSubprotocol = protocol;
}

/// The subprotocol the server selected, or null when none was
/// negotiated (or the connection has not completed yet).
public String getSelectedSubprotocol() {
return selectedSubprotocol;
}

/// Initiate the connection. May return immediately and complete
/// asynchronously; success is signalled via `sink().onConnect()`.
///
Expand Down
23 changes: 23 additions & 0 deletions CodenameOne/src/com/codename1/io/WebSocket.java
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,29 @@ public WebSocket onError(ErrorHandler handler) {
return this;
}

/// Offer one or more subprotocols (RFC 6455 `Sec-WebSocket-Protocol`),
/// in preference order, to negotiate during the handshake. Must be
/// called before [connect]. After the connection opens,
/// [getSelectedSubprotocol] returns the one the server chose (or null).
/// Returns `this` for chaining.
///
/// ```
/// WebSocket.build("wss://api.example.com/graphql")
/// .subprotocols("graphql-transport-ws")
/// .onConnect(w -> Log.p("using " + w.getSelectedSubprotocol()))
/// .connect();
/// ```
public WebSocket subprotocols(String... protocols) {
impl.setRequestedSubprotocols(protocols);
return this;
}

/// The subprotocol the server selected during the handshake, or null
/// when none was negotiated. Valid once the [ConnectHandler] has fired.
public String getSelectedSubprotocol() {
return impl.getSelectedSubprotocol();
}

/// Start the handshake using the platform default connect timeout.
/// Returns `this` for chaining; success is signalled asynchronously
/// via the registered [ConnectHandler].
Expand Down
Loading
Loading