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
36macro_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}