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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,18 @@ jobs:
run: bazelisk test //... --config=ci --config=asan --config=ubsan

consumer:
name: bazel consumer (quick start)
runs-on: ubuntu-24.04
name: bazel consumer (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-14, windows-2022]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
# The quick-start acceptance test (docs/quickstart.md): a standalone
# out-of-tree Bazel module consumes smithy_cpp via the rules in
# bazel/defs.bzl — generation runs inside the consumer's build graph.
# bazel/defs.bzl — generation runs inside the consumer's build graph,
# on every OS the runtime itself supports.
- name: bazel test (out-of-tree module)
working-directory: examples/bazel-consumer
run: bazelisk test //... --verbose_failures
Expand Down
1 change: 1 addition & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
bazel_dep(name = "boost.asio", version = "1.87.0.bcr.1")
bazel_dep(name = "boost.beast", version = "1.87.0")
bazel_dep(name = "nlohmann_json", version = "3.12.0.bcr.1")
bazel_dep(name = "zlib", version = "1.3.1.bcr.8")

# boost.container 1.87's registry overlay calls native.cc_test without a
# load(), the one stale file in the boost 1.87 graph (audited); patch it
Expand Down
3 changes: 2 additions & 1 deletion MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ See [`docs/PLAN.md`](docs/PLAN.md) for the full phased plan and
| 4 | Server generation (restJson1 + rpcv2Cbor) | ✅ Done — handlers, routing, serde, all HTTP bindings incl. `@httpPayload`/`@httpPrefixHeaders`, constraint validation, parser strictness, content negotiation; ~1,175 official conformance cases green ([docs/server-guide.md](docs/server-guide.md)) |
| 5 | Generated-client ↔ generated-server integration harness | ✅ Done — every fixture ships a generated integration suite: seeded random round-trips over loopback and real sockets, per-error mapping, unknown-member tolerance, mutation-checked ([docs/design/integration-testing.md](docs/design/integration-testing.md)) |
| 6 | Bazel rules, CLI, packaging (BCR + Maven Central), docs site | 🔨 In progress — `smithy_cpp_{types,client,server}_library` rules run the generator hermetically inside the build graph, out-of-tree consumer module tested in CI, CLI via `bazel run //codegen:generator` ([docs/quickstart.md](docs/quickstart.md)); BCR/Maven publishing deferred until production validation; docs site pending |
| 7 | Hardening, fuzzing, v0.1.0 | Not started |
| 7 | Hardening, fuzzing, v0.1.0 | 🔨 In progress — retries with full-jitter exponential backoff, gzip `@requestCompression` (client + server), consumer CI across linux/macos/windows ([docs/production-guide.md](docs/production-guide.md)) |
| 8 | Bidirectional streaming (event streams, WebSockets) | Not started |

## Building
Expand Down
1 change: 1 addition & 0 deletions bazel/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ _GENERATOR = Label("//codegen:generator")
_RUNTIME_DEPS = [
Label("//runtime:cbor"),
Label("//runtime:client"),
Label("//runtime:compression"),
Label("//runtime:core"),
Label("//runtime:http"),
Label("//runtime:json"),
Expand Down
5 changes: 2 additions & 3 deletions codegen/smithy-cpp-codegen/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,8 @@ val generateRpcv2CborProtocolTests = registerProtocolTestTask(
)

// The constraint-validation suite: httpMalformedRequestTests only (no
// httpRequestTests/httpResponseTests), so malformed generation is enabled for
// this module. The main suites' malformed tests (parser strictness) are a
// Phase 4d follow-up.
// httpRequestTests/httpResponseTests). The main suites above also run their
// malformed tests (parser strictness, since Phase 4d).
val generateRestJson1ValidationProtocolTests = registerProtocolTestTask(
"generateRestJson1ValidationProtocolTests",
"aws.protocoltests.restjson.validation#RestJsonValidation",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ static void run(
ProtocolGenerator protocol,
boolean hasClient,
boolean hasSerde,
boolean hasServer) {
boolean hasServer,
boolean hasCompression) {
CppSettings settings = context.settings();
StringBuilder out = new StringBuilder();
out.append(
Expand Down Expand Up @@ -59,6 +60,9 @@ static void run(
deps.add("\"" + pkg + ":client\"");
deps.add("\"" + pkg + ":core\"");
deps.add("\"" + pkg + ":http\"");
if (hasCompression) {
deps.add("\"" + pkg + ":compression\"");
}
for (String dep : protocol.runtimeDeps()) {
deps.add("\"" + pkg + dep + "\"");
}
Expand All @@ -83,6 +87,9 @@ static void run(
serverDeps.add("\"" + pkg + ":core\"");
serverDeps.add("\"" + pkg + ":http\"");
serverDeps.add("\"" + pkg + ":server\"");
if (hasCompression) {
serverDeps.add("\"" + pkg + ":compression\"");
}
for (String dep : protocol.runtimeDeps()) {
serverDeps.add("\"" + pkg + dep + "\"");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ private void writeSource(CppWriter w) {
w.openBlock("if (!request.body.empty()) {");
w.write("request.headers.Set(\"content-length\", std::to_string(request.body.size()));");
w.closeBlock("}");
w.write("return transport_->Send(request);");
w.write("return smithy::SendWithRetries(*transport_, request, config_.retry);");
w.closeBlock("}");
w.write("");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,15 @@ public void generateService(GenerateServiceDirective<CppContext, CppSettings> di
}
}
if (directive.settings().emitBuildFile()) {
BuildFileGenerator.run(directive.context(), protocol, hasClient, hasSerde, hasServer);
boolean hasCompression =
protocol != null
&& directive.context().model().getOperationShapes().stream()
.anyMatch(
op ->
op.hasTrait(
software.amazon.smithy.model.traits.RequestCompressionTrait.class));
BuildFileGenerator.run(
directive.context(), protocol, hasClient, hasSerde, hasServer, hasCompression);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ private void writeFixture(CppWriter w) {
w.write("handler_ = std::make_shared<ScriptedHandler>();");
w.write("server_ = std::make_unique<$LServer>(handler_);", name);
w.write("smithy::ClientConfig config;");
w.write("config.retry.max_attempts = 1; // wire-exact tests: no retries");
w.openBlock("if (GetParam() == TransportKind::kLoopback) {");
w.write("auto loopback = std::make_shared<smithy::http::Loopback>();");
w.write("ASSERT_TRUE(loopback->Start(server_->Handler()).ok());");
Expand Down Expand Up @@ -305,6 +306,7 @@ private void writeUnknownFieldTest(CppWriter w, OperationShape operation) {
"auto transport = std::make_shared<smithy::testing::MutatingTransport>(loopback, "
+ "inject);");
w.write("smithy::ClientConfig config;");
w.write("config.retry.max_attempts = 1; // wire-exact tests: no retries");
w.write("config.http_client = transport;");
w.write("auto client = *$LClient::Create(std::move(config));", name);
w.write("Rng rng{std::mt19937{99U}, /*fill_all=*/true};");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,62 @@ static String int64Bounds(software.amazon.smithy.model.shapes.ShapeType type) {
};
}

/**
* Client-side @requestCompression: gzip the request body once it reaches the configured minimum
* size, appending to any member-bound Content-Encoding header. Emitted after the body and every
* header binding; Send() computes content-length afterwards.
*/
static void writeRequestCompression(
CppWriter w, software.amazon.smithy.model.shapes.OperationShape operation) {
var trait =
operation.getTrait(software.amazon.smithy.model.traits.RequestCompressionTrait.class);
if (trait.isEmpty() || !trait.get().getEncodings().contains("gzip")) {
return;
}
w.addInclude("\"smithy/compression/gzip.h\"");
w.addInclude("<cstddef>");
w.write("// @requestCompression(gzip): applied last, appended to Content-Encoding.");
w.openBlock(
"if (request.body.size() >= "
+ "static_cast<std::size_t>(config_.request_min_compression_size_bytes)) {");
w.write("auto compressed = smithy::GzipCompress(request.body);");
w.write("if (!compressed) return std::move(compressed).error();");
w.write("request.body = *std::move(compressed);");
w.write("const auto existing_encoding = request.headers.Get(\"content-encoding\");");
w.write(
"request.headers.Set(\"content-encoding\", existing_encoding.has_value() && "
+ "!existing_encoding->empty() ? *existing_encoding + \", gzip\" : \"gzip\");");
w.closeBlock("}");
}

/**
* Server-side inverse: transparently gunzip request bodies for @requestCompression operations
* when the (final) Content-Encoding is gzip. Emitted at the top of the route lambda.
*/
static void writeRequestDecompression(
CppWriter w,
software.amazon.smithy.model.shapes.OperationShape operation,
String errorFn,
String errorCode) {
var trait =
operation.getTrait(software.amazon.smithy.model.traits.RequestCompressionTrait.class);
if (trait.isEmpty() || !trait.get().getEncodings().contains("gzip")) {
return;
}
w.addInclude("\"smithy/compression/gzip.h\"");
w.write("// @requestCompression(gzip): decode before parsing.");
w.openBlock(
"if (const auto request_encoding = request.headers.Get(\"content-encoding\"); "
+ "request_encoding.has_value() && (*request_encoding == \"gzip\" || "
+ "request_encoding->ends_with(\", gzip\"))) {");
w.write("auto decompressed = smithy::GzipDecompress(request.body);");
w.openBlock("if (!decompressed) {");
w.write("return $L(400, $S, \"invalid gzip request body\", {});", errorFn, errorCode);
w.closeBlock("}");
w.write("request.body = *std::move(decompressed);");
w.closeBlock("}");
}

/** HTTP status for a modeled error shape: @httpError, else @error class default. */
static int errorStatus(StructureShape shape) {
var httpError = shape.getTrait(software.amazon.smithy.model.traits.HttpErrorTrait.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ private void writeFixture(CppWriter w) {
w.openBlock("Fixture MakeFixture(const std::string& endpoint = \"\") {");
w.write("auto transport = std::make_shared<smithy::testing::CapturingTransport>();");
w.write("smithy::ClientConfig config;");
w.write("config.retry.max_attempts = 1; // wire-exact tests: no retries");
w.write("config.http_client = transport;");
w.write("config.endpoint = endpoint;");
w.write("// Create cannot fail when a transport is injected.");
Expand Down Expand Up @@ -678,6 +679,7 @@ private void writeMinimalRequestHelper(CppWriter w, OperationShape operation) {
w.openBlock("smithy::http::HttpRequest MinimalRequestFor$L() {", opName);
w.write("auto transport = std::make_shared<smithy::testing::CapturingTransport>();");
w.write("smithy::ClientConfig config;");
w.write("config.retry.max_attempts = 1; // wire-exact tests: no retries");
w.write("config.http_client = transport;");
w.write("auto client = *$L::Create(std::move(config));", clientType());
w.writeWithNoFormatting(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ public void writeOperationBody(
w.write(
"request.headers.Set(\"accept\", $S);", payloadContentType(context, operation, false));
}
ProtocolSupport.writeRequestCompression(w, operation);
w.write("auto response = Send(std::move(request));");
w.write("if (!response) return std::move(response).error();");
if (responseCode != null) {
Expand Down Expand Up @@ -1313,11 +1314,21 @@ public void writeServerRoute(
b ->
b.getLocation() == HttpBinding.Location.DOCUMENT
|| b.getLocation() == HttpBinding.Location.PAYLOAD);
boolean compressed =
operation
.getTrait(software.amazon.smithy.model.traits.RequestCompressionTrait.class)
.map(t -> t.getEncodings().contains("gzip"))
.orElse(false);
w.openBlock(
"(void)router_->Add($S, $S, [handler](const smithy::http::HttpRequest& request, "
"(void)router_->Add($S, $S, [handler](const smithy::http::HttpRequest& $L, "
+ "const smithy::server::RequestContext& context) -> smithy::http::HttpResponse {",
http.getMethod(),
pattern.toString());
pattern.toString(),
compressed ? "raw_request" : "request");
if (compressed) {
w.write("smithy::http::HttpRequest request = raw_request;");
ProtocolSupport.writeRequestDecompression(w, operation, "JsonError", "");
}
StructureShape inputShape = ProtocolSupport.inputShape(context, operation);
boolean noModeledInput =
inputShape.getId().toString().equals("smithy.api#Unit")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,23 @@ public void writeServerRoute(
String inputType = context.cppSymbols().toSymbol(input).getName();
String opName = CppReservedWords.escape(operation.getId().getName());

boolean compressed =
operation
.getTrait(software.amazon.smithy.model.traits.RequestCompressionTrait.class)
.map(t -> t.getEncodings().contains("gzip"))
.orElse(false);
w.openBlock(
"(void)router_->Add(\"POST\", \"/service/$L/operation/$L\", "
+ "[handler](const smithy::http::HttpRequest& request, "
+ "[handler](const smithy::http::HttpRequest& $L, "
+ "const smithy::server::RequestContext&) -> smithy::http::HttpResponse {",
service.getId().getName(),
operation.getId().getName());
operation.getId().getName(),
compressed ? "raw_request" : "request");
if (compressed) {
w.write("smithy::http::HttpRequest request = raw_request;");
ProtocolSupport.writeRequestDecompression(
w, operation, "CborError", "SerializationException");
}
w.openBlock(
"if (request.headers.Get(\"smithy-protocol\").value_or(\"\") != \"rpc-v2-cbor\") {");
w.write(
Expand Down Expand Up @@ -185,6 +196,7 @@ public void writeOperationBody(
in);
}

ProtocolSupport.writeRequestCompression(w, operation);
w.write("auto response = Send(std::move(request));");
w.write("if (!response) return std::move(response).error();");
w.write(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ private void writeSource(CppWriter w) {
w.write("auto loopback = std::make_shared<smithy::http::Loopback>();");
w.write("(void)loopback->Start(server.Handler());");
w.write("smithy::ClientConfig config;");
w.write("config.retry.max_attempts = 1; // wire-exact tests: no retries");
w.write("config.http_client = loopback;");
w.write("// Create cannot fail when a transport is injected.");
w.write("return *$LClient::Create(std::move(config));", name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ aws.protocoltests.restjson#RestJson any RestJsonInputAndOutputWithQuotedStringHe
# @httpChecksumRequired (Content-MD5) is not implemented yet.
aws.protocoltests.restjson#RestJson request RestJsonHttpChecksumRequired @httpChecksumRequired is not implemented yet

# @requestCompression (gzip) is not implemented yet.
aws.protocoltests.restjson#RestJson request SDKAppliedContentEncoding_restJson1 @requestCompression is not implemented yet
aws.protocoltests.restjson#RestJson request SDKAppendedGzipAfterProvidedEncoding_restJson1 @requestCompression is not implemented yet

# The test asserts an all-zero UUID, which needs an injectable token source.
aws.protocoltests.restjson#RestJson request RestJsonQueryIdempotencyTokenAutoFill deterministic idempotency tokens need an injectable source
Expand All @@ -48,9 +45,11 @@ aws.protocoltests.restjson#RestJson server-request RestJsonSupportsNaNFloatHeade
aws.protocoltests.restjson#RestJson server-request RestJsonSupportsNaNFloatQueryValues NaN input members compare unequal under operator==
smithy.protocoltests.rpcv2Cbor#RpcV2Protocol server-request RpcV2CborSupportsNaNFloatInputs NaN input members compare unequal under operator==

# @requestCompression (gzip) is not implemented yet.
aws.protocoltests.restjson#RestJson server-request SDKAppliedContentEncoding_restJson1 @requestCompression is not implemented yet
aws.protocoltests.restjson#RestJson server-request SDKAppendedGzipAfterProvidedEncoding_restJson1 @requestCompression is not implemented yet
# These cases define no wire body (client-only semantics: they assert the
# Content-Encoding header the client sends), so a server cannot parse the
# expected params from them.
aws.protocoltests.restjson#RestJson server-request SDKAppliedContentEncoding_restJson1 the case has no wire body for the server to parse
aws.protocoltests.restjson#RestJson server-request SDKAppendedGzipAfterProvidedEncoding_restJson1 the case has no wire body for the server to parse

# Absent query list params deserialize as unset members, not engaged empty lists.
aws.protocoltests.restjson#RestJson server-request RestJsonOmitsEmptyListQueryValues absent query lists stay unset (nullopt), not engaged empty lists
Expand Down
8 changes: 8 additions & 0 deletions docs/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,14 @@ without reading generator internals or touching Gradle.
test) — if the tutorial breaks, CI fails.
- Bazel rules tested against the out-of-tree consumer module on current Bazel 9 releases
(tracked via bazelisk pinning).
- **Follow-up**: run the consumer acceptance job on the full OS matrix (Linux/macOS/Windows),
not just Linux.
- **Follow-up — incremental development flows**: support and document the model-evolution loop
after initial integration: change the model (add an operation or member, tighten a
constraint), rebuild, and let the handler-interface compile errors guide the update;
regenerate vendored CLI output safely; keep hand-written handlers and tests working across
regenerations. Exercise the flow in CI (scripted evolve-and-rebuild against the consumer
example).

**Docs:** the docs site is the deliverable.

Expand Down
Loading
Loading