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
32 changes: 29 additions & 3 deletions bazel/tests/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,27 +1,53 @@
load("@rules_cc//cc:cc_test.bzl", "cc_test")
load("//bazel:defs.bzl", "smithy_cpp_client_library", "smithy_cpp_server_library", "smithy_cpp_types_library")
load(":defs_test.bzl", "defs_test_suite")

# Rule instances the analysis tests inspect; building them is also an in-tree
# compile test of rule-generated code.
smithy_cpp_client_library(
name = "greeter_client",
srcs = ["greeter.smithy"],
srcs = [
"greeter.smithy",
"greeter_restjson1.smithy",
],
namespace = "smithy::cpp::ruletest",
service = "smithy.cpp.ruletest#Greeter",
)

smithy_cpp_server_library(
name = "greeter_server",
srcs = ["greeter.smithy"],
srcs = [
"greeter.smithy",
"greeter_restjson1.smithy",
],
namespace = "smithy::cpp::ruletest",
service = "smithy.cpp.ruletest#Greeter",
)

smithy_cpp_types_library(
name = "greeter_types",
srcs = ["greeter.smithy"],
srcs = [
"greeter.smithy",
"greeter_restjson1.smithy",
],
namespace = "smithy::cpp::ruletest",
service = "smithy.cpp.ruletest#Greeter",
)

# Functional check that a model assembled from multiple files (base +
# protocol overlay) generates working code: the generated client round-trips
# against the generated server in-process.
cc_test(
name = "greeter_roundtrip_test",
size = "small",
srcs = ["greeter_roundtrip_test.cc"],
deps = [
":greeter_client",
":greeter_server",
"//runtime:client",
"//runtime:http",
"@googletest//:gtest_main",
],
)

defs_test_suite(name = "defs_tests")
9 changes: 9 additions & 0 deletions bazel/tests/defs_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ def _client_outputs_impl(env, target):
"false",
])

# Multi-file models: every srcs entry becomes its own --model flag (the
# base model plus the `apply` protocol overlay assemble into one model).
action.argv().contains_at_least([
"--model",
"bazel/tests/greeter.smithy",
"--model",
"bazel/tests/greeter_restjson1.smithy",
])

def _server_outputs_impl(env, target):
env.expect.that_collection(_files(target)).contains_exactly(_prefixed(
"greeter_server_smithy_gen",
Expand Down
7 changes: 3 additions & 4 deletions bazel/tests/greeter.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ $version: "2.0"

namespace smithy.cpp.ruletest

use aws.protocols#restJson1

/// Minimal service the rule analysis tests generate against.
@restJson1
/// Minimal service the rule analysis tests generate against. Deliberately
/// protocol-agnostic: greeter_restjson1.smithy binds the protocol with
/// `apply`, so every rule test also exercises multi-file model assembly.
service Greeter {
version: "2026-01-01"
operations: [Greet]
Expand Down
9 changes: 9 additions & 0 deletions bazel/tests/greeter_restjson1.smithy
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Protocol binding overlay for greeter.smithy: the base model stays
// protocol-agnostic; passing both files to a rule binds restJson1.
$version: "2.0"

namespace smithy.cpp.ruletest

use aws.protocols#restJson1

apply Greeter @restJson1
41 changes: 41 additions & 0 deletions bazel/tests/greeter_roundtrip_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// The multi-file model check: greeter.smithy (protocol-agnostic) plus the
// greeter_restjson1.smithy `apply` overlay assemble into one model inside the
// build graph, and the generated client and server actually work together.

#include <gtest/gtest.h>

#include <memory>
#include <utility>

#include "smithy/client/config.h"
#include "smithy/cpp/ruletest/client.h"
#include "smithy/cpp/ruletest/server.h"
#include "smithy/http/loopback.h"

namespace smithy::cpp::ruletest {
namespace {

class Handler final : public GreeterHandler {
public:
smithy::Outcome<GreetOutput> Greet(const GreetInput& input) override {
return GreetOutput{.greeting = "hello, " + input.name};
}
};

TEST(GreeterMultiModelTest, OverlayBoundServiceRoundTrips) {
GreeterServer server(std::make_shared<Handler>());
auto loopback = std::make_shared<smithy::http::Loopback>();
ASSERT_TRUE(loopback->Start(server.Handler()).ok());

smithy::ClientConfig config;
config.http_client = loopback;
auto client = GreeterClient::Create(std::move(config));
ASSERT_TRUE(client.ok()) << client.error().message();

const auto greeting = client->Greet(GreetInput{.name = "smithy"});
ASSERT_TRUE(greeting.ok()) << greeting.error().message();
EXPECT_EQ(greeting->greeting, "hello, smithy");
}

} // namespace
} // namespace smithy::cpp::ruletest
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public final class CppCodegenRunner {
private CppCodegenRunner() {}

public static void main(String[] args) {
String modelPath = null;
List<String> modelPaths = new ArrayList<>();
String service = null;
String namespace = null;
String runtimeTarget = "@smithy_cpp//runtime:core";
Expand All @@ -47,7 +47,7 @@ public static void main(String[] args) {
List<String> omitOperations = new ArrayList<>();
for (int i = 0; i + 1 < args.length; i += 2) {
switch (args[i]) {
case "--model" -> modelPath = args[i + 1];
case "--model" -> modelPaths.add(args[i + 1]);
case "--service" -> service = args[i + 1];
case "--namespace" -> namespace = args[i + 1];
case "--runtime-target" -> runtimeTarget = args[i + 1];
Expand All @@ -67,7 +67,7 @@ public static void main(String[] args) {
}

var assembler = Model.assembler().discoverModels(CppCodegenRunner.class.getClassLoader());
if (modelPath != null) {
for (String modelPath : modelPaths) {
assembler.addImport(Paths.get(modelPath));
}
Model model = assembler.assemble().unwrap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1406,6 +1406,6 @@ public void writeServerRoute(
w.write("auto outcome = handler->$L(*input);", opName);
w.write("if (!outcome) return ErrorToResponse(outcome.error());");
w.write("return Serialize$LResponse(*outcome);", opName);
w.closeBlock("});");
w.closeBlock("}, $S);", operation.getId().getName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ public void writeServerRoute(
"response.body = smithy::cbor::Encode(Serialize$L(*outcome)).ToString();",
SerdeCodeGen.serdeFunctionSuffix(context, output));
w.write("return response;");
w.closeBlock("});");
w.closeBlock("}, $S);", operation.getId().getName());
}

@Override
Expand Down
7 changes: 7 additions & 0 deletions docs/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,13 @@ the project) completes the tutorial without help; BCR + Maven Central packaging
providers, server-side authenticator interface (vendor-specific schemes such as SigV4 are out
of scope, per §2).
- **Pagination**: generated paginator iterators from `@paginated`.
- **Observability**: SDK-free hooks enriched for real backends — server observations carry the
matched operation name (router-stamped) and the incoming `traceparent` for log correlation;
a client attempt-observation interceptor; W3C Trace Context helpers
(parse/format/generate) plus a `PropagateTraceContext` client interceptor. An optional
`//runtime:otel` adapter mapping these hooks onto opentelemetry-cpp spans/metrics is
**post-0.1.0** (its dependency tree — protobuf, gRPC for OTLP — stays out of the dep-light
core; stabilize the hook shapes in production first).
- **Fuzzing**: libFuzzer harnesses for JSON deserialization, URI parsing, and the server's
request parsing (fed by the malformed-request corpus); OSS-Fuzz application once stable.
- **Performance**: benchmark suite (Google Benchmark) for serde and request throughput; publish
Expand Down
40 changes: 40 additions & 0 deletions docs/production-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,46 @@ the response last, and can short-circuit before the router runs. `Observe` is
the built-in structured-logging/metrics hook; its callback runs on the
transport's request thread, so keep it cheap or hand off.

## Observability

The runtime's observability story is deliberately SDK-free: enriched hooks
on both sides plus W3C Trace Context helpers, so any backend — including
OpenTelemetry — plugs in without the core taking a telemetry dependency.

**Server:** `Observe` (above) reports, per request: `method`, `target`,
`operation` (the Smithy operation that handled it, stamped by the generated
router; empty for 404/405 dispatch failures), `status`, `duration`, and
`trace_parent` — the incoming W3C `traceparent` header, verbatim, for log
correlation.

**Client:** two ready-made interceptors in
`smithy/client/observability.h`:

```cpp
// Metrics/logging: one callback per HTTP attempt (retries visible).
config.interceptors.push_back(smithy::ObserveAttempts(
[](const smithy::AttemptObservation& a) {
// a.method, a.target, a.attempt, a.status (-1 = transport error),
// a.error_message
}));

// Distributed tracing: sets a W3C traceparent header on every attempt that
// lacks one. Pass a callback returning your application's active trace
// context to join an existing trace; omit it to start fresh roots.
config.interceptors.push_back(smithy::PropagateTraceContext());
```

`smithy/http/trace_context.h` has the underlying helpers —
`ParseTraceparent`, `FormatTraceparent`, `GenerateTraceContext`,
`GenerateSpanId` — for building richer integrations (e.g. a server
middleware that opens a span from `RequestObservation::trace_parent`).

**OpenTelemetry:** not bundled, by design — opentelemetry-cpp's dependency
tree (protobuf, gRPC for OTLP) would violate the runtime's dep-light rule.
The hooks above map 1:1 onto OTel spans and metrics; an optional
`//runtime:otel` adapter is planned post-0.1.0 once the hook shapes have
survived production use (see PLAN.md).

## Server hardening

The production server transport (`BeastServerTransport`, ADR-0006) enforces
Expand Down
32 changes: 27 additions & 5 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,31 +41,53 @@ common --tool_java_runtime_version=remotejdk_17

## 2. Write a model

`model/todo.smithy` — a service, an operation or two, a modeled error. The service's protocol
comes from its trait: `aws.protocols#restJson1` or `smithy.protocols#rpcv2Cbor`.
`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`:

```smithy
// model/bindings/restjson1.smithy
$version: "2.0"
namespace acme.todo
use aws.protocols#restJson1
apply Todo @restJson1
```

(Applying the trait directly on the service works too, if you only ever want one protocol.)

## 3. Declare the generated libraries

`BUILD.bazel`:
`BUILD.bazel` — pass the base model plus the overlay that picks the protocol:

```starlark
load("@smithy_cpp//bazel:defs.bzl", "smithy_cpp_client_library", "smithy_cpp_server_library")

smithy_cpp_client_library(
name = "todo_client",
srcs = ["model/todo.smithy"],
srcs = [
"model/bindings/restjson1.smithy",
"model/todo.smithy",
],
namespace = "acme::todo",
service = "acme.todo#Todo",
)

smithy_cpp_server_library(
name = "todo_server",
srcs = ["model/todo.smithy"],
srcs = [
"model/bindings/restjson1.smithy",
"model/todo.smithy",
],
namespace = "acme::todo",
service = "acme.todo#Todo",
)
```

Because the protocol lives in the overlay, the same model generates for another protocol by
swapping the overlay — the consumer example binds `acme.todo#Todo` to **both** restJson1 and
rpcv2Cbor side by side (different `namespace` per binding keeps the headers apart); see
[`examples/bazel-consumer/BUILD.bazel`](../examples/bazel-consumer/BUILD.bazel).

Generation runs inside the build graph as a hermetic action — correct caching, no scripts, no
Gradle. Each target is an ordinary `cc_library`: depend on it, `#include "acme/todo/client.h"`,
done. (`smithy_cpp_types_library` exists too, for data types without a protocol.)
Expand Down
4 changes: 2 additions & 2 deletions docs/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ crates (PLAN §3.2a).
| `//runtime:core` | `smithy` | `Outcome<T, E>` + `Error` (ADR-0003), `Blob`, `Timestamp` (epoch-seconds / RFC 3339 date-time / IMF-fixdate http-date), `Document` (dynamic value + serde pivot), base64 |
| `//runtime:json` | `smithy::json` | `Document` ⇄ JSON text via nlohmann (blobs as base64, timestamps per stored format) |
| `//runtime:cbor` | `smithy::cbor` | `Document` ⇄ deterministic CBOR (RFC 8949; tag-1 timestamps; tolerant decoder) — ADR-0005 |
| `//runtime:http` | `smithy::http` | `Headers` (case-insensitive), URI percent-encoding per the Smithy HTTP binding rules, `HttpRequest`/`HttpResponse`, `HttpClient`/`HttpServerTransport` interfaces, `Loopback` in-memory transport, built-in `SocketHttpClient`/`SocketHttpServer` (test/reference only — ADR-0006) |
| `//runtime:http` | `smithy::http` | `Headers` (case-insensitive), URI percent-encoding per the Smithy HTTP binding rules, `HttpRequest`/`HttpResponse`, `HttpClient`/`HttpServerTransport` interfaces, `Loopback` in-memory transport, built-in `SocketHttpClient`/`SocketHttpServer` (test/reference only — ADR-0006), W3C `TraceContext` parse/format/generate |
| `//runtime:http_beast` | `smithy::http` | `BeastServerTransport` (ADR-0006): the production server transport on BCR modular Boost.Beast/asio — concurrent connections on a thread pool, keep-alive, per-connection timeouts, body- and header-size limits, graceful drain on Stop. Separate target so Boost stays out of dep-light builds |
| `//runtime:client` | `smithy` | `ClientConfig` (endpoint, timeout, user-agent, transport injection, `RetryPolicy`, request-compression threshold, `Interceptor` hooks around every attempt, `bearer_token`/`api_key` credential providers), `SendWithRetries` (full-jitter exponential backoff over transport errors and 429/5xx — see docs/production-guide.md) |
| `//runtime:client` | `smithy` | `ClientConfig` (endpoint, timeout, user-agent, transport injection, `RetryPolicy`, request-compression threshold, `Interceptor` hooks around every attempt, `bearer_token`/`api_key` credential providers), `SendWithRetries` (full-jitter exponential backoff over transport errors and 429/5xx), `ObserveAttempts` + `PropagateTraceContext` interceptors — see docs/production-guide.md |
| `//runtime:compression` | `smithy` | `GzipCompress`/`GzipDecompress` (zlib; decompression-bomb guard, trailing-garbage rejection) backing `@requestCompression` |
| `//runtime:server` | `smithy::server` | `Router` (literal > label > greedy precedence, 404/405/400), `RequestContext`, `MakeErrorResponse`, `ValidationFailure`, user-supplied `Middleware` + `Chain`, the `Observe` logging/metrics hook, and `RequireBearerAuth`/`RequireApiKeyHeader` guards |

Expand Down
35 changes: 33 additions & 2 deletions examples/bazel-consumer/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,27 +1,58 @@
load("@rules_cc//cc:defs.bzl", "cc_test")
load("@smithy_cpp//bazel:defs.bzl", "smithy_cpp_client_library", "smithy_cpp_server_library")

# The base model is protocol-agnostic (the upstream Smithy way); each pair of
# targets binds it to a concrete protocol with an `apply` overlay. The same
# model generates a REST client/server and an RPC client/server side by side.
smithy_cpp_client_library(
name = "todo_client",
srcs = ["model/todo.smithy"],
srcs = [
"model/bindings/restjson1.smithy",
"model/todo.smithy",
],
namespace = "acme::todo",
service = "acme.todo#Todo",
)

smithy_cpp_server_library(
name = "todo_server",
srcs = ["model/todo.smithy"],
srcs = [
"model/bindings/restjson1.smithy",
"model/todo.smithy",
],
namespace = "acme::todo",
service = "acme.todo#Todo",
)

smithy_cpp_client_library(
name = "todo_cbor_client",
srcs = [
"model/bindings/rpcv2cbor.smithy",
"model/todo.smithy",
],
namespace = "acme::todo::cbor",
service = "acme.todo#Todo",
)

smithy_cpp_server_library(
name = "todo_cbor_server",
srcs = [
"model/bindings/rpcv2cbor.smithy",
"model/todo.smithy",
],
namespace = "acme::todo::cbor",
service = "acme.todo#Todo",
)

# The generated client integration-tests the generated server (the Phase 5
# pattern): loopback and a real socket on an ephemeral port.
cc_test(
name = "todo_integration_test",
size = "small",
srcs = ["todo_integration_test.cc"],
deps = [
":todo_cbor_client",
":todo_cbor_server",
":todo_client",
":todo_server",
"@googletest//:gtest_main",
Expand Down
10 changes: 10 additions & 0 deletions examples/bazel-consumer/model/bindings/restjson1.smithy
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Protocol binding overlay: pairs with model/todo.smithy to bind the
// protocol-agnostic Todo service to restJson1. Pass both files to the
// generation rule; the base model never mentions a protocol.
$version: "2.0"

namespace acme.todo

use aws.protocols#restJson1

apply Todo @restJson1
10 changes: 10 additions & 0 deletions examples/bazel-consumer/model/bindings/rpcv2cbor.smithy
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Protocol binding overlay: pairs with model/todo.smithy to bind the same
// protocol-agnostic Todo service to rpcv2Cbor. The @http traits in the base
// model are simply ignored by this protocol.
$version: "2.0"

namespace acme.todo

use smithy.protocols#rpcv2Cbor

apply Todo @rpcv2Cbor
Loading
Loading