Skip to content

Commit 37a1484

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 f4e5ef5 commit 37a1484

47 files changed

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

0 commit comments

Comments
 (0)