aws_smithy_json/protocol/mod.rs
1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Protocol implementations for JSON-based Smithy protocols.
7
8pub mod aws_json_rpc;
9pub mod aws_rest_json_1;
10pub(crate) mod error;
11
12use crate::codec::{JsonCodec, JsonCodecSettings};
13use aws_smithy_schema::protocol::ServiceShapeNamespace;
14use aws_smithy_types::config_bag::ConfigBag;
15
16/// Returns a codec whose `default_namespace` is filled in from the config bag, or `None` when
17/// there is nothing to change.
18///
19/// `default_namespace` resolves relative `__type` discriminators on documents (see
20/// [`JsonCodecSettings::default_namespace`]). It is a *response*-parsing concern only — nothing
21/// on the serialization path reads it, because per the document-types SEP a serializer must
22/// always emit an absolute shape ID.
23///
24/// An explicitly configured namespace always wins: if the codec already carries one, this
25/// returns `None` and the caller uses the codec it already has. The fallback exists for a
26/// protocol constructed by hand and selected at runtime via `Config::builder().protocol(..)`,
27/// which cannot know the model's namespace and would otherwise leave every relative
28/// discriminator unresolved — the type registry would then silently fail to find the shape.
29///
30/// **Currently inert, deliberately.** `default_namespace` is read at exactly one place —
31/// [`JsonDeserializer::read_discriminated_document`](crate::codec::JsonDeserializer::read_discriminated_document)
32/// — and that method has no production caller: it is inherent on the concrete deserializer
33/// rather than on [`ShapeDeserializer`](aws_smithy_schema::serde::ShapeDeserializer), so it is
34/// unreachable through the boxed trait object every protocol's `deserialize_response` returns,
35/// and neither codegen nor any generated crate calls it. The same is true of the
36/// `with_default_namespace` call codegen emits, which configures the same setting. This fallback
37/// exists so that when the document/type-registry response path is wired up, a protocol selected
38/// at runtime resolves relative discriminators without the caller having to know the namespace —
39/// rather than that gap being discovered then. Its effect is asserted at the codec level below,
40/// which is the highest level at which it is currently observable.
41///
42/// **Cost.** Building the replacement clones the settings struct and re-`Arc`s it, measured at
43/// **~46 ns per response**. That is ~1% of the smallest structured-response parse measured on
44/// this path (~3.8 µs for a 339-byte body) and well inside its run-to-run noise, so it is not
45/// worth caching. It is also not paid by generated clients at all: codegen passes the namespace
46/// explicitly, so they take the `None` branch. If a profile ever disagrees, the resolved codec
47/// could be memoized in a `OnceLock` on the protocol — the bag entry is service-scoped and
48/// stored once per client, so it does not vary per request.
49pub(crate) fn codec_with_bag_namespace(codec: &JsonCodec, cfg: &ConfigBag) -> Option<JsonCodec> {
50 if codec.settings().default_namespace().is_some() {
51 return None;
52 }
53 let namespace = cfg.load::<ServiceShapeNamespace>()?;
54 let settings: JsonCodecSettings = codec
55 .settings()
56 .to_builder()
57 .default_namespace(namespace.as_str())
58 .build();
59 Some(JsonCodec::new(settings))
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use aws_smithy_schema::codec::Codec;
66 use aws_smithy_types::config_bag::Layer;
67
68 fn plain_codec() -> JsonCodec {
69 JsonCodec::new(JsonCodecSettings::builder().use_json_name(true).build())
70 }
71
72 fn codec_with_namespace(ns: &str) -> JsonCodec {
73 JsonCodec::new(
74 JsonCodecSettings::builder()
75 .use_json_name(true)
76 .default_namespace(ns)
77 .build(),
78 )
79 }
80
81 fn bag_with_namespace(ns: &'static str) -> ConfigBag {
82 let mut layer = Layer::new("test");
83 layer.store_put(ServiceShapeNamespace::new(ns));
84 ConfigBag::of_layers(vec![layer])
85 }
86
87 /// The whole point of the fallback: a protocol constructed by hand, with no namespace of its
88 /// own, picks up the model's namespace that generated clients leave in the bag.
89 #[test]
90 fn bag_supplies_namespace_when_codec_has_none() {
91 let resolved = codec_with_bag_namespace(
92 &plain_codec(),
93 &bag_with_namespace("com.amazonaws.dynamodb"),
94 )
95 .expect("a replacement codec is produced");
96 assert_eq!(
97 Some("com.amazonaws.dynamodb"),
98 resolved.settings().default_namespace()
99 );
100 }
101
102 /// An explicit `with_default_namespace` must win over the bag, so a caller can still override
103 /// a service whose discriminators are not in its own namespace.
104 #[test]
105 fn explicit_namespace_beats_the_bag() {
106 assert!(
107 codec_with_bag_namespace(
108 &codec_with_namespace("com.example.explicit"),
109 &bag_with_namespace("com.amazonaws.dynamodb"),
110 )
111 .is_none(),
112 "an explicitly configured namespace must not be replaced by the bag"
113 );
114 }
115
116 /// With neither source there is nothing to do, and the caller must keep its own codec rather
117 /// than pay for a rebuild.
118 #[test]
119 fn no_namespace_anywhere_is_a_no_op() {
120 assert!(codec_with_bag_namespace(&plain_codec(), &ConfigBag::base()).is_none());
121 }
122
123 /// Preserves the rest of the settings. A rebuild that silently reset `use_json_name` would
124 /// change wire behavior for every restJson1 response, so this is the load-bearing assertion.
125 /// `field_mapper` has no public accessor, so it is checked behaviorally: `use_json_name(true)`
126 /// means a member's `@jsonName` is honored when reading.
127 #[test]
128 fn rebuild_preserves_other_settings() {
129 let original = JsonCodec::new(
130 JsonCodecSettings::builder()
131 .use_json_name(true)
132 .default_timestamp_format(aws_smithy_types::date_time::Format::DateTime)
133 .max_depth(7)
134 .use_string_for_arbitrary_precision(true)
135 .build(),
136 );
137 let resolved = codec_with_bag_namespace(&original, &bag_with_namespace("com.example"))
138 .expect("a replacement codec is produced");
139 let before = original.settings();
140 let after = resolved.settings();
141 assert_eq!(
142 before.default_timestamp_format(),
143 after.default_timestamp_format()
144 );
145 assert_eq!(before.max_depth(), after.max_depth());
146 assert_eq!(
147 before.use_string_for_arbitrary_precision(),
148 after.use_string_for_arbitrary_precision()
149 );
150 assert_eq!(Some("com.example"), after.default_namespace());
151 }
152
153 /// The resolved codec is not merely labelled — it actually resolves a relative `__type`.
154 /// This is the highest level at which the fallback is observable today, because
155 /// `read_discriminated_document` is inherent on `JsonDeserializer` and has no production
156 /// caller; see the note on [`codec_with_bag_namespace`].
157 #[test]
158 fn resolved_codec_lifts_a_relative_discriminator() {
159 let input = br#"{"__type":"Capacity","CapacityUnits":1.0}"#;
160
161 let plain = plain_codec();
162 let mut before = plain.create_deserializer(&input[..]);
163 assert_eq!(
164 None,
165 before
166 .read_discriminated_document()
167 .expect("parses")
168 .discriminator(),
169 "without a namespace a relative __type must stay unresolved"
170 );
171
172 let resolved =
173 codec_with_bag_namespace(&plain, &bag_with_namespace("com.amazonaws.dynamodb"))
174 .expect("a replacement codec is produced");
175 let mut after = resolved.create_deserializer(&input[..]);
176 assert_eq!(
177 Some("com.amazonaws.dynamodb#Capacity"),
178 after
179 .read_discriminated_document()
180 .expect("parses")
181 .discriminator(),
182 "the bag-derived namespace must actually resolve the discriminator"
183 );
184 }
185}
186
187#[cfg(test)]
188mod codec_rebuild_tests {
189 use super::*;
190 use crate::codec::{JsonCodec, JsonCodecSettings};
191 use aws_smithy_schema::codec::Codec;
192 use aws_smithy_schema::protocol::ServiceShapeNamespace;
193 use aws_smithy_schema::serde::ShapeDeserializer;
194
195 /// `codec_with_bag_namespace` rebuilds the codec's settings to inject the
196 /// service namespace. It must override *only* that, carrying every other
197 /// setting through — otherwise selecting a protocol at runtime silently
198 /// resets strictness on a codec the caller configured. Verified through
199 /// observable behavior rather than the private fields: with
200 /// `allow_integral_float_numbers`, `1.0` reads as a long; without it, the
201 /// integer path rejects a floating-point value.
202 #[test]
203 fn bag_namespace_rebuild_preserves_other_settings() {
204 for flag in [false, true] {
205 let codec = JsonCodec::new(
206 JsonCodecSettings::builder()
207 .allow_integral_float_numbers(flag)
208 .build(),
209 );
210 let mut cfg = ConfigBag::base();
211 cfg.interceptor_state()
212 .store_put(ServiceShapeNamespace::new("com.example"));
213
214 let rebuilt = codec_with_bag_namespace(&codec, &cfg)
215 .expect("namespace is absent from the codec, so it must be rebuilt");
216
217 assert_eq!(rebuilt.settings().default_namespace(), Some("com.example"));
218 assert_eq!(
219 rebuilt
220 .create_deserializer(b"1.0")
221 .read_long(&aws_smithy_schema::prelude::LONG)
222 .is_ok(),
223 flag,
224 "allow_integral_float_numbers({flag}) must survive the rebuild"
225 );
226 }
227 }
228}