diff --git a/docs/lessons/0001-the-clientprotocol-contract.html b/docs/lessons/0001-the-clientprotocol-contract.html new file mode 100644 index 000000000..3aab62653 --- /dev/null +++ b/docs/lessons/0001-the-clientprotocol-contract.html @@ -0,0 +1,217 @@ + + + + + +Lesson 1 · The ClientProtocol Contract + + + +
+ +
smithy-java · protocol track · lesson 1
+

The ClientProtocol Contract

+

Before you can add a protocol, you need to know exactly what a protocol is in this framework. It's one interface with six methods. Learn them, and you have the skeleton you'll flesh out for the rest of the track.

+ +
+ Goal — Know the 6 methods & trace one call + Time — ~10 min + Mission link — the contract you'll implement +
+ +

1The one-sentence definition

+

A protocol is the thing that knows how to turn a typed input shape into a transport request, and a transport response back into a typed output shape. That's it. It is the translator between "Java objects the user holds" and "bytes on the wire."

+ +

In code, that's a single interface, generic over the request and response types it produces:

+
public interface ClientProtocol<RequestT, ResponseT> {
+    ShapeId id();                          // which protocol trait am I?
+    Codec payloadCodec();                   // how do I encode bodies?
+    MessageExchange<RequestT,ResponseT> messageExchange();
+
+    RequestT createRequest(operation, input, context, endpoint);     // shape → request
+    RequestT setServiceEndpoint(request, endpoint);                  // attach where to send
+    O        deserializeResponse(operation, ctx, errors, req, resp); // response → shape
+}
+

Source (verbatim signatures): client/client-core/.../client/core/ClientProtocol.java

+ +
Why generic over RequestT/ResponseT? Because a protocol isn't tied to HTTP. The type parameters let an HTTP protocol work with HttpRequest/HttpResponse while leaving the door open for other transports. The MessageExchange marker is how the pipeline checks a protocol and a transport actually speak the same request/response types before wiring them together.

+ +

2The six methods, by job

+

Split them into three that describe the protocol and three that do the work:

+ + + + + + + + + +
MethodJobThink of it as…
id()Returns the protocol trait's ShapeId, e.g. smithy.protocols#rpcv2Cbor."My name." The join key to the Smithy model.
payloadCodec()Returns the Codec used to encode/decode bodies (JSON, CBOR, XML…)."My encoder." Often a single shared static instance.
messageExchange()Declares the request/response pair (e.g. HTTP)."What transport I'm compatible with."
createRequest(...)Build the transport request from the operation + input shape. Serialize the body, set method/path/headers.The outbound half.
setServiceEndpoint(...)Merge the resolved endpoint (host/path) into the request.The "where to send it" step.
deserializeResponse(...)Turn the response into the output shape — or throw a modeled/unmodeled error.The inbound half.
+ +
The good news for HTTP protocols: you almost never implement all six yourself. HttpClientProtocol already gives you id(), messageExchange(), and setServiceEndpoint(). You're left writing the two interesting ones — createRequest and deserializeResponse — plus pointing payloadCodec() at a codec. We'll prove this in Lesson 2.

+ +

3Trace one call through the contract

+

Here's where the six methods sit in the life of a single client.call(input, operation). The protocol methods are highlighted; the rest is pipeline machinery you get for free.

+ +
    +
  1. You call client.call(input, operation) with a generated input shape.
  2. +
  3. The pipeline calls protocol.createRequest(operation, input, ctx, endpoint) → serializes input through payloadCodec() into a RequestT.
  4. +
  5. Endpoint resolution runs, then protocol.setServiceEndpoint(request, endpoint) attaches host + base path.
  6. +
  7. Auth resolves & signs the request (separate from the protocol).
  8. +
  9. The transport sends the request and returns a ResponseT.
  10. +
  11. The pipeline calls protocol.deserializeResponse(operation, ctx, errors, req, resp) → output shape, or a thrown error.
  12. +
  13. If retryable, the loop repeats from step 3. Otherwise you get your output shape back.
  14. +
+

This mirrors the call-pipeline sketch in the Knowledge-Transfer doc (§8 "Call Pipeline") — now you can see which steps are the protocol's job and which are the pipeline's.

+ + +
+

🔁 Quick check — instant feedback, no grading pressure

+ +
+

1. You're adding an HTTP-based protocol. Which two methods will you almost always have to write yourself?

+ + + +
Not quite — id() returns a constant and payloadCodec() usually just returns a shared codec instance. Those are trivial.
+
Exactly. The two "do the work" halves — outbound (createRequest) and inbound (deserializeResponse) — carry the protocol's real logic. The base class handles the rest.
+
These two come free from HttpClientProtocol for HTTP protocols — you wouldn't normally write them.
+
+ +
+

2. What does id() return, and why does it matter?

+ + + +
No — it's a stable, meaningful identifier, not random.
+
It's not the class name. It's a model-level identifier.
+
Right. id() returns something like smithy.protocols#rpcv2Cbor. That's how the framework matches a service's protocol trait to your protocol. We'll use this in the discovery lesson.
+
+ +
+

3. In the call lifecycle, what happens between createRequest and deserializeResponse?

+ + + +
Correct. The protocol builds the request; the pipeline then resolves the endpoint, signs, and hands it to the transport to send. The protocol re-enters only to deserialize the response.
+
No — createRequest only builds the request object. A separate transport sends it. Keeping these separate is what lets you swap transports.
+
The codec is fixed when the protocol is constructed, not mid-call.
+
+ +
+
+ +

4What you now know

+

You can state what a protocol is, name its six methods and their jobs, and point to where each one sits in a single call. That's the scaffold. In Lesson 2 we'll read the smallest real protocol in the repo — RpcV2CborProtocol, which is literally ~45 lines — and watch this contract get satisfied by inheritance, so you see how little code a new protocol actually needs.

+ +
+

💬 I'm your teacher — ask me anything

+

Genuinely unsure about something here? Don't move on with a fuzzy spot. Ask me in the chat, for example:

+

"Show me where setServiceEndpoint merges the path for HTTP"
+ • "What's the difference between a protocol and a transport again?"
+ • "Why is Schema the first arg to every serde method?"

+

When you're ready, just say "next lesson" and I'll build Lesson 2.

+
+ + +
+ + + + diff --git a/docs/lessons/0001-the-clientprotocol-contract.md b/docs/lessons/0001-the-clientprotocol-contract.md new file mode 100644 index 000000000..ca1be96e4 --- /dev/null +++ b/docs/lessons/0001-the-clientprotocol-contract.md @@ -0,0 +1,156 @@ +# Lesson 1 — The `ClientProtocol` Contract + +> **smithy-java · protocol track · lesson 1** +> **Goal:** Know the 6 methods of `ClientProtocol` and trace one call through them. +> **Time:** ~10 min · **Mission:** deep architecture mastery, using "add a new protocol" as the lens. + +This is the non-interactive companion to the HTML lesson. Before you can *add* a protocol to +smithy-java, you need to know exactly what a protocol *is* in this framework. It's one interface +with six methods. Learn them, and you have the skeleton you'll flesh out for the rest of the track. + +--- + +## 1. The one-sentence definition + +A **protocol** is the thing that knows how to turn a typed *input shape* into a transport +*request*, and a transport *response* back into a typed *output shape*. That's it. It is the +translator between "Java objects the user holds" and "bytes on the wire." + +In code, that's a single interface, generic over the request and response types it produces: + +```java +public interface ClientProtocol { + ShapeId id(); // which protocol trait am I? + Codec payloadCodec(); // how do I encode bodies? + MessageExchange messageExchange(); + + RequestT createRequest(operation, input, context, endpoint); // shape → request + RequestT setServiceEndpoint(request, endpoint); // attach where to send + O deserializeResponse(operation, ctx, errors, req, resp); // response → shape +} +``` + +> Source (verbatim signatures): +> [`client/client-core/.../client/core/ClientProtocol.java`](https://github.com/smithy-lang/smithy-java/blob/main/client/client-core/src/main/java/software/amazon/smithy/java/client/core/ClientProtocol.java) + +**Why generic over `RequestT`/`ResponseT`?** Because a protocol isn't tied to HTTP. The type +parameters let an HTTP protocol work with `HttpRequest`/`HttpResponse` while leaving the door open +for other transports. The `MessageExchange` marker is how the pipeline checks that a protocol and a +transport actually speak the same request/response types before wiring them together. + +--- + +## 2. The six methods, by job + +Split them into three that **describe** the protocol and three that **do the work**: + +| Method | Job | Think of it as… | +|---|---|---| +| `id()` | Returns the protocol trait's `ShapeId`, e.g. `smithy.protocols#rpcv2Cbor`. | "My name." The join key to the Smithy model. | +| `payloadCodec()` | Returns the `Codec` used to encode/decode bodies (JSON, CBOR, XML…). | "My encoder." Often a single shared static instance. | +| `messageExchange()` | Declares the request/response pair (e.g. HTTP). | "What transport I'm compatible with." | +| `createRequest(...)` | Build the transport request from the operation + input shape. Serialize the body, set method/path/headers. | **The outbound half.** | +| `setServiceEndpoint(...)` | Merge the resolved endpoint (host/path) into the request. | The "where to send it" step. | +| `deserializeResponse(...)` | Turn the response into the output shape — or throw a modeled/unmodeled error. | **The inbound half.** | + +> **The good news for HTTP protocols:** you almost never implement all six yourself. +> `HttpClientProtocol` already gives you `id()`, `messageExchange()`, and `setServiceEndpoint()`. +> You're left writing the two interesting ones — `createRequest` and `deserializeResponse` — plus +> pointing `payloadCodec()` at a codec. We'll prove this in Lesson 2. + +--- + +## 3. Trace one call through the contract + +Here's where the six methods sit in the life of a single `client.call(input, operation)`. The +**protocol methods are bold**; the rest is pipeline machinery you get for free. + +1. **You call** `client.call(input, operation)` with a generated input shape. +2. The pipeline calls **`protocol.createRequest(operation, input, ctx, endpoint)`** → serializes `input` through `payloadCodec()` into a `RequestT`. +3. Endpoint resolution runs, then **`protocol.setServiceEndpoint(request, endpoint)`** attaches host + base path. +4. Auth resolves & signs the request (separate from the protocol). +5. The **transport** sends the request and returns a `ResponseT`. +6. The pipeline calls **`protocol.deserializeResponse(operation, ctx, errors, req, resp)`** → output shape, or a thrown error. +7. If retryable, the loop repeats from step 3. Otherwise you get your output shape back. + +``` +client.call(input, operation) + │ + ▼ + ┌─────────────────────┐ + │ protocol.createRequest │ ◄── shape → request (PROTOCOL) + └─────────────────────┘ + │ + ▼ resolve endpoint → protocol.setServiceEndpoint (PROTOCOL) + ▼ resolve auth → sign + ▼ transport.send() ──────────► returns ResponseT + │ + ▼ + ┌──────────────────────────┐ + │ protocol.deserializeResponse │ ◄── response → shape (PROTOCOL) + └──────────────────────────┘ + │ + ▼ retryable? ── yes ──► back to setServiceEndpoint + │ + ▼ output shape +``` + +This mirrors the call-pipeline sketch in the Knowledge-Transfer doc (§8 "Call Pipeline") — now you +can see which steps are *the protocol's job* and which are the pipeline's. + +--- + +## 4. Quick check + +Test yourself. Answers are below — no peeking until you've committed. + +**Q1.** You're adding an HTTP-based protocol. Which two methods will you almost always have to write yourself? +- a) `id()` and `payloadCodec()` +- b) `createRequest()` and `deserializeResponse()` +- c) `setServiceEndpoint()` and `messageExchange()` + +**Q2.** What does `id()` return, and why does it matter? +- a) A random UUID, so two protocols never collide at runtime. +- b) The Java class name, used for logging only. +- c) The protocol trait's `ShapeId` — the join key between the Smithy model and the factory. + +**Q3.** In the call lifecycle, what happens *between* `createRequest` and `deserializeResponse`? +- a) Endpoint resolution, signing, and the transport actually sending the request. +- b) Nothing — `createRequest` sends the bytes itself. +- c) The codec is chosen for the first time. + +
+Show answers + +- **Q1 → b.** The two "do the work" halves — outbound (`createRequest`) and inbound + (`deserializeResponse`) — carry the protocol's real logic. `id()` returns a constant and + `payloadCodec()` usually just returns a shared codec instance; the base class handles + `setServiceEndpoint()` and `messageExchange()`. +- **Q2 → c.** `id()` returns something like `smithy.protocols#rpcv2Cbor`. That's how the framework + matches a service's protocol trait to *your* protocol. Used in the discovery lesson. +- **Q3 → a.** The protocol builds the request; the pipeline then resolves the endpoint, signs, and + hands it to the transport to send. The protocol re-enters only to deserialize the response. + Keeping "build" and "send" separate is what lets you swap transports. + +
+ +--- + +## 5. What you now know + +You can state what a protocol is, name its six methods and their jobs, and point to where each one +sits in a single call. That's the scaffold. In **Lesson 2** we'll read the smallest real protocol +in the repo — `RpcV2CborProtocol`, which is literally ~45 lines — and watch this contract get +satisfied by inheritance, so you see how little code a new protocol actually needs. + +--- + +## Reference + +- **Glossary:** [`reference/glossary.html`](../reference/glossary.html) — canonical vocabulary for this track. +- **The interface:** [`ClientProtocol.java`](https://github.com/smithy-lang/smithy-java/blob/main/client/client-core/src/main/java/software/amazon/smithy/java/client/core/ClientProtocol.java) +- **HTTP base class:** [`HttpClientProtocol.java`](https://github.com/smithy-lang/smithy-java/blob/main/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpClientProtocol.java) +- **Smallest real protocol (Lesson 2 preview):** [`RpcV2CborProtocol.java`](https://github.com/smithy-lang/smithy-java/blob/main/client/client-rpcv2-cbor/src/main/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocol.java) + +*This is a teaching lesson generated for architecture onboarding. Signatures are quoted from the +smithy-java `main` branch.* diff --git a/docs/lessons/0002-smallest-real-protocol.html b/docs/lessons/0002-smallest-real-protocol.html new file mode 100644 index 000000000..96d228db5 --- /dev/null +++ b/docs/lessons/0002-smallest-real-protocol.html @@ -0,0 +1,198 @@ + + + + + +Lesson 2 · The Smallest Real Protocol + + + +
+ +
smithy-java · protocol track · lesson 2
+

The Smallest Real Protocol

+

In Lesson 1 you learned the six-method contract. Now watch a real protocol satisfy it in ~45 lines. The trick: inheritance does almost all the work, and that tells you exactly how much code you'd write.

+ +
+ Goal — Read RpcV2CborProtocol & see the inheritance chain + Time — ~12 min + Builds on — Lesson 1 (the contract) +
+ +

1The whole protocol, in 45 lines

+

This is the complete CBOR client protocol — the entire file, lightly trimmed:

+
public final class RpcV2CborProtocol extends AbstractRpcV2ClientProtocol {
+    private static final String PAYLOAD_MEDIA_TYPE = "application/cbor";
+    private static final Codec CBOR_CODEC = Rpcv2CborCodec.builder().build();
+
+    public RpcV2CborProtocol(ShapeId service) {
+        super(Rpcv2CborTrait.ID, service, PAYLOAD_MEDIA_TYPE);
+    }
+
+    @Override protected Codec codec() { return CBOR_CODEC; }
+
+    // The factory — the SPI entry point (Lesson 6 covers this)
+    public static final class Factory implements ClientProtocolFactory<Rpcv2CborTrait> {
+        @Override public ShapeId id() { return Rpcv2CborTrait.ID; }
+        @Override public ClientProtocol<?,?> createProtocol(ProtocolSettings s, Rpcv2CborTrait t) {
+            return new RpcV2CborProtocol(s.service());
+        }
+    }
+}
+

Source: client/client-rpcv2-cbor/.../RpcV2CborProtocol.java

+ +

Notice what's missing: no createRequest, no deserializeResponse, no setServiceEndpoint. This class only declares two facts: "my codec is CBOR" and "my media type is application/cbor." Everything else is inherited.

+ +

2The inheritance chain

+

Each layer of the hierarchy contributes a slice of the six-method contract. Read it top-down — the work gets more specific as you descend:

+ +
    +
  • ClientProtocol<Req,Resp> — the interface. Declares all six methods. (Lesson 1)
  • +
  • ▲ implements
  • +
  • HttpClientProtocol — supplies id(), messageExchange() (HTTP), and setServiceEndpoint(). Now Req=HttpRequest, Resp=HttpResponse.
  • +
  • ▲ extends
  • +
  • AbstractRpcV2ClientProtocol — supplies createRequest() and deserializeResponse() for the RPC-v2 wire shape (POST to /service/X/operation/Y, body = serialized payload). Leaves one hole: abstract Codec codec().
  • +
  • ▲ extends
  • +
  • RpcV2CborProtocol — fills the hole: codec() → CBOR. Done.
  • +
+ +
The pattern to internalize: a sibling protocol, RpcV2JsonProtocol, would be the same 45 lines with JsonCodec and "application/json". The wire framing lives in the abstract base; the concrete class only picks a codec. That's the payoff of schema-directed serde (Lesson 3): the body encoding is a swappable knob.

+ +

3What the abstract base actually does

+

Look inside AbstractRpcV2ClientProtocol to see the contract being honored. Its createRequest is the outbound half from Lesson 1, made concrete:

+
public HttpRequest createRequest(operation, input, context, endpoint) {
+    var target = "/service/" + service.getName() + "/operation/" + operation.schema().id().getName();
+    var builder = templateRequest.toModifiableCopy();   // POST + smithy-protocol headers
+    builder.setUri(endpoint.withConcatPath(target));
+
+    if (operation.inputSchema().hasTrait(UNIT_TYPE_TRAIT)) {
+        builder.setBody(DataStream.ofEmpty());            // no input → empty body
+    } else {
+        builder.addHeader(CONTENT_TYPE, payloadMediaType);
+        builder.setBody(DataStream.ofByteBuffer(codec().serialize(input), payloadMediaType));
+    }                                                     // ▲ the codec turns the shape into bytes
+    return builder;
+}
+

And its deserializeResponse is the inbound half — status check, then hand the bytes to the codec:

+
public O deserializeResponse(operation, ctx, typeRegistry, request, response) {
+    if (response.statusCode() != 200)
+        throw errorDeserializer().createError(ctx, operation, typeRegistry, response);
+    var builder = operation.outputBuilder();
+    return codec().deserializeShape(response.body().asByteBuffer(), builder);
+}
+

Source: client/client-rpcv2/.../AbstractRpcV2ClientProtocol.java

+ +

See how codec() — the one method RpcV2CborProtocol overrides — is the pivot? The base class wrote all the framing logic in terms of a codec it doesn't yet know. The concrete subclass supplies it. This is the Template Method pattern, and it's how you'll structure a new protocol family.

+ +
+

🔁 Quick check

+ +
+

1. RpcV2CborProtocol overrides only codec(). Where does its createRequest() come from?

+ + + +
No — protocols are hand-written runtime classes; codegen generates the shapes, not the protocol.
+
Right. The abstract base implements the outbound/inbound halves generically; the subclass only plugs in the codec. Template Method.
+
ClientProtocol declares createRequest but provides no body — it's an interface method, not a default.
+
+ +
+

2. You want to add an "RPC v2 + MessagePack" protocol. Based on this lesson, what's the minimal shape of the work?

+ + + +
Overkill — the RPC framing is identical; only the body encoding differs.
+
The transport isn't the issue — it's still HTTP. Only the payload codec changes.
+
Exactly. If the wire framing matches RPC v2, you inherit it and supply a codec + media type — ~45 lines, like CBOR.
+
+ +
+

3. In createRequest, what specifically turns the typed input shape into bytes?

+ + + +
Correct. The protocol delegates body encoding to its Codec. The protocol decides framing (URI, headers); the codec decides bytes. We unpack how the codec does it — with zero reflection — in Lesson 3.
+
No reflection anywhere — smithy-java avoids it on principle (performance). The codec uses schema-directed push serialization.
+
The transport only sends already-serialized bytes; it never touches the shape.
+
+
+
+ +

4What you now know

+

A concrete protocol can be tiny because the contract is satisfied by a chain of base classes, each contributing a slice. You can read RpcV2CborProtocol and immediately say which class supplies which method — and you know that adding a same-family protocol means picking a codec, not rewriting framing. Next, Lesson 3 answers the question this raises: how does codec().serialize(input) work without reflection, and why does that decoupling exist?

+ +
+

💬 Ask me anything

+

"Show me RpcV2JsonProtocol — is it really the same shape?"
+ • "What's in the templateRequest that gets copied per call?"
+ • "When would I NOT be able to reuse an abstract base like this?"

+

Say "next lesson" for Lesson 3: schema-directed serde.

+
+ + +
+ + + diff --git a/docs/lessons/0002-smallest-real-protocol.md b/docs/lessons/0002-smallest-real-protocol.md new file mode 100644 index 000000000..84ca60709 --- /dev/null +++ b/docs/lessons/0002-smallest-real-protocol.md @@ -0,0 +1,148 @@ +# Lesson 2 — The Smallest Real Protocol + +> **smithy-java · protocol track · lesson 2** +> **Goal:** Read `RpcV2CborProtocol` and see the contract satisfied by inheritance. +> **Time:** ~12 min · **Builds on:** Lesson 1 (the six-method contract). + +In Lesson 1 you learned the six-method `ClientProtocol` contract. Now watch a *real* protocol +satisfy it in ~45 lines. The trick: inheritance does almost all the work — and that tells you +exactly how much code *you'd* write. + +--- + +## 1. The whole protocol, in 45 lines + +This is the complete CBOR client protocol — the entire file, lightly trimmed: + +```java +public final class RpcV2CborProtocol extends AbstractRpcV2ClientProtocol { + private static final String PAYLOAD_MEDIA_TYPE = "application/cbor"; + private static final Codec CBOR_CODEC = Rpcv2CborCodec.builder().build(); + + public RpcV2CborProtocol(ShapeId service) { + super(Rpcv2CborTrait.ID, service, PAYLOAD_MEDIA_TYPE); + } + + @Override protected Codec codec() { return CBOR_CODEC; } + + // The factory — the SPI entry point (Lesson 5 covers this) + public static final class Factory implements ClientProtocolFactory { + @Override public ShapeId id() { return Rpcv2CborTrait.ID; } + @Override public ClientProtocol createProtocol(ProtocolSettings s, Rpcv2CborTrait t) { + return new RpcV2CborProtocol(s.service()); + } + } +} +``` + +> Source: [`client/client-rpcv2-cbor/.../RpcV2CborProtocol.java`](https://github.com/smithy-lang/smithy-java/blob/main/client/client-rpcv2-cbor/src/main/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocol.java) + +Notice what's *missing*: no `createRequest`, no `deserializeResponse`, no `setServiceEndpoint`. +This class declares only **two facts** — "my codec is CBOR" and "my media type is +`application/cbor`." Everything else is inherited. + +--- + +## 2. The inheritance chain + +Each layer contributes a slice of the six-method contract. Read it top-down; the work gets more +specific as you descend: + +``` +ClientProtocol ← the interface; declares all six methods (Lesson 1) + ▲ implements +HttpClientProtocol ← supplies id(), messageExchange() (HTTP), + setServiceEndpoint(). Req=HttpRequest, Resp=HttpResponse + ▲ extends +AbstractRpcV2ClientProtocol ← supplies createRequest() + deserializeResponse() for the + RPC-v2 wire shape. Leaves one hole: abstract Codec codec() + ▲ extends +RpcV2CborProtocol ← fills the hole: codec() → CBOR. Done. +``` + +**The pattern to internalize:** a sibling protocol, `RpcV2JsonProtocol`, would be the *same 45 +lines* with `JsonCodec` and `"application/json"`. The wire framing lives in the abstract base; the +concrete class only picks a codec. That's the payoff of schema-directed serde (Lesson 3): the body +encoding is a swappable knob. + +--- + +## 3. What the abstract base actually does + +Inside `AbstractRpcV2ClientProtocol`, the contract is honored. `createRequest` is Lesson 1's +outbound half, made concrete: + +```java +public HttpRequest createRequest(operation, input, context, endpoint) { + var target = "/service/" + service.getName() + "/operation/" + operation.schema().id().getName(); + var builder = templateRequest.toModifiableCopy(); // POST + smithy-protocol headers + builder.setUri(endpoint.withConcatPath(target)); + + if (operation.inputSchema().hasTrait(UNIT_TYPE_TRAIT)) { + builder.setBody(DataStream.ofEmpty()); // no input → empty body + } else { + builder.addHeader(CONTENT_TYPE, payloadMediaType); + builder.setBody(DataStream.ofByteBuffer(codec().serialize(input), payloadMediaType)); + } // ▲ the codec turns the shape into bytes + return builder; +} +``` + +And `deserializeResponse` is the inbound half — status check, then hand the bytes to the codec: + +```java +public O deserializeResponse(operation, ctx, typeRegistry, request, response) { + if (response.statusCode() != 200) + throw errorDeserializer().createError(ctx, operation, typeRegistry, response); + var builder = operation.outputBuilder(); + return codec().deserializeShape(response.body().asByteBuffer(), builder); +} +``` + +> Source: [`client/client-rpcv2/.../AbstractRpcV2ClientProtocol.java`](https://github.com/smithy-lang/smithy-java/blob/main/client/client-rpcv2/src/main/java/software/amazon/smithy/java/client/rpcv2/AbstractRpcV2ClientProtocol.java) + +`codec()` — the one method `RpcV2CborProtocol` overrides — is the pivot. The base class wrote all +the framing logic *in terms of* a codec it doesn't yet know; the concrete subclass supplies it. +This is the **Template Method** pattern, and it's how you'll structure a new protocol family. + +--- + +## 4. Quick check + +**Q1.** `RpcV2CborProtocol` overrides only `codec()`. Where does its `createRequest()` come from? +- a) Generated by Smithy codegen at build time. +- b) Inherited from `AbstractRpcV2ClientProtocol`, which writes the RPC framing in terms of `codec()`. +- c) From `ClientProtocol` as a default method. + +**Q2.** You want to add "RPC v2 + MessagePack." What's the minimal shape of the work? +- a) Reimplement `createRequest`/`deserializeResponse` from scratch. +- b) Fork `HttpClientProtocol` and change the transport. +- c) Extend `AbstractRpcV2ClientProtocol`, return a MessagePack `Codec` from `codec()`, set the media type. + +**Q3.** In `createRequest`, what turns the typed `input` into bytes? +- a) `codec().serialize(input)` — the codec drives serialization. +- b) Java reflection over the input's fields. +- c) The transport, after the request is built. + +
Answers + +- **Q1 → b.** Template Method: the abstract base implements the outbound/inbound halves generically; the subclass plugs in the codec. (Codegen generates *shapes*, not protocols.) +- **Q2 → c.** If the wire framing matches RPC v2, you inherit it and supply a codec + media type — ~45 lines, like CBOR. The transport is still HTTP; only the body encoding changes. +- **Q3 → a.** The protocol delegates body encoding to its `Codec`. No reflection anywhere (smithy-java avoids it on principle); the codec uses schema-directed push serialization — Lesson 3. + +
+ +--- + +## 5. What you now know + +A concrete protocol can be tiny because the contract is satisfied by a chain of base classes, each +contributing a slice. You can read `RpcV2CborProtocol` and say which class supplies which method — +and you know adding a same-family protocol means picking a codec, not rewriting framing. + +**Next — Lesson 3:** *how* does `codec().serialize(input)` work without reflection, and why does +that decoupling exist? + +## Reference +- [Glossary](../reference/glossary.html) +- [`RpcV2CborProtocol.java`](https://github.com/smithy-lang/smithy-java/blob/main/client/client-rpcv2-cbor/src/main/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocol.java) · [`AbstractRpcV2ClientProtocol.java`](https://github.com/smithy-lang/smithy-java/blob/main/client/client-rpcv2/src/main/java/software/amazon/smithy/java/client/rpcv2/AbstractRpcV2ClientProtocol.java) diff --git a/docs/lessons/0003-schema-directed-serde.html b/docs/lessons/0003-schema-directed-serde.html new file mode 100644 index 000000000..f0a43ab28 --- /dev/null +++ b/docs/lessons/0003-schema-directed-serde.html @@ -0,0 +1,192 @@ + + + + + +Lesson 3 · Schema-Directed Serde + + + +
+ +
smithy-java · protocol track · lesson 3
+

Schema-Directed Serde

+

Lesson 2 ended on codec().serialize(input). How does that produce JSON or CBOR from a generated shape — with no reflection, and without the shape knowing which protocol is active? This is the single most important idea in the framework. Get it, and protocols stop being mysterious.

+ +
+ Goal — Understand why protocols ⊥ generated code + Time — ~12 min + Builds on — Lesson 2 (codec is the pivot) +
+ +

1The problem it solves

+

You have generated shape classes (PutItemInput, etc.) and you have protocols (JSON, CBOR, XML). The naive design couples them: each shape knows how to write itself as JSON. But then adding a protocol means regenerating every shape, and the shape carries N serializers. smithy-java refuses that.

+

The insight: put a Schema between them. The shape knows its schema and pushes its members; the protocol reads schemas and decides wire format. Neither imports the other.

+ +
One line to remember: the Schema is the first argument to every serde method. That's not a style choice — it's the entire decoupling mechanism. The schema is the contract; the protocol and the shape only ever talk through it.
+ +

2The two sides: push and interpret

+
+
+

① The shape pushes (generated)

+
public void serializeMembers(
+        ShapeSerializer ser) {
+  ser.writeString($SCHEMA_NAME, name);
+  ser.writeInteger($SCHEMA_AGE, age);
+}
+

Each member is pushed with its schema. The shape has no idea if this is JSON or CBOR.

+
+
+

② The codec interprets

+
void writeString(Schema s, String v) {
+  // JSON codec: look up the
+  // precomputed field-name bytes
+  // for s, arraycopy, write v
+}
+

The codec implements ShapeSerializer. It reads the schema to decide bytes.

+
+
+ +

The interface that connects them — every method takes a Schema first:

+
public interface ShapeSerializer extends Flushable, AutoCloseable {
+    void writeStruct(Schema schema, SerializableStruct struct);
+    void writeString(Schema schema, String value);
+    void writeInteger(Schema schema, int value);
+    void writeTimestamp(Schema schema, Instant value);
+    <T> void writeList(Schema schema, T state, int size, BiConsumer<T, ShapeSerializer> consumer);
+    // ... every write takes the member's Schema first
+}
+

Source: core/.../core/serde/ShapeSerializer.java

+ +

3Deserialization inverts it: a callback by index

+

Reading is the mirror image. The codec reads wire bytes, resolves them to a member schema, and calls back. The generated builder dispatches on memberIndex() — which the compiler turns into a tableswitch, no string compares:

+
decoder.readStruct($SCHEMA, this, (builder, member, de) -> {
+    switch (member.memberIndex()) {        // JVM tableswitch — O(1)
+        case 0 -> builder.name(de.readString(member));
+        case 1 -> builder.age(de.readInteger(member));
+    }
+});
+

The codec owns the field loop; the shape owns the per-field action. Again: they meet only at the schema.

+ +

4Why no trait lookups on the hot path

+

"The schema decides the wire format" sounds like it'd inspect traits (@jsonName, @timestampFormat) at write time. It doesn't — that would be slow. Instead, each protocol precomputes what it needs onto the schema once, into a schema extension:

+ + + + + +
ExtensionPrecomputesSo writeString can…
SmithyJsonSchemaExtensionsbyte[][] field-name tables (with quotes+colon, honoring @jsonName)do table[memberIndex]System.arraycopy
CborSchemaExtensionsbyte[][] CBOR-encoded field namessame O(1) array index
HttpBindingSchemaExtensionsbinding location (HEADER/QUERY/LABEL/BODY), timestamp formatsroute the member to the right HTTP place
+

So at serialization time there are no trait lookups, no hashing — just an array index and a memory copy. The trait reading happened once, at schema construction. You'll meet the extension SPI properly in Lesson 5; for now just hold the shape of it.

+ +
The protocol-author payoff: because the same serializeMembers() works for any codec, adding a protocol never requires regenerating shapes. You write a codec (or reuse one) + a schema extension to precompute your wire data. The generated code is already protocol-agnostic and waiting for you.
+ +
+

🔁 Quick check

+ +
+

1. Why is Schema the first argument to every ShapeSerializer method?

+ + + +
It's used for far more than logging — it carries the precomputed wire data the codec needs.
+
No such requirement exists; this is a deliberate design choice.
+
Exactly. The schema is the meeting point. That's what lets one generated shape serialize as JSON, CBOR, or XML with no changes.
+
+ +
+

2. A teammate says "let's read the @jsonName trait inside writeString to get the field name." Why does the codebase avoid this?

+ + + +
Traits are available — they're just not consulted per-write for performance reasons.
+
Right. Precompute-once, index-many. Trait inspection per field per request would be death by a thousand cuts (Tenet 3: Performance).
+
@jsonName is a JSON concern; and the point is when it's read, not which protocol.
+
+ +
+

3. On the deserialize side, the generated builder does switch(member.memberIndex()). Why an integer index instead of matching field names?

+ + + +
Correct. The codec resolves wire bytes → member schema (with a fast speculative path), then the shape dispatches by dense integer index. Fast and allocation-free.
+
Field names are known from the schema; the index is a performance optimization over comparing them.
+
Unrelated to ServiceLoader — this is purely a dispatch optimization.
+
+
+
+ +

5What you now know

+

You can explain why a protocol and a generated shape never know about each other, and how the Schema-as-first-argument design makes one shape serialize to any format. You know wire decisions are precomputed into schema extensions, not read per-write. That's the conceptual core. Lesson 4 zooms into the Codec itself — the object your protocol returns from payloadCodec() — and what it takes to pick or build one.

+ +
+

💬 Ask me anything

+

"Show me a real generated serializeMembers() in the repo"
+ • "How does the codec resolve wire bytes back to a memberIndex?" (the speculative fast path)
+ • "What about the document type — shapes with no fixed schema?"

+

Say "next lesson" for Lesson 4: the Codec layer.

+
+ + +
+ + + diff --git a/docs/lessons/0003-schema-directed-serde.md b/docs/lessons/0003-schema-directed-serde.md new file mode 100644 index 000000000..a68a8bc22 --- /dev/null +++ b/docs/lessons/0003-schema-directed-serde.md @@ -0,0 +1,145 @@ +# Lesson 3 — Schema-Directed Serde + +> **smithy-java · protocol track · lesson 3** +> **Goal:** Understand *why* protocols and generated code are decoupled. +> **Time:** ~12 min · **Builds on:** Lesson 2 (the codec is the pivot). + +Lesson 2 ended on `codec().serialize(input)`. *How* does that produce JSON or CBOR from a generated +shape — with no reflection, and without the shape knowing which protocol is active? This is the +single most important idea in the framework. + +--- + +## 1. The problem it solves + +You have generated shape classes (`PutItemInput`, etc.) and protocols (JSON, CBOR, XML). The naive +design couples them: each shape knows how to write itself as JSON. Then adding a protocol means +regenerating every shape, and each shape carries N serializers. smithy-java refuses that. + +The insight: put a **`Schema`** between them. The shape knows its schema and pushes its members; +the protocol reads schemas and decides wire format. **Neither imports the other.** + +> **One line to remember:** the `Schema` is the first argument to *every* serde method. That's not +> a style choice — it's the entire decoupling mechanism. The schema is the contract; the protocol +> and the shape only ever talk *through* it. + +--- + +## 2. The two sides: push and interpret + +**① The shape pushes (generated code):** + +```java +public void serializeMembers(ShapeSerializer ser) { + ser.writeString($SCHEMA_NAME, name); // each member pushed WITH its schema + ser.writeInteger($SCHEMA_AGE, age); // the shape has no idea if this is JSON or CBOR +} +``` + +**② The codec interprets:** it implements `ShapeSerializer` and reads the schema to decide bytes. + +```java +void writeString(Schema s, String v) { + // JSON codec: look up the precomputed field-name bytes for s, arraycopy, write v +} +``` + +The interface that connects them — every method takes a `Schema` first: + +```java +public interface ShapeSerializer extends Flushable, AutoCloseable { + void writeStruct(Schema schema, SerializableStruct struct); + void writeString(Schema schema, String value); + void writeInteger(Schema schema, int value); + void writeTimestamp(Schema schema, Instant value); + void writeList(Schema schema, T state, int size, BiConsumer consumer); + // ... every write takes the member's Schema first +} +``` + +> Source: [`core/.../core/serde/ShapeSerializer.java`](https://github.com/smithy-lang/smithy-java/blob/main/core/src/main/java/software/amazon/smithy/java/core/serde/ShapeSerializer.java) + +--- + +## 3. Deserialization inverts it: a callback by index + +Reading is the mirror image. The codec reads wire bytes, resolves them to a member schema, and +calls back. The generated builder dispatches on `memberIndex()`, which the compiler turns into a +`tableswitch` — no string compares: + +```java +decoder.readStruct($SCHEMA, this, (builder, member, de) -> { + switch (member.memberIndex()) { // JVM tableswitch — O(1) + case 0 -> builder.name(de.readString(member)); + case 1 -> builder.age(de.readInteger(member)); + } +}); +``` + +The codec owns the field loop; the shape owns the per-field action. Again: they meet only at the +schema. + +--- + +## 4. Why no trait lookups on the hot path + +"The schema decides the wire format" sounds like it'd inspect traits (`@jsonName`, +`@timestampFormat`) at write time. It doesn't — that would be slow. Instead, each protocol +**precomputes** what it needs onto the schema *once*, into a *schema extension*: + +| Extension | Precomputes | So `writeString` can… | +|---|---|---| +| `SmithyJsonSchemaExtensions` | `byte[][]` field-name tables (with quotes+colon, honoring `@jsonName`) | do `table[memberIndex]` → `System.arraycopy` | +| `CborSchemaExtensions` | `byte[][]` CBOR-encoded field names | same O(1) array index | +| `HttpBindingSchemaExtensions` | binding location (HEADER/QUERY/LABEL/BODY), timestamp formats | route the member to the right HTTP place | + +At serialization there are *no* trait lookups, no hashing — just an array index and a memory copy. +The trait reading happened once, at schema construction. (You'll meet the extension SPI properly in +Lesson 5.) + +> **The protocol-author payoff:** because the same `serializeMembers()` works for any codec, +> *adding a protocol never requires regenerating shapes*. You write a codec (or reuse one) + a +> schema extension to precompute your wire data. The generated code is already protocol-agnostic +> and waiting for you. + +--- + +## 5. Quick check + +**Q1.** Why is `Schema` the first argument to every `ShapeSerializer` method? +- a) For logging/debugging — it identifies the field by name. +- b) Java requires interface methods to share a first parameter. +- c) It's the decoupling contract: the shape pushes a member + its schema; the codec reads the schema to decide wire format. + +**Q2.** Why not read the `@jsonName` trait inside `writeString` to get the field name? +- a) Traits aren't available at runtime. +- b) It's on the hot path; the field-name bytes are precomputed into a schema extension once — `writeString` just indexes + arraycopies. +- c) `@jsonName` only exists for XML protocols. + +**Q3.** Why `switch(member.memberIndex())` instead of matching field names on deserialize? +- a) The compiler turns it into a `tableswitch` — O(1) dispatch, no string comparisons. +- b) Field names aren't known until runtime. +- c) It's required by the `ServiceLoader` SPI. + +
Answers + +- **Q1 → c.** The schema is the meeting point — that's what lets one generated shape serialize as JSON, CBOR, or XML unchanged. +- **Q2 → b.** Precompute-once, index-many. Per-field trait inspection would be death by a thousand cuts (Tenet 3: Performance). +- **Q3 → a.** The codec resolves wire bytes → member schema (with a speculative fast path), then the shape dispatches by dense integer index. Fast and allocation-free. + +
+ +--- + +## 6. What you now know + +You can explain *why* a protocol and a generated shape never know about each other, and how the +`Schema`-as-first-argument design makes one shape serialize to any format. Wire decisions are +precomputed into schema extensions, not read per-write. + +**Next — Lesson 4:** the `Codec` itself — the object your protocol returns from `payloadCodec()`, +and the decision of whether to reuse or write one. + +## Reference +- [Glossary](../reference/glossary.html) +- [`ShapeSerializer.java`](https://github.com/smithy-lang/smithy-java/blob/main/core/src/main/java/software/amazon/smithy/java/core/serde/ShapeSerializer.java) diff --git a/docs/lessons/0004-the-codec-layer.html b/docs/lessons/0004-the-codec-layer.html new file mode 100644 index 000000000..2f1784960 --- /dev/null +++ b/docs/lessons/0004-the-codec-layer.html @@ -0,0 +1,174 @@ + + + + + +Lesson 4 · The Codec Layer + + + +
+ +
smithy-java · protocol track · lesson 4
+

The Codec Layer

+

Your protocol returns a Codec from payloadCodec(). That object is the bridge between schema-carrying shapes and raw bytes. This lesson shows what the Codec interface actually is — and the key decision: reuse a codec vs write one.

+ +
+ Goal — Know what a Codec is & when to write one + Time — ~11 min + Builds on — Lesson 3 (the serde interfaces) +
+ +

1The interface is tiny

+

A Codec is just a factory for the two visitor implementations you met in Lesson 3, plus two convenience methods:

+
public interface Codec extends AutoCloseable {
+    ShapeSerializer   createSerializer(OutputStream sink);     // struct → bytes
+    ShapeDeserializer createDeserializer(ByteBuffer source);    // bytes → struct
+
+    // Convenience, built on the two above:
+    default ByteBuffer serialize(SerializableShape shape) { ... }
+    default <T> T deserializeShape(ByteBuffer src, ShapeBuilder<T> builder) { ... }
+}
+

Source: core/.../core/serde/Codec.java

+ +

That's the whole contract. When AbstractRpcV2ClientProtocol called codec().serialize(input) in Lesson 2, it used the default serialize here, which spins up a ShapeSerializer, lets the shape push its members into it, and collects the bytes.

+ +

2The three codecs that ship today

+ + + + + +
CodecFormatNotes
JsonCodecJSONPluggable JsonSerdeProvider; options for useJsonName, timestamp format, pretty-print. Hand-written parser on byte[] — no Jackson on the hot path.
Rpcv2CborCodecCBORPluggable CborSerdeProvider; binary, compact. VarHandle for big-endian reads.
XmlCodecXMLStAX-based; wrapperElements for AWS Query; honors XML traits.
+

Note how protocols configure, not reimplement, these. RestJsonClientProtocol builds its codec with options tuned for REST JSON:

+
this.codec = JsonCodec.builder()
+    .useJsonName(true)            // respect @jsonName
+    .useTimestampFormat(true)     // respect @timestampFormat
+    .defaultNamespace(service.getNamespace())
+    .build();
+

Source: RestJsonClientProtocol.java

+ +

3The decision: reuse or write?

+
Default to reuse. If your new protocol's body is JSON, CBOR, or XML — even with quirks — you configure an existing codec via its builder. You write a new codec only when the byte format itself is genuinely new (MessagePack, Protobuf, a bespoke binary layout). That's a much rarer and bigger task than "add a protocol."
+ +

Two distinct axes, often confused:

+ + + + +
AxisWhat changesExample
ProtocolFraming: URI, method, headers, how errors are signaled, where members bindrestJson1 vs AWS JSON 1.0 — both use JSON
CodecThe payload byte encoding itselfJSON vs CBOR vs XML
+

restJson1 and AWS JSON 1.0 share JsonCodec but are different protocols. rpcv2Cbor and a hypothetical rpcv2Json share framing but differ by codec. Knowing which axis you're moving on tells you how much work you're signing up for.

+ +

If you do write a codec, the steps (from the KT doc's "Adding a new codec"):

+
1. Implement Codec, ShapeSerializer, ShapeDeserializer in a new module
+2. Add a SchemaExtensionProvider to precompute trait-derived wire data  // Lesson 5
+3. Register the codec's extension via ServiceLoader in META-INF/services/
+4. Add protocol compliance tests                                       // Lesson 7
+ +
+

🔁 Quick check

+ +
+

1. What are the two methods that define a Codec (everything else is a default)?

+ + + +
Not the actual method names — the codec produces visitor objects, it doesn't encode directly.
+
Right. A codec is a factory for a ShapeSerializer and a ShapeDeserializer. Those visitors do the work.
+
Those are the default convenience methods built on top of the two factory methods.
+
+ +
+

2. Your new protocol sends JSON bodies but with AWS-JSON-style framing (X-Amz-Target header, full-body payload). Do you write a new codec?

+ + + +
Protocol ≠ codec. restJson1 and AWS JSON are different protocols sharing one JSON codec.
+
Headers are framing, not payload bytes. The body is still JSON.
+
Exactly. Framing lives in the protocol; the codec only encodes the body. Reuse JsonCodec.
+
+ +
+

3. How does RestJsonClientProtocol make its codec honor @jsonName?

+ + + +
Correct. Codecs are configured through their builders — a flag flips precomputed-field-name behavior. No subclassing, no per-request trait reads.
+
No subclassing needed — it's a builder option.
+
That'd be a hot-path trait lookup, which the design forbids (Lesson 3).
+
+
+
+ +

4What you now know

+

You can describe the Codec interface (two factory methods + two conveniences), name the three shipping codecs, and — crucially — separate the protocol axis (framing) from the codec axis (byte format). That distinction is what makes "add a protocol" usually a small job. Lesson 5 covers the schema-extension SPI: how a codec/protocol precomputes its wire data and registers it so the framework finds it.

+ +
+

💬 Ask me anything

+

"Walk me through JsonCodec.serialize end-to-end for a 2-field struct"
+ • "What's a JsonSerdeProvider and why is it pluggable?"
+ • "Is XmlCodec really safe to reuse? The KT doc calls it less battle-tested."

+

Say "next lesson" for Lesson 5: the schema-extension SPI.

+
+ + +
+ + + diff --git a/docs/lessons/0004-the-codec-layer.md b/docs/lessons/0004-the-codec-layer.md new file mode 100644 index 000000000..17bfd49aa --- /dev/null +++ b/docs/lessons/0004-the-codec-layer.md @@ -0,0 +1,127 @@ +# Lesson 4 — The Codec Layer + +> **smithy-java · protocol track · lesson 4** +> **Goal:** Know what a `Codec` is and when to write one vs reuse one. +> **Time:** ~11 min · **Builds on:** Lesson 3 (the serde interfaces). + +Your protocol returns a `Codec` from `payloadCodec()`. That object is the bridge between +schema-carrying shapes and raw bytes. This lesson shows what the interface actually is — and the key +decision: *reuse* a codec vs *write* one. + +--- + +## 1. The interface is tiny + +A `Codec` is just a factory for the two visitor implementations from Lesson 3, plus two convenience +methods: + +```java +public interface Codec extends AutoCloseable { + ShapeSerializer createSerializer(OutputStream sink); // struct → bytes + ShapeDeserializer createDeserializer(ByteBuffer source); // bytes → struct + + // Convenience, built on the two above: + default ByteBuffer serialize(SerializableShape shape) { ... } + default T deserializeShape(ByteBuffer src, ShapeBuilder builder) { ... } +} +``` + +> Source: [`core/.../core/serde/Codec.java`](https://github.com/smithy-lang/smithy-java/blob/main/core/src/main/java/software/amazon/smithy/java/core/serde/Codec.java) + +When `AbstractRpcV2ClientProtocol` called `codec().serialize(input)` in Lesson 2, it used the +`default serialize` here: it spins up a `ShapeSerializer`, lets the shape push its members into it, +and collects the bytes. + +--- + +## 2. The three codecs that ship today + +| Codec | Format | Notes | +|---|---|---| +| `JsonCodec` | JSON | Pluggable `JsonSerdeProvider`; options for `useJsonName`, timestamp format, pretty-print. Hand-written parser on `byte[]` — no Jackson on the hot path. | +| `Rpcv2CborCodec` | CBOR | Pluggable `CborSerdeProvider`; binary, compact. `VarHandle` for big-endian reads. | +| `XmlCodec` | XML | StAX-based; `wrapperElements` for AWS Query; honors XML traits. | + +Protocols **configure**, not reimplement, these. `RestJsonClientProtocol` builds its codec with +options tuned for REST JSON: + +```java +this.codec = JsonCodec.builder() + .useJsonName(true) // respect @jsonName + .useTimestampFormat(true) // respect @timestampFormat + .defaultNamespace(service.getNamespace()) + .build(); +``` + +> Source: [`RestJsonClientProtocol.java`](https://github.com/smithy-lang/smithy-java/blob/main/aws/client/aws-client-restjson/src/main/java/software/amazon/smithy/java/aws/client/restjson/RestJsonClientProtocol.java) + +--- + +## 3. The decision: reuse or write? + +> **Default to reuse.** If your new protocol's body is JSON, CBOR, or XML — even with quirks — you +> configure an existing codec via its builder. You write a *new* codec only when the **byte format +> itself** is genuinely new (MessagePack, Protobuf, a bespoke binary layout). That's a much rarer +> and bigger task than "add a protocol." + +Two distinct axes, often confused: + +| Axis | What changes | Example | +|---|---|---| +| **Protocol** | Framing: URI, method, headers, error signaling, where members bind | restJson1 vs AWS JSON 1.0 — *both use JSON* | +| **Codec** | The payload byte encoding itself | JSON vs CBOR vs XML | + +restJson1 and AWS JSON 1.0 share `JsonCodec` but are different *protocols*. rpcv2Cbor and a +hypothetical rpcv2Json share framing but differ by *codec*. Knowing which axis you're moving on +tells you how much work you're signing up for. + +If you *do* write a codec, the steps (from the KT doc's "Adding a new codec"): + +``` +1. Implement Codec, ShapeSerializer, ShapeDeserializer in a new module +2. Add a SchemaExtensionProvider to precompute trait-derived wire data // Lesson 5 +3. Register the codec's extension via ServiceLoader in META-INF/services/ +4. Add protocol compliance tests // Lesson 7 +``` + +--- + +## 4. Quick check + +**Q1.** The two methods that *define* a `Codec` (everything else is a default)? +- a) `encode()` and `decode()` +- b) `createSerializer(OutputStream)` and `createDeserializer(ByteBuffer)` +- c) `serialize()` and `deserializeShape()` + +**Q2.** Your new protocol sends JSON bodies but with AWS-JSON-style framing (`X-Amz-Target`, full-body payload). Do you write a new codec? +- a) Yes — different protocol means different codec. +- b) Yes — the header changes the byte format. +- c) No — the byte format is still JSON. Reuse `JsonCodec`; put framing in your `ClientProtocol`. + +**Q3.** How does `RestJsonClientProtocol` make its codec honor `@jsonName`? +- a) `JsonCodec.builder().useJsonName(true)`. +- b) It subclasses `JsonCodec`. +- c) It reads `@jsonName` at request time in `createRequest`. + +
Answers + +- **Q1 → b.** A codec is a *factory* for a `ShapeSerializer` and `ShapeDeserializer`. `serialize`/`deserializeShape` are defaults built on top. +- **Q2 → c.** Protocol ≠ codec. Framing lives in the protocol; the codec only encodes the body. restJson1 and AWS JSON are different protocols sharing one JSON codec. +- **Q3 → a.** Codecs are configured through their builders — a flag flips precomputed-field-name behavior. No subclassing, no per-request trait reads (Lesson 3). + +
+ +--- + +## 5. What you now know + +You can describe the `Codec` interface, name the three shipping codecs, and — crucially — separate +the **protocol axis** (framing) from the **codec axis** (byte format). That distinction is what +makes "add a protocol" usually a small job. + +**Next — Lesson 5:** the schema-extension SPI and `ServiceLoader` discovery — how wire data is +precomputed and how the framework *finds* your protocol. + +## Reference +- [Glossary](../reference/glossary.html) +- [`Codec.java`](https://github.com/smithy-lang/smithy-java/blob/main/core/src/main/java/software/amazon/smithy/java/core/serde/Codec.java) diff --git a/docs/lessons/0005-extensions-and-discovery.html b/docs/lessons/0005-extensions-and-discovery.html new file mode 100644 index 000000000..09618a925 --- /dev/null +++ b/docs/lessons/0005-extensions-and-discovery.html @@ -0,0 +1,181 @@ + + + + + +Lesson 5 · Extensions & Discovery + + + +
+ +
smithy-java · protocol track · lesson 5
+

Extensions & Discovery

+

Two SPIs make a protocol real. Schema extensions precompute your wire data (Lesson 3's promise, kept). ServiceLoader is how the framework finds your protocol at all. Miss either and your protocol is silently absent.

+ +
+ Goal — Precompute wire data & register a protocol + Time — ~13 min + Builds on — Lessons 3–4 (serde & codecs) +
+ +

1Schema extensions: precompute once, index forever

+

Lesson 3 said wire decisions are precomputed, not read per-write. The mechanism is a pair of types. A key (a unique integer ID → array slot):

+
public final class SchemaExtensionKey<T> {
+    private static final AtomicInteger NEXT_ID = new AtomicInteger();
+    final int id;
+    public SchemaExtensionKey() { this.id = NEXT_ID.getAndIncrement(); }  // O(1) lookup later
+}
+

And a provider that computes the value for each schema (discovered via ServiceLoader):

+
public interface SchemaExtensionProvider<T> {
+    SchemaExtensionKey<T> key();        // which slot
+    T provide(Schema schema);            // compute wire data from the schema's traits
+}
+

Source: SchemaExtensionProvider.java · SchemaExtensionKey.java

+ +
The contract that bites: provide()'s result is published across threads via a plain array store (no lock, benign race). The Javadoc requires the returned object's fields to be all final — which is why these are records. The JMM guarantees final-field visibility once the constructor returns (JLS 17.5). Return a mutable object and you have a data race.
+ +

At serialization, the codec does schema.getExtension(KEY) — an array index, no hashing — to get its precomputed bytes/formatters. This is the "no trait lookups on the hot path" claim made concrete.

+ +

2Registering it: META-INF/services

+

A provider that isn't registered never runs. Registration is a plain-text file named after the fully-qualified interface, containing the implementation class name(s):

+
+ codecs/json-codec/src/main/resources/META-INF/services/
  software.amazon.smithy.java.core.schema.SchemaExtensionProvider
+ software.amazon.smithy.java.json.JsonSchemaExtensions
+ software.amazon.smithy.java.json.smithy.SmithyJsonSchemaExtensions +
+

Real file, verbatim — note JSON registers two providers. CBOR and HTTP-binding each register their own.

+ +

3The other SPI: finding the protocol itself

+

Same mechanism, different interface. Your ClientProtocolFactory (the Factory inner class from Lesson 2) is registered under its interface name:

+
+ client/client-rpcv2-cbor/src/main/resources/META-INF/services/
  software.amazon.smithy.java.client.core.ClientProtocolFactory
+ software.amazon.smithy.java.client.rpcv2.RpcV2CborProtocol$Factory +
+

(The $Factory is the nested class.) Now watch the framework consume it — this is the actual discovery loop, from DetectProtocolPlugin:

+
static {
+    for (var impl : ServiceLoader.load(ClientProtocolFactory.class, ...)) {
+        PROTOCOL_FACTORIES.add(impl);     // every registered factory, loaded once
+    }
+}
+// later, given a service's protocol traits:
+for (var impl : PROTOCOL_FACTORIES) {
+    if (protocols.containsKey(impl.id())) {       // trait ShapeId == factory.id()
+        return impl.createProtocol(settings, protocols.get(impl.id()));
+    }
+}
+

Source: DetectProtocolPlugin.java

+ +

This closes the loop opened in Lesson 1: id() returns the protocol trait's ShapeId precisely so this matching works. The service model says "I speak smithy.protocols#rpcv2Cbor"; the framework finds the factory whose id() equals that ShapeId, and builds it with ProtocolSettings.

+ +
⚠️ The #1 "my protocol doesn't work" bug: the class is written correctly but the META-INF/services line is missing, misspelled, or points to the wrong class (forgetting the $Factory suffix for a nested class). ServiceLoader fails silently — no error, the protocol is just never found. Check this file first.
+ +

4The protocol trait, briefly

+

The join key — the ShapeId — comes from a Smithy protocol trait applied to the service, e.g. @rpcv2Cbor or @restJson1. These traits are marked with @protocolDefinition in their Smithy model and ship as *Trait Java classes (Rpcv2CborTrait.ID, RestJson1Trait.ID) you reference directly. For an existing standard protocol the trait already exists; for a brand-new one you'd define the trait in a Smithy model first. Either way, factory.id() returns YourTrait.ID.

+ +
+

🔁 Quick check

+ +
+

1. Why must a SchemaExtensionProvider return a record (or all-final object)?

+ + + +
Conciseness is a bonus, not the reason — the reason is thread-safety.
+
Right. The benign-race, lock-free publication is safe only because final fields are visible after construction. A mutable extension would be a data race.
+
ServiceLoader instantiates any class with a no-arg constructor; records aren't special to it.
+
+ +
+

2. You wrote a perfect ClientProtocol + Factory, but the client says "no matching protocol." First thing to check?

+ + + +
Shapes are protocol-agnostic; regenerating won't help discovery.
+
That'd cause a serialization bug, not a "protocol not found" — discovery happens before any serialization.
+
Exactly. Silent ServiceLoader failure from a missing/typo'd registration is the classic cause. Verify that file before anything else.
+
+ +
+

3. How does the framework match a service to your protocol implementation?

+ + + +
Correct. factory.id() == YourTrait.ID, and the service's @yourProtocol trait carries that same ShapeId. That's why Lesson 1 stressed what id() returns.
+
Package names are irrelevant to matching — only the ShapeId matters.
+
No alphabetical logic; it's ShapeId equality. (When multiple match, the first found wins — see the code.)
+
+
+
+ +

5What you now know

+

You can wire the two SPIs that make a protocol exist: a SchemaExtensionProvider (record-typed, registered) to precompute wire data, and a ClientProtocolFactory (registered under its interface) so ServiceLoader discovery matches your id() to a service's protocol trait. You know the silent-failure trap. Lesson 6 uses all this to compare the two base-class families head-to-head, so you can pick the right parent for a new protocol.

+ +
+

💬 Ask me anything

+

"Show me CborSchemaExtensions.provide() — what does it precompute?"
+ • "How do I define a brand-new @protocolDefinition trait in Smithy?"
+ • "What's the difference between AutoClientPlugin discovery and this?"

+

Say "next lesson" for Lesson 6: choosing your base class.

+
+ + +
+ + + diff --git a/docs/lessons/0005-extensions-and-discovery.md b/docs/lessons/0005-extensions-and-discovery.md new file mode 100644 index 000000000..8a6e7c32d --- /dev/null +++ b/docs/lessons/0005-extensions-and-discovery.md @@ -0,0 +1,155 @@ +# Lesson 5 — Extensions & Discovery + +> **smithy-java · protocol track · lesson 5** +> **Goal:** Precompute wire data and register a protocol so the framework finds it. +> **Time:** ~13 min · **Builds on:** Lessons 3–4 (serde & codecs). + +Two SPIs make a protocol real. **Schema extensions** precompute your wire data (Lesson 3's promise, +kept). **`ServiceLoader`** is how the framework *finds* your protocol at all. Miss either and your +protocol is silently absent. + +--- + +## 1. Schema extensions: precompute once, index forever + +The mechanism is a pair of types. A **key** (a unique integer ID → array slot): + +```java +public final class SchemaExtensionKey { + private static final AtomicInteger NEXT_ID = new AtomicInteger(); + final int id; + public SchemaExtensionKey() { this.id = NEXT_ID.getAndIncrement(); } // O(1) lookup later +} +``` + +And a **provider** that computes the value for each schema (discovered via `ServiceLoader`): + +```java +public interface SchemaExtensionProvider { + SchemaExtensionKey key(); // which slot + T provide(Schema schema); // compute wire data from the schema's traits +} +``` + +> Source: [`SchemaExtensionProvider.java`](https://github.com/smithy-lang/smithy-java/blob/main/core/src/main/java/software/amazon/smithy/java/core/schema/SchemaExtensionProvider.java) · [`SchemaExtensionKey.java`](https://github.com/smithy-lang/smithy-java/blob/main/core/src/main/java/software/amazon/smithy/java/core/schema/SchemaExtensionKey.java) + +> **The contract that bites:** `provide()`'s result is published across threads via a *plain* array +> store (no lock, benign race). The Javadoc requires the returned object's fields to be **all +> `final`** — which is why these are **records**. The JMM guarantees final-field visibility once +> the constructor returns (JLS 17.5). Return a mutable object and you have a data race. + +At serialization, the codec does `schema.getExtension(KEY)` — an array index, no hashing — to get +its precomputed bytes/formatters. This is the "no trait lookups on the hot path" claim made concrete. + +--- + +## 2. Registering it: `META-INF/services` + +A provider that isn't registered never runs. Registration is a plain-text file named after the +*fully-qualified interface*, containing the implementation class name(s): + +``` +File: codecs/json-codec/src/main/resources/META-INF/services/ + software.amazon.smithy.java.core.schema.SchemaExtensionProvider + +software.amazon.smithy.java.json.JsonSchemaExtensions +software.amazon.smithy.java.json.smithy.SmithyJsonSchemaExtensions +``` + +Real file, verbatim — note JSON registers *two* providers. CBOR and HTTP-binding each register +their own. + +--- + +## 3. The other SPI: finding the protocol itself + +Same mechanism, different interface. Your `ClientProtocolFactory` (the `Factory` inner class from +Lesson 2) is registered under *its* interface name: + +``` +File: client/client-rpcv2-cbor/src/main/resources/META-INF/services/ + software.amazon.smithy.java.client.core.ClientProtocolFactory + +software.amazon.smithy.java.client.rpcv2.RpcV2CborProtocol$Factory +``` + +(The `$Factory` is the nested class.) The framework consumes it in `DetectProtocolPlugin`: + +```java +static { + for (var impl : ServiceLoader.load(ClientProtocolFactory.class, ...)) { + PROTOCOL_FACTORIES.add(impl); // every registered factory, loaded once + } +} +// later, given a service's protocol traits: +for (var impl : PROTOCOL_FACTORIES) { + if (protocols.containsKey(impl.id())) { // trait ShapeId == factory.id() + return impl.createProtocol(settings, protocols.get(impl.id())); + } +} +``` + +> Source: [`DetectProtocolPlugin.java`](https://github.com/smithy-lang/smithy-java/blob/main/client/dynamic-client/src/main/java/software/amazon/smithy/java/dynamicclient/plugins/DetectProtocolPlugin.java) + +This closes the loop from Lesson 1: `id()` returns the protocol trait's `ShapeId` precisely so this +matching works. The service model says "I speak `smithy.protocols#rpcv2Cbor`"; the framework finds +the factory whose `id()` equals that ShapeId and builds it with `ProtocolSettings`. (When multiple +match, the first found wins.) + +> ⚠️ **The #1 "my protocol doesn't work" bug:** the class is correct but the `META-INF/services` +> line is missing, misspelled, or points to the wrong class (forgetting `$Factory` for a nested +> class). `ServiceLoader` fails *silently* — no error, the protocol is just never found. Check this +> file first. + +--- + +## 4. The protocol trait, briefly + +The join key — the `ShapeId` — comes from a Smithy *protocol trait* applied to the service, e.g. +`@rpcv2Cbor` or `@restJson1`. These traits are marked `@protocolDefinition` in their Smithy model +and ship as `*Trait` Java classes (`Rpcv2CborTrait.ID`, `RestJson1Trait.ID`) you reference directly. +For an existing standard protocol the trait already exists; for a brand-new one you'd define the +trait in a Smithy model first. Either way, `factory.id()` returns `YourTrait.ID`. + +--- + +## 5. Quick check + +**Q1.** Why must a `SchemaExtensionProvider` return a record (or all-final object)? +- a) Records are shorter to write. +- b) The result is published across threads via a plain array store with no lock; final fields guarantee safe visibility (JLS 17.5). +- c) `ServiceLoader` only instantiates records. + +**Q2.** Perfect `ClientProtocol` + `Factory`, but the client says "no matching protocol." First check? +- a) Recompile the generated shapes. +- b) Whether the codec supports your timestamp format. +- c) The `META-INF/services/...ClientProtocolFactory` file — present? correct class? `$Factory` suffix? + +**Q3.** How does the framework match a service to your protocol implementation? +- a) It compares each registered factory's `id()` (a `ShapeId`) against the protocol traits on the service. +- b) By the Java package name of the protocol class. +- c) Alphabetically by class name. + +
Answers + +- **Q1 → b.** The lock-free, benign-race publication is safe *only* because final fields are visible after construction. A mutable extension would be a data race. +- **Q2 → c.** Silent `ServiceLoader` failure from a missing/typo'd registration is the classic cause. Verify that file before anything else. +- **Q3 → a.** `factory.id() == YourTrait.ID`, and the service's `@yourProtocol` trait carries that same ShapeId. That's why Lesson 1 stressed what `id()` returns. + +
+ +--- + +## 6. What you now know + +You can wire the two SPIs that make a protocol exist: a `SchemaExtensionProvider` (record-typed, +registered) to precompute wire data, and a `ClientProtocolFactory` (registered under its interface) +so `ServiceLoader` discovery matches your `id()` to a service's protocol trait. You know the +silent-failure trap. + +**Next — Lesson 6:** compare the two base-class families head-to-head, so you can pick the right +parent for a new protocol. + +## Reference +- [Glossary](../reference/glossary.html) +- [`SchemaExtensionProvider.java`](https://github.com/smithy-lang/smithy-java/blob/main/core/src/main/java/software/amazon/smithy/java/core/schema/SchemaExtensionProvider.java) · [`DetectProtocolPlugin.java`](https://github.com/smithy-lang/smithy-java/blob/main/client/dynamic-client/src/main/java/software/amazon/smithy/java/dynamicclient/plugins/DetectProtocolPlugin.java) diff --git a/docs/lessons/0006-choosing-your-base-class.html b/docs/lessons/0006-choosing-your-base-class.html new file mode 100644 index 000000000..43e863e2c --- /dev/null +++ b/docs/lessons/0006-choosing-your-base-class.html @@ -0,0 +1,185 @@ + + + + + +Lesson 6 · Choosing Your Base Class + + + +
+ +
smithy-java · protocol track · lesson 6
+

Choosing Your Base Class

+

The biggest decision when adding an HTTP protocol is which parent to extend: HttpClientProtocol (RPC) or HttpBindingClientProtocol (REST). Pick right and you inherit the correct half of the work. This lesson makes the choice mechanical.

+ +
+ Goal — Pick RPC vs REST base correctly + Time — ~12 min + Builds on — Lessons 2 & 5 +
+ +

1The fork in the road

+

Both bases implement ClientProtocol<HttpRequest, HttpResponse> and both give you id(), messageExchange(), setServiceEndpoint() for free. They differ in how the request body and members are arranged — which determines how much of createRequest/deserializeResponse you write.

+ +
+
+

HttpClientProtocol → RPC

+

The whole input is one serialized payload. Routing is by operation name (path or header). You write createRequest/deserializeResponse (or extend a family base like AbstractRpcV2ClientProtocol that already did).

+
    +
  • rpcv2Cbor, rpcv2Json
  • +
  • AWS JSON 1.0 / 1.1 (X-Amz-Target)
  • +
  • AWS Query, EC2 Query (Action form param)
  • +
+
+
+

HttpBindingClientProtocol<F> → REST

+

Members scatter across method/path/query/headers/body via @http* traits. The HTTP binding layer does the scattering. You write almost nothing — just declare codec, media type, and error deserializer.

+
    +
  • restJson1
  • +
  • restXml
  • +
+
+
+ +

2What REST gives you for free

+

This is the entire abstract surface of HttpBindingClientProtocol — the binding layer already implemented createRequest and deserializeResponse generically:

+
public abstract class HttpBindingClientProtocol<F extends Frame<?>> extends HttpClientProtocol {
+    abstract protected String payloadMediaType();              // you supply
+    abstract protected HttpErrorDeserializer getErrorDeserializer(Context ctx); // you supply
+    protected boolean omitEmptyPayload() { return false; }   // you may override
+
+    // createRequest + deserializeResponse: ALREADY IMPLEMENTED via HttpBinding
+}
+

So a REST protocol like RestJsonClientProtocol mostly just configures a JsonCodec and an error deserializer. The member-to-HTTP-location mapping (@httpHeader, @httpLabel, …) is handled by HttpBindingSchemaExtensions — the extension you met in Lesson 5.

+

Source: HttpBindingClientProtocol.java

+ +

3What the real protocols extend (verified)

+ + + + + + + + +
ProtocolExtendsFamily
RestJsonClientProtocolHttpBindingClientProtocol<AwsEventFrame>REST
RestXmlClientProtocolHttpBindingClientProtocol<AwsEventFrame>REST
RpcV2CborProtocolAbstractRpcV2ClientProtocolHttpClientProtocolRPC
AwsJsonProtocol (sealed)HttpClientProtocol (permits 1.0 & 1.1)RPC
AwsQueryClientProtocolHttpClientProtocolRPC
Ec2QueryClientProtocolHttpClientProtocolRPC
+

These are the literal class … extends … lines in the repo. Pattern: anything "REST-ish" (URI templates + scattered bindings) extends the binding base; anything "call this operation with this body" extends the plain HTTP base.

+ +
Decision rule. Ask: "Does the Smithy model use @http, @httpLabel, @httpQuery on operations/members?" — Yes → REST → HttpBindingClientProtocol. No; the operation name routes and the whole input is the body → RPC → HttpClientProtocol (and check whether a family base like AbstractRpcV2ClientProtocol already fits, so you write ~45 lines instead of two full methods).
+ +

4The non-HTTP escape hatch

+

Not all protocols are HTTP. ClientProtocol is generic over <RequestT, ResponseT> precisely so you could implement it directly for a different transport — your messageExchange() would return a non-HTTP exchange, and the pipeline would only pair you with a matching transport (recall findProtocolMatchingTransport from Lesson 5). That's a much larger undertaking; for the overwhelming majority of new protocols, one of the two HTTP bases is the right parent.

+ +
+

🔁 Quick check

+ +
+

1. A protocol's Smithy model puts @httpLabel on path members and @httpQuery on others. Which base?

+ + + +
Both are HTTP — the binding traits are the tell. With @httpLabel/@httpQuery you want the binding base so you don't hand-write the scattering.
+
Right. HTTP binding traits ⇒ REST ⇒ HttpBindingClientProtocol. You'd inherit createRequest/deserializeResponse and just supply codec + media type + error deserializer.
+
Way too much work — and unnecessary, since it's a standard HTTP/REST shape.
+
+ +
+

2. What do you typically NOT have to write in a HttpBindingClientProtocol subclass?

+ + + +
That's abstract — you must supply it.
+
Also abstract — you must supply it.
+
Correct. That's the whole point of the binding base: the two big methods are done; you just declare codec, media type, and error handling.
+
+ +
+

3. AWS JSON 1.0 routes by an X-Amz-Target header and sends the full input as the JSON body. Which family, and why?

+ + + +
Right. Using a header for routing isn't the same as @httpHeader member bindings. The body is one payload ⇒ RPC. Indeed AwsJsonProtocol extends HttpClientProtocol.
+
A routing header ≠ HTTP binding traits on members. The input isn't scattered; it's one JSON body. That's RPC.
+
It's plain HTTP — no custom transport needed.
+
+
+
+ +

5What you now know

+

You can look at a protocol's Smithy model and pick its base class in one question (does it use @http* member bindings?), and you know what each base hands you for free. You've now seen every structural piece: the contract, a tiny concrete example, schema-directed serde, codecs, the two SPIs, and base-class selection. Lesson 7 closes the loop with the thing that proves you got it right — protocol compliance tests.

+ +
+

💬 Ask me anything

+

"Why is AwsJsonProtocol sealed with two permits?"
+ • "Show me how AwsQueryClientProtocol builds its form body in createRequest"
+ • "What's the <F extends Frame> type param on the binding base for?" (event streaming)

+

Say "next lesson" for Lesson 7: proving it with compliance tests.

+
+ + +
+ + + diff --git a/docs/lessons/0006-choosing-your-base-class.md b/docs/lessons/0006-choosing-your-base-class.md new file mode 100644 index 000000000..dbf6a91f5 --- /dev/null +++ b/docs/lessons/0006-choosing-your-base-class.md @@ -0,0 +1,119 @@ +# Lesson 6 — Choosing Your Base Class + +> **smithy-java · protocol track · lesson 6** +> **Goal:** Pick RPC (`HttpClientProtocol`) vs REST (`HttpBindingClientProtocol`) correctly. +> **Time:** ~12 min · **Builds on:** Lessons 2 & 5. + +The biggest decision when adding an HTTP protocol is which parent to extend. Pick right and you +inherit the correct half of the work. + +--- + +## 1. The fork in the road + +Both bases implement `ClientProtocol` and both give you `id()`, +`messageExchange()`, `setServiceEndpoint()` for free. They differ in *how the request body and +members are arranged* — which determines how much of `createRequest`/`deserializeResponse` you write. + +| | `HttpClientProtocol` → **RPC** | `HttpBindingClientProtocol` → **REST** | +|---|---|---| +| **Body shape** | Whole input is one serialized payload | Members scatter across method/path/query/headers/body via `@http*` traits | +| **Routing** | By operation name (path or header) | URI templates + bindings | +| **You write** | `createRequest`/`deserializeResponse` (or extend a family base that did) | Almost nothing — codec, media type, error deserializer | +| **Examples** | rpcv2Cbor, AWS JSON 1.0/1.1, AWS Query, EC2 Query | restJson1, restXml | + +--- + +## 2. What REST gives you for free + +This is the entire abstract surface of `HttpBindingClientProtocol` — the binding layer already +implemented `createRequest` and `deserializeResponse` generically: + +```java +public abstract class HttpBindingClientProtocol> extends HttpClientProtocol { + abstract protected String payloadMediaType(); // you supply + abstract protected HttpErrorDeserializer getErrorDeserializer(Context ctx); // you supply + protected boolean omitEmptyPayload() { return false; } // you may override + + // createRequest + deserializeResponse: ALREADY IMPLEMENTED via HttpBinding +} +``` + +So a REST protocol like `RestJsonClientProtocol` mostly configures a `JsonCodec` and an error +deserializer. The member-to-HTTP-location mapping (`@httpHeader`, `@httpLabel`, …) is handled by +`HttpBindingSchemaExtensions` — the extension from Lesson 5. + +> Source: [`HttpBindingClientProtocol.java`](https://github.com/smithy-lang/smithy-java/blob/main/client/client-http-binding/src/main/java/software/amazon/smithy/java/client/http/binding/HttpBindingClientProtocol.java) + +--- + +## 3. What the real protocols extend (verified) + +| Protocol | Extends | Family | +|---|---|---| +| `RestJsonClientProtocol` | `HttpBindingClientProtocol` | REST | +| `RestXmlClientProtocol` | `HttpBindingClientProtocol` | REST | +| `RpcV2CborProtocol` | `AbstractRpcV2ClientProtocol` → `HttpClientProtocol` | RPC | +| `AwsJsonProtocol` (sealed) | `HttpClientProtocol` (permits 1.0 & 1.1) | RPC | +| `AwsQueryClientProtocol` | `HttpClientProtocol` | RPC | +| `Ec2QueryClientProtocol` | `HttpClientProtocol` | RPC | + +These are the literal `class … extends …` lines in the repo. Pattern: anything "REST-ish" (URI +templates + scattered bindings) extends the binding base; anything "call this operation with this +body" extends the plain HTTP base. + +> **Decision rule.** Ask: *"Does the Smithy model use `@http`, `@httpLabel`, `@httpQuery` on +> operations/members?"* — Yes → REST → `HttpBindingClientProtocol`. No; the operation name routes +> and the whole input is the body → RPC → `HttpClientProtocol` (and check whether a family base like +> `AbstractRpcV2ClientProtocol` already fits, so you write ~45 lines instead of two full methods). + +--- + +## 4. The non-HTTP escape hatch + +Not all protocols are HTTP. `ClientProtocol` is generic over `` precisely so +you *could* implement it directly for a different transport — your `messageExchange()` would return +a non-HTTP exchange, and the pipeline would only pair you with a matching transport (recall +`findProtocolMatchingTransport` from Lesson 5). That's a much larger undertaking; for the +overwhelming majority of new protocols, one of the two HTTP bases is the right parent. + +--- + +## 5. Quick check + +**Q1.** A model puts `@httpLabel` on path members and `@httpQuery` on others. Which base? +- a) `HttpClientProtocol` — it's still HTTP. +- b) `HttpBindingClientProtocol` — `@http*` traits mean members scatter; the binding layer handles it. +- c) Implement `ClientProtocol` directly. + +**Q2.** What do you typically NOT write in a `HttpBindingClientProtocol` subclass? +- a) `payloadMediaType()` +- b) `getErrorDeserializer()` +- c) `createRequest()` and `deserializeResponse()` — the binding layer implements them. + +**Q3.** AWS JSON 1.0 routes by `X-Amz-Target` and sends the full input as the JSON body. Which family? +- a) RPC (`HttpClientProtocol`) — operation-name routing + whole-body payload, no `@http*` member bindings. +- b) REST — it uses HTTP headers, so it's binding-based. +- c) Neither — it needs a custom transport. + +
Answers + +- **Q1 → b.** HTTP binding traits ⇒ REST ⇒ `HttpBindingClientProtocol`. You inherit createRequest/deserializeResponse and supply codec + media type + error deserializer. +- **Q2 → c.** That's the whole point of the binding base: the two big methods are done. +- **Q3 → a.** Using *a* header for routing ≠ `@httpHeader` member bindings. The input is one JSON body ⇒ RPC. Indeed `AwsJsonProtocol extends HttpClientProtocol`. + +
+ +--- + +## 6. What you now know + +You can look at a protocol's Smithy model and pick its base class in one question (does it use +`@http*` member bindings?), and you know what each base hands you for free. + +**Next — Lesson 7 (finale):** prove the result with protocol compliance tests, and recap the whole +add-a-protocol flow as a checklist. + +## Reference +- [Glossary](../reference/glossary.html) +- [`HttpBindingClientProtocol.java`](https://github.com/smithy-lang/smithy-java/blob/main/client/client-http-binding/src/main/java/software/amazon/smithy/java/client/http/binding/HttpBindingClientProtocol.java) diff --git a/docs/lessons/0007-proving-it-compliance-tests.html b/docs/lessons/0007-proving-it-compliance-tests.html new file mode 100644 index 000000000..d897b5f5a --- /dev/null +++ b/docs/lessons/0007-proving-it-compliance-tests.html @@ -0,0 +1,189 @@ + + + + + +Lesson 7 · Proving It — Compliance Tests + + + +
+ +
smithy-java · protocol track · lesson 7 · finale
+

Proving It — Compliance Tests

+

A protocol you can't prove correct is a liability. smithy-java's answer is protocol compliance tests: Smithy-defined request/response cases run against your real code. This is the authoritative correctness bar — and the last piece of adding a protocol.

+ +
+ Goal — Prove a protocol & recap the whole flow + Time — ~12 min + Builds on — the whole track +
+ +

1Why compliance tests, not just unit tests

+

The Smithy spec ships @httpRequestTests and @httpResponseTests — declarative cases that say "given this input, the wire bytes must be exactly this" (and vice-versa for responses). The protocol-test-harness runs them against your actual protocol + codec. From the engineering tenets:

+
A passing unit test that fails a compliance test is a bug. Compliance tests are the source of truth because they test the contract the wire format actually promises — not your interpretation of it. Run them before and after any serde/codec/binding change.
+ +

2What a compliance test class looks like

+

Strikingly little code — the harness generates the cases; you just declare the service under test and how to compare. This is the entire CBOR client test:

+
@ProtocolTest(
+    service = "smithy.protocoltests.rpcv2Cbor#RpcV2Protocol",
+    testType = TestType.CLIENT)
+public class RpcV2CborProtocolTests {
+
+    @HttpClientRequestTests
+    @ProtocolTestFilter(skipTests = { "...known-broken-case..." })
+    public void requestTest(DataStream expected, DataStream actual) {
+        CborComparator.assertEquals(expected.asByteBuffer(), actual.asByteBuffer());
+    }
+
+    @HttpClientResponseTests
+    public void responseTest(Runnable test) { test.run(); }
+}
+

Source: RpcV2CborProtocolTests.java · @ProtocolTest is a JUnit 5 extension (ProtocolTest.java).

+ +

Three things make this work, and you supply all three in the module's build.gradle.kts:

+
plugins { id("smithy-java.protocol-testing-conventions") }   // wires the harness + integ tests
+
+testImplementation(libs.smithy.protocol.tests)             // the Smithy-defined test models
+
+// generate test sources from the protocol's test service:
+addGenerateSrcsTask(generator, "rpcv2Cbor", "smithy.protocoltests.rpcv2Cbor#RpcV2Protocol")
+

Source: client-rpcv2-cbor/build.gradle.kts

+ +

3Running them

+
# the authoritative correctness check across all protocols
+./gradlew :protocol-test-harness:test
+
+# your protocol's own compliance suite (it = integration tests)
+./gradlew :client:client-rpcv2-cbor:integ
+

The protocol-testing-conventions plugin even wires test to be finalized by integ, so compliance runs as part of the normal suite. A protocol without a green compliance run is not done.

+ +

4The whole flow, end to end

+

Everything from the track, as the checklist you'd actually follow to add an HTTP protocol:

+
    +
  1. Get the protocol trait. Reference an existing *Trait.ID, or define a new @protocolDefinition trait in a Smithy model. Lesson 5 · the join key
  2. +
  3. Pick your base class. @http* member bindings → HttpBindingClientProtocol (REST); operation-name routing + whole-body → HttpClientProtocol (RPC). Lesson 6
  4. +
  5. Choose a codec. Reuse JsonCodec/Rpcv2CborCodec/XmlCodec via its builder; write a new Codec only if the byte format is genuinely new. Lesson 4
  6. +
  7. Implement the class. Override the handful of abstract methods (codec, media type, error deserializer). Inherit createRequest/deserializeResponse where possible. Lessons 1–2
  8. +
  9. Add a schema extension if needed. Precompute trait-derived wire data as a record; needed when your wire format reads new traits. Lessons 3 & 5
  10. +
  11. Register both SPIs. ClientProtocolFactory (and any SchemaExtensionProvider) in META-INF/services/. The silent-failure trap. Lesson 5
  12. +
  13. Prove it. Add a @ProtocolTest class, wire the gradle conventions, run the compliance suite green. this lesson
  14. +
+ +
+

🔁 Final check

+ +
+

1. Your unit tests pass but a @httpRequestTests case fails. What's the verdict?

+ + + +
No — the compliance case encodes the wire contract. If it fails, your bytes are wrong.
+
Right. "A passing unit test that fails a compliance test is a bug." The wire format is the contract; the compliance suite is the source of truth.
+
They're not independent — the compliance test is the higher authority. A conflict means fix the code.
+
+ +
+

2. How many wire-format assertion cases do you hand-write in a @ProtocolTest class?

+ + + +
No — you don't enumerate operations; the test model already defines the cases.
+
Definitely not per member — that's what the generated cases cover.
+
Correct. You wire the gradle conventions + point at the test service; the harness supplies the cases. Your job is the comparator (e.g. CborComparator.assertEquals).
+
+ +
+

3. Final synthesis: a teammate adds "rpcv2Json." They reuse JsonCodec, extend AbstractRpcV2ClientProtocol, register the factory — but the client can't find it. Most likely cause?

+ + + +
Right — and you diagnosed it from Lesson 5. Discovery is ShapeId-matched over ServiceLoader-registered factories; a bad registration fails silently. Check that file first.
+
Codecs are orthogonal to RPC/REST — JsonCodec is fine for an RPC protocol.
+
Shapes are protocol-agnostic (Lesson 3) — no regeneration needed.
+
+
+
+ +
🏁
+

Track complete

+

You can now trace a call through the contract, read any existing protocol and name its base class, explain why protocols and generated code are decoupled, wire both SPIs, pick the right parent, and prove the result with compliance tests. That's deep architectural mastery — earned through the lens of one concrete task.

+ +
+

Where to go next (beyond this track)

+

Server side: ServerProtocol / ServerProtocolProvider / ProtocolResolver — the mirror image, for accepting requests.
+ • Build a toy protocol end-to-end in a scratch module and watch the compliance suite go green.
+ • Read AwsQueryClientProtocol — the most "different" protocol (form-encoded body, XML response), to test your model.

+
+ +
+

💬 Your teacher is still here

+

"Let's build a toy protocol end-to-end in a scratch module."
+ • "Now teach me the server-side protocol path."
+ • "Quiz me harder — give me a scenario-based design question."

+

Say the word and we'll keep going.

+
+ +
Lesson 7 of the smithy-java protocol track · Glossary · Prev: Lesson 6 · 🏁 Track complete
+
+ + + diff --git a/docs/lessons/0007-proving-it-compliance-tests.md b/docs/lessons/0007-proving-it-compliance-tests.md new file mode 100644 index 000000000..eeb05bafb --- /dev/null +++ b/docs/lessons/0007-proving-it-compliance-tests.md @@ -0,0 +1,141 @@ +# Lesson 7 — Proving It: Compliance Tests (Finale) + +> **smithy-java · protocol track · lesson 7 · finale** +> **Goal:** Prove a protocol with compliance tests, and recap the whole flow. +> **Time:** ~12 min · **Builds on:** the whole track. + +A protocol you can't prove correct is a liability. smithy-java's answer is *protocol compliance +tests*: Smithy-defined request/response cases run against your real code. This is the authoritative +correctness bar — and the last piece of adding a protocol. + +--- + +## 1. Why compliance tests, not just unit tests + +The Smithy spec ships `@httpRequestTests` and `@httpResponseTests` — declarative cases that say +"given this input, the wire bytes must be exactly this" (and vice-versa for responses). The +`protocol-test-harness` runs them against your actual protocol + codec. + +> **A passing unit test that fails a compliance test is a bug.** Compliance tests are the source of +> truth because they test the contract the wire format actually promises — not your interpretation +> of it. Run them before *and* after any serde/codec/binding change. + +--- + +## 2. What a compliance test class looks like + +Strikingly little code — the harness generates the cases; you declare the service under test and how +to compare. This is the entire CBOR client test: + +```java +@ProtocolTest( + service = "smithy.protocoltests.rpcv2Cbor#RpcV2Protocol", + testType = TestType.CLIENT) +public class RpcV2CborProtocolTests { + + @HttpClientRequestTests + @ProtocolTestFilter(skipTests = { "...known-broken-case..." }) + public void requestTest(DataStream expected, DataStream actual) { + CborComparator.assertEquals(expected.asByteBuffer(), actual.asByteBuffer()); + } + + @HttpClientResponseTests + public void responseTest(Runnable test) { test.run(); } +} +``` + +> Source: [`RpcV2CborProtocolTests.java`](https://github.com/smithy-lang/smithy-java/blob/main/client/client-rpcv2-cbor/src/it/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocolTests.java) · `@ProtocolTest` is a JUnit 5 extension ([`ProtocolTest.java`](https://github.com/smithy-lang/smithy-java/blob/main/protocol-test-harness/src/main/java/software/amazon/smithy/java/protocoltests/harness/ProtocolTest.java)). + +Three things make this work; you supply all three in the module's `build.gradle.kts`: + +```kotlin +plugins { id("smithy-java.protocol-testing-conventions") } // wires the harness + integ tests + +testImplementation(libs.smithy.protocol.tests) // the Smithy-defined test models + +// generate test sources from the protocol's test service: +addGenerateSrcsTask(generator, "rpcv2Cbor", "smithy.protocoltests.rpcv2Cbor#RpcV2Protocol") +``` + +> Source: [`client-rpcv2-cbor/build.gradle.kts`](https://github.com/smithy-lang/smithy-java/blob/main/client/client-rpcv2-cbor/build.gradle.kts) + +--- + +## 3. Running them + +```bash +# the authoritative correctness check across all protocols +./gradlew :protocol-test-harness:test + +# your protocol's own compliance suite (it = integration tests) +./gradlew :client:client-rpcv2-cbor:integ +``` + +The `protocol-testing-conventions` plugin even wires `test` to be finalized by `integ`, so +compliance runs as part of the normal suite. A protocol without a green compliance run is not done. + +--- + +## 4. The whole flow, end to end + +Everything from the track, as the checklist to add an HTTP protocol: + +1. **Get the protocol trait.** Reference an existing `*Trait.ID`, or define a new + `@protocolDefinition` trait in a Smithy model. *(Lesson 5 — the join key)* +2. **Pick your base class.** `@http*` member bindings → `HttpBindingClientProtocol` (REST); + operation-name routing + whole-body → `HttpClientProtocol` (RPC). *(Lesson 6)* +3. **Choose a codec.** Reuse `JsonCodec`/`Rpcv2CborCodec`/`XmlCodec` via its builder; write a new + `Codec` only if the byte format is genuinely new. *(Lesson 4)* +4. **Implement the class.** Override the handful of abstract methods (codec, media type, error + deserializer). Inherit `createRequest`/`deserializeResponse` where possible. *(Lessons 1–2)* +5. **Add a schema extension if needed.** Precompute trait-derived wire data as a record; needed when + your wire format reads new traits. *(Lessons 3 & 5)* +6. **Register both SPIs.** `ClientProtocolFactory` (and any `SchemaExtensionProvider`) in + `META-INF/services/`. The silent-failure trap. *(Lesson 5)* +7. **Prove it.** Add a `@ProtocolTest` class, wire the gradle conventions, run the compliance suite + green. *(this lesson)* + +--- + +## 5. Final check + +**Q1.** Unit tests pass but a `@httpRequestTests` case fails. Verdict? +- a) The compliance test is too strict; ship it. +- b) It's a bug in your protocol — compliance tests are the authoritative correctness bar. +- c) Unit and compliance tests are independent; ignore the conflict. + +**Q2.** How many wire-format assertion cases do you hand-write in a `@ProtocolTest` class? +- a) One per operation. +- b) One per member. +- c) None — the harness generates cases from the Smithy test model; you declare the service and a comparison method. + +**Q3.** A teammate adds "rpcv2Json": reuses `JsonCodec`, extends `AbstractRpcV2ClientProtocol`, registers the factory — but the client can't find it. Most likely cause? +- a) The `META-INF/services/...ClientProtocolFactory` entry is missing or names the wrong class (forgot `$Factory`). +- b) `JsonCodec` can't be used by an RPC protocol. +- c) They need to regenerate every shape for the new protocol. + +
Answers + +- **Q1 → b.** "A passing unit test that fails a compliance test is a bug." The wire format is the contract; the compliance suite is the source of truth. +- **Q2 → c.** You wire the gradle conventions + point at the test service; the harness supplies the cases. Your job is the comparator (e.g. `CborComparator.assertEquals`). +- **Q3 → a.** Discovery is ShapeId-matched over `ServiceLoader`-registered factories; a bad registration fails silently. Codecs are orthogonal to RPC/REST, and shapes are protocol-agnostic — neither (b) nor (c) applies. + +
+ +--- + +## 🏁 Track complete + +You can now trace a call through the contract, read any existing protocol and name its base class, +explain why protocols and generated code are decoupled, wire both SPIs, pick the right parent, and +prove the result with compliance tests. That's deep architectural mastery — earned through the lens +of one concrete task. + +### Where to go next (beyond this track) +- **Server side:** `ServerProtocol` / `ServerProtocolProvider` / `ProtocolResolver` — the mirror image, for accepting requests. +- **Build a toy protocol end-to-end** in a scratch module and watch the compliance suite go green. +- **Read `AwsQueryClientProtocol`** — the most "different" protocol (form-encoded body, XML response), to test your model. + +## Reference +- [Glossary](../reference/glossary.html) +- [`RpcV2CborProtocolTests.java`](https://github.com/smithy-lang/smithy-java/blob/main/client/client-rpcv2-cbor/src/it/java/software/amazon/smithy/java/client/rpcv2/RpcV2CborProtocolTests.java) · [`ProtocolTest.java`](https://github.com/smithy-lang/smithy-java/blob/main/protocol-test-harness/src/main/java/software/amazon/smithy/java/protocoltests/harness/ProtocolTest.java) diff --git a/docs/lessons/index.html b/docs/lessons/index.html new file mode 100644 index 000000000..2aefae903 --- /dev/null +++ b/docs/lessons/index.html @@ -0,0 +1,54 @@ + + + + + +smithy-java · Protocol Track + + + +
+
smithy-java · onboarding
+

How to Add a New Protocol

+

A seven-lesson track that teaches the smithy-java client architecture through one concrete task: adding a protocol. Each lesson is short, interactive, and grounded in real code.

+ +
Mission: deep architectural mastery. "Adding a protocol" touches every layer — contract, serde, codecs, SPI discovery, base classes, tests — so mastering it means understanding how the whole framework fits together.
+ +
    +
  1. foundations

    The ClientProtocol Contract

    One interface, six methods. What a protocol is, and a traced call through it.

  2. +
  3. worked example

    The Smallest Real Protocol

    RpcV2CborProtocol in ~45 lines — the contract satisfied by inheritance.

  4. +
  5. core idea

    Schema-Directed Serde

    Why protocols and generated code never know about each other. The Schema is the contract.

  6. +
  7. layer

    The Codec Layer

    What a Codec is, the three that ship, and when to reuse vs write one.

  8. +
  9. SPI

    Extensions & Discovery

    Precompute wire data; register via ServiceLoader so the framework finds you.

  10. +
  11. decision

    Choosing Your Base Class

    RPC vs REST: HttpClientProtocol vs HttpBindingClientProtocol, made mechanical.

  12. +
  13. 🏁 finale

    Proving It — Compliance Tests

    The authoritative correctness bar, plus the whole add-a-protocol checklist.

  14. +
+ + +
+ + diff --git a/docs/media/lesson-01-poster.png b/docs/media/lesson-01-poster.png new file mode 100644 index 000000000..161955229 Binary files /dev/null and b/docs/media/lesson-01-poster.png differ diff --git a/docs/media/lesson-01-walkthrough.gif b/docs/media/lesson-01-walkthrough.gif new file mode 100644 index 000000000..6788218ea Binary files /dev/null and b/docs/media/lesson-01-walkthrough.gif differ diff --git a/docs/media/lesson-01-walkthrough.mp4 b/docs/media/lesson-01-walkthrough.mp4 new file mode 100644 index 000000000..10d001450 Binary files /dev/null and b/docs/media/lesson-01-walkthrough.mp4 differ diff --git a/docs/media/protocol-track-overview.gif b/docs/media/protocol-track-overview.gif new file mode 100644 index 000000000..b78200cae Binary files /dev/null and b/docs/media/protocol-track-overview.gif differ diff --git a/docs/media/protocol-track-poster.png b/docs/media/protocol-track-poster.png new file mode 100644 index 000000000..d3cadb5f2 Binary files /dev/null and b/docs/media/protocol-track-poster.png differ diff --git a/docs/media/protocol-track-walkthrough.mp4 b/docs/media/protocol-track-walkthrough.mp4 new file mode 100644 index 000000000..cf166e544 Binary files /dev/null and b/docs/media/protocol-track-walkthrough.mp4 differ diff --git a/docs/reference/glossary.html b/docs/reference/glossary.html new file mode 100644 index 000000000..5fad0182f --- /dev/null +++ b/docs/reference/glossary.html @@ -0,0 +1,160 @@ + + + + + +Glossary — Protocols in smithy-java + + + +
+
+
smithy-java · reference
+

Glossary: Protocols & Serde

+

The shared vocabulary for adding a new protocol. Every lesson uses these terms exactly as defined here.

+
+ +

The protocol contract

+
+
+
ClientProtocol<RequestT, ResponseT> core interface
+
The interface every client protocol implements. It turns an operation + input shape into a transport request, and a transport response back into an output shape. Parameterized by the request/response types it works with (e.g. HttpRequest/HttpResponse). + client/client-core/.../client/core/ClientProtocol.java
+
+
+
ClientProtocolFactory<T extends Trait> SPI entry point
+
Creates a ClientProtocol from a Smithy protocol trait. This is the class registered with ServiceLoader. Its id() is the protocol trait's ShapeId; its createProtocol(settings, trait) builds the instance. + client/client-core/.../client/core/ClientProtocolFactory.java
+
+
+
ProtocolSettings
+
The bundle of info handed to a factory at construction time: the service ShapeId, optional service version (AWS Query needs it), and the service Schema (carries service-level traits like xmlNamespace). + client/client-core/.../client/core/ProtocolSettings.java
+
+
+
MessageExchange<RequestT, ResponseT>
+
A pure marker interface naming the request/response pair a protocol and a transport speak. The pipeline checks that a protocol's exchange matches the transport's before wiring them together. For HTTP it's HttpMessageExchange.INSTANCE. + client/client-core/.../client/core/MessageExchange.java
+
+
+ +

Serialization core

+
+
+
Schema runtime shape
+
A trimmed-down runtime representation of a Smithy shape — just enough to drive serde. It is the first argument to every serde method. Protocols read schemas (and their precomputed extensions) to decide wire format; they never inspect raw traits on the hot path. + core/.../core/schema/Schema.java
+
+
+
Codec
+
The payload encoder/decoder a protocol delegates to. Produces a ShapeSerializer (struct → bytes) and a ShapeDeserializer (bytes → struct). Examples: JsonCodec, Rpcv2CborCodec, XmlCodec. A protocol picks a codec; it rarely writes one. + core/.../core/serde/Codec.java
+
+
+
ShapeSerializer / ShapeDeserializer
+
The visitor interfaces a codec implements. Generated shapes "push" their members into a ShapeSerializer (writeString(schema, value), …). Deserialization is a callback: the codec reads bytes, resolves a member schema, and calls back by memberIndex. + core/.../core/serde/
+
+
+
Schema-directed serde key idea
+
The central design move: generated code and protocols never know about each other. The Schema is the contract between them. Same serializeMembers() works for JSON, CBOR, or XML — switching protocols needs no regeneration.
+
+
+
SchemaExtension / SchemaExtensionProvider
+
Protocol-specific data precomputed onto a schema once (UTF-8 field-name byte tables, timestamp formatters, HTTP binding locations). Accessed via a SchemaExtensionKey at O(1). This is why no trait lookups happen during serialization. + core/.../core/schema/ (e.g. SmithyJsonSchemaExtensions, CborSchemaExtensions, HttpBindingSchemaExtensions)
+
+
+ +

Protocol families

+
+
+
HttpClientProtocol RPC base
+
Abstract base for HTTP protocols where the protocol builds the request directly (no @http* binding traits). Used by RPC-style protocols (rpcv2Cbor, AWS JSON). Supplies id(), messageExchange(), and setServiceEndpoint(). + client/client-http/.../client/http/HttpClientProtocol.java
+
+
+
HttpBindingClientProtocol REST base
+
Abstract base for REST-style protocols that map @httpHeader, @httpQuery, @httpLabel, @httpPayload to the wire automatically via the HTTP binding layer. Used by restJson1, restXml. + client/client-http-binding/.../client/http/binding/HttpBindingClientProtocol.java
+
+
+
RPC vs REST
+
RPC: operation name routes the call (path or header), whole body is one serialized payload. REST: HTTP binding traits scatter members across method/path/query/headers/body. The base class you extend is the single biggest decision when adding a protocol.
+
+
+ +

Discovery & testing

+
+
+
ServiceLoader / META-INF/services discovery
+
The universal extension mechanism. A protocol is "found" only if its Factory is listed in META-INF/services/software.amazon.smithy.java.client.core.ClientProtocolFactory. Missing line = feature silently absent.
+
+
+
Protocol trait
+
A Smithy trait (e.g. smithy.protocols#rpcv2Cbor, aws.protocols#restJson1) applied to a service shape, marked with @protocolDefinition. Its ShapeId is the join key between the model and the ClientProtocolFactory.
+
+
+
Protocol compliance tests authoritative
+
Smithy-defined @httpRequestTests / @httpResponseTests run against real client/server code by the protocol-test-harness. A unit test that passes but a compliance test that fails is a bug. This is how correctness is proven. + protocol-test-harness/ · annotated via @ProtocolTest, @HttpClientRequestTests
+
+
+ +
+ Reading paths: ... stands for the package prefix + src/main/java/software/amazon/smithy/java. Full source lives at + github.com/smithy-lang/smithy-java. +
+ + +
+ +