aws_smithy_schema/
lib.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6/* Automatically managed default lints */
7#![cfg_attr(docsrs, feature(doc_cfg))]
8/* End of automatically managed default lints */
9
10//! Runtime schema types for Smithy shapes.
11//!
12//! This module provides the core types for representing Smithy schemas at runtime,
13//! enabling protocol-agnostic serialization and deserialization.
14
15mod schema {
16    pub mod shape_id;
17    pub mod shape_type;
18    pub mod trait_map;
19    pub mod trait_type;
20    pub mod traits;
21
22    pub mod codec;
23    pub mod http_protocol;
24    pub mod prelude;
25    pub mod protocol;
26    pub mod serde;
27}
28
29pub use schema::shape_id::ShapeId;
30pub use schema::shape_type::ShapeType;
31pub use schema::trait_map::TraitMap;
32pub use schema::trait_type::Trait;
33pub use schema::trait_type::{AnnotationTrait, DocumentTrait, StringTrait};
34
35pub mod prelude {
36    pub use crate::schema::prelude::*;
37}
38
39pub mod serde {
40    pub use crate::schema::serde::*;
41}
42
43pub mod traits {
44    pub use crate::schema::traits::*;
45}
46
47pub mod codec {
48    pub use crate::schema::codec::*;
49}
50
51pub mod protocol {
52    pub use crate::schema::protocol::*;
53}
54
55pub mod http_protocol {
56    pub use crate::schema::http_protocol::*;
57}
58
59/// A Smithy schema — a lightweight runtime representation of a Smithy shape.
60///
61/// Contains the shape's ID, type, traits relevant to serialization, and
62/// references to member schemas (for aggregate types).
63///
64/// Schemas are constructed at compile time (via `const`) for generated code
65/// and prelude types. The Smithy type system is closed, so no extensibility
66/// via trait objects is needed.
67use schema::traits as trait_types;
68
69#[derive(Debug)]
70pub struct Schema {
71    id: ShapeId,
72    shape_type: ShapeType,
73    /// Member name if this is a member schema.
74    member_name: Option<&'static str>,
75    /// Member index for position-based lookup in generated code.
76    member_index: Option<usize>,
77    /// Shape-type-specific member data.
78    members: SchemaMembers,
79
80    // -- Known serde trait fields (const-constructable) --
81    // IMPORTANT: These fields and their `with_*` setters must stay in sync with
82    // `knownTraitSetter` in `SchemaGenerator.kt`. If a new known trait is added
83    // here, a corresponding entry must be added in the codegen.
84    sensitive: Option<trait_types::SensitiveTrait>,
85    json_name: Option<trait_types::JsonNameTrait>,
86    timestamp_format: Option<trait_types::TimestampFormatTrait>,
87    xml_name: Option<trait_types::XmlNameTrait>,
88    xml_attribute: Option<trait_types::XmlAttributeTrait>,
89    xml_flattened: Option<trait_types::XmlFlattenedTrait>,
90    xml_namespace: Option<trait_types::XmlNamespaceTrait>,
91    http_header: Option<trait_types::HttpHeaderTrait>,
92    http_label: Option<trait_types::HttpLabelTrait>,
93    http_payload: Option<trait_types::HttpPayloadTrait>,
94    http_prefix_headers: Option<trait_types::HttpPrefixHeadersTrait>,
95    http_query: Option<trait_types::HttpQueryTrait>,
96    http_query_params: Option<trait_types::HttpQueryParamsTrait>,
97    http_response_code: Option<trait_types::HttpResponseCodeTrait>,
98    /// The `@http` trait — an operation-level trait included on the input schema
99    /// for convenience so the protocol serializer can construct the request URI.
100    http: Option<trait_types::HttpTrait>,
101    streaming: Option<trait_types::StreamingTrait>,
102    event_header: Option<trait_types::EventHeaderTrait>,
103    event_payload: Option<trait_types::EventPayloadTrait>,
104    host_label: Option<trait_types::HostLabelTrait>,
105    media_type: Option<trait_types::MediaTypeTrait>,
106
107    /// Fallback for unknown/custom traits. `None` in const contexts (no allocation).
108    traits: Option<&'static std::sync::LazyLock<TraitMap>>,
109}
110
111/// Shape-type-specific member references.
112#[derive(Debug)]
113enum SchemaMembers {
114    /// No members (simple types).
115    None,
116    /// Structure or union members.
117    Struct { members: &'static [&'static Schema] },
118    /// List member schema.
119    List { member: &'static Schema },
120    /// Map key and value schemas.
121    Map {
122        key: &'static Schema,
123        value: &'static Schema,
124    },
125}
126
127impl Schema {
128    /// Default values for all trait fields (should only be used by constructors as a spread source).
129    const EMPTY_TRAITS: Self = Self {
130        id: ShapeId::from_static("", "", ""),
131        shape_type: ShapeType::Boolean,
132        member_name: None,
133        member_index: None,
134        members: SchemaMembers::None,
135        sensitive: None,
136        json_name: None,
137        timestamp_format: None,
138        xml_name: None,
139        xml_attribute: None,
140        xml_flattened: None,
141        xml_namespace: None,
142        http_header: None,
143        http_label: None,
144        http_payload: None,
145        http_prefix_headers: None,
146        http_query: None,
147        http_query_params: None,
148        http_response_code: None,
149        http: None,
150        streaming: None,
151        event_header: None,
152        event_payload: None,
153        host_label: None,
154        media_type: None,
155        traits: None,
156    };
157
158    /// Creates a schema for a simple type (no members).
159    pub const fn new(id: ShapeId, shape_type: ShapeType) -> Self {
160        Self {
161            id,
162            shape_type,
163            ..Self::EMPTY_TRAITS
164        }
165    }
166
167    /// Creates a schema for a structure or union type.
168    pub const fn new_struct(
169        id: ShapeId,
170        shape_type: ShapeType,
171        members: &'static [&'static Schema],
172    ) -> Self {
173        Self {
174            id,
175            shape_type,
176            members: SchemaMembers::Struct { members },
177            ..Self::EMPTY_TRAITS
178        }
179    }
180
181    /// Creates a schema for a list type.
182    pub const fn new_list(id: ShapeId, member: &'static Schema) -> Self {
183        Self {
184            id,
185            shape_type: ShapeType::List,
186            members: SchemaMembers::List { member },
187            ..Self::EMPTY_TRAITS
188        }
189    }
190
191    /// Creates a schema for a map type.
192    pub const fn new_map(id: ShapeId, key: &'static Schema, value: &'static Schema) -> Self {
193        Self {
194            id,
195            shape_type: ShapeType::Map,
196            members: SchemaMembers::Map { key, value },
197            ..Self::EMPTY_TRAITS
198        }
199    }
200
201    /// Creates a member schema wrapping a target schema.
202    pub const fn new_member(
203        id: ShapeId,
204        shape_type: ShapeType,
205        member_name: &'static str,
206        member_index: usize,
207    ) -> Self {
208        Self {
209            id,
210            shape_type,
211            member_name: Some(member_name),
212            member_index: Some(member_index),
213            ..Self::EMPTY_TRAITS
214        }
215    }
216
217    /// Returns the Shape ID of this schema.
218    pub fn shape_id(&self) -> &ShapeId {
219        &self.id
220    }
221
222    /// Returns the shape type.
223    pub fn shape_type(&self) -> ShapeType {
224        self.shape_type
225    }
226
227    /// Returns the fallback trait map for unknown/custom traits.
228    pub fn traits(&self) -> Option<&TraitMap> {
229        self.traits.map(|lazy| &**lazy)
230    }
231
232    // -- Known trait accessors --
233
234    /// Returns the `@sensitive` trait if present.
235    pub fn sensitive(&self) -> Option<&trait_types::SensitiveTrait> {
236        self.sensitive.as_ref()
237    }
238
239    /// Returns the `@jsonName` value if present.
240    pub fn json_name(&self) -> Option<&trait_types::JsonNameTrait> {
241        self.json_name.as_ref()
242    }
243
244    /// Returns the `@timestampFormat` if present.
245    pub fn timestamp_format(&self) -> Option<&trait_types::TimestampFormatTrait> {
246        self.timestamp_format.as_ref()
247    }
248
249    /// Returns the `@xmlName` value if present.
250    pub fn xml_name(&self) -> Option<&trait_types::XmlNameTrait> {
251        self.xml_name.as_ref()
252    }
253
254    /// Returns the `@httpHeader` value if present.
255    /// Returns `true` if this member schema has any HTTP response binding trait
256    /// (`@httpHeader`, `@httpResponseCode`, `@httpPrefixHeaders`, or `@httpPayload`).
257    pub fn has_http_response_binding(&self) -> bool {
258        self.http_header.is_some()
259            || self.http_response_code.is_some()
260            || self.http_prefix_headers.is_some()
261            || self.http_payload.is_some()
262    }
263
264    pub fn http_header(&self) -> Option<&trait_types::HttpHeaderTrait> {
265        self.http_header.as_ref()
266    }
267
268    /// Returns the `@httpQuery` value if present.
269    pub fn http_query(&self) -> Option<&trait_types::HttpQueryTrait> {
270        self.http_query.as_ref()
271    }
272
273    /// Returns the `@httpLabel` trait if present.
274    pub fn http_label(&self) -> Option<&trait_types::HttpLabelTrait> {
275        self.http_label.as_ref()
276    }
277
278    /// Returns the `@httpPayload` trait if present.
279    pub fn http_payload(&self) -> Option<&trait_types::HttpPayloadTrait> {
280        self.http_payload.as_ref()
281    }
282
283    /// Returns the `@httpPrefixHeaders` value if present.
284    pub fn http_prefix_headers(&self) -> Option<&trait_types::HttpPrefixHeadersTrait> {
285        self.http_prefix_headers.as_ref()
286    }
287
288    /// Returns the `@mediaType` trait if present.
289    pub fn media_type(&self) -> Option<&trait_types::MediaTypeTrait> {
290        self.media_type.as_ref()
291    }
292
293    /// Returns the `@httpQueryParams` trait if present.
294    pub fn http_query_params(&self) -> Option<&trait_types::HttpQueryParamsTrait> {
295        self.http_query_params.as_ref()
296    }
297
298    /// Returns the `@httpResponseCode` trait if present.
299    pub fn http_response_code(&self) -> Option<&trait_types::HttpResponseCodeTrait> {
300        self.http_response_code.as_ref()
301    }
302
303    /// Returns the `@http` trait if present.
304    ///
305    /// This is an operation-level trait included on the input schema for
306    /// convenience so the protocol serializer can construct the request URI.
307    pub fn http(&self) -> Option<&trait_types::HttpTrait> {
308        self.http.as_ref()
309    }
310
311    // -- Const setters for builder-style construction in generated code --
312
313    /// Sets the `@sensitive` trait.
314    pub const fn with_sensitive(mut self) -> Self {
315        self.sensitive = Some(trait_types::SensitiveTrait);
316        self
317    }
318
319    /// Sets the `@jsonName` trait.
320    pub const fn with_json_name(mut self, value: &'static str) -> Self {
321        self.json_name = Some(trait_types::JsonNameTrait::new(value));
322        self
323    }
324
325    /// Sets the `@timestampFormat` trait.
326    pub const fn with_timestamp_format(mut self, format: trait_types::TimestampFormat) -> Self {
327        self.timestamp_format = Some(trait_types::TimestampFormatTrait::new(format));
328        self
329    }
330
331    /// Sets the `@xmlName` trait.
332    pub const fn with_xml_name(mut self, value: &'static str) -> Self {
333        self.xml_name = Some(trait_types::XmlNameTrait::new(value));
334        self
335    }
336
337    /// Sets the `@xmlAttribute` trait.
338    pub const fn with_xml_attribute(mut self) -> Self {
339        self.xml_attribute = Some(trait_types::XmlAttributeTrait);
340        self
341    }
342
343    /// Sets the `@xmlFlattened` trait.
344    pub const fn with_xml_flattened(mut self) -> Self {
345        self.xml_flattened = Some(trait_types::XmlFlattenedTrait);
346        self
347    }
348
349    /// Sets the `@httpHeader` trait.
350    pub const fn with_http_header(mut self, value: &'static str) -> Self {
351        self.http_header = Some(trait_types::HttpHeaderTrait::new(value));
352        self
353    }
354
355    /// Sets the `@httpLabel` trait.
356    pub const fn with_http_label(mut self) -> Self {
357        self.http_label = Some(trait_types::HttpLabelTrait);
358        self
359    }
360
361    /// Sets the `@httpPayload` trait.
362    pub const fn with_http_payload(mut self) -> Self {
363        self.http_payload = Some(trait_types::HttpPayloadTrait);
364        self
365    }
366
367    /// Sets the `@httpPrefixHeaders` trait.
368    pub const fn with_http_prefix_headers(mut self, value: &'static str) -> Self {
369        self.http_prefix_headers = Some(trait_types::HttpPrefixHeadersTrait::new(value));
370        self
371    }
372
373    /// Sets the `@httpQuery` trait.
374    pub const fn with_http_query(mut self, value: &'static str) -> Self {
375        self.http_query = Some(trait_types::HttpQueryTrait::new(value));
376        self
377    }
378
379    /// Sets the `@httpQueryParams` trait.
380    pub const fn with_http_query_params(mut self) -> Self {
381        self.http_query_params = Some(trait_types::HttpQueryParamsTrait);
382        self
383    }
384
385    /// Sets the `@httpResponseCode` trait.
386    pub const fn with_http_response_code(mut self) -> Self {
387        self.http_response_code = Some(trait_types::HttpResponseCodeTrait);
388        self
389    }
390
391    /// Sets the `@http` trait (operation-level, included on input schema for convenience).
392    pub const fn with_http(mut self, http: trait_types::HttpTrait) -> Self {
393        self.http = Some(http);
394        self
395    }
396
397    /// Sets the `@streaming` trait.
398    pub const fn with_streaming(mut self) -> Self {
399        self.streaming = Some(trait_types::StreamingTrait);
400        self
401    }
402
403    /// Sets the `@eventHeader` trait.
404    pub const fn with_event_header(mut self) -> Self {
405        self.event_header = Some(trait_types::EventHeaderTrait);
406        self
407    }
408
409    /// Sets the `@eventPayload` trait.
410    pub const fn with_event_payload(mut self) -> Self {
411        self.event_payload = Some(trait_types::EventPayloadTrait);
412        self
413    }
414
415    /// Sets the `@hostLabel` trait.
416    pub const fn with_host_label(mut self) -> Self {
417        self.host_label = Some(trait_types::HostLabelTrait);
418        self
419    }
420
421    /// Sets the `@mediaType` trait.
422    pub const fn with_media_type(mut self, value: &'static str) -> Self {
423        self.media_type = Some(trait_types::MediaTypeTrait::new(value));
424        self
425    }
426
427    /// Sets the `@xmlNamespace` trait.
428    pub const fn with_xml_namespace(mut self) -> Self {
429        self.xml_namespace = Some(trait_types::XmlNamespaceTrait);
430        self
431    }
432
433    /// Sets the fallback trait map for unknown/custom traits.
434    pub const fn with_traits(mut self, traits: &'static std::sync::LazyLock<TraitMap>) -> Self {
435        self.traits = Some(traits);
436        self
437    }
438
439    /// Returns the member name if this is a member schema.
440    pub fn member_name(&self) -> Option<&str> {
441        self.member_name
442    }
443
444    /// Returns the member index for member schemas.
445    ///
446    /// This is used internally by generated code for efficient member lookup.
447    /// Consumer code should not rely on specific position values as they may change.
448    pub fn member_index(&self) -> Option<usize> {
449        self.member_index
450    }
451
452    /// Returns the member schema by name (for structures and unions).
453    pub fn member_schema(&self, name: &str) -> Option<&Schema> {
454        match &self.members {
455            SchemaMembers::Struct { members } => members
456                .iter()
457                .find(|m| m.member_name == Some(name))
458                .copied(),
459            _ => None,
460        }
461    }
462
463    /// Returns the member name and schema by position index (for structures and unions).
464    ///
465    /// This is an optimization for generated code to avoid string lookups.
466    /// Consumer code should not rely on specific position values as they may change.
467    pub fn member_schema_by_index(&self, index: usize) -> Option<&Schema> {
468        match &self.members {
469            SchemaMembers::Struct { members } => members.get(index).copied(),
470            _ => None,
471        }
472    }
473
474    /// Returns the member schemas (for structures and unions).
475    pub fn members(&self) -> &[&Schema] {
476        match &self.members {
477            SchemaMembers::Struct { members } => members,
478            _ => &[],
479        }
480    }
481
482    /// Returns the member schema for collections (list member or map value).
483    pub fn member(&self) -> Option<&Schema> {
484        match &self.members {
485            SchemaMembers::List { member } => Some(member),
486            SchemaMembers::Map { value, .. } => Some(value),
487            _ => None,
488        }
489    }
490
491    /// Returns the key schema for maps.
492    pub fn key(&self) -> Option<&Schema> {
493        match &self.members {
494            SchemaMembers::Map { key, .. } => Some(key),
495            _ => None,
496        }
497    }
498
499    // -- convenience predicates --
500
501    /// Returns true if this is a member schema.
502    pub fn is_member(&self) -> bool {
503        self.shape_type.is_member()
504    }
505
506    /// Returns true if this is a structure schema.
507    pub fn is_structure(&self) -> bool {
508        self.shape_type == ShapeType::Structure
509    }
510
511    /// Returns true if this is a union schema.
512    pub fn is_union(&self) -> bool {
513        self.shape_type == ShapeType::Union
514    }
515
516    /// Returns true if this is a list schema.
517    pub fn is_list(&self) -> bool {
518        self.shape_type == ShapeType::List
519    }
520
521    /// Returns true if this is a map schema.
522    pub fn is_map(&self) -> bool {
523        self.shape_type == ShapeType::Map
524    }
525
526    /// Returns true if this is a blob schema.
527    pub fn is_blob(&self) -> bool {
528        self.shape_type == ShapeType::Blob
529    }
530
531    /// Returns true if this is a string schema.
532    pub fn is_string(&self) -> bool {
533        self.shape_type == ShapeType::String
534    }
535}
536
537#[cfg(test)]
538mod test {
539    use crate::{shape_id, Schema, ShapeType, Trait, TraitMap};
540
541    // Simple test trait implementation
542    #[derive(Debug)]
543    struct TestTrait {
544        id: crate::ShapeId,
545        #[allow(dead_code)]
546        value: String,
547    }
548
549    impl Trait for TestTrait {
550        fn trait_id(&self) -> &crate::ShapeId {
551            &self.id
552        }
553
554        fn as_any(&self) -> &dyn std::any::Any {
555            self
556        }
557    }
558
559    #[test]
560    fn test_shape_type_simple() {
561        assert!(ShapeType::String.is_simple());
562        assert!(ShapeType::Integer.is_simple());
563        assert!(ShapeType::Boolean.is_simple());
564        assert!(!ShapeType::Structure.is_simple());
565        assert!(!ShapeType::List.is_simple());
566    }
567
568    #[test]
569    fn test_shape_type_aggregate() {
570        assert!(ShapeType::Structure.is_aggregate());
571        assert!(ShapeType::Union.is_aggregate());
572        assert!(ShapeType::List.is_aggregate());
573        assert!(ShapeType::Map.is_aggregate());
574        assert!(!ShapeType::String.is_aggregate());
575    }
576
577    #[test]
578    fn test_shape_type_member() {
579        assert!(ShapeType::Member.is_member());
580        assert!(!ShapeType::String.is_member());
581        assert!(!ShapeType::Structure.is_member());
582    }
583
584    #[test]
585    fn test_shape_id_parsing() {
586        let id = shape_id!("smithy.api", "String");
587        assert_eq!(id.namespace(), "smithy.api");
588        assert_eq!(id.shape_name(), "String");
589        assert_eq!(id.member_name(), None);
590    }
591
592    #[test]
593    fn test_shape_id_with_member() {
594        let id = shape_id!("com.example", "MyStruct", "member");
595        assert_eq!(id.namespace(), "com.example");
596        assert_eq!(id.shape_name(), "MyStruct");
597        assert_eq!(id.member_name(), Some("member"));
598    }
599
600    #[test]
601    fn test_trait_map() {
602        let mut map = TraitMap::new();
603        assert!(map.is_empty());
604        assert_eq!(map.len(), 0);
605
606        let trait_id = shape_id!("smithy.api", "required");
607        let test_trait = Box::new(TestTrait {
608            id: trait_id,
609            value: "test".to_string(),
610        });
611
612        map.insert(test_trait);
613        assert!(!map.is_empty());
614        assert_eq!(map.len(), 1);
615        assert!(map.contains(&trait_id));
616
617        let retrieved = map.get(&trait_id);
618        assert!(retrieved.is_some());
619    }
620
621    #[test]
622    fn test_schema_predicates() {
623        let schema = Schema::new(shape_id!("com.example", "MyStruct"), ShapeType::Structure);
624
625        assert!(schema.is_structure());
626        assert!(!schema.is_union());
627        assert!(!schema.is_list());
628        assert!(!schema.is_member());
629    }
630
631    #[test]
632    fn test_schema_basic() {
633        let schema = Schema::new(shape_id!("smithy.api", "String"), ShapeType::String);
634
635        assert_eq!(schema.shape_id().as_str(), "smithy.api#String");
636        assert_eq!(schema.shape_type(), ShapeType::String);
637        assert!(schema.traits().is_none());
638        assert!(schema.member_name().is_none());
639        assert!(schema.member_schema("test").is_none());
640        assert!(schema.member_schema_by_index(0).is_none());
641    }
642}