Skip to content

Commit 004a1ae

Browse files
authored
CBOR Router: Fix case sensitivity in service name handling (#4156)
## Fix CBOR Router to preserve service name case ### Context The CBOR Router was normalizing service names to pascal case, which caused routing failures for services with mixed-case naming conventions in their definitions. For example, a service defined as: ```service SomeSERVICE {}``` should be routed as `/service/SomeSERVICE/operation/op`. Previously, this would have failed to route properly. ### Solution This PR modifies the router to preserve the original case of service names as they appear in service definitions, ensuring proper matching during the routing process. ### Testing - Added test cases with mixed-case service names to verify correct routing behavior - Verified that the router now properly handles service names with both upper and lowercase characters
1 parent f967395 commit 004a1ae

File tree

3 files changed

+181
-1
lines changed

3 files changed

+181
-1
lines changed

.changelog/1748945736.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
applies_to:
3+
- server
4+
authors:
5+
- drganjoo
6+
references: []
7+
breaking: true
8+
new_feature: false
9+
bug_fix: true
10+
---
11+
Fixed SmithyRpcV2CBOR Router to properly respect case in service names, preventing routing failures for services with mixed-case service shape ID.

codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerServiceGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class ServerServiceGenerator(
8585
protocol.serverRouterRequestSpec(
8686
operationShape,
8787
operationName,
88-
serviceName,
88+
serviceId.name,
8989
smithyHttpServer.resolve("routing::request_spec"),
9090
)
9191
val functionName = RustReservedWords.escapeIfNeeded(operationName.toSnakeCase())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.rust.codegen.server.smithy.protocols.serialize
6+
7+
import org.junit.jupiter.api.Test
8+
import software.amazon.smithy.model.shapes.ShapeId
9+
import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency
10+
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
11+
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
12+
import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams
13+
import software.amazon.smithy.rust.codegen.core.testutil.ServerAdditionalSettings
14+
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
15+
import software.amazon.smithy.rust.codegen.core.testutil.testModule
16+
import software.amazon.smithy.rust.codegen.core.testutil.tokioTest
17+
import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverIntegrationTest
18+
19+
class CborServiceShapePreservesCasing {
20+
val model =
21+
"""
22+
namespace test
23+
24+
use smithy.rust#serde
25+
use smithy.protocols#rpcv2Cbor
26+
use smithy.framework#ValidationException
27+
28+
@rpcv2Cbor
29+
service SampleServiceWITHDifferentCASE {
30+
operations: [SampleOP],
31+
}
32+
operation SampleOP {
33+
input:= { x: String }
34+
output:= { y: String }
35+
}
36+
""".asSmithyModel(smithyVersion = "2")
37+
38+
val codegenScope =
39+
arrayOf(
40+
"SerdeJson" to CargoDependency.SerdeJson.toDevDependency().toType(),
41+
"Ciborium" to CargoDependency.Ciborium.toDevDependency().toType(),
42+
"Hyper" to RuntimeType.Hyper,
43+
"Http" to RuntimeType.Http,
44+
"Tower" to RuntimeType.Tower,
45+
"HashMap" to RuntimeType.HashMap,
46+
*RuntimeType.preludeScope,
47+
)
48+
49+
@Test
50+
fun `service shape ID is preserved`() {
51+
val serviceShape = model.expectShape(ShapeId.from("test#SampleServiceWITHDifferentCASE"))
52+
serverIntegrationTest(
53+
model,
54+
params = IntegrationTestParams(service = serviceShape.id.toString(), additionalSettings = ServerAdditionalSettings.builder().generateCodegenComments().toObjectNode()),
55+
) { _codegenContext, rustCrate ->
56+
rustCrate.testModule {
57+
rustTemplate(
58+
"""
59+
async fn handler(input: crate::input::SampleOpInput) -> crate::output::SampleOpOutput {
60+
assert_eq!(
61+
input.x.expect("missing value for x"),
62+
"test",
63+
"input does not contain the correct data"
64+
);
65+
crate::output::SampleOpOutput {
66+
y: Some("test response".to_owned()),
67+
}
68+
}
69+
70+
fn get_input() -> Vec<u8> {
71+
let json = r##"{"x": "test"}"##;
72+
let value: #{SerdeJson}::Value = #{SerdeJson}::from_str(json).expect("cannot parse JSON");
73+
let mut cbor_data = #{Vec}::new();
74+
#{Ciborium}::ser::into_writer(&value, &mut cbor_data)
75+
.expect("cannot write JSON to CBOR");
76+
cbor_data
77+
}
78+
""",
79+
*codegenScope,
80+
)
81+
82+
tokioTest("success_response") {
83+
rustTemplate(
84+
"""
85+
let config = crate::SampleServiceWithDifferentCaseConfig::builder().build();
86+
let service = crate::SampleServiceWithDifferentCase::builder(config)
87+
.sample_op(handler)
88+
.build()
89+
.expect("could not build service");
90+
91+
let cbor_data = get_input();
92+
// Create a test request
93+
let request = #{Http}::Request::builder()
94+
.uri("/service/SampleServiceWITHDifferentCASE/operation/SampleOP")
95+
.method("POST")
96+
.header("content-type", "application/cbor")
97+
.header("Smithy-Protocol", "rpc-v2-cbor")
98+
.body(#{Hyper}::Body::from(cbor_data))
99+
.expect("Failed to build request");
100+
101+
let response = #{Tower}::ServiceExt::oneshot(service, request)
102+
.await
103+
.expect("Failed to call service");
104+
assert!(response.status().is_success());
105+
106+
let body_bytes = #{Hyper}::body::to_bytes(response.into_body())
107+
.await
108+
.expect("could not get bytes from the body");
109+
let data: #{HashMap}<String, serde_json::Value> =
110+
#{Ciborium}::de::from_reader(body_bytes.as_ref()).expect("could not convert into BTreeMap");
111+
112+
let value = data.get("y")
113+
.and_then(|y| y.as_str())
114+
.expect("y does not exist");
115+
assert_eq!(value, "test response", "response doesn't contain expected value");
116+
""",
117+
*codegenScope,
118+
)
119+
}
120+
121+
tokioTest("incorrect_case_fails") {
122+
rustTemplate(
123+
"""
124+
let config = crate::SampleServiceWithDifferentCaseConfig::builder().build();
125+
let service = crate::SampleServiceWithDifferentCase::builder(config)
126+
.sample_op(handler)
127+
.build()
128+
.expect("could not build service");
129+
130+
let cbor_data = get_input();
131+
// Test with incorrect case in service name
132+
let request = #{Http}::Request::builder()
133+
.uri("/service/SampleServiceWithDifferentCase/operation/SampleOP")
134+
.method("POST")
135+
.header("content-type", "application/cbor")
136+
.header("Smithy-Protocol", "rpc-v2-cbor")
137+
.body(#{Hyper}::Body::from(cbor_data.clone()))
138+
.expect("failed to build request");
139+
140+
let response = #{Tower}::ServiceExt::oneshot(service.clone(), request)
141+
.await
142+
.expect("failed to call service");
143+
144+
// Should return 404 Not Found
145+
assert_eq!(response.status(), #{Http}::StatusCode::NOT_FOUND);
146+
147+
// Test with incorrect case in operation name
148+
let request = #{Http}::Request::builder()
149+
.uri("/service/SampleServiceWITHDifferentCASE/operation/sampleop") // lowercase operation
150+
.method("POST")
151+
.header("content-type", "application/cbor")
152+
.header("Smithy-Protocol", "rpc-v2-cbor")
153+
.body(#{Hyper}::Body::from(cbor_data))
154+
.expect("failed to build request");
155+
156+
let response = #{Tower}::ServiceExt::oneshot(service, request)
157+
.await
158+
.expect("failed to call service");
159+
160+
// Should return 404 Not Found
161+
assert_eq!(response.status(), #{Http}::StatusCode::NOT_FOUND);
162+
""",
163+
*codegenScope,
164+
)
165+
}
166+
}
167+
}
168+
}
169+
}

0 commit comments

Comments
 (0)