Skip to content

Commit fbac6ab

Browse files
committed
Add quarkus-smithy extension and Smithy Vert.x server
Smithy services run inside Quarkus applications via a new extension that mounts every CDI-discovered Service bean on Quarkus's main HTTP router. Smithy operations share the Quarkus HTTP server's port — no separate Smithy listener. ## Customer-facing surface Users produce a `@Produces Service` bean (the generated service stub): @ApplicationScoped public class CoffeeShopServerConfig { @produces @singleton Service coffeeShop() { return CoffeeShop.builder() .addCreateOrderOperation(new CreateOrder()) .addGetMenuOperation(new GetMenu()) .addGetOrderOperation(new GetOrder()) .build(); } } The extension mounts a `SmithyVertxServer` on Quarkus's `Router` as a single catch-all route. Per-request, a `ProtocolResolver` iterates a precision-ordered list of `ServerProtocol`s and returns one of three outcomes: - claim -> dispatch to the operation - no-claim -> ctx.next() (delegate to a sibling handler) - claim-and-reject -> 404 directly (request is Smithy's but malformed) This implements the Smithy 2.0 Wire-protocol-selection guide. End-to-end CoffeeShop example at `examples/quarkus-server/`. Standalone Gradle build that consumes smithy-java via mavenLocal — required because including it as a subproject causes Quarkus dev-mode workspace discovery to substitute sibling raw `build/classes` for the published jars, splitting classloaders in ways that break `:codecs:json-codec`'s shadowJar. ## Modules - `:server:server-vertx` — `SmithyVertxServer implements Handler<RoutingContext>`, `ServerOptions`, `VertxRequestHeaders`. 18 integration tests against a real Vert.x HTTP server. - `:quarkus-smithy` (runtime) — `SmithyVertxRecorder` and `SmithyServerConfig` (`@ConfigMapping` for `quarkus.smithy.server.{path-prefix, workers, shutdown-grace}`). The recorder collects `Service` beans from Arc, walks TCCL+own-loader for `ServerProtocolProvider`s (required because `ProtocolResolver`'s static SPI cache can't see runtime jars under `QuarkusClassLoader`), constructs the server, mounts it on the main router, and registers ordered shutdown tasks. - `:quarkus-smithy-deployment` (build-time) — `SmithyProcessor` `@BuildStep`s and `SmithyCodeGenProvider` that hooks Smithy code generation into `quarkusGenerateCode` (no `smithy-base` Gradle plugin needed). - `:quarkus-smithy-integration-tests` — `SmithyCodeGenProviderTest`. ## Cross-module changes - `:server:server-core` - `ProtocolResolver` gains `resolveOrEmpty(...)` returning `Optional<ServiceProtocolResolutionResult>` and a second ctor accepting a pre-loaded protocol list (for callers like the Quarkus recorder where the static SPI cache is blind). - `HttpResponseSerializer` is new: status + header-copy + content-type/length logic shared between Netty and the Vert.x server. Body exposed as the underlying `DataStream` so Netty preserves zero-copy via `Unpooled.wrappedBuffer(ByteBuffer)`. - `ServerProtocolProvider.precision()` Javadoc documents the AWS service-protocol scale (rpcv2Cbor=1 ... restXml=8). - `:server:server-netty` — `HttpRequestHandler.writeResponse` adopts `HttpResponseSerializer`. Behavior unchanged. - Provider precision values: `RpcV2CborProtocolProvider` -> 1, `RpcV2JsonProtocolProvider` -> 2, `AwsRestJson1ProtocolProvider` -> 7. Previously all 0 (precision sort was a no-op against classpath order). - `:aws:server:aws-server-restjson` — drops dead `routes` field and `smithyToVertxPath` helper (the Vert.x server no longer enumerates per-operation routes). ## Verification - `./gradlew :server:server-vertx:check` green. 19 integration tests against a real Vert.x HTTP server: protocol resolution outcomes, HTTP/2 round-trip, lifecycle, options, precision regression. - `./gradlew :server:server-core:check` green. New tests: `HttpResponseSerializerTest` (6) and `ProtocolResolverTest` (7). - `./gradlew :quarkus-smithy:build :quarkus-smithy-deployment:build` green (with `--no-configuration-cache` to work around a pre-existing Quarkus extension-validation gradle-plugin issue). - `examples/quarkus-server` end-to-end (`quarkusDev`): `GET /menu`, `PUT /order`, `GET /order/<id>` all 200 under restJson1; CoffeeShop also reachable via `POST /service/CoffeeShop/operation/<Op>` with `smithy-protocol: rpc-v2-cbor` and `smithy-protocol: rpc-v2-json` headers; rpcv2 path with no header -> Quarkus default 404 via ctx.next(); rpcv2 path with header but malformed URI -> server 404 with empty body (claim-and-reject). The empty-body 404 vs Quarkus's default-page 404 confirms the resolution-outcome distinctions are observable. ## Known limitations (deferred) - `@streaming Blob` operations not supported. The recorder installs Vert.x's `BodyHandler` upstream of the server, fully buffering request bodies before resolution runs. - Native-image support is out of scope for this cut. - CORS support on the Vert.x server (Netty has it). - Cross-service `@http(uri)` collisions are silent at construction time — the matcher's tie-break wins.
1 parent 7809638 commit fbac6ab

52 files changed

Lines changed: 4631 additions & 19 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

aws/server/aws-server-restjson/src/main/java/software/amazon/smithy/java/aws/server/restjson/AwsRestJson1Protocol.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ final class AwsRestJson1Protocol extends ServerProtocol {
5353
var httpMethodToMatchers = new HashMap<String, UriMatcherMapBuilder<Operation<?, ?>>>();
5454
for (Service service : services) {
5555
for (var operation : service.getAllOperations()) {
56-
// Only process operations with HTTP trait.
5756
var httpTrait = operation.getApiOperation()
5857
.schema()
5958
.getTrait(TraitKey.HTTP_TRAIT);

aws/server/aws-server-restjson/src/main/java/software/amazon/smithy/java/aws/server/restjson/AwsRestJson1ProtocolProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ public ShapeId getProtocolId() {
2525

2626
@Override
2727
public int precision() {
28-
return 0;
28+
return 7;
2929
}
3030
}

examples/quarkus-server/README.md

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
## Example: Quarkus Server
2+
3+
A Smithy-Java service running inside a Quarkus application via the
4+
[`quarkus-smithy` extension](../../quarkus-smithy/README.md). Smithy
5+
operations share Quarkus's HTTP port — no separate Smithy listener.
6+
7+
### Run it
8+
9+
```console
10+
# from smithy-java/ — publishes the smithy-java jars to your local repo
11+
./gradlew publishToMavenLocal
12+
13+
# from smithy-java/examples/quarkus-server/
14+
./gradlew quarkusDev
15+
```
16+
17+
The server listens on `http://localhost:8080`. Watch the boot log for
18+
the recorder's mount line (`Smithy mounted at /* with N service(s)`)
19+
and the server's own construction line (`Smithy server constructed
20+
with N service(s), M operation(s); protocols (precision order): []`).
21+
22+
Re-run `publishToMavenLocal` whenever you change smithy-java sources.
23+
24+
### Curl the operations
25+
26+
The CoffeeShop service declares `@restJson1 @rpcv2Cbor @rpcv2Json`, so
27+
each operation is reachable via any of three on-the-wire shapes. The
28+
server picks one per request in protocol-precision order
29+
(rpcv2Cbor → rpcv2Json → restJson1).
30+
31+
#### restJson1 — HTTP routes via `@http(method, uri)`
32+
33+
```console
34+
curl http://localhost:8080/menu
35+
curl -X PUT http://localhost:8080/order -H 'Content-Type: application/json' \
36+
-d '{"coffeeType":"LATTE"}'
37+
curl http://localhost:8080/order/<id-from-PUT-response>
38+
```
39+
40+
#### rpcv2Cbor — `/service/CoffeeShop/operation/<Op>` + `smithy-protocol` header
41+
42+
```console
43+
# GetMenu — empty input is the CBOR empty map (0xa0)
44+
printf '\xa0' > /tmp/empty.cbor
45+
curl -X POST http://localhost:8080/service/CoffeeShop/operation/GetMenu \
46+
-H 'smithy-protocol: rpc-v2-cbor' -H 'content-type: application/cbor' \
47+
--data-binary @/tmp/empty.cbor
48+
49+
# CreateOrder — {"coffeeType":"LATTE"}
50+
python3 -c 'import sys; sys.stdout.buffer.write(bytes([0xa1, 0x6a]) + b"coffeeType" + bytes([0x65]) + b"LATTE")' \
51+
> /tmp/createorder.cbor
52+
curl -X POST http://localhost:8080/service/CoffeeShop/operation/CreateOrder \
53+
-H 'smithy-protocol: rpc-v2-cbor' -H 'content-type: application/cbor' \
54+
--data-binary @/tmp/createorder.cbor
55+
```
56+
57+
The response body is CBOR; pipe through `xxd` or a CBOR diagnostic tool
58+
to read it.
59+
60+
#### rpcv2Json — same URI scheme, JSON body
61+
62+
```console
63+
curl -X POST http://localhost:8080/service/CoffeeShop/operation/GetMenu \
64+
-H 'smithy-protocol: rpc-v2-json' -H 'content-type: application/json' -d '{}'
65+
66+
curl -X POST http://localhost:8080/service/CoffeeShop/operation/CreateOrder \
67+
-H 'smithy-protocol: rpc-v2-json' -H 'content-type: application/json' \
68+
-d '{"coffeeType":"ESPRESSO"}'
69+
70+
curl -X POST http://localhost:8080/service/CoffeeShop/operation/GetOrder \
71+
-H 'smithy-protocol: rpc-v2-json' -H 'content-type: application/json' \
72+
-d '{"id":"<id-from-CreateOrder>"}'
73+
```
74+
75+
#### Fall-through behavior
76+
77+
The server distinguishes three outcomes per request, observable as
78+
distinct 404 shapes:
79+
80+
```console
81+
# no-claim → ctx.next() → Quarkus default 404 (text/plain ~358B page)
82+
curl -i -X POST http://localhost:8080/service/CoffeeShop/operation/GetMenu
83+
84+
# claim-and-reject → server 404 with empty body
85+
curl -i -X POST http://localhost:8080/service/CoffeeShop/operation/ \
86+
-H 'smithy-protocol: rpc-v2-cbor' -H 'content-type: application/cbor' \
87+
--data-binary @/tmp/empty.cbor
88+
89+
# unrelated path → ctx.next() → Quarkus (or your own sibling handler)
90+
curl -i http://localhost:8080/q/notreal
91+
```
92+
93+
The empty-body 404 (claim-and-reject) vs the Quarkus default-page 404
94+
(no-claim) is the observable signal for routing correctness. A request
95+
that *claimed* the rpcv2-cbor protocol but failed URI parsing is
96+
intercepted before `ctx.next()`, so a sibling handler can't
97+
misinterpret it. A request that no protocol claimed falls through, so
98+
Quarkus (or any other Vert.x handler on the same router) gets a chance
99+
to serve it.
100+
101+
### Hot reload
102+
103+
While `quarkusDev` is running:
104+
105+
- Edit `CreateOrder.java` (e.g., change a status string), save → re-curl
106+
`PUT /order`. The response reflects the change without a restart.
107+
- Edit `src/main/smithy/coffee.smithy` (e.g., add a member), save → the
108+
`CodeGenProvider` regenerates the stub and the recorder removes the
109+
previous Vert.x route before installing the new one.
110+
111+
### Path-prefix mode
112+
113+
To put Smithy operations under `/api/smithy/...` (so REST endpoints can
114+
own the root), set in `src/main/resources/application.properties`:
115+
116+
```properties
117+
quarkus.smithy.server.path-prefix=/api/smithy
118+
```
119+
120+
`@http(uri:"/menu")` then becomes reachable at `/api/smithy/menu`. Verify:
121+
122+
```console
123+
curl -i http://localhost:8080/api/smithy/menu # 200
124+
curl -i http://localhost:8080/menu # 404
125+
```
126+
127+
In dev mode you can also live-edit this from the Dev UI Configuration
128+
tile — see below.
129+
130+
### Packaged jar (prod profile)
131+
132+
```console
133+
./gradlew quarkusBuild
134+
java -jar build/quarkus-app/quarkus-run.jar
135+
```
136+
137+
Run the same curl probes against this — they should all 200, boot is
138+
sub-2s.
139+
140+
### Dev UI
141+
142+
While `quarkusDev` is running, open `http://localhost:8080/q/dev-ui`.
143+
144+
There is no Smithy-specific Dev UI card today (none of the extension's
145+
build steps emit a `CardPageBuildItem`), so use the standard tiles:
146+
147+
- **Endpoints** — confirms the Smithy server's catch-all route
148+
alongside Quarkus's own routes.
149+
- **Configuration** — search for `quarkus.smithy.server` to live-edit
150+
`path-prefix`, `workers`, and `shutdown-grace`.
151+
- **ArC** — confirms the `@Produces Service` bean is present and
152+
unremovable (the extension marks it via `UnremovableBeanBuildItem`).
153+
- **Build Steps** — confirms `SmithyProcessor` ran and which build
154+
items it produced.
155+
- **Continuous Testing** — press `r` in the dev terminal (or open the
156+
tile) to re-run tests on save.
157+
158+
---
159+
160+
### How it's wired
161+
162+
The user produces a `@Produces Service` bean (the generated `CoffeeShop`
163+
stub) and the extension mounts a `SmithyVertxServer` from the upstream
164+
`:server:server-vertx` module on Quarkus's main HTTP router:
165+
166+
```java
167+
@ApplicationScoped
168+
public class CoffeeShopServerConfig {
169+
170+
@Produces
171+
@Singleton
172+
Service coffeeShop() {
173+
return CoffeeShop.builder()
174+
.addCreateOrderOperation(new CreateOrder())
175+
.addGetMenuOperation(new GetMenu())
176+
.addGetOrderOperation(new GetOrder())
177+
.build();
178+
}
179+
}
180+
```
181+
182+
#### Project layout
183+
184+
```
185+
.
186+
├── build.gradle.kts ← apply io.quarkus, depend on quarkus-smithy
187+
├── settings.gradle.kts
188+
├── gradle.properties
189+
├── smithy-build.json ← project root, configures java-codegen
190+
├── src/main/smithy/ ← .smithy models (Quarkus convention)
191+
│ ├── coffee.smithy
192+
│ ├── main.smithy
193+
│ └── order.smithy
194+
├── src/main/java/.../CoffeeShopServerConfig.java
195+
├── src/main/java/.../CreateOrder.java
196+
├── src/main/java/.../GetMenu.java
197+
├── src/main/java/.../GetOrder.java
198+
└── src/main/resources/application.properties
199+
```
200+
201+
No `afterEvaluate { ... srcDir(...) }` wiring. No
202+
`compileJava.dependsOn(smithyBuild)`. The `quarkus-smithy` extension's
203+
`CodeGenProvider` runs as part of `quarkusGenerateCode`, generates Java
204+
sources directly into Quarkus's
205+
`build/classes/java/quarkus-generated-sources/smithy/` output directory,
206+
and `compileJava` picks them up automatically.
207+
208+
#### Why a standalone Gradle build
209+
210+
This example is intentionally not included in `smithy-java`'s root
211+
`settings.gradle.kts`. Quarkus dev-mode workspace discovery would
212+
otherwise substitute sibling smithy-java projects' raw `build/classes`
213+
directories for their published jars — bypassing
214+
`:codecs:json-codec`'s shadowJar (which relocates Jackson 3) and
215+
splitting the classloader graph in ways that break the
216+
`SchemaExtensionProvider` SPI lookup. Running standalone, against the
217+
locally-published jars, makes the example behave exactly the way a
218+
real customer's project would.
219+
220+
### Running the extension's tests
221+
222+
These live in the parent smithy-java build, not in this example:
223+
224+
```console
225+
# from smithy-java/
226+
./gradlew :server:server-vertx:test # Smithy Vert.x server tests
227+
./gradlew :quarkus-smithy-integration-tests:test # extension integ
228+
./gradlew :aws:server:aws-server-restjson:integ # protocol integ
229+
```
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
plugins {
2+
`java-library`
3+
id("io.quarkus") version "3.35.3"
4+
}
5+
6+
// This example is a standalone Gradle build (it is *not* included in
7+
// smithy-java's root settings.gradle.kts). All smithy-java dependencies
8+
// are resolved from mavenLocal, the same way a real customer would
9+
// consume them. To rebuild after changing smithy-java sources, run
10+
// `gradle publishToMavenLocal` from the smithy-java root first.
11+
repositories {
12+
mavenLocal()
13+
mavenCentral()
14+
}
15+
16+
val quarkusPlatformGroupId: String by project
17+
val quarkusPlatformArtifactId: String by project
18+
val quarkusPlatformVersion: String by project
19+
val smithyJavaVersion: String by project
20+
21+
dependencies {
22+
// The quarkus-smithy extension. Brings in:
23+
// - SmithyVertxRecorder (mounts services on Quarkus's HTTP router)
24+
// - the deployment-time CodeGenProvider that runs Smithy code generation
25+
// during quarkusGenerateCode (no smithy-base Gradle plugin needed)
26+
// - quarkus-vertx-http transitively (Smithy operations share the
27+
// Quarkus HTTP server's port, per ADR-0003)
28+
// - the upstream :server:server-vertx module
29+
implementation("software.amazon.smithy.java:quarkus-smithy:$smithyJavaVersion")
30+
31+
// Quarkus runtime
32+
implementation(enforcedPlatform("$quarkusPlatformGroupId:$quarkusPlatformArtifactId:$quarkusPlatformVersion"))
33+
implementation("io.quarkus:quarkus-arc")
34+
35+
// Server-side protocol implementations. The bridge looks for
36+
// ServerProtocol providers via ServiceLoader; the user adds
37+
// whichever protocol jar(s) their .smithy services declare. The
38+
// CoffeeShop service in this example declares all three:
39+
// @restJson1 + @rpcv2Cbor + @rpcv2Json. Each request resolves to
40+
// exactly one protocol per the precision-ordered list (rpcv2Cbor
41+
// first, then rpcv2Json, then restJson1).
42+
implementation("software.amazon.smithy.java:aws-server-restjson:$smithyJavaVersion")
43+
implementation("software.amazon.smithy.java:server-rpcv2-cbor:$smithyJavaVersion")
44+
implementation("software.amazon.smithy.java:server-rpcv2-json:$smithyJavaVersion")
45+
}
46+
47+
java {
48+
toolchain {
49+
languageVersion = JavaLanguageVersion.of(25)
50+
}
51+
sourceCompatibility = JavaVersion.VERSION_25
52+
targetCompatibility = JavaVersion.VERSION_25
53+
}
54+
55+
// .smithy files live under src/main/smithy/ — Quarkus's CodeGenProvider
56+
// finds them automatically and IntelliJ's Gradle import surfaces them
57+
// without extra source-set wiring.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Pinned to whatever version is in mavenLocal. Before running `quarkusBuild`
2+
# or `quarkusDev` from this directory, publish the smithy-java artifacts:
3+
# (from smithy-java/) gradle :quarkus-smithy:publishToMavenLocal :quarkus-smithy-deployment:publishToMavenLocal
4+
smithyJavaVersion=1.2.0
5+
quarkusPluginVersion=3.35.3
6+
quarkusPlatformGroupId=io.quarkus.platform
7+
quarkusPlatformArtifactId=quarkus-bom
8+
quarkusPlatformVersion=3.35.3
47.8 KB
Binary file not shown.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
distributionBase=GRADLE_USER_HOME
2+
distributionPath=wrapper/dists
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
4+
networkTimeout=10000
5+
validateDistributionUrl=true
6+
zipStoreBase=GRADLE_USER_HOME
7+
zipStorePath=wrapper/dists

0 commit comments

Comments
 (0)