diff --git a/bazel/tests/BUILD.bazel b/bazel/tests/BUILD.bazel index afeec6ff..d4bff776 100644 --- a/bazel/tests/BUILD.bazel +++ b/bazel/tests/BUILD.bazel @@ -1,3 +1,4 @@ +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") @@ -5,23 +6,48 @@ load(":defs_test.bzl", "defs_test_suite") # 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") diff --git a/bazel/tests/defs_test.bzl b/bazel/tests/defs_test.bzl index 28fb6c4f..73a50965 100644 --- a/bazel/tests/defs_test.bzl +++ b/bazel/tests/defs_test.bzl @@ -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", diff --git a/bazel/tests/greeter.smithy b/bazel/tests/greeter.smithy index 2d54892a..7adf6725 100644 --- a/bazel/tests/greeter.smithy +++ b/bazel/tests/greeter.smithy @@ -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] diff --git a/bazel/tests/greeter_restjson1.smithy b/bazel/tests/greeter_restjson1.smithy new file mode 100644 index 00000000..be2c3a19 --- /dev/null +++ b/bazel/tests/greeter_restjson1.smithy @@ -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 diff --git a/bazel/tests/greeter_roundtrip_test.cc b/bazel/tests/greeter_roundtrip_test.cc new file mode 100644 index 00000000..c0954bc8 --- /dev/null +++ b/bazel/tests/greeter_roundtrip_test.cc @@ -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 + +#include +#include + +#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 Greet(const GreetInput& input) override { + return GreetOutput{.greeting = "hello, " + input.name}; + } +}; + +TEST(GreeterMultiModelTest, OverlayBoundServiceRoundTrips) { + GreeterServer server(std::make_shared()); + auto loopback = std::make_shared(); + 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 diff --git a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/CppCodegenRunner.java b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/CppCodegenRunner.java index 97fcd364..a9625285 100644 --- a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/CppCodegenRunner.java +++ b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/CppCodegenRunner.java @@ -34,7 +34,7 @@ public final class CppCodegenRunner { private CppCodegenRunner() {} public static void main(String[] args) { - String modelPath = null; + List modelPaths = new ArrayList<>(); String service = null; String namespace = null; String runtimeTarget = "@smithy_cpp//runtime:core"; @@ -47,7 +47,7 @@ public static void main(String[] args) { List 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]; @@ -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(); diff --git a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/RestJson1Protocol.java b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/RestJson1Protocol.java index d0b0db3c..e8ead268 100644 --- a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/RestJson1Protocol.java +++ b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/RestJson1Protocol.java @@ -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()); } } diff --git a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/Rpcv2CborProtocol.java b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/Rpcv2CborProtocol.java index d5fef58c..a91c00c8 100644 --- a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/Rpcv2CborProtocol.java +++ b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/Rpcv2CborProtocol.java @@ -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 diff --git a/docs/PLAN.md b/docs/PLAN.md index efe6953b..a995d2ff 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -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 diff --git a/docs/production-guide.md b/docs/production-guide.md index c37d0bc4..5b4bcbf4 100644 --- a/docs/production-guide.md +++ b/docs/production-guide.md @@ -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 diff --git a/docs/quickstart.md b/docs/quickstart.md index ac116f85..9a852d9d 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -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.) diff --git a/docs/runtime.md b/docs/runtime.md index c51aa4d5..ee5fd818 100644 --- a/docs/runtime.md +++ b/docs/runtime.md @@ -10,9 +10,9 @@ crates (PLAN §3.2a). | `//runtime:core` | `smithy` | `Outcome` + `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 | diff --git a/examples/bazel-consumer/BUILD.bazel b/examples/bazel-consumer/BUILD.bazel index f7817b37..eb923b16 100644 --- a/examples/bazel-consumer/BUILD.bazel +++ b/examples/bazel-consumer/BUILD.bazel @@ -1,20 +1,49 @@ 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( @@ -22,6 +51,8 @@ cc_test( size = "small", srcs = ["todo_integration_test.cc"], deps = [ + ":todo_cbor_client", + ":todo_cbor_server", ":todo_client", ":todo_server", "@googletest//:gtest_main", diff --git a/examples/bazel-consumer/model/bindings/restjson1.smithy b/examples/bazel-consumer/model/bindings/restjson1.smithy new file mode 100644 index 00000000..27d5bc34 --- /dev/null +++ b/examples/bazel-consumer/model/bindings/restjson1.smithy @@ -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 diff --git a/examples/bazel-consumer/model/bindings/rpcv2cbor.smithy b/examples/bazel-consumer/model/bindings/rpcv2cbor.smithy new file mode 100644 index 00000000..1d92a0b1 --- /dev/null +++ b/examples/bazel-consumer/model/bindings/rpcv2cbor.smithy @@ -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 diff --git a/examples/bazel-consumer/model/todo.smithy b/examples/bazel-consumer/model/todo.smithy index 041181f7..65ce9573 100644 --- a/examples/bazel-consumer/model/todo.smithy +++ b/examples/bazel-consumer/model/todo.smithy @@ -2,11 +2,14 @@ $version: "2.0" namespace acme.todo -use aws.protocols#restJson1 - /// A deliberately small service: enough to show routing, bodies, constraint /// validation, and a modeled error end to end. -@restJson1 +/// +/// Deliberately protocol-agnostic (the upstream Smithy way): the @http +/// bindings below describe HTTP semantics without picking a wire protocol. +/// The overlays next to this file bind the service to a concrete protocol +/// with `apply` — see bindings/restjson1.smithy and bindings/rpcv2cbor.smithy +/// — so one model can serve REST and RPC without being edited. service Todo { version: "2026-01-01" operations: [AddTask, GetTask] diff --git a/examples/bazel-consumer/todo_integration_test.cc b/examples/bazel-consumer/todo_integration_test.cc index 88cf50a8..3465a302 100644 --- a/examples/bazel-consumer/todo_integration_test.cc +++ b/examples/bazel-consumer/todo_integration_test.cc @@ -8,6 +8,8 @@ #include #include +#include "acme/todo/cbor/client.h" +#include "acme/todo/cbor/server.h" #include "acme/todo/client.h" #include "acme/todo/server.h" #include "smithy/client/config.h" @@ -108,4 +110,40 @@ INSTANTIATE_TEST_SUITE_P(Transports, TodoIntegrationTest, return info.param == Transport::kLoopback ? "Loopback" : "Socket"; }); +// The same protocol-agnostic model, bound to rpcv2Cbor by a different +// `apply` overlay (model/bindings/rpcv2cbor.smithy): identical handler +// semantics over a completely different wire protocol. +TEST(TodoCborTest, SameModelServesRpcv2Cbor) { + class CborHandler final : public acme::todo::cbor::TodoHandler { + public: + smithy::Outcome AddTask( + const acme::todo::cbor::AddTaskInput& input) override { + return acme::todo::cbor::AddTaskOutput{.taskId = "task-1", .title = input.title}; + } + smithy::Outcome GetTask( + const acme::todo::cbor::GetTaskInput& input) override { + smithy::Error error = smithy::Error::Modeled("NoSuchTask", "no task: " + input.taskId); + error.set_detail(acme::todo::cbor::NoSuchTask{.message = "no task: " + input.taskId}); + return error; + } + }; + + acme::todo::cbor::TodoServer server(std::make_shared()); + auto loopback = std::make_shared(); + ASSERT_TRUE(loopback->Start(server.Handler()).ok()); + smithy::ClientConfig config; + config.http_client = loopback; + auto client = acme::todo::cbor::TodoClient::Create(std::move(config)); + ASSERT_TRUE(client.ok()) << client.error().message(); + + const auto added = client->AddTask(acme::todo::cbor::AddTaskInput{.title = "ship it"}); + ASSERT_TRUE(added.ok()) << added.error().message(); + EXPECT_EQ(added->taskId, "task-1"); + EXPECT_EQ(added->title, "ship it"); + + const auto missing = client->GetTask(acme::todo::cbor::GetTaskInput{.taskId = "nope"}); + ASSERT_FALSE(missing.ok()); + EXPECT_EQ(missing.error().code(), "NoSuchTask"); +} + } // namespace diff --git a/examples/cafe/BUILD.bazel b/examples/cafe/BUILD.bazel index b507a178..19658b42 100644 --- a/examples/cafe/BUILD.bazel +++ b/examples/cafe/BUILD.bazel @@ -9,6 +9,7 @@ cc_test( deps = [ "//examples/cafe/generated:client", "//runtime:cbor", + "//runtime:compression", "@googletest//:gtest_main", ], ) diff --git a/examples/cafe/generated/BUILD.bazel b/examples/cafe/generated/BUILD.bazel index 50dc767e..ab3ec099 100644 --- a/examples/cafe/generated/BUILD.bazel +++ b/examples/cafe/generated/BUILD.bazel @@ -32,6 +32,7 @@ cc_library( ":types", "//runtime:cbor", "//runtime:client", + "//runtime:compression", "//runtime:core", "//runtime:http", ], @@ -46,6 +47,7 @@ cc_library( ":serde", ":types", "//runtime:cbor", + "//runtime:compression", "//runtime:core", "//runtime:http", "//runtime:server", diff --git a/examples/cafe/generated/src/client.cc b/examples/cafe/generated/src/client.cc index 67479ded..a3aadfe4 100644 --- a/examples/cafe/generated/src/client.cc +++ b/examples/cafe/generated/src/client.cc @@ -1,5 +1,6 @@ // Code generated by smithy-cpp (cpp-codegen). DO NOT EDIT. +#include #include #include #include @@ -7,6 +8,7 @@ #include "example/cafe/client.h" #include "example/cafe/serde.h" #include "smithy/cbor/cbor.h" +#include "smithy/compression/gzip.h" #include "smithy/core/blob.h" #include "smithy/core/document_serde.h" #include "smithy/core/uuid.h" @@ -152,6 +154,14 @@ smithy::Outcome CafeClient::OrderCoffee(const OrderCoffeeInpu request.headers.Set("smithy-protocol", "rpc-v2-cbor"); request.headers.Set("content-type", "application/cbor"); request.body = smithy::cbor::Encode(SerializeOrderCoffeeInput(prepared)).ToString(); + // @requestCompression(gzip): applied last, appended to Content-Encoding. + if (request.body.size() >= static_cast(config_.request_min_compression_size_bytes)) { + auto compressed = smithy::GzipCompress(request.body); + if (!compressed) return std::move(compressed).error(); + request.body = *std::move(compressed); + const auto existing_encoding = request.headers.Get("content-encoding"); + request.headers.Set("content-encoding", existing_encoding.has_value() && !existing_encoding->empty() ? *existing_encoding + ", gzip" : "gzip"); + } auto response = Send(std::move(request)); if (!response) return std::move(response).error(); if (response->status != 200) return DeserializeOrderCoffeeError(*response); diff --git a/examples/cafe/generated/src/server.cc b/examples/cafe/generated/src/server.cc index 662196cb..25db28e9 100644 --- a/examples/cafe/generated/src/server.cc +++ b/examples/cafe/generated/src/server.cc @@ -10,6 +10,7 @@ #include "example/cafe/serde.h" #include "example/cafe/server.h" #include "smithy/cbor/cbor.h" +#include "smithy/compression/gzip.h" #include "smithy/core/blob.h" #include "smithy/core/document.h" #include "smithy/core/text.h" @@ -140,8 +141,17 @@ CafeServer::CafeServer(std::shared_ptr handler) response.headers.Set("content-type", "application/cbor"); response.body = smithy::cbor::Encode(SerializeGetOrderOutput(*outcome)).ToString(); return response; - }); - (void)router_->Add("POST", "/service/Cafe/operation/OrderCoffee", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext&) -> smithy::http::HttpResponse { + }, "GetOrder"); + (void)router_->Add("POST", "/service/Cafe/operation/OrderCoffee", [handler](const smithy::http::HttpRequest& raw_request, const smithy::server::RequestContext&) -> smithy::http::HttpResponse { + smithy::http::HttpRequest request = raw_request; + // @requestCompression(gzip): decode before parsing. + if (const auto request_encoding = request.headers.Get("content-encoding"); request_encoding.has_value() && (*request_encoding == "gzip" || request_encoding->ends_with(", gzip"))) { + auto decompressed = smithy::GzipDecompress(request.body); + if (!decompressed) { + return CborError(400, "SerializationException", "invalid gzip request body", {}); + } + request.body = *std::move(decompressed); + } if (request.headers.Get("smithy-protocol").value_or("") != "rpc-v2-cbor") { return CborError(400, "SerializationException", "expected smithy-protocol: rpc-v2-cbor", {}); } @@ -171,7 +181,7 @@ CafeServer::CafeServer(std::shared_ptr handler) response.headers.Set("content-type", "application/cbor"); response.body = smithy::cbor::Encode(SerializeOrderCoffeeOutput(*outcome)).ToString(); return response; - }); + }, "OrderCoffee"); } smithy::http::RequestHandler CafeServer::Handler() const { diff --git a/examples/cafe/generated_client_test.cc b/examples/cafe/generated_client_test.cc index b297d0a8..4f1a3f07 100644 --- a/examples/cafe/generated_client_test.cc +++ b/examples/cafe/generated_client_test.cc @@ -12,6 +12,7 @@ #include "example/cafe/client.h" #include "example/cafe/serde.h" #include "smithy/cbor/cbor.h" +#include "smithy/compression/gzip.h" #include "smithy/http/transport.h" namespace example::cafe { @@ -242,5 +243,51 @@ TEST_F(CafeClientTest, ApiKeyHeaderComesFromConfig) { EXPECT_EQ(transport->last_request.headers.Get("x-api-key"), "cafe-key"); } +// @requestCompression(encodings: ["gzip"]) on OrderCoffee: bodies at or +// above the configured threshold are gzipped (Content-Encoding set, payload +// decompresses back to the CBOR that would otherwise have been sent); +// smaller bodies go out untouched. +TEST_F(CafeClientTest, SmallOrderBodiesAreNotCompressed) { + transport_->next_response.headers.Set("smithy-protocol", "rpc-v2-cbor"); + transport_->next_response.body = EncodeBody(Document(DocumentMap{})); + (void)client_->OrderCoffee(OrderCoffeeInput{.coffeeType = CoffeeType::FromString("LATTE")}); + EXPECT_FALSE(transport_->last_request.headers.Get("content-encoding").has_value()); + EXPECT_TRUE(DecodeBody(transport_->last_request.body).ok()); +} + +TEST_F(CafeClientTest, ThresholdZeroCompressesEverything) { + auto transport = std::make_shared(); + smithy::ClientConfig config; + config.http_client = transport; + config.request_min_compression_size_bytes = 0; + auto client = CafeClient::Create(std::move(config)); + ASSERT_TRUE(client.ok()); + transport->next_response.headers.Set("smithy-protocol", "rpc-v2-cbor"); + transport->next_response.body = EncodeBody(Document(DocumentMap{})); + + (void)client->OrderCoffee(OrderCoffeeInput{.coffeeType = CoffeeType::FromString("LATTE")}); + EXPECT_EQ(transport->last_request.headers.Get("content-encoding"), "gzip"); + const auto decompressed = smithy::GzipDecompress(transport->last_request.body); + ASSERT_TRUE(decompressed.ok()) << decompressed.error().message(); + const auto doc = DecodeBody(*decompressed); + ASSERT_TRUE(doc.ok()); + EXPECT_TRUE(doc->as_map().contains("coffeeType")); +} + +TEST_F(CafeClientTest, LargeOrderBodiesCompressAtTheDefaultThreshold) { + // clientToken is an unconstrained string: pad the body past 10240 bytes. + OrderCoffeeInput input{.coffeeType = CoffeeType::FromString("LATTE"), + .clientToken = std::string(16 * 1024, 't')}; + transport_->next_response.headers.Set("smithy-protocol", "rpc-v2-cbor"); + transport_->next_response.body = EncodeBody(Document(DocumentMap{})); + (void)client_->OrderCoffee(input); + EXPECT_EQ(transport_->last_request.headers.Get("content-encoding"), "gzip"); + const auto decompressed = smithy::GzipDecompress(transport_->last_request.body); + ASSERT_TRUE(decompressed.ok()); + const auto doc = DecodeBody(*decompressed); + ASSERT_TRUE(doc.ok()); + EXPECT_EQ(doc->as_map().at("clientToken").as_string(), input.clientToken.value()); +} + } // namespace } // namespace example::cafe diff --git a/examples/cafe/model/cafe.smithy b/examples/cafe/model/cafe.smithy index 604ed25e..82eeb40f 100644 --- a/examples/cafe/model/cafe.smithy +++ b/examples/cafe/model/cafe.smithy @@ -18,6 +18,7 @@ service Cafe { operations: [OrderCoffee, GetOrder] } +@requestCompression(encodings: ["gzip"]) operation OrderCoffee { input := { @required diff --git a/examples/roundtrip/rest/BUILD.bazel b/examples/roundtrip/rest/BUILD.bazel new file mode 100644 index 00000000..70ef70e2 --- /dev/null +++ b/examples/roundtrip/rest/BUILD.bazel @@ -0,0 +1,13 @@ +load("@rules_cc//cc:defs.bzl", "cc_test") + +cc_test( + name = "api_key_wire_test", + size = "small", + srcs = ["api_key_wire_test.cc"], + deps = [ + "//examples/roundtrip/rest/generated:client", + "//runtime:client", + "//runtime:http", + "@googletest//:gtest_main", + ], +) diff --git a/examples/roundtrip/rest/api_key_wire_test.cc b/examples/roundtrip/rest/api_key_wire_test.cc new file mode 100644 index 00000000..7ce35764 --- /dev/null +++ b/examples/roundtrip/rest/api_key_wire_test.cc @@ -0,0 +1,62 @@ +// Wire-level checks for @httpApiKeyAuth(in: "query"): the generated client +// appends the key to the target, taking the '?' branch when the operation +// produced no query string and the '&' branch when it did — and the key +// itself is percent-encoded on the way. + +#include + +#include +#include +#include + +#include "example/roundtrip/rest/client.h" +#include "smithy/client/config.h" +#include "smithy/http/transport.h" + +namespace example::roundtrip::rest { +namespace { + +class CapturingTransport final : public smithy::http::HttpClient { + public: + smithy::Outcome Send( + const smithy::http::HttpRequest& request) override { + last_request = request; + return smithy::http::HttpResponse{200, {}, ""}; + } + + smithy::http::HttpRequest last_request; +}; + +RoundTripRestClient MakeClient(std::shared_ptr transport) { + smithy::ClientConfig config; + config.http_client = std::move(transport); + config.api_key = [] { return std::string("k&e y"); }; // needs encoding + return *RoundTripRestClient::Create(std::move(config)); +} + +TEST(ApiKeyQueryTest, StartsTheQueryStringWhenThereIsNone) { + auto transport = std::make_shared(); + auto client = MakeClient(transport); + (void)client.DescribeSink(DescribeSinkInput{.sinkId = "s1"}); + EXPECT_EQ(transport->last_request.target, "/sinks/s1?api-key=k%26e%20y"); +} + +TEST(ApiKeyQueryTest, AppendsToAnExistingQueryString) { + auto transport = std::make_shared(); + auto client = MakeClient(transport); + (void)client.PutSink(PutSinkInput{.sinkId = "s1", .tag = "prod"}); + EXPECT_EQ(transport->last_request.target, "/sinks/s1?tag=prod&api-key=k%26e%20y"); +} + +TEST(ApiKeyQueryTest, AbsentProviderLeavesTheTargetAlone) { + auto transport = std::make_shared(); + smithy::ClientConfig config; + config.http_client = transport; + auto client = RoundTripRestClient::Create(std::move(config)); + ASSERT_TRUE(client.ok()); + (void)client->DescribeSink(DescribeSinkInput{.sinkId = "s1"}); + EXPECT_EQ(transport->last_request.target, "/sinks/s1"); +} + +} // namespace +} // namespace example::roundtrip::rest diff --git a/examples/roundtrip/rest/generated/src/server.cc b/examples/roundtrip/rest/generated/src/server.cc index 61950daa..530a2b77 100644 --- a/examples/roundtrip/rest/generated/src/server.cc +++ b/examples/roundtrip/rest/generated/src/server.cc @@ -397,7 +397,7 @@ RoundTripRestServer::RoundTripRestServer(std::shared_ptr h auto outcome = handler->DescribeSink(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeDescribeSinkResponse(*outcome); - }); + }, "DescribeSink"); (void)router_->Add("PUT", "/sinks/{sinkId}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -422,7 +422,7 @@ RoundTripRestServer::RoundTripRestServer(std::shared_ptr h auto outcome = handler->PutSink(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializePutSinkResponse(*outcome); - }); + }, "PutSink"); (void)router_->Add("POST", "/sinks/{sinkId}/attachment", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -442,7 +442,7 @@ RoundTripRestServer::RoundTripRestServer(std::shared_ptr h auto outcome = handler->UploadAttachment(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeUploadAttachmentResponse(*outcome); - }); + }, "UploadAttachment"); } smithy::http::RequestHandler RoundTripRestServer::Handler() const { diff --git a/examples/roundtrip/rpc/generated/src/server.cc b/examples/roundtrip/rpc/generated/src/server.cc index 4a128db4..d0d2daf0 100644 --- a/examples/roundtrip/rpc/generated/src/server.cc +++ b/examples/roundtrip/rpc/generated/src/server.cc @@ -152,7 +152,7 @@ RoundTripRpcServer::RoundTripRpcServer(std::shared_ptr hand response.headers.Set("content-type", "application/cbor"); response.body = smithy::cbor::Encode(SerializePutSinkRpcOutput(*outcome)).ToString(); return response; - }); + }, "PutSinkRpc"); } smithy::http::RequestHandler RoundTripRpcServer::Handler() const { diff --git a/examples/weather/generated/include/example/weather/client.h b/examples/weather/generated/include/example/weather/client.h index c38fdfb0..aa4d8e8d 100644 --- a/examples/weather/generated/include/example/weather/client.h +++ b/examples/weather/generated/include/example/weather/client.h @@ -30,6 +30,10 @@ class WeatherClient { smithy::Outcome GetCity(const GetCityInput& input) const; smithy::Outcome GetCurrentTime(const GetCurrentTimeInput& input = {}) const; smithy::Outcome GetForecast(const GetForecastInput& input) const; + /// Fetches a rendered report by its slash-separated path. The greedy label + /// keeps embedded slashes: GET /reports/2026/q3/summary routes here with + /// reportPath = "2026/q3/summary". + smithy::Outcome GetReport(const GetReportInput& input) const; smithy::Outcome ListCities(const ListCitiesInput& input) const; /// Pages ListCities until the service stops returning a next token (@paginated). ListCitiesPaginator PaginateListCities(ListCitiesInput input) const; diff --git a/examples/weather/generated/include/example/weather/serde.h b/examples/weather/generated/include/example/weather/serde.h index 30a05fe7..b645089c 100644 --- a/examples/weather/generated/include/example/weather/serde.h +++ b/examples/weather/generated/include/example/weather/serde.h @@ -56,4 +56,10 @@ smithy::Outcome DeserializeGetCurrentTimeInput(const smithy smithy::Document SerializeGetCurrentTimeOutput(const GetCurrentTimeOutput& value); smithy::Outcome DeserializeGetCurrentTimeOutput(const smithy::Document& doc); +smithy::Document SerializeGetReportInput(const GetReportInput& value); +smithy::Outcome DeserializeGetReportInput(const smithy::Document& doc); + +smithy::Document SerializeGetReportOutput(const GetReportOutput& value); +smithy::Outcome DeserializeGetReportOutput(const smithy::Document& doc); + } // namespace example::weather diff --git a/examples/weather/generated/include/example/weather/server.h b/examples/weather/generated/include/example/weather/server.h index 23adefdc..eb944788 100644 --- a/examples/weather/generated/include/example/weather/server.h +++ b/examples/weather/generated/include/example/weather/server.h @@ -23,6 +23,10 @@ class WeatherHandler { virtual smithy::Outcome GetCity(const GetCityInput& input) = 0; virtual smithy::Outcome GetCurrentTime(const GetCurrentTimeInput& input) = 0; virtual smithy::Outcome GetForecast(const GetForecastInput& input) = 0; + /// Fetches a rendered report by its slash-separated path. The greedy label + /// keeps embedded slashes: GET /reports/2026/q3/summary routes here with + /// reportPath = "2026/q3/summary". + virtual smithy::Outcome GetReport(const GetReportInput& input) = 0; virtual smithy::Outcome ListCities(const ListCitiesInput& input) = 0; }; diff --git a/examples/weather/generated/include/example/weather/types.h b/examples/weather/generated/include/example/weather/types.h index 8eafc00f..61aa1c31 100644 --- a/examples/weather/generated/include/example/weather/types.h +++ b/examples/weather/generated/include/example/weather/types.h @@ -103,4 +103,19 @@ struct GetCurrentTimeOutput { friend bool operator==(const GetCurrentTimeOutput&, const GetCurrentTimeOutput&) = default; }; + +struct GetReportInput { + std::string reportPath{}; + + friend bool operator==(const GetReportInput&, const GetReportInput&) = default; +}; + + +struct GetReportOutput { + std::string path{}; + std::int64_t sizeBytes{}; + + friend bool operator==(const GetReportOutput&, const GetReportOutput&) = default; +}; + } // namespace example::weather diff --git a/examples/weather/generated/src/client.cc b/examples/weather/generated/src/client.cc index 7fcb51a4..1cd648b6 100644 --- a/examples/weather/generated/src/client.cc +++ b/examples/weather/generated/src/client.cc @@ -228,6 +228,22 @@ smithy::Outcome WeatherClient::GetForecast(const GetForecastI return DeserializeGetForecastOutput(*body_doc); } +smithy::Outcome WeatherClient::GetReport(const GetReportInput& input) const { + std::string target = path_prefix_; + target += "/reports"; + target += "/"; + target += smithy::http::EncodeGreedyPathSegment(input.reportPath); + smithy::http::HttpRequest request; + request.method = "GET"; + request.target = std::move(target); + auto response = Send(std::move(request)); + if (!response) return std::move(response).error(); + if (response->status != 200) return GenericError(ParseError(*response)); + auto body_doc = smithy::json::Decode(response->body); + if (!body_doc) return std::move(body_doc).error(); + return DeserializeGetReportOutput(*body_doc); +} + smithy::Outcome WeatherClient::ListCities(const ListCitiesInput& input) const { std::string target = path_prefix_; target += "/cities"; diff --git a/examples/weather/generated/src/serde.cc b/examples/weather/generated/src/serde.cc index ea3f3e61..9f6c1267 100644 --- a/examples/weather/generated/src/serde.cc +++ b/examples/weather/generated/src/serde.cc @@ -353,4 +353,53 @@ smithy::Outcome DeserializeGetCurrentTimeOutput(const smit return out; } +smithy::Document SerializeGetReportInput(const GetReportInput& value) { + smithy::DocumentMap map; + map.emplace("reportPath", smithy::Document(value.reportPath)); + return smithy::Document(std::move(map)); +} + +smithy::Outcome DeserializeGetReportInput(const smithy::Document& doc) { + if (!doc.is_map()) return smithy::Error::Serialization("GetReportInput: expected a map on the wire"); + GetReportInput out; + { + const smithy::Document* member = doc.Find("reportPath"); + if (member == nullptr || member->is_null()) { + return smithy::Error::Serialization("GetReportInput: missing required member: reportPath"); + } + if (!member->is_string()) return smithy::Error::Serialization("GetReportInput.reportPath: unexpected type on the wire"); + out.reportPath = member->as_string(); + } + return out; +} + +smithy::Document SerializeGetReportOutput(const GetReportOutput& value) { + smithy::DocumentMap map; + map.emplace("path", smithy::Document(value.path)); + map.emplace("sizeBytes", smithy::Document(static_cast(value.sizeBytes))); + return smithy::Document(std::move(map)); +} + +smithy::Outcome DeserializeGetReportOutput(const smithy::Document& doc) { + if (!doc.is_map()) return smithy::Error::Serialization("GetReportOutput: expected a map on the wire"); + GetReportOutput out; + { + const smithy::Document* member = doc.Find("path"); + if (member == nullptr || member->is_null()) { + return smithy::Error::Serialization("GetReportOutput: missing required member: path"); + } + if (!member->is_string()) return smithy::Error::Serialization("GetReportOutput.path: unexpected type on the wire"); + out.path = member->as_string(); + } + { + const smithy::Document* member = doc.Find("sizeBytes"); + if (member == nullptr || member->is_null()) { + return smithy::Error::Serialization("GetReportOutput: missing required member: sizeBytes"); + } + if (!member->is_int()) return smithy::Error::Serialization("GetReportOutput.sizeBytes: unexpected type on the wire"); + out.sizeBytes = static_cast(member->as_int()); + } + return out; +} + } // namespace example::weather diff --git a/examples/weather/generated/src/server.cc b/examples/weather/generated/src/server.cc index 6f27f880..9a4fb4af 100644 --- a/examples/weather/generated/src/server.cc +++ b/examples/weather/generated/src/server.cc @@ -236,6 +236,30 @@ smithy::http::HttpResponse SerializeGetForecastResponse(const GetForecastOutput& return response; } +smithy::Outcome ParseGetReportInput(const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context, std::vector* validation_failures) { + (void)request; + (void)context; + (void)validation_failures; + GetReportInput input{}; + { + const std::string& label_value = context.labels.at("reportPath"); + input.reportPath = label_value; + } + return input; +} + +smithy::http::HttpResponse SerializeGetReportResponse(const GetReportOutput& output) { + (void)output; + smithy::http::HttpResponse response; + response.status = 200; + smithy::DocumentMap body_map; + body_map.emplace("path", smithy::Document(output.path)); + body_map.emplace("sizeBytes", smithy::Document(static_cast(output.sizeBytes))); + response.headers.Set("content-type", "application/json"); + response.body = smithy::json::Encode(smithy::Document(std::move(body_map))); + return response; +} + smithy::Outcome ParseListCitiesInput(const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context, std::vector* validation_failures) { (void)request; (void)context; @@ -296,7 +320,7 @@ WeatherServer::WeatherServer(std::shared_ptr handler) auto outcome = handler->DeleteCity(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeDeleteCityResponse(*outcome); - }); + }, "DeleteCity"); (void)router_->Add("GET", "/cities/{cityId}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -321,7 +345,7 @@ WeatherServer::WeatherServer(std::shared_ptr handler) auto outcome = handler->GetCity(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeGetCityResponse(*outcome); - }); + }, "GetCity"); (void)router_->Add("GET", "/current-time", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -344,7 +368,7 @@ WeatherServer::WeatherServer(std::shared_ptr handler) auto outcome = handler->GetCurrentTime(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeGetCurrentTimeResponse(*outcome); - }); + }, "GetCurrentTime"); (void)router_->Add("GET", "/cities/{cityId}/forecast", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -369,7 +393,30 @@ WeatherServer::WeatherServer(std::shared_ptr handler) auto outcome = handler->GetForecast(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeGetForecastResponse(*outcome); - }); + }, "GetForecast"); + (void)router_->Add("GET", "/reports/{reportPath+}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { + // Content-Type validation per the HTTP binding spec (415), then Accept (406); + // the malformed-request suite pins the error-identity headers. A missing + // content-type is tolerated, and blob payloads without @mediaType accept + // any content type / accept. + if (const auto content_type = request.headers.Get("content-type"); content_type.has_value() && smithy::http::MediaTypeOf(*content_type) != "application/json") { + auto error_response = JsonError(415, "", "unsupported media type", {}); + error_response.headers.Set("x-amzn-errortype", "UnsupportedMediaTypeException"); + return error_response; + } + if (const auto accept = request.headers.Get("accept"); accept.has_value() && !smithy::http::AcceptMatches(*accept, "application/json")) { + auto error_response = JsonError(406, "", "not acceptable", {}); + error_response.headers.Set("x-amzn-errortype", "NotAcceptableException"); + return error_response; + } + std::vector validation_failures; + auto input = ParseGetReportInput(request, context, &validation_failures); + if (!validation_failures.empty()) return ValidationErrorResponse(validation_failures); + if (!input) return ErrorToResponse(input.error()); + auto outcome = handler->GetReport(*input); + if (!outcome) return ErrorToResponse(outcome.error()); + return SerializeGetReportResponse(*outcome); + }, "GetReport"); (void)router_->Add("GET", "/cities", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -392,7 +439,7 @@ WeatherServer::WeatherServer(std::shared_ptr handler) auto outcome = handler->ListCities(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeListCitiesResponse(*outcome); - }); + }, "ListCities"); } smithy::http::RequestHandler WeatherServer::Handler() const { diff --git a/examples/weather/generated/tests/integration_test.cc b/examples/weather/generated/tests/integration_test.cc index 0e35da13..9917b883 100644 --- a/examples/weather/generated/tests/integration_test.cc +++ b/examples/weather/generated/tests/integration_test.cc @@ -150,6 +150,19 @@ GetCurrentTimeOutput RandomGetCurrentTimeOutput(Rng& rng) { return v; } +GetReportInput RandomGetReportInput(Rng& rng) { + GetReportInput v{}; + v.reportPath = rng.Text(1, 9); + return v; +} + +GetReportOutput RandomGetReportOutput(Rng& rng) { + GetReportOutput v{}; + v.path = rng.Text(1, 9); + v.sizeBytes = static_cast(rng.Int(-4611686018427387904LL, 4611686018427387903LL)); + return v; +} + class ScriptedHandler final : public WeatherHandler { public: smithy::Outcome DeleteCity(const DeleteCityInput& input) override { @@ -184,6 +197,14 @@ class ScriptedHandler final : public WeatherHandler { std::optional lastGetForecast; GetForecastOutput nextGetForecastOutput{}; std::optional nextGetForecastError; + smithy::Outcome GetReport(const GetReportInput& input) override { + lastGetReport = input; + if (nextGetReportError.has_value()) return *nextGetReportError; + return nextGetReportOutput; + } + std::optional lastGetReport; + GetReportOutput nextGetReportOutput{}; + std::optional nextGetReportError; smithy::Outcome ListCities(const ListCitiesInput& input) override { lastListCities = input; if (nextListCitiesError.has_value()) return *nextListCitiesError; @@ -329,6 +350,32 @@ TEST_P(WeatherIntegrationTest, GetForecastMaximalRoundTrips) { EXPECT_EQ(*outcome, output); } +TEST_P(WeatherIntegrationTest, GetReportRandomRoundTrips) { + Rng rng{std::mt19937{20260707U}, /*fill_all=*/false}; + for (int iteration = 0; iteration < 8; ++iteration) { + const GetReportInput input = RandomGetReportInput(rng); + GetReportOutput output = RandomGetReportOutput(rng); + handler_->nextGetReportOutput = output; + const auto outcome = client_->GetReport(input); + ASSERT_TRUE(outcome.ok()) << outcome.error().message(); + ASSERT_TRUE(handler_->lastGetReport.has_value()); + EXPECT_EQ(*handler_->lastGetReport, input); + EXPECT_EQ(*outcome, output); + } +} + +TEST_P(WeatherIntegrationTest, GetReportMaximalRoundTrips) { + Rng rng{std::mt19937{7U}, /*fill_all=*/true}; + const GetReportInput input = RandomGetReportInput(rng); + GetReportOutput output = RandomGetReportOutput(rng); + handler_->nextGetReportOutput = output; + const auto outcome = client_->GetReport(input); + ASSERT_TRUE(outcome.ok()) << outcome.error().message(); + ASSERT_TRUE(handler_->lastGetReport.has_value()); + EXPECT_EQ(*handler_->lastGetReport, input); + EXPECT_EQ(*outcome, output); +} + TEST_P(WeatherIntegrationTest, ListCitiesRandomRoundTrips) { Rng rng{std::mt19937{20260707U}, /*fill_all=*/false}; for (int iteration = 0; iteration < 8; ++iteration) { @@ -478,6 +525,32 @@ TEST(WeatherIntegrationUnknownMembers, GetForecastToleratesUnknownResponseMember EXPECT_EQ(*outcome, output); } +TEST(WeatherIntegrationUnknownMembers, GetReportToleratesUnknownResponseMembers) { + auto handler = std::make_shared(); + WeatherServer server(handler); + auto loopback = std::make_shared(); + ASSERT_TRUE(loopback->Start(server.Handler()).ok()); + auto inject = [](smithy::http::HttpResponse& response) { + auto doc = smithy::json::Decode(response.body); + if (!doc.ok() || !doc->is_map()) return; + auto map = doc->as_map(); + map.insert_or_assign("smithy_cpp_unknown_member", smithy::Document(42)); + response.body = smithy::json::Encode(smithy::Document(std::move(map))); + }; + auto transport = std::make_shared(loopback, inject); + smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries + config.http_client = transport; + auto client = *WeatherClient::Create(std::move(config)); + Rng rng{std::mt19937{99U}, /*fill_all=*/true}; + const GetReportInput input = RandomGetReportInput(rng); + GetReportOutput output = RandomGetReportOutput(rng); + handler->nextGetReportOutput = output; + const auto outcome = client.GetReport(input); + ASSERT_TRUE(outcome.ok()) << outcome.error().message(); + EXPECT_EQ(*outcome, output); +} + TEST(WeatherIntegrationUnknownMembers, ListCitiesToleratesUnknownResponseMembers) { auto handler = std::make_shared(); WeatherServer server(handler); diff --git a/examples/weather/generated/tests/smoke_test.cc b/examples/weather/generated/tests/smoke_test.cc index f8e420a0..83fda780 100644 --- a/examples/weather/generated/tests/smoke_test.cc +++ b/examples/weather/generated/tests/smoke_test.cc @@ -46,6 +46,13 @@ GetForecastOutput MinimalGetForecastOutput() { }(); } +GetReportOutput MinimalGetReportOutput() { + return [] { + GetReportOutput v{}; + return v; + }(); +} + ListCitiesOutput MinimalListCitiesOutput() { return [] { ListCitiesOutput v{}; @@ -71,6 +78,10 @@ class SmokeHandler : public WeatherHandler { (void)input; return MinimalGetForecastOutput(); } + smithy::Outcome GetReport(const GetReportInput& input) override { + (void)input; + return MinimalGetReportOutput(); + } smithy::Outcome ListCities(const ListCitiesInput& input) override { (void)input; return MinimalListCitiesOutput(); @@ -140,6 +151,19 @@ TEST(WeatherSmokeTest, GetForecastRoundTrips) { EXPECT_EQ(*outcome, MinimalGetForecastOutput()); } +TEST(WeatherSmokeTest, GetReportRoundTrips) { + WeatherClient client = MakeClient(std::make_shared()); + GetReportInput input = [] { + GetReportInput v{}; + return v; + }(); + // @httpLabel members must be non-empty to route. + input.reportPath = "smoke"; + const auto outcome = client.GetReport(input); + ASSERT_TRUE(outcome.ok()) << outcome.error().message(); + EXPECT_EQ(*outcome, MinimalGetReportOutput()); +} + TEST(WeatherSmokeTest, ListCitiesRoundTrips) { WeatherClient client = MakeClient(std::make_shared()); const ListCitiesInput input = [] { diff --git a/examples/weather/generated_server_e2e_test.cc b/examples/weather/generated_server_e2e_test.cc index e98956f5..1dd62e2c 100644 --- a/examples/weather/generated_server_e2e_test.cc +++ b/examples/weather/generated_server_e2e_test.cc @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -14,8 +15,10 @@ #include "example/weather/server.h" #include "examples/weather/handwritten/weather_client.h" #include "smithy/client/interceptor.h" +#include "smithy/client/observability.h" #include "smithy/http/loopback.h" #include "smithy/http/message.h" +#include "smithy/http/trace_context.h" #include "smithy/server/middleware.h" namespace example::weather { @@ -24,7 +27,7 @@ namespace { namespace hw = example::weather::handwritten; // Reference implementation of the GENERATED handler interface. -class ReferenceHandler final : public WeatherHandler { +class ReferenceHandler : public WeatherHandler { public: smithy::Outcome GetCity(const GetCityInput& input) override { if (input.cityId != "seattle") { @@ -68,6 +71,12 @@ class ReferenceHandler final : public WeatherHandler { (void)input; return GetCurrentTimeOutput{.time = smithy::Timestamp::FromEpochMilliseconds(1398796238500)}; } + + smithy::Outcome GetReport(const GetReportInput& input) override { + // Echo the decoded path so tests can assert label-decoding fidelity. + return GetReportOutput{.path = input.reportPath, + .sizeBytes = static_cast(input.reportPath.size())}; + } }; class GeneratedServerEndToEndTest : public testing::Test { @@ -247,6 +256,202 @@ TEST_F(GeneratedServerEndToEndTest, PaginatorWalksAllPages) { EXPECT_FALSE(still_done->has_value()); } +// Observability end to end (Phase 7c): the client propagates a W3C trace +// context and observes its attempts; the server's Observe middleware reports +// the matched operation and the incoming traceparent for correlation. +TEST_F(GeneratedServerEndToEndTest, TraceContextAndOperationFlowThroughObservability) { + std::vector served; + auto handler = smithy::server::Chain( + {smithy::server::Observe( + [&](const smithy::server::RequestObservation& o) { served.push_back(o); })}, + server_->Handler()); + auto loopback = std::make_shared(); + ASSERT_TRUE(loopback->Start(handler).ok()); + + std::vector attempts; + smithy::ClientConfig config; + config.http_client = loopback; + config.interceptors.push_back(smithy::PropagateTraceContext()); + config.interceptors.push_back( + smithy::ObserveAttempts([&](const smithy::AttemptObservation& a) { attempts.push_back(a); })); + auto client = example::weather::WeatherClient::Create(std::move(config)); + ASSERT_TRUE(client.ok()); + + const auto city = client->GetCity(example::weather::GetCityInput{.cityId = "seattle"}); + ASSERT_TRUE(city.ok()) << city.error().message(); + + ASSERT_EQ(served.size(), 1u); + EXPECT_EQ(served[0].operation, "GetCity"); + const auto trace = smithy::http::ParseTraceparent(served[0].trace_parent); + ASSERT_TRUE(trace.has_value()) << served[0].trace_parent; + EXPECT_TRUE(trace->sampled); + + ASSERT_EQ(attempts.size(), 1u); + EXPECT_EQ(attempts[0].attempt, 1); + EXPECT_EQ(attempts[0].status, 200); + EXPECT_EQ(attempts[0].method, "GET"); + + // Dispatch failures report an empty operation. + smithy::http::HttpRequest unrouted; + unrouted.method = "GET"; + unrouted.target = "/no/such/route"; + (void)handler(unrouted); + ASSERT_EQ(served.size(), 2u); + EXPECT_EQ(served[1].status, 404); + EXPECT_TRUE(served[1].operation.empty()); +} + +// Greedy labels ({reportPath+}) keep their embedded slashes and decode +// percent-encoded characters segment by segment. +TEST_F(GeneratedServerEndToEndTest, GreedyLabelKeepsSlashesAndDecodes) { + auto loopback = std::make_shared(); + ASSERT_TRUE(loopback->Start(server_->Handler()).ok()); + smithy::ClientConfig config; + config.http_client = loopback; + auto client = example::weather::WeatherClient::Create(std::move(config)); + ASSERT_TRUE(client.ok()); + + for (const std::string path : { + std::string("2026/q3/summary.pdf"), // plain multi-segment + std::string("a b/c%d/e?f"), // space, percent, question mark + std::string("weird&seg=ment/#frag/two"), // ampersand, equals, hash + std::string("caf\xc3\xa9/na\xc3\xafve"), // UTF-8 segments + }) { + const auto report = client->GetReport(example::weather::GetReportInput{.reportPath = path}); + ASSERT_TRUE(report.ok()) << "path: " << path << ": " << report.error().message(); + EXPECT_EQ(report->path, path); + EXPECT_EQ(report->sizeBytes, static_cast(path.size())); + } +} + +// Hostile strings in a plain @httpLabel. CityId is @pattern-constrained to +// "^[A-Za-z0-9 ]+$", so this pins two behaviors: values inside the charset +// (including tricky spacing) round-trip exactly — the handler's error message +// echoes the decoded id, proving encode -> route -> decode fidelity — and +// URI-hostile values outside it are rejected by generated validation with a +// clean 400 ValidationException, never a crash, mis-route, or mangled echo. +TEST_F(GeneratedServerEndToEndTest, HostileLabelValuesSurviveTheRoundTrip) { + auto loopback = std::make_shared(); + ASSERT_TRUE(loopback->Start(server_->Handler()).ok()); + smithy::ClientConfig config; + config.http_client = loopback; + auto client = example::weather::WeatherClient::Create(std::move(config)); + ASSERT_TRUE(client.ok()); + + for (const std::string city : { + std::string("abc def"), // %20 in the label segment + std::string("a b"), // consecutive spaces survive + std::string(" leading"), // leading space + std::string("trailing "), // trailing space + std::string("UPPER lower 0123456789"), + }) { + const auto outcome = client->GetCity(example::weather::GetCityInput{.cityId = city}); + ASSERT_FALSE(outcome.ok()) << "city: " << city; + EXPECT_EQ(outcome.error().code(), "NoSuchResource") << city; + EXPECT_EQ(outcome.error().message(), "no city: " + city); + } + + for (const std::string city : { + std::string("san jos\xc3\xa9"), // UTF-8 outside the pattern + std::string("x?y#z&w=v"), // every URI delimiter + std::string("100%25 legit"), // literal percent-escape text + std::string("+plus+signs+"), // '+' must not decode to space + std::string("dot./..dot"), // dot segments must not normalize + }) { + const auto outcome = client->GetCity(example::weather::GetCityInput{.cityId = city}); + ASSERT_FALSE(outcome.ok()) << "city: " << city; + EXPECT_EQ(outcome.error().code(), "ValidationException") + << city << ": " << outcome.error().message(); + } +} + +// A pagination token full of URI-hostile characters must survive the +// output-body -> input-query round trip the paginator drives. +TEST_F(GeneratedServerEndToEndTest, PaginatorRoundTripsHostileTokens) { + static const std::string kNastyToken = "a b&c=d?e+f/g%h#i"; + class PagingHandler final : public ReferenceHandler { + public: + smithy::Outcome ListCities(const ListCitiesInput& input) override { + ListCitiesOutput out; + if (!input.nextToken.has_value()) { + out.items.push_back(CitySummary{.cityId = "page1", .name = "Page One"}); + out.nextToken = kNastyToken; + return out; + } + // The token must arrive exactly as issued, through the query string. + if (*input.nextToken != kNastyToken) { + return smithy::Error::Modeled("NoSuchResource", "token corrupted: " + *input.nextToken); + } + out.items.push_back(CitySummary{.cityId = "page2", .name = "Page Two"}); + return out; + } + }; + + WeatherServer server(std::make_shared()); + auto loopback = std::make_shared(); + ASSERT_TRUE(loopback->Start(server.Handler()).ok()); + smithy::ClientConfig config; + config.http_client = loopback; + auto client = example::weather::WeatherClient::Create(std::move(config)); + ASSERT_TRUE(client.ok()); + + auto paginator = client->PaginateListCities(example::weather::ListCitiesInput{}); + const auto first = paginator.Next(); + ASSERT_TRUE(first.ok()) << first.error().message(); + ASSERT_TRUE(first->has_value()); + EXPECT_EQ((*first)->items[0].cityId, "page1"); + + const auto second = paginator.Next(); + ASSERT_TRUE(second.ok()) << second.error().message(); + ASSERT_TRUE(second->has_value()) << "pagination ended early"; + EXPECT_EQ((*second)->items[0].cityId, "page2"); + + const auto done = paginator.Next(); + ASSERT_TRUE(done.ok()); + EXPECT_FALSE(done->has_value()); +} + +// Everything at once: transient transport failures under retry, bearer auth +// from config, trace propagation and attempt observation via interceptors, +// auth + observation middleware on the server. Client-side hooks see every +// attempt; the server sees exactly one authorized request. +TEST_F(GeneratedServerEndToEndTest, FullStackLayeringSurvivesRetries) { + std::vector served; + auto handler = smithy::server::Chain( + {smithy::server::RequireBearerAuth( + [](const std::string& token) { return token == "stack-token"; }), + smithy::server::Observe( + [&](const smithy::server::RequestObservation& o) { served.push_back(o); })}, + server_->Handler()); + auto loopback = std::make_shared(); + ASSERT_TRUE(loopback->Start(handler).ok()); + + std::vector attempts; + smithy::ClientConfig config; + config.http_client = std::make_shared(loopback); // 2 failures first + config.retry.sleep = [](std::chrono::milliseconds) {}; + config.bearer_token = [] { return std::string("stack-token"); }; + config.interceptors.push_back(smithy::PropagateTraceContext()); + config.interceptors.push_back( + smithy::ObserveAttempts([&](const smithy::AttemptObservation& a) { attempts.push_back(a); })); + auto client = example::weather::WeatherClient::Create(std::move(config)); + ASSERT_TRUE(client.ok()); + + const auto city = client->GetCity(example::weather::GetCityInput{.cityId = "seattle"}); + ASSERT_TRUE(city.ok()) << city.error().message(); + + ASSERT_EQ(attempts.size(), 3u); + EXPECT_EQ(attempts[0].status, -1); + EXPECT_EQ(attempts[1].status, -1); + EXPECT_EQ(attempts[2].status, 200); + EXPECT_EQ(attempts[2].attempt, 3); + + // Only the successful attempt reached the server, authorized and traced. + ASSERT_EQ(served.size(), 1u); + EXPECT_EQ(served[0].operation, "GetCity"); + EXPECT_TRUE(smithy::http::ParseTraceparent(served[0].trace_parent).has_value()); +} + TEST_F(GeneratedServerEndToEndTest, DeleteCityIs204WithNoBody) { smithy::http::HttpRequest request; request.method = "DELETE"; diff --git a/examples/weather/model/weather.smithy b/examples/weather/model/weather.smithy index 68219718..20987da3 100644 --- a/examples/weather/model/weather.smithy +++ b/examples/weather/model/weather.smithy @@ -16,7 +16,7 @@ use aws.protocols#restJson1 service Weather { version: "2026-07-06" resources: [City] - operations: [GetCurrentTime] + operations: [GetCurrentTime, GetReport] } resource City { @@ -124,6 +124,27 @@ operation GetForecast { errors: [NoSuchResource] } +/// Fetches a rendered report by its slash-separated path. The greedy label +/// keeps embedded slashes: GET /reports/2026/q3/summary routes here with +/// reportPath = "2026/q3/summary". +@readonly +@http(method: "GET", uri: "/reports/{reportPath+}") +operation GetReport { + input := { + @required + @httpLabel + reportPath: String + } + + output := { + @required + path: String + + @required + sizeBytes: Long + } +} + @readonly @http(method: "GET", uri: "/current-time") operation GetCurrentTime { diff --git a/protocol-tests/restjson1-validation/generated/src/server.cc b/protocol-tests/restjson1-validation/generated/src/server.cc index 7f07b638..c420d6c6 100644 --- a/protocol-tests/restjson1-validation/generated/src/server.cc +++ b/protocol-tests/restjson1-validation/generated/src/server.cc @@ -1984,7 +1984,7 @@ RestJsonValidationServer::RestJsonValidationServer(std::shared_ptrMalformedEnum(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedEnumResponse(*outcome); - }); + }, "MalformedEnum"); (void)router_->Add("POST", "/MalformedLength", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -2004,7 +2004,7 @@ RestJsonValidationServer::RestJsonValidationServer(std::shared_ptrMalformedLength(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedLengthResponse(*outcome); - }); + }, "MalformedLength"); (void)router_->Add("POST", "/MalformedLengthOverride", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -2024,7 +2024,7 @@ RestJsonValidationServer::RestJsonValidationServer(std::shared_ptrMalformedLengthOverride(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedLengthOverrideResponse(*outcome); - }); + }, "MalformedLengthOverride"); (void)router_->Add("POST", "/MalformedLengthQueryString", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -2044,7 +2044,7 @@ RestJsonValidationServer::RestJsonValidationServer(std::shared_ptrMalformedLengthQueryString(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedLengthQueryStringResponse(*outcome); - }); + }, "MalformedLengthQueryString"); (void)router_->Add("POST", "/MalformedPattern", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -2064,7 +2064,7 @@ RestJsonValidationServer::RestJsonValidationServer(std::shared_ptrMalformedPattern(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedPatternResponse(*outcome); - }); + }, "MalformedPattern"); (void)router_->Add("POST", "/MalformedPatternOverride", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -2084,7 +2084,7 @@ RestJsonValidationServer::RestJsonValidationServer(std::shared_ptrMalformedPatternOverride(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedPatternOverrideResponse(*outcome); - }); + }, "MalformedPatternOverride"); (void)router_->Add("POST", "/MalformedRange", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -2104,7 +2104,7 @@ RestJsonValidationServer::RestJsonValidationServer(std::shared_ptrMalformedRange(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedRangeResponse(*outcome); - }); + }, "MalformedRange"); (void)router_->Add("POST", "/MalformedRangeOverride", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -2124,7 +2124,7 @@ RestJsonValidationServer::RestJsonValidationServer(std::shared_ptrMalformedRangeOverride(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedRangeOverrideResponse(*outcome); - }); + }, "MalformedRangeOverride"); (void)router_->Add("POST", "/MalformedRequired", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -2142,7 +2142,7 @@ RestJsonValidationServer::RestJsonValidationServer(std::shared_ptrMalformedRequired(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedRequiredResponse(*outcome); - }); + }, "MalformedRequired"); (void)router_->Add("POST", "/MalformedUniqueItems", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -2162,7 +2162,7 @@ RestJsonValidationServer::RestJsonValidationServer(std::shared_ptrMalformedUniqueItems(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedUniqueItemsResponse(*outcome); - }); + }, "MalformedUniqueItems"); (void)router_->Add("POST", "/SensitiveValidation", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -2182,7 +2182,7 @@ RestJsonValidationServer::RestJsonValidationServer(std::shared_ptrSensitiveValidation(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeSensitiveValidationResponse(*outcome); - }); + }, "SensitiveValidation"); } smithy::http::RequestHandler RestJsonValidationServer::Handler() const { diff --git a/protocol-tests/restjson1/generated/src/server.cc b/protocol-tests/restjson1/generated/src/server.cc index 779637e6..43da3745 100644 --- a/protocol-tests/restjson1/generated/src/server.cc +++ b/protocol-tests/restjson1/generated/src/server.cc @@ -4634,7 +4634,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->AllQueryStringTypes(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeAllQueryStringTypesResponse(*outcome); - }); + }, "AllQueryStringTypes"); (void)router_->Add("GET", "/ConstantAndVariableQueryString", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4652,7 +4652,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->ConstantAndVariableQueryString(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeConstantAndVariableQueryStringResponse(*outcome); - }); + }, "ConstantAndVariableQueryString"); (void)router_->Add("GET", "/ConstantQueryString/{hello}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4670,7 +4670,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->ConstantQueryString(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeConstantQueryStringResponse(*outcome); - }); + }, "ConstantQueryString"); (void)router_->Add("POST", "/ContentTypeParameters", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4688,7 +4688,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->ContentTypeParameters(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeContentTypeParametersResponse(*outcome); - }); + }, "ContentTypeParameters"); (void)router_->Add("POST", "/DatetimeOffsets", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4711,7 +4711,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->DatetimeOffsets(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeDatetimeOffsetsResponse(*outcome); - }); + }, "DatetimeOffsets"); (void)router_->Add("PUT", "/DocumentType", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4734,7 +4734,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->DocumentType(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeDocumentTypeResponse(*outcome); - }); + }, "DocumentType"); (void)router_->Add("PUT", "/DocumentTypeAsMapValue", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4757,7 +4757,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->DocumentTypeAsMapValue(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeDocumentTypeAsMapValueResponse(*outcome); - }); + }, "DocumentTypeAsMapValue"); (void)router_->Add("PUT", "/DocumentTypeAsPayload", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4780,7 +4780,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->DocumentTypeAsPayload(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeDocumentTypeAsPayloadResponse(*outcome); - }); + }, "DocumentTypeAsPayload"); (void)router_->Add("POST", "/EmptyInputAndEmptyOutput", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4798,7 +4798,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->EmptyInputAndEmptyOutput(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeEmptyInputAndEmptyOutputResponse(*outcome); - }); + }, "EmptyInputAndEmptyOutput"); (void)router_->Add("POST", "/EndpointOperation", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4816,7 +4816,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->EndpointOperation(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeEndpointOperationResponse(*outcome); - }); + }, "EndpointOperation"); (void)router_->Add("POST", "/EndpointWithHostLabelOperation", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4834,7 +4834,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->EndpointWithHostLabelOperation(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeEndpointWithHostLabelOperationResponse(*outcome); - }); + }, "EndpointWithHostLabelOperation"); (void)router_->Add("POST", "/FractionalSeconds", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4857,7 +4857,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->FractionalSeconds(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeFractionalSecondsResponse(*outcome); - }); + }, "FractionalSeconds"); (void)router_->Add("PUT", "/GreetingWithErrors", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4875,7 +4875,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->GreetingWithErrors(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeGreetingWithErrorsResponse(*outcome); - }); + }, "GreetingWithErrors"); (void)router_->Add("GET", "/HostWithPathOperation", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4893,7 +4893,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->HostWithPathOperation(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeHostWithPathOperationResponse(*outcome); - }); + }, "HostWithPathOperation"); (void)router_->Add("POST", "/HttpChecksumRequired", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4916,7 +4916,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->HttpChecksumRequired(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeHttpChecksumRequiredResponse(*outcome); - }); + }, "HttpChecksumRequired"); (void)router_->Add("POST", "/EnumPayload", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4941,7 +4941,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->HttpEnumPayload(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeHttpEnumPayloadResponse(*outcome); - }); + }, "HttpEnumPayload"); (void)router_->Add("POST", "/HttpPayloadTraits", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4954,7 +4954,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->HttpPayloadTraits(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeHttpPayloadTraitsResponse(*outcome); - }); + }, "HttpPayloadTraits"); (void)router_->Add("POST", "/HttpPayloadTraitsWithMediaType", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -4977,7 +4977,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->HttpPayloadTraitsWithMediaType(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeHttpPayloadTraitsWithMediaTypeResponse(*outcome); - }); + }, "HttpPayloadTraitsWithMediaType"); (void)router_->Add("PUT", "/HttpPayloadWithStructure", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5000,7 +5000,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->HttpPayloadWithStructure(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeHttpPayloadWithStructureResponse(*outcome); - }); + }, "HttpPayloadWithStructure"); (void)router_->Add("PUT", "/HttpPayloadWithUnion", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5023,7 +5023,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->HttpPayloadWithUnion(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeHttpPayloadWithUnionResponse(*outcome); - }); + }, "HttpPayloadWithUnion"); (void)router_->Add("GET", "/HttpPrefixHeaders", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5041,7 +5041,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->HttpPrefixHeaders(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeHttpPrefixHeadersResponse(*outcome); - }); + }, "HttpPrefixHeaders"); (void)router_->Add("GET", "/HttpPrefixHeadersResponse", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5059,7 +5059,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->HttpPrefixHeadersInResponse(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeHttpPrefixHeadersInResponseResponse(*outcome); - }); + }, "HttpPrefixHeadersInResponse"); (void)router_->Add("GET", "/FloatHttpLabels/{float}/{double}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5077,7 +5077,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->HttpRequestWithFloatLabels(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeHttpRequestWithFloatLabelsResponse(*outcome); - }); + }, "HttpRequestWithFloatLabels"); (void)router_->Add("GET", "/HttpRequestWithGreedyLabelInPath/foo/{foo}/baz/{baz+}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5095,7 +5095,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->HttpRequestWithGreedyLabelInPath(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeHttpRequestWithGreedyLabelInPathResponse(*outcome); - }); + }, "HttpRequestWithGreedyLabelInPath"); (void)router_->Add("GET", "/HttpRequestWithLabels/{string}/{short}/{integer}/{long}/{float}/{double}/{boolean}/{timestamp}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5113,7 +5113,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->HttpRequestWithLabels(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeHttpRequestWithLabelsResponse(*outcome); - }); + }, "HttpRequestWithLabels"); (void)router_->Add("GET", "/HttpRequestWithLabelsAndTimestampFormat/{memberEpochSeconds}/{memberHttpDate}/{memberDateTime}/{defaultFormat}/{targetEpochSeconds}/{targetHttpDate}/{targetDateTime}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5131,7 +5131,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->HttpRequestWithLabelsAndTimestampFormat(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeHttpRequestWithLabelsAndTimestampFormatResponse(*outcome); - }); + }, "HttpRequestWithLabelsAndTimestampFormat"); (void)router_->Add("GET", "/ReDosLiteral/{str}/(a+)+", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5149,7 +5149,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->HttpRequestWithRegexLiteral(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeHttpRequestWithRegexLiteralResponse(*outcome); - }); + }, "HttpRequestWithRegexLiteral"); (void)router_->Add("PUT", "/HttpResponseCode", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5167,7 +5167,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->HttpResponseCode(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeHttpResponseCodeResponse(*outcome); - }); + }, "HttpResponseCode"); (void)router_->Add("POST", "/StringPayload", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5190,7 +5190,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->HttpStringPayload(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeHttpStringPayloadResponse(*outcome); - }); + }, "HttpStringPayload"); (void)router_->Add("GET", "/IgnoreQueryParamsInResponse", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5213,7 +5213,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->IgnoreQueryParamsInResponse(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeIgnoreQueryParamsInResponseResponse(*outcome); - }); + }, "IgnoreQueryParamsInResponse"); (void)router_->Add("POST", "/InputAndOutputWithHeaders", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5233,7 +5233,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->InputAndOutputWithHeaders(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeInputAndOutputWithHeadersResponse(*outcome); - }); + }, "InputAndOutputWithHeaders"); (void)router_->Add("POST", "/JsonBlobs", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5256,7 +5256,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->JsonBlobs(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeJsonBlobsResponse(*outcome); - }); + }, "JsonBlobs"); (void)router_->Add("PUT", "/JsonEnums", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5281,7 +5281,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->JsonEnums(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeJsonEnumsResponse(*outcome); - }); + }, "JsonEnums"); (void)router_->Add("PUT", "/JsonIntEnums", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5306,7 +5306,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->JsonIntEnums(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeJsonIntEnumsResponse(*outcome); - }); + }, "JsonIntEnums"); (void)router_->Add("PUT", "/JsonLists", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5331,7 +5331,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->JsonLists(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeJsonListsResponse(*outcome); - }); + }, "JsonLists"); (void)router_->Add("POST", "/JsonMaps", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5356,7 +5356,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->JsonMaps(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeJsonMapsResponse(*outcome); - }); + }, "JsonMaps"); (void)router_->Add("POST", "/JsonTimestamps", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5379,7 +5379,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->JsonTimestamps(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeJsonTimestampsResponse(*outcome); - }); + }, "JsonTimestamps"); (void)router_->Add("PUT", "/JsonUnions", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5404,7 +5404,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->JsonUnions(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeJsonUnionsResponse(*outcome); - }); + }, "JsonUnions"); (void)router_->Add("POST", "/MalformedAcceptWithBody", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5427,7 +5427,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedAcceptWithBody(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedAcceptWithBodyResponse(*outcome); - }); + }, "MalformedAcceptWithBody"); (void)router_->Add("POST", "/MalformedAcceptWithGenericString", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5450,7 +5450,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedAcceptWithGenericString(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedAcceptWithGenericStringResponse(*outcome); - }); + }, "MalformedAcceptWithGenericString"); (void)router_->Add("POST", "/MalformedAcceptWithPayload", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5473,7 +5473,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedAcceptWithPayload(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedAcceptWithPayloadResponse(*outcome); - }); + }, "MalformedAcceptWithPayload"); (void)router_->Add("POST", "/MalformedBlob", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5491,7 +5491,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedBlob(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedBlobResponse(*outcome); - }); + }, "MalformedBlob"); (void)router_->Add("POST", "/MalformedBoolean/{booleanInPath}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5509,7 +5509,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedBoolean(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedBooleanResponse(*outcome); - }); + }, "MalformedBoolean"); (void)router_->Add("POST", "/MalformedByte/{byteInPath}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5527,7 +5527,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedByte(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedByteResponse(*outcome); - }); + }, "MalformedByte"); (void)router_->Add("POST", "/MalformedContentTypeWithBody", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5545,7 +5545,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedContentTypeWithBody(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedContentTypeWithBodyResponse(*outcome); - }); + }, "MalformedContentTypeWithBody"); (void)router_->Add("POST", "/MalformedContentTypeWithGenericString", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5563,7 +5563,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedContentTypeWithGenericString(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedContentTypeWithGenericStringResponse(*outcome); - }); + }, "MalformedContentTypeWithGenericString"); (void)router_->Add("POST", "/MalformedContentTypeWithoutBody", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5581,7 +5581,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedContentTypeWithoutBody(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedContentTypeWithoutBodyResponse(*outcome); - }); + }, "MalformedContentTypeWithoutBody"); (void)router_->Add("POST", "/MalformedContentTypeWithoutBodyEmptyInput", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5599,7 +5599,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedContentTypeWithoutBodyEmptyInput(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedContentTypeWithoutBodyEmptyInputResponse(*outcome); - }); + }, "MalformedContentTypeWithoutBodyEmptyInput"); (void)router_->Add("POST", "/MalformedContentTypeWithPayload", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5617,7 +5617,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedContentTypeWithPayload(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedContentTypeWithPayloadResponse(*outcome); - }); + }, "MalformedContentTypeWithPayload"); (void)router_->Add("POST", "/MalformedDouble/{doubleInPath}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5635,7 +5635,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedDouble(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedDoubleResponse(*outcome); - }); + }, "MalformedDouble"); (void)router_->Add("POST", "/MalformedFloat/{floatInPath}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5653,7 +5653,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedFloat(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedFloatResponse(*outcome); - }); + }, "MalformedFloat"); (void)router_->Add("POST", "/MalformedInteger/{integerInPath}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5671,7 +5671,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedInteger(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedIntegerResponse(*outcome); - }); + }, "MalformedInteger"); (void)router_->Add("POST", "/MalformedList", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5689,7 +5689,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedList(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedListResponse(*outcome); - }); + }, "MalformedList"); (void)router_->Add("POST", "/MalformedLong/{longInPath}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5707,7 +5707,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedLong(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedLongResponse(*outcome); - }); + }, "MalformedLong"); (void)router_->Add("POST", "/MalformedMap", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5725,7 +5725,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedMap(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedMapResponse(*outcome); - }); + }, "MalformedMap"); (void)router_->Add("POST", "/MalformedRequestBody", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5743,7 +5743,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedRequestBody(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedRequestBodyResponse(*outcome); - }); + }, "MalformedRequestBody"); (void)router_->Add("POST", "/MalformedShort/{shortInPath}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5761,7 +5761,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedShort(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedShortResponse(*outcome); - }); + }, "MalformedShort"); (void)router_->Add("POST", "/MalformedString", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5779,7 +5779,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedString(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedStringResponse(*outcome); - }); + }, "MalformedString"); (void)router_->Add("POST", "/MalformedTimestampBodyDateTime", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5797,7 +5797,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedTimestampBodyDateTime(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedTimestampBodyDateTimeResponse(*outcome); - }); + }, "MalformedTimestampBodyDateTime"); (void)router_->Add("POST", "/MalformedTimestampBodyDefault", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5815,7 +5815,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedTimestampBodyDefault(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedTimestampBodyDefaultResponse(*outcome); - }); + }, "MalformedTimestampBodyDefault"); (void)router_->Add("POST", "/MalformedTimestampBodyHttpDate", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5833,7 +5833,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedTimestampBodyHttpDate(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedTimestampBodyHttpDateResponse(*outcome); - }); + }, "MalformedTimestampBodyHttpDate"); (void)router_->Add("POST", "/MalformedTimestampHeaderDateTime", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5851,7 +5851,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedTimestampHeaderDateTime(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedTimestampHeaderDateTimeResponse(*outcome); - }); + }, "MalformedTimestampHeaderDateTime"); (void)router_->Add("POST", "/MalformedTimestampHeaderDefault", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5869,7 +5869,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedTimestampHeaderDefault(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedTimestampHeaderDefaultResponse(*outcome); - }); + }, "MalformedTimestampHeaderDefault"); (void)router_->Add("POST", "/MalformedTimestampHeaderEpoch", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5887,7 +5887,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedTimestampHeaderEpoch(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedTimestampHeaderEpochResponse(*outcome); - }); + }, "MalformedTimestampHeaderEpoch"); (void)router_->Add("POST", "/MalformedTimestampPathDefault/{timestamp}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5905,7 +5905,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedTimestampPathDefault(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedTimestampPathDefaultResponse(*outcome); - }); + }, "MalformedTimestampPathDefault"); (void)router_->Add("POST", "/MalformedTimestampPathEpoch/{timestamp}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5923,7 +5923,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedTimestampPathEpoch(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedTimestampPathEpochResponse(*outcome); - }); + }, "MalformedTimestampPathEpoch"); (void)router_->Add("POST", "/MalformedTimestampPathHttpDate/{timestamp}", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5941,7 +5941,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedTimestampPathHttpDate(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedTimestampPathHttpDateResponse(*outcome); - }); + }, "MalformedTimestampPathHttpDate"); (void)router_->Add("POST", "/MalformedTimestampQueryDefault", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5959,7 +5959,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedTimestampQueryDefault(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedTimestampQueryDefaultResponse(*outcome); - }); + }, "MalformedTimestampQueryDefault"); (void)router_->Add("POST", "/MalformedTimestampQueryEpoch", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5977,7 +5977,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedTimestampQueryEpoch(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedTimestampQueryEpochResponse(*outcome); - }); + }, "MalformedTimestampQueryEpoch"); (void)router_->Add("POST", "/MalformedTimestampQueryHttpDate", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -5995,7 +5995,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedTimestampQueryHttpDate(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedTimestampQueryHttpDateResponse(*outcome); - }); + }, "MalformedTimestampQueryHttpDate"); (void)router_->Add("POST", "/MalformedUnion", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6013,7 +6013,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MalformedUnion(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMalformedUnionResponse(*outcome); - }); + }, "MalformedUnion"); (void)router_->Add("GET", "/MediaTypeHeader", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6031,7 +6031,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->MediaTypeHeader(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeMediaTypeHeaderResponse(*outcome); - }); + }, "MediaTypeHeader"); (void)router_->Add("POST", "/NoInputAndNoOutput", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6049,7 +6049,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->NoInputAndNoOutput(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeNoInputAndNoOutputResponse(*outcome); - }); + }, "NoInputAndNoOutput"); (void)router_->Add("POST", "/NoInputAndOutputOutput", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6067,7 +6067,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->NoInputAndOutput(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeNoInputAndOutputResponse(*outcome); - }); + }, "NoInputAndOutput"); (void)router_->Add("GET", "/NullAndEmptyHeadersClient", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6085,7 +6085,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->NullAndEmptyHeadersClient(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeNullAndEmptyHeadersClientResponse(*outcome); - }); + }, "NullAndEmptyHeadersClient"); (void)router_->Add("GET", "/NullAndEmptyHeadersServer", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6103,7 +6103,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->NullAndEmptyHeadersServer(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeNullAndEmptyHeadersServerResponse(*outcome); - }); + }, "NullAndEmptyHeadersServer"); (void)router_->Add("GET", "/OmitsNullSerializesEmptyString", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6121,7 +6121,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->OmitsNullSerializesEmptyString(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeOmitsNullSerializesEmptyStringResponse(*outcome); - }); + }, "OmitsNullSerializesEmptyString"); (void)router_->Add("POST", "/OmitsSerializingEmptyLists", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6141,7 +6141,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->OmitsSerializingEmptyLists(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeOmitsSerializingEmptyListsResponse(*outcome); - }); + }, "OmitsSerializingEmptyLists"); (void)router_->Add("POST", "/OperationWithDefaults", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6166,7 +6166,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->OperationWithDefaults(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeOperationWithDefaultsResponse(*outcome); - }); + }, "OperationWithDefaults"); (void)router_->Add("POST", "/OperationWithNestedStructure", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6189,7 +6189,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->OperationWithNestedStructure(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeOperationWithNestedStructureResponse(*outcome); - }); + }, "OperationWithNestedStructure"); (void)router_->Add("POST", "/PostPlayerAction", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6212,7 +6212,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->PostPlayerAction(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializePostPlayerActionResponse(*outcome); - }); + }, "PostPlayerAction"); (void)router_->Add("POST", "/PostUnionWithJsonName", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6235,7 +6235,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->PostUnionWithJsonName(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializePostUnionWithJsonNameResponse(*outcome); - }); + }, "PostUnionWithJsonName"); (void)router_->Add("POST", "/requestcompression/putcontentwithencoding", [handler](const smithy::http::HttpRequest& raw_request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { smithy::http::HttpRequest request = raw_request; // @requestCompression(gzip): decode before parsing. @@ -6262,7 +6262,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->PutWithContentEncoding(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializePutWithContentEncodingResponse(*outcome); - }); + }, "PutWithContentEncoding"); (void)router_->Add("POST", "/QueryIdempotencyTokenAutoFill", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6280,7 +6280,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->QueryIdempotencyTokenAutoFill(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeQueryIdempotencyTokenAutoFillResponse(*outcome); - }); + }, "QueryIdempotencyTokenAutoFill"); (void)router_->Add("POST", "/StringListMap", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6298,7 +6298,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->QueryParamsAsStringListMap(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeQueryParamsAsStringListMapResponse(*outcome); - }); + }, "QueryParamsAsStringListMap"); (void)router_->Add("POST", "/Precedence", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6316,7 +6316,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->QueryPrecedence(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeQueryPrecedenceResponse(*outcome); - }); + }, "QueryPrecedence"); (void)router_->Add("GET", "/responseCodeHttpFallback", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6334,7 +6334,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->ResponseCodeHttpFallback(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeResponseCodeHttpFallbackResponse(*outcome); - }); + }, "ResponseCodeHttpFallback"); (void)router_->Add("GET", "/responseCodeRequired", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6352,7 +6352,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->ResponseCodeRequired(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeResponseCodeRequiredResponse(*outcome); - }); + }, "ResponseCodeRequired"); (void)router_->Add("PUT", "/SimpleScalarProperties", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6375,7 +6375,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->SimpleScalarProperties(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeSimpleScalarPropertiesResponse(*outcome); - }); + }, "SimpleScalarProperties"); (void)router_->Add("PUT", "/SparseJsonLists", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6398,7 +6398,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->SparseJsonLists(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeSparseJsonListsResponse(*outcome); - }); + }, "SparseJsonLists"); (void)router_->Add("POST", "/SparseJsonMaps", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6423,7 +6423,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->SparseJsonMaps(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeSparseJsonMapsResponse(*outcome); - }); + }, "SparseJsonMaps"); (void)router_->Add("POST", "/body", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6446,7 +6446,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->TestBodyStructure(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeTestBodyStructureResponse(*outcome); - }); + }, "TestBodyStructure"); (void)router_->Add("GET", "/no_input_no_payload", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6464,7 +6464,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->TestGetNoInputNoPayload(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeTestGetNoInputNoPayloadResponse(*outcome); - }); + }, "TestGetNoInputNoPayload"); (void)router_->Add("GET", "/no_payload", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6482,7 +6482,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->TestGetNoPayload(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeTestGetNoPayloadResponse(*outcome); - }); + }, "TestGetNoPayload"); (void)router_->Add("POST", "/blob_payload", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6495,7 +6495,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->TestPayloadBlob(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeTestPayloadBlobResponse(*outcome); - }); + }, "TestPayloadBlob"); (void)router_->Add("POST", "/payload", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6518,7 +6518,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->TestPayloadStructure(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeTestPayloadStructureResponse(*outcome); - }); + }, "TestPayloadStructure"); (void)router_->Add("POST", "/no_input_no_payload", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6536,7 +6536,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->TestPostNoInputNoPayload(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeTestPostNoInputNoPayloadResponse(*outcome); - }); + }, "TestPostNoInputNoPayload"); (void)router_->Add("POST", "/no_payload", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6554,7 +6554,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->TestPostNoPayload(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeTestPostNoPayloadResponse(*outcome); - }); + }, "TestPostNoPayload"); (void)router_->Add("POST", "/TimestampFormatHeaders", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6572,7 +6572,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->TimestampFormatHeaders(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeTimestampFormatHeadersResponse(*outcome); - }); + }, "TimestampFormatHeaders"); (void)router_->Add("POST", "/UnitInputAndOutput", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing @@ -6590,7 +6590,7 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) auto outcome = handler->UnitInputAndOutput(*input); if (!outcome) return ErrorToResponse(outcome.error()); return SerializeUnitInputAndOutputResponse(*outcome); - }); + }, "UnitInputAndOutput"); } smithy::http::RequestHandler RestJsonServer::Handler() const { diff --git a/protocol-tests/rpcv2cbor/generated/src/server.cc b/protocol-tests/rpcv2cbor/generated/src/server.cc index 323fb7e6..08f799f1 100644 --- a/protocol-tests/rpcv2cbor/generated/src/server.cc +++ b/protocol-tests/rpcv2cbor/generated/src/server.cc @@ -227,7 +227,7 @@ RpcV2ProtocolServer::RpcV2ProtocolServer(std::shared_ptr h response.headers.Set("content-type", "application/cbor"); response.body = smithy::cbor::Encode(SerializeEmptyInputOutputOutput(*outcome)).ToString(); return response; - }); + }, "EmptyInputOutput"); (void)router_->Add("POST", "/service/RpcV2Protocol/operation/Float16", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext&) -> smithy::http::HttpResponse { if (request.headers.Get("smithy-protocol").value_or("") != "rpc-v2-cbor") { return CborError(400, "SerializationException", "expected smithy-protocol: rpc-v2-cbor", {}); @@ -255,7 +255,7 @@ RpcV2ProtocolServer::RpcV2ProtocolServer(std::shared_ptr h response.headers.Set("content-type", "application/cbor"); response.body = smithy::cbor::Encode(SerializeFloat16Output(*outcome)).ToString(); return response; - }); + }, "Float16"); (void)router_->Add("POST", "/service/RpcV2Protocol/operation/FractionalSeconds", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext&) -> smithy::http::HttpResponse { if (request.headers.Get("smithy-protocol").value_or("") != "rpc-v2-cbor") { return CborError(400, "SerializationException", "expected smithy-protocol: rpc-v2-cbor", {}); @@ -283,7 +283,7 @@ RpcV2ProtocolServer::RpcV2ProtocolServer(std::shared_ptr h response.headers.Set("content-type", "application/cbor"); response.body = smithy::cbor::Encode(SerializeFractionalSecondsOutput(*outcome)).ToString(); return response; - }); + }, "FractionalSeconds"); (void)router_->Add("POST", "/service/RpcV2Protocol/operation/GreetingWithErrors", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext&) -> smithy::http::HttpResponse { if (request.headers.Get("smithy-protocol").value_or("") != "rpc-v2-cbor") { return CborError(400, "SerializationException", "expected smithy-protocol: rpc-v2-cbor", {}); @@ -311,7 +311,7 @@ RpcV2ProtocolServer::RpcV2ProtocolServer(std::shared_ptr h response.headers.Set("content-type", "application/cbor"); response.body = smithy::cbor::Encode(SerializeGreetingWithErrorsOutput(*outcome)).ToString(); return response; - }); + }, "GreetingWithErrors"); (void)router_->Add("POST", "/service/RpcV2Protocol/operation/NoInputOutput", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext&) -> smithy::http::HttpResponse { if (request.headers.Get("smithy-protocol").value_or("") != "rpc-v2-cbor") { return CborError(400, "SerializationException", "expected smithy-protocol: rpc-v2-cbor", {}); @@ -339,7 +339,7 @@ RpcV2ProtocolServer::RpcV2ProtocolServer(std::shared_ptr h response.headers.Set("content-type", "application/cbor"); response.body = smithy::cbor::Encode(SerializeNoInputOutputOutput(*outcome)).ToString(); return response; - }); + }, "NoInputOutput"); (void)router_->Add("POST", "/service/RpcV2Protocol/operation/OperationWithDefaults", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext&) -> smithy::http::HttpResponse { if (request.headers.Get("smithy-protocol").value_or("") != "rpc-v2-cbor") { return CborError(400, "SerializationException", "expected smithy-protocol: rpc-v2-cbor", {}); @@ -370,7 +370,7 @@ RpcV2ProtocolServer::RpcV2ProtocolServer(std::shared_ptr h response.headers.Set("content-type", "application/cbor"); response.body = smithy::cbor::Encode(SerializeOperationWithDefaultsOutput(*outcome)).ToString(); return response; - }); + }, "OperationWithDefaults"); (void)router_->Add("POST", "/service/RpcV2Protocol/operation/OptionalInputOutput", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext&) -> smithy::http::HttpResponse { if (request.headers.Get("smithy-protocol").value_or("") != "rpc-v2-cbor") { return CborError(400, "SerializationException", "expected smithy-protocol: rpc-v2-cbor", {}); @@ -398,7 +398,7 @@ RpcV2ProtocolServer::RpcV2ProtocolServer(std::shared_ptr h response.headers.Set("content-type", "application/cbor"); response.body = smithy::cbor::Encode(SerializeOptionalInputOutputOutput(*outcome)).ToString(); return response; - }); + }, "OptionalInputOutput"); (void)router_->Add("POST", "/service/RpcV2Protocol/operation/RpcV2CborDenseMaps", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext&) -> smithy::http::HttpResponse { if (request.headers.Get("smithy-protocol").value_or("") != "rpc-v2-cbor") { return CborError(400, "SerializationException", "expected smithy-protocol: rpc-v2-cbor", {}); @@ -429,7 +429,7 @@ RpcV2ProtocolServer::RpcV2ProtocolServer(std::shared_ptr h response.headers.Set("content-type", "application/cbor"); response.body = smithy::cbor::Encode(SerializeRpcV2CborDenseMapsOutput(*outcome)).ToString(); return response; - }); + }, "RpcV2CborDenseMaps"); (void)router_->Add("POST", "/service/RpcV2Protocol/operation/RpcV2CborLists", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext&) -> smithy::http::HttpResponse { if (request.headers.Get("smithy-protocol").value_or("") != "rpc-v2-cbor") { return CborError(400, "SerializationException", "expected smithy-protocol: rpc-v2-cbor", {}); @@ -460,7 +460,7 @@ RpcV2ProtocolServer::RpcV2ProtocolServer(std::shared_ptr h response.headers.Set("content-type", "application/cbor"); response.body = smithy::cbor::Encode(SerializeRpcV2CborListsOutput(*outcome)).ToString(); return response; - }); + }, "RpcV2CborLists"); (void)router_->Add("POST", "/service/RpcV2Protocol/operation/RpcV2CborSparseMaps", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext&) -> smithy::http::HttpResponse { if (request.headers.Get("smithy-protocol").value_or("") != "rpc-v2-cbor") { return CborError(400, "SerializationException", "expected smithy-protocol: rpc-v2-cbor", {}); @@ -491,7 +491,7 @@ RpcV2ProtocolServer::RpcV2ProtocolServer(std::shared_ptr h response.headers.Set("content-type", "application/cbor"); response.body = smithy::cbor::Encode(SerializeRpcV2CborSparseMapsOutput(*outcome)).ToString(); return response; - }); + }, "RpcV2CborSparseMaps"); (void)router_->Add("POST", "/service/RpcV2Protocol/operation/SimpleScalarProperties", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext&) -> smithy::http::HttpResponse { if (request.headers.Get("smithy-protocol").value_or("") != "rpc-v2-cbor") { return CborError(400, "SerializationException", "expected smithy-protocol: rpc-v2-cbor", {}); @@ -519,7 +519,7 @@ RpcV2ProtocolServer::RpcV2ProtocolServer(std::shared_ptr h response.headers.Set("content-type", "application/cbor"); response.body = smithy::cbor::Encode(SerializeSimpleScalarPropertiesOutput(*outcome)).ToString(); return response; - }); + }, "SimpleScalarProperties"); (void)router_->Add("POST", "/service/RpcV2Protocol/operation/SparseNullsOperation", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext&) -> smithy::http::HttpResponse { if (request.headers.Get("smithy-protocol").value_or("") != "rpc-v2-cbor") { return CborError(400, "SerializationException", "expected smithy-protocol: rpc-v2-cbor", {}); @@ -547,7 +547,7 @@ RpcV2ProtocolServer::RpcV2ProtocolServer(std::shared_ptr h response.headers.Set("content-type", "application/cbor"); response.body = smithy::cbor::Encode(SerializeSparseNullsOperationOutput(*outcome)).ToString(); return response; - }); + }, "SparseNullsOperation"); } smithy::http::RequestHandler RpcV2ProtocolServer::Handler() const { diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index e75ee915..33e39b55 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -62,6 +62,7 @@ cc_library( srcs = [ "src/http/headers.cc", "src/http/socket_transport.cc", + "src/http/trace_context.cc", "src/http/uri.cc", ], hdrs = [ @@ -69,6 +70,7 @@ cc_library( "include/smithy/http/loopback.h", "include/smithy/http/message.h", "include/smithy/http/socket_transport.h", + "include/smithy/http/trace_context.h", "include/smithy/http/transport.h", "include/smithy/http/uri.h", ], @@ -108,6 +110,17 @@ cc_test( ], ) +cc_test( + name = "trace_context_test", + size = "small", + srcs = ["tests/http/trace_context_test.cc"], + copts = COPTS, + deps = [ + ":http", + "@googletest//:gtest_main", + ], +) + cc_test( name = "http_test", size = "small", @@ -198,10 +211,14 @@ cc_test( cc_library( name = "client", - srcs = ["src/client/retry.cc"], + srcs = [ + "src/client/observability.cc", + "src/client/retry.cc", + ], hdrs = [ "include/smithy/client/config.h", "include/smithy/client/interceptor.h", + "include/smithy/client/observability.h", "include/smithy/client/retry.h", ], copts = COPTS, diff --git a/runtime/include/smithy/client/observability.h b/runtime/include/smithy/client/observability.h new file mode 100644 index 00000000..64c7a9eb --- /dev/null +++ b/runtime/include/smithy/client/observability.h @@ -0,0 +1,41 @@ +#ifndef SMITHY_CLIENT_OBSERVABILITY_H_ +#define SMITHY_CLIENT_OBSERVABILITY_H_ + +#include +#include +#include +#include + +#include "smithy/client/interceptor.h" +#include "smithy/http/trace_context.h" + +namespace smithy { + +// One HTTP attempt made by a generated client, as seen by ObserveAttempts. +struct AttemptObservation { + std::string method; + std::string target; + int attempt = 0; // 1-based; retries observe 2, 3, ... + int status = 0; // HTTP status, or -1 on a transport error + std::string error_message; // transport error text; empty otherwise +}; + +// The client-side logging/metrics hook: an interceptor reporting every +// attempt (count = callbacks, retries visible via attempt > 1). The callback +// runs on the calling thread after each attempt; keep it cheap or hand off. +std::shared_ptr ObserveAttempts( + std::function callback); + +// W3C Trace Context propagation: sets a traceparent header on every attempt +// that does not already carry one. current supplies the application's active +// trace context (e.g. the current span); when null or empty, a fresh root +// context is generated per attempt. Register alongside your other +// interceptors: +// +// config.interceptors.push_back(smithy::PropagateTraceContext()); +std::shared_ptr PropagateTraceContext( + std::function()> current = nullptr); + +} // namespace smithy + +#endif // SMITHY_CLIENT_OBSERVABILITY_H_ diff --git a/runtime/include/smithy/http/message.h b/runtime/include/smithy/http/message.h index 42b0d7f8..2ac0e61e 100644 --- a/runtime/include/smithy/http/message.h +++ b/runtime/include/smithy/http/message.h @@ -25,6 +25,11 @@ struct HttpResponse { int status = 200; Headers headers; Body body; + // Server-side annotation, never written to the wire: the Smithy operation + // whose route produced this response (stamped by the generated router so + // observability middleware can label by operation; empty on 404/405/400 + // dispatch failures and hand-rolled handlers). + std::string operation; }; } // namespace smithy::http diff --git a/runtime/include/smithy/http/trace_context.h b/runtime/include/smithy/http/trace_context.h new file mode 100644 index 00000000..8b24e2dc --- /dev/null +++ b/runtime/include/smithy/http/trace_context.h @@ -0,0 +1,34 @@ +#ifndef SMITHY_HTTP_TRACE_CONTEXT_H_ +#define SMITHY_HTTP_TRACE_CONTEXT_H_ + +#include +#include +#include + +namespace smithy::http { + +// W3C Trace Context (https://www.w3.org/TR/trace-context/): the traceparent +// header's fields, SDK-free. Backend integration (OpenTelemetry etc.) builds +// on these helpers; docs/production-guide.md has the wiring. +struct TraceContext { + std::string trace_id; // 32 lowercase hex digits, never all zeros + std::string parent_id; // 16 lowercase hex digits, never all zeros + bool sampled = true; // trace-flags bit 0 +}; + +// Parses a traceparent header value. nullopt on malformed input, an +// unsupported all-ones version, or all-zero ids. +std::optional ParseTraceparent(std::string_view value); + +// "00---". +std::string FormatTraceparent(const TraceContext& context); + +// A fresh root context: random trace and parent ids, sampled. +TraceContext GenerateTraceContext(); + +// A fresh random 16-hex-digit span id (for deriving child contexts). +std::string GenerateSpanId(); + +} // namespace smithy::http + +#endif // SMITHY_HTTP_TRACE_CONTEXT_H_ diff --git a/runtime/include/smithy/server/middleware.h b/runtime/include/smithy/server/middleware.h index 72ab978a..68a39760 100644 --- a/runtime/include/smithy/server/middleware.h +++ b/runtime/include/smithy/server/middleware.h @@ -28,6 +28,13 @@ http::RequestHandler Chain(std::vector middleware, http::RequestHand struct RequestObservation { std::string method; std::string target; + // The Smithy operation that handled the request (from the generated + // router's HttpResponse::operation annotation); empty for 404/405/400 + // dispatch failures. + std::string operation; + // The request's W3C traceparent header, verbatim, for log correlation; + // empty when absent. See smithy/http/trace_context.h to parse it. + std::string trace_parent; int status = 0; std::chrono::milliseconds duration{0}; }; diff --git a/runtime/include/smithy/server/router.h b/runtime/include/smithy/server/router.h index dba7161d..433bd6a0 100644 --- a/runtime/include/smithy/server/router.h +++ b/runtime/include/smithy/server/router.h @@ -34,8 +34,11 @@ using RouteHandler = class Router { public: // Fails on invalid patterns and on route conflicts (same method + pattern - // shape) — the generator surfaces this at build time. - Outcome Add(std::string_view method, std::string_view pattern, RouteHandler handler); + // shape) — the generator surfaces this at build time. A non-empty + // operation name is stamped onto HttpResponse::operation for + // observability middleware. + Outcome Add(std::string_view method, std::string_view pattern, RouteHandler handler, + std::string_view operation = ""); // Full dispatch: 404 (no pattern match), 405 (pattern match, wrong method, // with an Allow header), 400 (malformed target); otherwise the handler's @@ -51,6 +54,7 @@ class Router { std::string method; std::vector segments; RouteHandler handler; + std::string operation; }; static Outcome> ParsePattern(std::string_view pattern); diff --git a/runtime/src/client/observability.cc b/runtime/src/client/observability.cc new file mode 100644 index 00000000..e5262066 --- /dev/null +++ b/runtime/src/client/observability.cc @@ -0,0 +1,68 @@ +#include "smithy/client/observability.h" + +#include + +namespace smithy { +namespace { + +class AttemptObserver final : public Interceptor { + public: + explicit AttemptObserver(std::function callback) + : callback_(std::move(callback)) {} + + void ReadAfterTransmit(const http::HttpRequest& request, + const Outcome& outcome, int attempt) override { + AttemptObservation observation; + observation.method = request.method; + observation.target = request.target; + observation.attempt = attempt; + if (outcome.ok()) { + observation.status = outcome->status; + } else { + observation.status = -1; + observation.error_message = outcome.error().message(); + } + callback_(observation); + } + + private: + std::function callback_; +}; + +class TracePropagator final : public Interceptor { + public: + explicit TracePropagator(std::function()> current) + : current_(std::move(current)) {} + + void ModifyBeforeTransmit(http::HttpRequest& request, int attempt) override { + (void)attempt; + if (request.headers.Get("traceparent").has_value()) { + return; // Explicitly set by the caller or another interceptor. + } + std::optional context; + if (current_ != nullptr) { + context = current_(); + } + if (!context.has_value()) { + context = http::GenerateTraceContext(); + } + request.headers.Set("traceparent", http::FormatTraceparent(*context)); + } + + private: + std::function()> current_; +}; + +} // namespace + +std::shared_ptr ObserveAttempts( + std::function callback) { + return std::make_shared(std::move(callback)); +} + +std::shared_ptr PropagateTraceContext( + std::function()> current) { + return std::make_shared(std::move(current)); +} + +} // namespace smithy diff --git a/runtime/src/http/trace_context.cc b/runtime/src/http/trace_context.cc new file mode 100644 index 00000000..ae2ced13 --- /dev/null +++ b/runtime/src/http/trace_context.cc @@ -0,0 +1,87 @@ +#include "smithy/http/trace_context.h" + +#include +#include +#include +#include + +namespace smithy::http { +namespace { + +bool IsLowerHex(std::string_view text) { + return std::ranges::all_of( + text, [](char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); }); +} + +bool AllZeros(std::string_view text) { + return text.find_first_not_of('0') == std::string_view::npos; +} + +std::string RandomHex(int digits) { + thread_local std::mt19937_64 engine{std::random_device{}()}; + static constexpr std::string_view kHex = "0123456789abcdef"; + std::string out; + out.reserve(static_cast(digits)); + std::uint64_t bits = 0; + for (int i = 0; i < digits; ++i) { + if (i % 16 == 0) bits = engine(); + out.push_back(kHex[bits & 0xF]); + bits >>= 4; + } + return out; +} + +} // namespace + +std::optional ParseTraceparent(std::string_view value) { + // version(2) '-' trace-id(32) '-' parent-id(16) '-' flags(2); longer + // values are allowed for future versions if a '-' follows. + if (value.size() < 55 || value[2] != '-' || value[35] != '-' || value[52] != '-') { + return std::nullopt; + } + if (value.size() > 55 && value[55] != '-') { + return std::nullopt; + } + const std::string_view version = value.substr(0, 2); + const std::string_view trace_id = value.substr(3, 32); + const std::string_view parent_id = value.substr(36, 16); + const std::string_view flags = value.substr(53, 2); + if (!IsLowerHex(version) || version == "ff" || !IsLowerHex(trace_id) || !IsLowerHex(parent_id) || + !IsLowerHex(flags)) { + return std::nullopt; + } + if (AllZeros(trace_id) || AllZeros(parent_id)) { + return std::nullopt; + } + TraceContext context; + context.trace_id = std::string(trace_id); + context.parent_id = std::string(parent_id); + const char low = flags[1]; + const int low_value = low <= '9' ? low - '0' : low - 'a' + 10; + context.sampled = (low_value & 1) != 0; + return context; +} + +std::string FormatTraceparent(const TraceContext& context) { + return "00-" + context.trace_id + "-" + context.parent_id + (context.sampled ? "-01" : "-00"); +} + +TraceContext GenerateTraceContext() { + TraceContext context; + do { + context.trace_id = RandomHex(32); + } while (AllZeros(context.trace_id)); + context.parent_id = GenerateSpanId(); + context.sampled = true; + return context; +} + +std::string GenerateSpanId() { + std::string id; + do { + id = RandomHex(16); + } while (AllZeros(id)); + return id; +} + +} // namespace smithy::http diff --git a/runtime/src/server/middleware.cc b/runtime/src/server/middleware.cc index b10c85e0..6c8fc8d8 100644 --- a/runtime/src/server/middleware.cc +++ b/runtime/src/server/middleware.cc @@ -29,6 +29,8 @@ Middleware Observe(std::function callback, RequestObservation observation; observation.method = request.method; observation.target = request.target; + observation.operation = response.operation; + observation.trace_parent = request.headers.Get("traceparent").value_or(""); observation.status = response.status; observation.duration = std::chrono::duration_cast(now() - start); callback(observation); diff --git a/runtime/src/server/router.cc b/runtime/src/server/router.cc index f15d35cd..7e595590 100644 --- a/runtime/src/server/router.cc +++ b/runtime/src/server/router.cc @@ -58,7 +58,8 @@ Outcome> Router::ParsePattern(std::string_view patt return segments; } -Outcome Router::Add(std::string_view method, std::string_view pattern, RouteHandler handler) { +Outcome Router::Add(std::string_view method, std::string_view pattern, RouteHandler handler, + std::string_view operation) { auto segments = ParsePattern(pattern); if (!segments) return std::move(segments).error(); for (const RouteEntry& existing : routes_) { @@ -77,7 +78,8 @@ Outcome Router::Add(std::string_view method, std::string_view pattern, Rou std::string(pattern)); } } - routes_.push_back(RouteEntry{std::string(method), std::move(*segments), std::move(handler)}); + routes_.push_back(RouteEntry{std::string(method), std::move(*segments), std::move(handler), + std::string(operation)}); return Unit{}; } @@ -177,7 +179,9 @@ http::HttpResponse Router::Route(const http::HttpRequest& request) const { RequestContext context; context.labels = std::move(best_labels); context.query_params = target->query_params; - return best->handler(request, context); + http::HttpResponse response = best->handler(request, context); + if (!best->operation.empty()) response.operation = best->operation; + return response; } } // namespace smithy::server diff --git a/runtime/tests/http/trace_context_test.cc b/runtime/tests/http/trace_context_test.cc new file mode 100644 index 00000000..d9ad3023 --- /dev/null +++ b/runtime/tests/http/trace_context_test.cc @@ -0,0 +1,70 @@ +#include "smithy/http/trace_context.h" + +#include + +#include + +namespace smithy::http { +namespace { + +constexpr char kTrace[] = "0af7651916cd43dd8448eb211c80319c"; +constexpr char kParent[] = "b7ad6b7169203331"; + +TEST(TraceContextTest, ParsesAValidTraceparent) { + const auto context = ParseTraceparent(std::string("00-") + kTrace + "-" + kParent + "-01"); + ASSERT_TRUE(context.has_value()); + EXPECT_EQ(context->trace_id, kTrace); + EXPECT_EQ(context->parent_id, kParent); + EXPECT_TRUE(context->sampled); + + const auto unsampled = ParseTraceparent(std::string("00-") + kTrace + "-" + kParent + "-00"); + ASSERT_TRUE(unsampled.has_value()); + EXPECT_FALSE(unsampled->sampled); +} + +TEST(TraceContextTest, AcceptsFutureVersionsWithTrailingFields) { + const auto context = ParseTraceparent(std::string("01-") + kTrace + "-" + kParent + "-01-extra"); + EXPECT_TRUE(context.has_value()); +} + +TEST(TraceContextTest, RejectsMalformedValues) { + EXPECT_FALSE(ParseTraceparent("").has_value()); + EXPECT_FALSE(ParseTraceparent("garbage").has_value()); + EXPECT_FALSE(ParseTraceparent(std::string("00-") + kTrace + "-" + kParent).has_value()); + // Uppercase hex is invalid per the spec. + EXPECT_FALSE( + ParseTraceparent(std::string("00-0AF7651916CD43DD8448EB211C80319C-") + kParent + "-01") + .has_value()); + // All-zero ids are invalid. + EXPECT_FALSE( + ParseTraceparent(std::string("00-00000000000000000000000000000000-") + kParent + "-01") + .has_value()); + EXPECT_FALSE(ParseTraceparent(std::string("00-") + kTrace + "-0000000000000000-01").has_value()); + // Version ff is forbidden. + EXPECT_FALSE(ParseTraceparent(std::string("ff-") + kTrace + "-" + kParent + "-01").has_value()); + // Version 00 followed by more fields is malformed (no '-' at 55). + EXPECT_FALSE(ParseTraceparent(std::string("00-") + kTrace + "-" + kParent + "-01x").has_value()); +} + +TEST(TraceContextTest, FormatRoundTrips) { + TraceContext context{kTrace, kParent, true}; + EXPECT_EQ(FormatTraceparent(context), std::string("00-") + kTrace + "-" + kParent + "-01"); + const auto parsed = ParseTraceparent(FormatTraceparent(context)); + ASSERT_TRUE(parsed.has_value()); + EXPECT_EQ(parsed->trace_id, context.trace_id); + EXPECT_EQ(parsed->parent_id, context.parent_id); + context.sampled = false; + EXPECT_EQ(FormatTraceparent(context), std::string("00-") + kTrace + "-" + kParent + "-00"); +} + +TEST(TraceContextTest, GeneratedContextsAreValidAndDistinct) { + const TraceContext a = GenerateTraceContext(); + const TraceContext b = GenerateTraceContext(); + EXPECT_TRUE(ParseTraceparent(FormatTraceparent(a)).has_value()); + EXPECT_TRUE(ParseTraceparent(FormatTraceparent(b)).has_value()); + EXPECT_NE(a.trace_id, b.trace_id); + EXPECT_NE(GenerateSpanId(), GenerateSpanId()); +} + +} // namespace +} // namespace smithy::http