diff --git a/README.md b/README.md index 7a33203c..c8e67d7e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,11 @@ Smithy code generators for C++ — generate idiomatic C++ clients and servers from [Smithy](https://smithy.io) models, plus the shared C++ runtime they build on. +**Start here → [docs/quickstart.md](docs/quickstart.md):** empty directory to a generated C++ +client integration-testing a generated C++ server, in one Bazel module — no prior Smithy +experience assumed. Day 2 (evolving the model) is +[docs/model-evolution.md](docs/model-evolution.md). + - **Vendor-neutral:** implements Smithy and its protocol specs; nothing AWS-specific. The REST protocol is [`alloy#simpleRestJson`](https://disneystreaming.github.io/smithy4s/docs/protocols/simple-rest-json/overview/) (the neutral protocol smithy4s uses — so smithy-cpp clients and smithy4s services interoperate). @@ -11,22 +16,40 @@ Smithy code generators for C++ — generate idiomatic C++ clients and servers fr - **Bazel-native:** Bazel 9 is the sole supported build system for the repo and consumers. - **Client tests server:** generated clients integration-test generated servers in CI. -See [`docs/PLAN.md`](docs/PLAN.md) for the full phased plan and -[`docs/adr/`](docs/adr/) for architecture decisions. - -## Status - -| Phase | Scope | Status | -|---|---|---| -| 0 | Foundations: Bazel 9 workspace, codegen skeleton, CI, ADRs, fixture models | ✅ Done | -| 1 | C++ runtime library (`smithy/core`, `json`, `cbor`, `http`, `client`, `server`) | ✅ Done ([docs/runtime.md](docs/runtime.md)) incl. Boost.Beast production server transport (ADR-0006) | -| 2 | Codegen plugin + type generation | ✅ Done — types generated for both fixtures, golden+compile+behavior tested ([docs/design/codegen-architecture.md](docs/design/codegen-architecture.md)) | -| 3 | Client generation (simpleRestJson + rpcv2Cbor) | ✅ Done — serde + clients for both protocols with typed errors ([docs/generated-types.md](docs/generated-types.md)); official conformance suites green (documented exclusions) | -| 4 | Server generation (simpleRestJson + rpcv2Cbor) | ✅ Done — handlers, routing, serde, all HTTP bindings incl. `@httpPayload`/`@httpPrefixHeaders`, constraint validation, parser strictness, content negotiation; official conformance suites green ([docs/server-guide.md](docs/server-guide.md)) | -| 5 | Generated-client ↔ generated-server integration harness | ✅ Done — every fixture ships a generated integration suite: seeded random round-trips over loopback and real sockets, per-error mapping, unknown-member tolerance, mutation-checked ([docs/design/integration-testing.md](docs/design/integration-testing.md)) | -| 6 | Bazel rules, CLI, packaging (BCR + Maven Central), docs site | 🔨 In progress — `smithy_cpp_{types,client,server}_library` rules run the generator hermetically inside the build graph, out-of-tree consumer module tested in CI, CLI via `bazel run //codegen:generator` ([docs/quickstart.md](docs/quickstart.md)); model-evolution loop documented and exercised in CI ([docs/model-evolution.md](docs/model-evolution.md)); BCR/Maven publishing deferred until production validation; docs site pending | -| 7 | Hardening, fuzzing, v0.1.0 | ✅ Done — retries with full-jitter exponential backoff, gzip `@requestCompression` (client + server), client interceptors + server middleware (auth/logging/metrics seams), `@httpBearerAuth`/`@httpApiKeyAuth` credential wiring, generated `@paginated` paginators, Beast graceful drain + header limits, consumer CI across linux/macos/windows ([docs/production-guide.md](docs/production-guide.md)); **BeastHttpClient** production client transport with TLS both directions (ADR-0007); Google Benchmark suite (informational CI job); release engineering — [CHANGELOG](CHANGELOG.md), [versioning/compatibility policy](docs/versioning.md), **v0.1.0**; **protocol realignment to vendor-neutral** — dropped AWS `restJson1` for `alloy#simpleRestJson` + its conformance suite, Smithy 1.58, no `aws.*` on the generator classpath; **`smithy.cpp.protocols#jsonRpc2`** — JSON-RPC 2.0 over a single POST endpoint with an authored conformance suite, a calculator example with hand-rolled-peer interop tests, and a consumer overlay | -| 8 | Bidirectional streaming (event streams, WebSockets) | Not started | +## What works today (v0.1.0) + +- **Clients and servers for all three protocols**, each green against a conformance suite in CI + (the official alloy and rpcv2Cbor suites, an authored one for jsonRpc2) with documented, + must-shrink exclusion lists. +- **The full generated surface:** typed structs/unions/enums, serde, HTTP bindings, constraint + validation with suite-exact `ValidationException` output (ReDoS-safe `@pattern`), typed + modeled errors, paginators, idempotency tokens, and gzip request compression. +- **In-graph generation:** `smithy_cpp_{types,client,server}_library` run the generator + hermetically inside the Bazel build graph — no scripts, no JVM to install. An out-of-tree + consumer module is CI-tested on Linux/macOS/Windows, plus a CLI for generating elsewhere. +- **Production serving and calling** over Boost.Beast with TLS in both directions, retries with + jittered backoff, client interceptors, server middleware, and bearer/API-key auth wiring. +- **Hardening in CI:** sanitizer jobs, libFuzzer harnesses, hostile-input test banks, and every + fixture's generated client integration-testing its generated server. + +Not yet: bidirectional streaming (event streams/WebSockets) and Bazel Central Registry / Maven +publishing — consumers pin a git commit for now. Roadmap and per-phase status live in +[`docs/PLAN.md`](docs/PLAN.md). + +## Documentation + +| Doc | What it covers | +|---|---| +| [quickstart.md](docs/quickstart.md) | Model → generated client + server, from an empty directory | +| [model-evolution.md](docs/model-evolution.md) | Day 2: changing the model, regeneration, drift detection | +| [generated-types.md](docs/generated-types.md) | The Smithy → C++ mapping contract | +| [server-guide.md](docs/server-guide.md) | What the generated server does before/after your handler | +| [production-guide.md](docs/production-guide.md) | Real transports, TLS, retries, auth, middleware | +| [runtime.md](docs/runtime.md) | The `smithy-cpp-runtime` library, module by module | +| [development.md](docs/development.md) | Building, testing, and linting this repo | +| [versioning.md](docs/versioning.md) | Compatibility policy; [CHANGELOG.md](CHANGELOG.md) has releases | +| [PLAN.md](docs/PLAN.md) | The phased roadmap; [adr/](docs/adr) records architecture decisions | +| [design/](docs/design) | Internals: codegen architecture, the integration-test harness; [fuzzing.md](docs/fuzzing.md) covers the fuzz setup | ## Building diff --git a/docs/quickstart.md b/docs/quickstart.md index 81941702..840cd5db 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,9 +1,44 @@ # Quick start: model → client + server in one Bazel module -From an empty directory to a generated C++ client integration-testing a generated C++ server, -without touching the generator's internals. The finished result of every step below lives at +From an empty directory to a generated C++ client integration-testing a generated C++ server — +no prior [Smithy](https://smithy.io) experience assumed, and no generator internals to learn. + +If Smithy is new to you: it's an interface-definition language. You describe a service once — +its operations, their inputs and outputs, the errors they can raise — in a small `.smithy` +text file, and code generators produce the client, the server scaffolding, and the wire +handling in whatever language you need. smithy-cpp is that generator for C++. You write the +model and the business logic; parsing, routing, validation, serialization, and error mapping +are generated. + +The finished result of every step below lives at [`examples/bazel-consumer/`](../examples/bazel-consumer) — CI builds that module standalone on -every commit, so this tutorial cannot silently rot. +every commit, so this tutorial cannot silently rot. How the pieces fit: + +```mermaid +flowchart LR + subgraph model["you write: the model"] + M["model/todo.smithy
service + operations + error"] + B["model/bindings/simplerestjson.smithy
picks the wire protocol"] + end + subgraph gen["generated inside the build graph"] + C[":todo_client
TodoClient, typed inputs/outputs, serde"] + S[":todo_server
TodoServer: routing, parsing,
validation + TodoHandler interface"] + end + subgraph cpp["you write: the C++"] + H["MyHandler
implements TodoHandler"] + T["todo_integration_test.cc
client drives server end to end"] + end + M --> C + B --> C + M --> S + B --> S + S -. "pure-virtual
interface" .-> H + C --> T + H --> T +``` + +The integration test then drives the loop end to end: `TodoClient` → HTTP (in-memory loopback +or a real socket) → `TodoServer` → your `MyHandler` → back out as a typed response. ## 1. Create a Bazel module @@ -26,7 +61,8 @@ bazel_dep(name = "googletest", version = "1.17.0.bcr.2") bazel_dep(name = "rules_cc", version = "0.2.17") ``` -`.bazelrc` (C++20 is the runtime baseline; the generator runs on a hermetic Java 17 toolchain): +`.bazelrc` (C++20 is the runtime baseline; the generator runs on a hermetic Java 17 toolchain, +so you never install or invoke Java yourself): ``` common --enable_platform_specific_config @@ -39,11 +75,102 @@ common --java_runtime_version=remotejdk_17 common --tool_java_runtime_version=remotejdk_17 ``` -## 2. Write a model +## 2. Write the model -`model/todo.smithy` — a service, an operation or two, a modeled error. Keep the model -protocol-agnostic, the upstream Smithy way: `@http` traits describe HTTP semantics without -picking a wire protocol. Bind a concrete protocol in a small overlay file with `apply`: +`model/todo.smithy` — a deliberately small task tracker: add a task, fetch it back, and one +thing that can go wrong. This is the entire file: + +```smithy +$version: "2.0" + +namespace acme.todo + +/// A tiny task tracker: create a task, fetch it back. +service Todo { + version: "2026-01-01" + operations: [AddTask, GetTask] +} + +@http(method: "POST", uri: "/tasks") +operation AddTask { + input := { + @required + @length(min: 1, max: 256) + title: String + } + + output := { + @required + taskId: String + + @required + title: String + } +} + +@readonly +@http(method: "GET", uri: "/tasks/{taskId}") +operation GetTask { + input := { + @required + @httpLabel + taskId: String + } + + output := { + @required + taskId: String + + @required + title: String + + done: Boolean + } + + errors: [NoSuchTask] +} + +@error("client") +@httpError(404) +structure NoSuchTask { + @required + message: String +} +``` + +Reading it as a Smithy newcomer: + +- **`namespace acme.todo`** scopes every name in the file; `acme.todo#Todo` is the service's + full identity (you'll pass it to the build rule in step 3). +- **`service Todo`** is the entry point: it lists the operations clients can call. It becomes + `TodoClient`, the pure-virtual `TodoHandler` interface, and `TodoServer` in C++. +- **`operation AddTask`** declares one callable action. The `input :=` / `output :=` blocks + are inline structure definitions — each becomes a plain C++ struct (`AddTaskInput`, + `AddTaskOutput`), and the operation becomes a method on both the client and the handler. +- **The `@...` annotations are traits** — metadata attached to shapes and members. They do all + the heavy lifting: + - `@required` makes a member mandatory; non-required members map to `std::optional` in + C++ and to "absent on the wire" in JSON. + - `@length(min: 1, max: 256)` is a constraint: the generated *server* rejects violations + with a 400 `ValidationException` before your handler ever runs. (`@pattern`, `@range`, + and friends work the same way.) + - `@http` / `@httpLabel` describe HTTP semantics: `AddTask` is `POST /tasks` with the input + as the JSON body; `GetTask` binds `taskId` into the path as `GET /tasks/{taskId}`. + - `@error("client")` + `@httpError(404)` make `NoSuchTask` a modeled error: the server maps + it to a 404, and the client surfaces it as a *typed* value, not a string. +- **`errors: [NoSuchTask]`** declares which errors an operation can raise, so both sides know + the full contract. + +On the wire, that model means: + +| Call | Request | Success | Error | +|---|---|---|---| +| `AddTask` | `POST /tasks` `{"title": "buy milk"}` | `{"taskId": "task-1", "title": "buy milk"}` | 400 `ValidationException` (e.g. empty title) | +| `GetTask` | `GET /tasks/task-1` | `{"taskId": "task-1", "title": "buy milk", "done": false}` | 404 `NoSuchTask` | + +Notice the model never names a wire protocol — `@http` describes *HTTP semantics*, not an +encoding. That's deliberate (and the upstream-Smithy way): the concrete protocol is bound in a +tiny overlay file, using `apply` to attach a trait to the service from outside the base model: ```smithy // model/bindings/simplerestjson.smithy @@ -53,7 +180,9 @@ use alloy#simpleRestJson apply Todo @simpleRestJson ``` -(Applying the trait directly on the service works too, if you only ever want one protocol.) +(Applying the trait directly on the service works too, if you only ever want one protocol. +Keeping it in an overlay lets the same model also serve rpcv2Cbor or jsonRpc2 — the consumer +example binds all three side by side.) ## 3. Declare the generated libraries @@ -94,22 +223,56 @@ done. (`smithy_cpp_types_library` exists too, for data types without a protocol. ## 4. Implement the handler and test it with the generated client -The server library gives you a pure-virtual `TodoHandler` and a `TodoServer`; the client library -a `TodoClient`. Wire them together over the in-memory loopback (or a real socket) exactly like +The server library gives you a pure-virtual `TodoHandler`; implementing it is the only place +business logic lives. One method per operation, typed input to typed `Outcome` (a value or an +error — no exceptions): + +```cpp +class InMemoryHandler final : public TodoHandler { + public: + smithy::Outcome AddTask(const AddTaskInput& input) override { + const std::string id = "task-" + std::to_string(next_id_++); + titles_[id] = input.title; + return AddTaskOutput{.taskId = id, .title = input.title}; + } + + smithy::Outcome GetTask(const GetTaskInput& input) override { + const auto it = titles_.find(input.taskId); + if (it == titles_.end()) { + smithy::Error error = smithy::Error::Modeled("NoSuchTask", "no task: " + input.taskId); + error.set_detail(NoSuchTask{.message = "no task: " + input.taskId}); + return error; // the server turns this into the modeled 404 + } + return GetTaskOutput{.taskId = input.taskId, .title = it->second, .done = false}; + } + + private: + int next_id_ = 1; + std::map titles_; +}; +``` + +Then wire the generated server to the generated client over the in-memory loopback (or a real +socket) exactly like [`todo_integration_test.cc`](../examples/bazel-consumer/todo_integration_test.cc): ```cpp -TodoServer server(std::make_shared()); +TodoServer server(std::make_shared()); auto loopback = std::make_shared(); (void)loopback->Start(server.Handler()); smithy::ClientConfig config; config.http_client = loopback; auto client = *TodoClient::Create(std::move(config)); + +auto added = client.AddTask(AddTaskInput{.title = "buy milk"}); // Outcome +auto missing = client.GetTask(GetTaskInput{.taskId = "nope"}); +// missing.error().detail() is the typed 404 from the handler. ``` -Routing, serde, constraint validation (400 `ValidationException` before your handler runs), -content negotiation, and modeled-error mapping are all generated — see -[server-guide.md](server-guide.md) for what the server does on your behalf. +Everything between the client call and your handler — routing, JSON parsing, constraint +validation (400 `ValidationException` before your handler runs), content negotiation, and +modeled-error mapping — is generated; see [server-guide.md](server-guide.md) for what the +server does on your behalf. ## 5. Run it @@ -139,3 +302,12 @@ Once the integration is running, the model keeps changing — new fields, new op tightened constraints. [model-evolution.md](model-evolution.md) covers that loop: how edits propagate through the build graph (or through regeneration for vendored output), how to review generated diffs, and how CI catches drift and unimplemented handler methods. + +## Where to go next + +- [generated-types.md](generated-types.md) — the full Smithy → C++ mapping contract. +- [server-guide.md](server-guide.md) — everything the generated server does before and after + your handler. +- [production-guide.md](production-guide.md) — real transports, TLS, retries, middleware. +- [Smithy's own docs](https://smithy.io/2.0/quickstart.html) — the language beyond what this + tutorial uses.