Skip to main content

stratosphere_core/
resource_specification.rs

1use std::collections::BTreeMap;
2
3use nom::{
4    IResult, Parser,
5    bytes::complete::{tag, take_while, take_while1},
6    combinator::{all_consuming, recognize},
7    multi::many0,
8    sequence::{pair, preceded},
9};
10
11#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
12pub struct FeatureName(String);
13
14impl FeatureName {
15    #[must_use]
16    pub fn as_str(&self) -> &str {
17        &self.0
18    }
19}
20
21impl std::fmt::Display for FeatureName {
22    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
23        write!(formatter, "{}", self.0)
24    }
25}
26
27#[derive(Debug, serde::Deserialize)]
28#[serde(deny_unknown_fields, rename_all = "PascalCase")]
29struct ResourceSpecificationInner<'a> {
30    #[serde(borrow = "'a")]
31    property_types: PropertyTypeMap<'a>,
32    resource_specification_version: ResourceSpecificationVersion<'a>,
33    resource_types: ResourceTypeMap<'a>,
34}
35
36#[derive(Debug)]
37pub struct ResourceSpecification<'a> {
38    pub property_types: PropertyTypeMap<'a>,
39    pub resource_specification_version: ResourceSpecificationVersion<'a>,
40    pub resource_types: ResourceTypeMap<'a>,
41    feature_names: BTreeMap<ServiceIdentifier<'a>, FeatureName>,
42}
43
44impl<'a> From<ResourceSpecificationInner<'a>> for ResourceSpecification<'a> {
45    fn from(inner: ResourceSpecificationInner<'a>) -> Self {
46        let feature_names = inner
47            .resource_types
48            .keys()
49            .map(|resource_type_name| {
50                let service = resource_type_name.service.clone();
51                let feature_name = FeatureName(format!(
52                    "{}_{}",
53                    service.vendor_name.as_str().to_lowercase(),
54                    service.service_name.as_str().to_lowercase()
55                ));
56                (service, feature_name)
57            })
58            .collect();
59
60        Self {
61            property_types: inner.property_types,
62            resource_specification_version: inner.resource_specification_version,
63            resource_types: inner.resource_types,
64            feature_names,
65        }
66    }
67}
68
69impl<'a> ResourceSpecification<'a> {
70    fn load_from_file() -> ResourceSpecification<'static> {
71        let inner: ResourceSpecificationInner<'static> = serde_json::from_slice(include_bytes!(
72            "../CloudFormationResourceSpecification.json"
73        ))
74        .unwrap();
75        inner.into()
76    }
77
78    pub fn feature_names(&self) -> impl Iterator<Item = &FeatureName> {
79        self.feature_names.values()
80    }
81
82    #[must_use]
83    pub fn feature_name(&self, service: &ServiceIdentifier<'a>) -> &FeatureName {
84        self.feature_names
85            .get(service)
86            .expect("unknown service identifier")
87    }
88
89    pub fn services_with_feature_names(
90        &self,
91    ) -> impl Iterator<Item = (&ServiceIdentifier<'a>, &FeatureName)> {
92        self.feature_names.iter()
93    }
94}
95
96static INSTANCE: std::sync::LazyLock<ResourceSpecification> =
97    std::sync::LazyLock::new(ResourceSpecification::load_from_file);
98
99#[must_use]
100pub fn instance() -> &'static ResourceSpecification<'static> {
101    &INSTANCE
102}
103
104pub type PropertyTypeMap<'a> = BTreeMap<PropertyTypeName<'a>, PropertyType<'a>>;
105pub type ResourceAttributesMap<'a> = BTreeMap<ResourceAttributeName<'a>, ResourceAttribute<'a>>;
106pub type ResourceTypeMap<'a> = BTreeMap<ResourceTypeName<'a>, ResourceType<'a>>;
107pub type ResourceTypePropertiesMap<'a> =
108    BTreeMap<ResourceTypePropertyName<'a>, ResourceTypeProperty<'a>>;
109
110pub type PropertyTypePropertiesMap<'a> = BTreeMap<PropertyName<'a>, PropertyTypeProperty<'a>>;
111
112/// Parser for base identifier pattern: `[a-zA-Z]+[a-zA-Z0-9]*`
113fn parse_base_identifier(input: &str) -> IResult<&str, &str> {
114    recognize(pair(
115        take_while1(|char: char| char.is_ascii_alphabetic()),
116        take_while(|char: char| char.is_ascii_alphanumeric()),
117    ))
118    .parse(input)
119}
120
121/// Parser for `::identifier` segment
122fn parse_colons_identifier(input: &str) -> IResult<&str, &str> {
123    preceded(tag("::"), parse_base_identifier).parse(input)
124}
125
126/// Parser for resource attribute name pattern: `identifier(.identifier)*`
127fn parse_resource_attribute_name(input: &str) -> IResult<&str, &str> {
128    recognize(pair(
129        parse_base_identifier,
130        many0(preceded(tag("."), parse_base_identifier)),
131    ))
132    .parse(input)
133}
134
135/// Macro to generate `std::str::FromStr` for zero copy str wrapped newtypes
136macro_rules! identifier {
137    ($struct: ident) => {
138        identifier!($struct, parse_base_identifier);
139    };
140    ($struct: ident, $parser: ident) => {
141        #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize)]
142        pub struct $struct<'a>(pub &'a str);
143
144        impl $struct<'_> {
145            pub fn as_str(&self) -> &str {
146                self.0
147            }
148        }
149
150        impl AsRef<str> for $struct<'_> {
151            fn as_ref(&self) -> &str {
152                self.0
153            }
154        }
155
156        impl<'a> std::convert::TryFrom<&'a str> for $struct<'a> {
157            type Error = String;
158
159            fn try_from(value: &'a str) -> Result<Self, Self::Error> {
160                let count = value.chars().count();
161
162                if count < 1 {
163                    return Err(concat!(stringify!($struct), " min length: 1 violated").to_string());
164                }
165
166                if count > 128 {
167                    return Err(
168                        concat!(stringify!($struct), " max length: 128 violated",).to_string()
169                    );
170                }
171
172                match all_consuming($parser).parse(value) {
173                    Ok((_remaining, _parsed)) => Ok(Self(value)),
174                    Err(_error) => Err(format!(
175                        concat!(
176                            stringify!($struct),
177                            " does not match expected pattern, value: {}"
178                        ),
179                        value
180                    )),
181                }
182            }
183        }
184
185        impl std::fmt::Display for $struct<'_> {
186            fn fmt(&self, formatter: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
187                write!(formatter, "{}", self.0)
188            }
189        }
190    };
191}
192
193identifier!(ResourceAttributeName, parse_resource_attribute_name);
194identifier!(ResourceName);
195identifier!(ResourceTypePropertyName);
196identifier!(ServiceName);
197identifier!(PropertyName);
198identifier!(VendorName);
199
200impl quote::ToTokens for ServiceName<'_> {
201    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
202        let str_value = self.as_str();
203
204        stream.extend(quote::quote! {
205            crate::resource_specification::ServiceName(#str_value)
206        })
207    }
208}
209
210impl quote::ToTokens for ResourceName<'_> {
211    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
212        let str_value = self.as_str();
213
214        stream.extend(quote::quote! {
215            crate::resource_specification::ResourceName(#str_value)
216        })
217    }
218}
219
220impl ResourceName<'_> {
221    /// Converts the resource name to a safe Rust module identifier (lowercase, escaped if keyword)
222    #[must_use]
223    pub fn to_module_ident(&self) -> syn::Ident {
224        crate::token::mk_safe_ident(self.as_str().to_lowercase())
225    }
226}
227
228impl quote::ToTokens for VendorName<'_> {
229    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
230        let str_value = self.as_str();
231
232        stream.extend(quote::quote! {
233            crate::resource_specification::VendorName(#str_value)
234        })
235    }
236}
237
238#[derive(Debug, serde::Deserialize)]
239pub struct Documentation<'a>(pub &'a str);
240
241impl Documentation<'_> {
242    #[must_use]
243    pub fn as_str(&self) -> &str {
244        self.0
245    }
246}
247
248#[derive(Debug, serde::Deserialize)]
249pub struct ResourceSpecificationVersion<'a>(pub &'a str);
250
251#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
252pub struct ServiceIdentifier<'a> {
253    pub vendor_name: VendorName<'a>,
254    pub service_name: ServiceName<'a>,
255}
256
257impl ServiceIdentifier<'_> {
258    #[must_use]
259    pub fn provides(&self, resource_type: &ResourceTypeName) -> bool {
260        *self == resource_type.service
261    }
262}
263
264impl quote::ToTokens for ServiceIdentifier<'_> {
265    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
266        let vendor_name = &self.vendor_name;
267        let service_name = &self.service_name;
268
269        stream.extend(quote::quote! {
270            crate::resource_specification::ServiceIdentifier {
271                service_name: #service_name,
272                vendor_name: #vendor_name,
273            }
274        })
275    }
276}
277
278impl std::fmt::Display for ServiceIdentifier<'_> {
279    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
280        write!(formatter, "{}::{}", self.vendor_name, self.service_name)
281    }
282}
283
284fn parse_service_identifier(input: &str) -> IResult<&str, ServiceIdentifier<'_>> {
285    pair(parse_base_identifier, parse_colons_identifier)
286        .map(|(vendor_name, service_name)| ServiceIdentifier {
287            vendor_name: VendorName(vendor_name),
288            service_name: ServiceName(service_name),
289        })
290        .parse(input)
291}
292
293impl<'a> std::convert::TryFrom<&'a str> for ServiceIdentifier<'a> {
294    type Error = String;
295
296    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
297        match all_consuming(parse_service_identifier).parse(value) {
298            Ok((_remaining, service_identifier)) => Ok(service_identifier),
299            Err(_error) => Err(format!("Invalid value: {value}")),
300        }
301    }
302}
303
304#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
305pub struct ResourceTypeName<'a> {
306    pub service: ServiceIdentifier<'a>,
307    pub resource_name: ResourceName<'a>,
308}
309
310impl serde::Serialize for ResourceTypeName<'_> {
311    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
312        serializer.serialize_str(&self.to_string())
313    }
314}
315
316impl quote::ToTokens for ResourceTypeName<'_> {
317    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
318        let service = &self.service;
319        let resource_name = &self.resource_name;
320
321        stream.extend(quote::quote! {
322            crate::resource_specification::ResourceTypeName {
323                service: #service,
324                resource_name: #resource_name,
325            }
326        })
327    }
328}
329
330fn parse_resource_type_name(input: &str) -> IResult<&str, ResourceTypeName<'_>> {
331    pair(parse_service_identifier, parse_colons_identifier)
332        .map(|(service, resource_name)| ResourceTypeName {
333            service,
334            resource_name: ResourceName(resource_name),
335        })
336        .parse(input)
337}
338
339impl<'a> std::convert::TryFrom<&'a str> for ResourceTypeName<'a> {
340    type Error = String;
341
342    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
343        match all_consuming(parse_resource_type_name).parse(value) {
344            Ok((_remaining, resource_type_name)) => Ok(resource_type_name),
345            Err(_error) => Err(format!("Invalid value: {value}")),
346        }
347    }
348}
349
350impl<'a, 'de: 'a> serde::Deserialize<'de> for ResourceTypeName<'a> {
351    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
352        <&'a str as serde::de::Deserialize<'de>>::deserialize(deserializer)
353            .and_then(|value| Self::try_from(value).map_err(serde::de::Error::custom))
354    }
355}
356
357impl std::fmt::Display for ResourceTypeName<'_> {
358    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
359        write!(formatter, "{}::{}", self.service, self.resource_name)
360    }
361}
362
363#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
364pub enum PropertyTypeName<'a> {
365    PropertyTypeName(ResourcePropertyTypeName<'a>),
366    Tag,
367}
368
369impl std::fmt::Display for PropertyTypeName<'_> {
370    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
371        match self {
372            Self::PropertyTypeName(name) => {
373                write!(
374                    formatter,
375                    "{}::{}::{}.{}",
376                    name.vendor_name, name.service_name, name.resource_name, name.property_name
377                )
378            }
379            Self::Tag => write!(formatter, "Tag"),
380        }
381    }
382}
383
384fn parse_resource_property_type_name(input: &str) -> IResult<&str, ResourcePropertyTypeName<'_>> {
385    pair(
386        parse_resource_type_name,
387        preceded(tag("."), parse_base_identifier),
388    )
389    .map(|(resource_type, property_name)| ResourcePropertyTypeName {
390        vendor_name: resource_type.service.vendor_name,
391        service_name: resource_type.service.service_name,
392        resource_name: resource_type.resource_name,
393        property_name: PropertyName(property_name),
394    })
395    .parse(input)
396}
397
398impl<'a> std::convert::TryFrom<&'a str> for PropertyTypeName<'a> {
399    type Error = String;
400
401    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
402        if value == "Tag" {
403            Ok(PropertyTypeName::Tag)
404        } else {
405            match all_consuming(parse_resource_property_type_name).parse(value) {
406                Ok((_remaining, resource_property_type_name)) => Ok(
407                    PropertyTypeName::PropertyTypeName(resource_property_type_name),
408                ),
409                Err(_error) => Err(format!("Invalid value: {value}")),
410            }
411        }
412    }
413}
414
415#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
416pub struct ResourcePropertyTypeName<'a> {
417    pub vendor_name: VendorName<'a>,
418    pub service_name: ServiceName<'a>,
419    pub resource_name: ResourceName<'a>,
420    pub property_name: PropertyName<'a>,
421}
422
423impl<'a, 'de: 'a> serde::Deserialize<'de> for PropertyTypeName<'a> {
424    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
425        <&'a str as serde::de::Deserialize<'de>>::deserialize(deserializer).and_then(|value| {
426            std::convert::TryFrom::try_from(value).map_err(serde::de::Error::custom)
427        })
428    }
429}
430
431#[derive(Debug, serde::Deserialize)]
432#[serde(deny_unknown_fields, rename_all = "PascalCase")]
433pub struct ResourceType<'a> {
434    #[serde(borrow = "'a")]
435    pub documentation: Documentation<'a>,
436    pub attributes: Option<ResourceAttributesMap<'a>>,
437    pub additional_properties: Option<bool>,
438    pub properties: ResourceTypePropertiesMap<'a>,
439}
440
441#[derive(Debug, serde::Deserialize)]
442#[serde(deny_unknown_fields, rename_all = "PascalCase")]
443pub struct ResourceAttribute<'a> {
444    pub primitive_item_type: Option<PrimitiveItemType>,
445    #[serde(borrow = "'a")]
446    pub item_type: Option<TypeReference<'a>>,
447    pub primitive_type: Option<PrimitiveType>,
448    pub r#type: Option<TypeReference<'a>>,
449}
450
451#[derive(Debug, serde::Deserialize)]
452#[serde(deny_unknown_fields, rename_all = "PascalCase")]
453pub struct ResourceTypeProperty<'a> {
454    #[serde(borrow = "'a")]
455    pub documentation: Documentation<'a>,
456    pub duplicates_allowed: Option<bool>,
457    pub item_type: Option<TypeReference<'a>>,
458    pub primitive_type: Option<PrimitiveType>,
459    pub primitive_item_type: Option<PrimitiveItemType>,
460    pub r#type: Option<TypeReference<'a>>,
461    pub required: bool,
462    pub update_type: UpdateType,
463}
464
465#[derive(Debug, serde::Deserialize)]
466#[serde(deny_unknown_fields, rename_all = "PascalCase")]
467pub struct PropertyType<'a> {
468    #[serde(borrow = "'a")]
469    pub documentation: Documentation<'a>,
470    pub item_type: Option<TypeReference<'a>>,
471    pub properties: Option<PropertyTypePropertiesMap<'a>>,
472    pub r#type: Option<TypeReference<'a>>,
473    pub primitive_type: Option<PrimitiveType>,
474    pub required: Option<bool>,
475    pub update_type: Option<UpdateType>,
476}
477
478#[derive(Debug, serde::Deserialize)]
479#[serde(deny_unknown_fields, rename_all = "PascalCase")]
480pub struct PropertyTypeProperty<'a> {
481    #[serde(borrow = "'a")]
482    pub documentation: Documentation<'a>,
483    pub duplicates_allowed: Option<bool>,
484    pub item_type: Option<TypeReference<'a>>,
485    pub primitive_item_type: Option<PrimitiveItemType>,
486    pub primitive_type: Option<PrimitiveType>,
487    pub r#type: Option<TypeReference<'a>>,
488    pub required: bool,
489    pub update_type: UpdateType,
490}
491
492#[derive(Debug, serde::Deserialize)]
493#[serde(deny_unknown_fields, rename_all = "PascalCase")]
494pub enum UpdateType {
495    Conditional,
496    Immutable,
497    Mutable,
498}
499
500#[derive(Debug, serde::Deserialize)]
501#[serde(deny_unknown_fields, rename_all = "PascalCase")]
502pub enum PrimitiveType {
503    Boolean,
504    Double,
505    Integer,
506    Json,
507    Long,
508    String,
509    Timestamp,
510}
511
512#[derive(Debug, serde::Deserialize)]
513#[serde(deny_unknown_fields, rename_all = "PascalCase")]
514pub enum PrimitiveItemType {
515    Double,
516    Integer,
517    Json,
518    Long,
519    String,
520}
521
522#[derive(Debug)]
523pub enum TypeReference<'a> {
524    List,
525    Map,
526    Tag,
527    Subproperty(PropertyName<'a>),
528}
529
530impl<'a, 'de: 'a> serde::Deserialize<'de> for TypeReference<'a> {
531    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
532        <&'a str as serde::de::Deserialize<'de>>::deserialize(deserializer).and_then(|value| {
533            if value == "List" {
534                Ok(Self::List)
535            } else if value == "Map" {
536                Ok(Self::Map)
537            } else if value == "Tag" {
538                Ok(Self::Tag)
539            } else {
540                match PropertyName::try_from(value) {
541                    Ok(value) => Ok(Self::Subproperty(value)),
542                    Err(error) => Err(serde::de::Error::custom(format!("Invalid value: {error}"))),
543                }
544            }
545        })
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552
553    #[test]
554    fn parses_resource_specification() {
555        eprintln!("{:#?}", &*INSTANCE);
556    }
557
558    #[test]
559    fn feature_names_contains_aws_s3() {
560        let spec = instance();
561        let feature_names: Vec<_> = spec.feature_names().collect();
562
563        assert!(feature_names.iter().any(|name| name.as_str() == "aws_s3"));
564    }
565
566    #[test]
567    fn feature_name_for_s3_service() {
568        let spec = instance();
569        let s3_service = ServiceIdentifier {
570            vendor_name: VendorName("AWS"),
571            service_name: ServiceName("S3"),
572        };
573
574        let feature_name = spec.feature_name(&s3_service);
575
576        assert_eq!(feature_name.as_str(), "aws_s3");
577    }
578}