stratosphere_core/
resource_specification.rs

1use std::collections::BTreeMap;
2
3#[derive(Debug, serde::Deserialize)]
4#[serde(deny_unknown_fields, rename_all = "PascalCase")]
5pub struct ResourceSpecification<'a> {
6    #[serde(borrow = "'a")]
7    pub property_types: PropertyTypeMap<'a>,
8    pub resource_specification_version: ResourceSpecificationVersion<'a>,
9    pub resource_types: ResourceTypeMap<'a>,
10}
11
12impl ResourceSpecification<'_> {
13    fn load_from_file() -> ResourceSpecification<'static> {
14        serde_json::from_slice(include_bytes!(
15            "../CloudFormationResourceSpecification.json"
16        ))
17        .unwrap()
18    }
19}
20
21static INSTANCE: std::sync::LazyLock<ResourceSpecification> =
22    std::sync::LazyLock::new(ResourceSpecification::load_from_file);
23
24pub fn instance() -> &'static ResourceSpecification<'static> {
25    &INSTANCE
26}
27
28pub type PropertyTypeMap<'a> = BTreeMap<PropertyTypeName<'a>, PropertyType<'a>>;
29pub type ResourceAttributesMap<'a> = BTreeMap<ResourceAttributeName<'a>, ResourceAttribute<'a>>;
30pub type ResourceTypeMap<'a> = BTreeMap<ResourceTypeName<'a>, ResourceType<'a>>;
31pub type ResourceTypePropertiesMap<'a> =
32    BTreeMap<ResourceTypePropertyName<'a>, ResourceTypeProperty<'a>>;
33
34pub type PropertyTypePropertiesMap<'a> = BTreeMap<PropertyName<'a>, PropertyTypeProperty<'a>>;
35
36/// Macro to generate `std::str::FromStr` for zero copy str wrapped newtypes
37macro_rules! identifier {
38    ($struct: ident) => {
39        identifier!($struct, r#"[a-zA-Z]+[a-zA-Z0-9]*"#);
40    };
41    ($struct: ident, $pattern: literal) => {
42        #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize)]
43        pub struct $struct<'a>(pub &'a str);
44
45        impl $struct<'_> {
46            pub fn as_str(&self) -> &str {
47                self.0
48            }
49
50            #[allow(unused)]
51            const BASE_PATTERN: &'static str = $pattern;
52        }
53
54        impl AsRef<str> for $struct<'_> {
55            fn as_ref(&self) -> &str {
56                self.0
57            }
58        }
59
60        impl<'a> std::convert::TryFrom<&'a str> for $struct<'a> {
61            type Error = String;
62
63            fn try_from(value: &'a str) -> Result<Self, Self::Error> {
64                let count = value.chars().count();
65
66                if count < 1 {
67                    return Err(concat!(stringify!($struct), " min length: 1 violated").to_string());
68                }
69
70                if count > 128 {
71                    return Err(
72                        concat!(stringify!($struct), " max length: 128 violated",).to_string()
73                    );
74                }
75
76                let syntax = concat!(r#"\A"#, $pattern, r#"\z"#);
77
78                let pattern = regex_lite::Regex::new(syntax).unwrap();
79
80                if !pattern.is_match(value) {
81                    return Err(format!(
82                        concat!(
83                            stringify!($struct),
84                            " does not match pattern: {}, value: {}",
85                        ),
86                        syntax, value
87                    ));
88                }
89
90                Ok(Self(value))
91            }
92        }
93
94        impl std::fmt::Display for $struct<'_> {
95            fn fmt(&self, formatter: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
96                write!(formatter, "{}", self.0)
97            }
98        }
99    };
100}
101
102identifier!(
103    ResourceAttributeName,
104    r#"\A[a-zA-Z]+[a-zA-Z0-9]*(\.[a-zA-Z]+[a-zA-Z0-9]*)*\z"#
105);
106identifier!(ResourceName);
107identifier!(ResourceTypePropertyName);
108identifier!(ServiceName);
109identifier!(PropertyName);
110identifier!(VendorName);
111
112impl quote::ToTokens for ServiceName<'_> {
113    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
114        let str_value = self.as_str();
115
116        stream.extend(quote::quote! {
117            stratosphere::resource_specification::ServiceName(#str_value)
118        })
119    }
120}
121
122impl quote::ToTokens for ResourceName<'_> {
123    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
124        let str_value = self.as_str();
125
126        stream.extend(quote::quote! {
127            stratosphere::resource_specification::ResourceName(#str_value)
128        })
129    }
130}
131
132impl quote::ToTokens for VendorName<'_> {
133    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
134        let str_value = self.as_str();
135
136        stream.extend(quote::quote! {
137            stratosphere::resource_specification::VendorName(#str_value)
138        })
139    }
140}
141
142#[derive(Debug, serde::Deserialize)]
143pub struct Documentation<'a>(pub &'a str);
144
145impl Documentation<'_> {
146    pub fn as_str(&self) -> &str {
147        self.0
148    }
149}
150
151#[derive(Debug, serde::Deserialize)]
152pub struct ResourceSpecificationVersion<'a>(pub &'a str);
153
154#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
155pub struct ServiceIdentifier<'a> {
156    pub vendor_name: VendorName<'a>,
157    pub service_name: ServiceName<'a>,
158}
159
160impl ServiceIdentifier<'_> {
161    pub fn provides(&self, resource_type: &ResourceTypeName) -> bool {
162        *self == resource_type.service
163    }
164}
165
166impl quote::ToTokens for ServiceIdentifier<'_> {
167    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
168        let vendor_name = &self.vendor_name;
169        let service_name = &self.service_name;
170
171        stream.extend(quote::quote! {
172            stratosphere::resource_specification::ServiceIdentifier {
173                service_name: #service_name,
174                vendor_name: #vendor_name,
175            }
176        })
177    }
178}
179
180impl std::fmt::Display for ServiceIdentifier<'_> {
181    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
182        write!(formatter, "{}::{}", self.vendor_name, self.service_name)
183    }
184}
185
186impl<'a> std::convert::TryFrom<&'a str> for ServiceIdentifier<'a> {
187    type Error = String;
188
189    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
190        let pattern = regex_lite::Regex::new(&format!(
191            r#"\A(?<vendor_name>{})::(?<service_name>{})\z"#,
192            VendorName::BASE_PATTERN,
193            ServiceName::BASE_PATTERN,
194        ))
195        .unwrap();
196
197        if let Some(captures) = pattern.captures(value) {
198            Ok(ServiceIdentifier {
199                vendor_name: VendorName(captures.name("vendor_name").unwrap().as_str()),
200                service_name: ServiceName(captures.name("service_name").unwrap().as_str()),
201            })
202        } else {
203            Err(format!("Invalid value: {value}"))
204        }
205    }
206}
207
208#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
209pub struct ResourceTypeName<'a> {
210    pub service: ServiceIdentifier<'a>,
211    pub resource_name: ResourceName<'a>,
212}
213
214impl serde::Serialize for ResourceTypeName<'_> {
215    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
216        serializer.serialize_str(&self.to_string())
217    }
218}
219
220impl quote::ToTokens for ResourceTypeName<'_> {
221    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
222        let service = &self.service;
223        let resource_name = &self.resource_name;
224
225        stream.extend(quote::quote! {
226            stratosphere::resource_specification::ResourceTypeName {
227                service: #service,
228                resource_name: #resource_name,
229            }
230        })
231    }
232}
233
234impl<'a> std::convert::TryFrom<&'a str> for ResourceTypeName<'a> {
235    type Error = String;
236
237    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
238        let pattern = regex_lite::Regex::new(&format!(
239            r#"\A(?<vendor_name>{})::(?<service_name>{})::(?<resource_name>{})\z"#,
240            VendorName::BASE_PATTERN,
241            ServiceName::BASE_PATTERN,
242            ResourceName::BASE_PATTERN,
243        ))
244        .unwrap();
245
246        if let Some(captures) = pattern.captures(value) {
247            Ok(ResourceTypeName {
248                service: ServiceIdentifier {
249                    vendor_name: VendorName(captures.name("vendor_name").unwrap().as_str()),
250                    service_name: ServiceName(captures.name("service_name").unwrap().as_str()),
251                },
252                resource_name: ResourceName(captures.name("resource_name").unwrap().as_str()),
253            })
254        } else {
255            Err(format!("Invalid value: {value}"))
256        }
257    }
258}
259
260impl<'a, 'de: 'a> serde::Deserialize<'de> for ResourceTypeName<'a> {
261    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
262        <&'a str as serde::de::Deserialize<'de>>::deserialize(deserializer)
263            .and_then(|value| Self::try_from(value).map_err(serde::de::Error::custom))
264    }
265}
266
267impl std::fmt::Display for ResourceTypeName<'_> {
268    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
269        write!(formatter, "{}::{}", self.service, self.resource_name)
270    }
271}
272
273#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
274pub enum PropertyTypeName<'a> {
275    PropertyTypeName(ResourcePropertyTypeName<'a>),
276    Tag,
277}
278
279impl std::fmt::Display for PropertyTypeName<'_> {
280    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
281        match self {
282            Self::PropertyTypeName(name) => {
283                write!(
284                    formatter,
285                    "{}::{}::{}.{}",
286                    name.vendor_name, name.service_name, name.resource_name, name.property_name
287                )
288            }
289            Self::Tag => write!(formatter, "Tag"),
290        }
291    }
292}
293
294impl<'a> std::convert::TryFrom<&'a str> for PropertyTypeName<'a> {
295    type Error = String;
296
297    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
298        if value == "Tag" {
299            Ok(PropertyTypeName::Tag)
300        } else {
301            let pattern = regex_lite::Regex::new(&format!(
302                r#"\A(?<vendor_name>{})::(?<service_name>{})::(?<resource_name>{})\.(?<property_name>{})\z"#,
303                VendorName::BASE_PATTERN,
304                ServiceName::BASE_PATTERN,
305                ResourceName::BASE_PATTERN,
306                PropertyName::BASE_PATTERN
307            ))
308            .unwrap();
309
310            if let Some(captures) = pattern.captures(value) {
311                Ok(PropertyTypeName::PropertyTypeName(
312                    ResourcePropertyTypeName {
313                        vendor_name: VendorName(captures.name("vendor_name").unwrap().as_str()),
314                        service_name: ServiceName(captures.name("service_name").unwrap().as_str()),
315                        resource_name: ResourceName(
316                            captures.name("resource_name").unwrap().as_str(),
317                        ),
318                        property_name: PropertyName(
319                            captures.name("property_name").unwrap().as_str(),
320                        ),
321                    },
322                ))
323            } else {
324                Err(format!("Invalid value: {value}"))
325            }
326        }
327    }
328}
329
330#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
331pub struct ResourcePropertyTypeName<'a> {
332    pub vendor_name: VendorName<'a>,
333    pub service_name: ServiceName<'a>,
334    pub resource_name: ResourceName<'a>,
335    pub property_name: PropertyName<'a>,
336}
337
338impl<'a, 'de: 'a> serde::Deserialize<'de> for PropertyTypeName<'a> {
339    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
340        <&'a str as serde::de::Deserialize<'de>>::deserialize(deserializer).and_then(|value| {
341            std::convert::TryFrom::try_from(value).map_err(serde::de::Error::custom)
342        })
343    }
344}
345
346#[derive(Debug, serde::Deserialize)]
347#[serde(deny_unknown_fields, rename_all = "PascalCase")]
348pub struct ResourceType<'a> {
349    #[serde(borrow = "'a")]
350    pub documentation: Documentation<'a>,
351    pub attributes: Option<ResourceAttributesMap<'a>>,
352    pub additional_properties: Option<bool>,
353    pub properties: ResourceTypePropertiesMap<'a>,
354}
355
356#[derive(Debug, serde::Deserialize)]
357#[serde(deny_unknown_fields, rename_all = "PascalCase")]
358pub struct ResourceAttribute<'a> {
359    pub primitive_item_type: Option<PrimitiveItemType>,
360    #[serde(borrow = "'a")]
361    pub item_type: Option<TypeReference<'a>>,
362    pub primitive_type: Option<PrimitiveType>,
363    pub r#type: Option<TypeReference<'a>>,
364}
365
366#[derive(Debug, serde::Deserialize)]
367#[serde(deny_unknown_fields, rename_all = "PascalCase")]
368pub struct ResourceTypeProperty<'a> {
369    #[serde(borrow = "'a")]
370    pub documentation: Documentation<'a>,
371    pub duplicates_allowed: Option<bool>,
372    pub item_type: Option<TypeReference<'a>>,
373    pub primitive_type: Option<PrimitiveType>,
374    pub primitive_item_type: Option<PrimitiveItemType>,
375    pub r#type: Option<TypeReference<'a>>,
376    pub required: bool,
377    pub update_type: UpdateType,
378}
379
380#[derive(Debug, serde::Deserialize)]
381#[serde(deny_unknown_fields, rename_all = "PascalCase")]
382pub struct PropertyType<'a> {
383    #[serde(borrow = "'a")]
384    pub documentation: Documentation<'a>,
385    pub item_type: Option<TypeReference<'a>>,
386    pub properties: Option<PropertyTypePropertiesMap<'a>>,
387    pub r#type: Option<TypeReference<'a>>,
388    pub primitive_type: Option<PrimitiveType>,
389    pub required: Option<bool>,
390    pub update_type: Option<UpdateType>,
391}
392
393#[derive(Debug, serde::Deserialize)]
394#[serde(deny_unknown_fields, rename_all = "PascalCase")]
395pub struct PropertyTypeProperty<'a> {
396    #[serde(borrow = "'a")]
397    pub documentation: Documentation<'a>,
398    pub duplicates_allowed: Option<bool>,
399    pub item_type: Option<TypeReference<'a>>,
400    pub primitive_item_type: Option<PrimitiveItemType>,
401    pub primitive_type: Option<PrimitiveType>,
402    pub r#type: Option<TypeReference<'a>>,
403    pub required: bool,
404    pub update_type: UpdateType,
405}
406
407#[derive(Debug, serde::Deserialize)]
408#[serde(deny_unknown_fields, rename_all = "PascalCase")]
409pub enum UpdateType {
410    Conditional,
411    Immutable,
412    Mutable,
413}
414
415#[derive(Debug, serde::Deserialize)]
416#[serde(deny_unknown_fields, rename_all = "PascalCase")]
417pub enum PrimitiveType {
418    Boolean,
419    Double,
420    Integer,
421    Json,
422    Long,
423    String,
424    Timestamp,
425}
426
427#[derive(Debug, serde::Deserialize)]
428#[serde(deny_unknown_fields, rename_all = "PascalCase")]
429pub enum PrimitiveItemType {
430    Double,
431    Integer,
432    Json,
433    String,
434}
435
436#[derive(Debug)]
437pub enum TypeReference<'a> {
438    List,
439    Map,
440    Tag,
441    Subproperty(PropertyName<'a>),
442}
443
444impl<'a, 'de: 'a> serde::Deserialize<'de> for TypeReference<'a> {
445    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
446        <&'a str as serde::de::Deserialize<'de>>::deserialize(deserializer).and_then(|value| {
447            if value == "List" {
448                Ok(Self::List)
449            } else if value == "Map" {
450                Ok(Self::Map)
451            } else if value == "Tag" {
452                Ok(Self::Tag)
453            } else {
454                match PropertyName::try_from(value) {
455                    Ok(value) => Ok(Self::Subproperty(value)),
456                    Err(error) => Err(serde::de::Error::custom(format!("Invalid value: {error}"))),
457                }
458            }
459        })
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    #[test]
468    fn parses_resource_specification() {
469        eprintln!("{:#?}", &*INSTANCE);
470    }
471}