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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 39 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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

Expand Down
202 changes: 187 additions & 15 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
@@ -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<br/>service + operations + error"]
B["model/bindings/simplerestjson.smithy<br/>picks the wire protocol"]
end
subgraph gen["generated inside the build graph"]
C[":todo_client<br/>TodoClient, typed inputs/outputs, serde"]
S[":todo_server<br/>TodoServer: routing, parsing,<br/>validation + TodoHandler interface"]
end
subgraph cpp["you write: the C++"]
H["MyHandler<br/>implements TodoHandler"]
T["todo_integration_test.cc<br/>client drives server end to end"]
end
M --> C
B --> C
M --> S
B --> S
S -. "pure-virtual<br/>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

Expand All @@ -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
Expand All @@ -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<T>` 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
Expand All @@ -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

Expand Down Expand Up @@ -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<AddTaskOutput> 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<GetTaskOutput> 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<std::string, std::string> 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<MyHandler>());
TodoServer server(std::make_shared<InMemoryHandler>());
auto loopback = std::make_shared<smithy::http::Loopback>();
(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<AddTaskOutput>
auto missing = client.GetTask(GetTaskInput{.taskId = "nope"});
// missing.error().detail<NoSuchTask>() 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

Expand Down Expand Up @@ -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.
Loading