Skip to main content

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}