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
112fn 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
121fn parse_colons_identifier(input: &str) -> IResult<&str, &str> {
123 preceded(tag("::"), parse_base_identifier).parse(input)
124}
125
126fn 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
135macro_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 #[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}