1use crate::config::CodeGeneratorConfig;
2use crate::emit::{CodeGenerator, Registry};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use serde_reflection::{ContainerFormat, Format, Named, VariantFormat};
6use std::collections::BTreeMap;
7use std::fs::File;
8use std::io::BufReader;
9use std::io::Write;
10use std::path::Path;
11
12pub const RESERVED_WORDS: [&str; 32] = [
13 "as",
14 "break",
15 "pub const",
16 "continue",
17 "else",
18 "enum",
19 "false",
20 "fn",
21 "for",
22 "if",
23 "impl",
24 "in",
25 "let",
26 "loop",
27 "match",
28 "mod",
29 "mut",
30 "ref",
31 "return",
32 "self",
33 "Self",
34 "static",
35 "super",
36 "trait",
37 "true",
38 "type",
39 "unsafe",
40 "use",
41 "where",
42 "while",
43 "const",
44 "box",
45];
46
47#[derive(Serialize, Deserialize, Debug, Clone)]
48enum Void {}
49
50#[derive(Serialize, Deserialize, Debug, Clone, Default)]
51pub struct TerraformSchemaExport {
52 provider_schemas: BTreeMap<String, Schema>,
53 format_version: String,
54}
55
56#[derive(Serialize, Deserialize, Debug, Clone, Default)]
57pub struct Schema {
58 provider: Provider,
59 data_source_schemas: Option<BTreeMap<String, SchemaItem>>,
60 resource_schemas: Option<BTreeMap<String, SchemaItem>>,
61}
62
63#[derive(Serialize, Deserialize, Debug, Clone, Default)]
64pub struct Provider {
65 version: i64,
66 block: Block,
67}
68
69#[derive(Serialize, Deserialize, Debug, Clone, Default)]
70pub struct SchemaItem {
71 version: i64,
72 block: Block,
73}
74
75#[derive(Serialize, Deserialize, Debug, Clone, Default)]
76pub struct Block {
77 attributes: Option<BTreeMap<String, Attribute>>,
78 block_types: Option<BTreeMap<String, NestedBlock>>,
79}
80
81#[derive(Serialize, Deserialize, Debug, Clone)]
82#[serde(rename_all = "lowercase")]
83pub enum StringKind {
84 Plain,
85 Markdown,
86}
87
88#[derive(Serialize, Deserialize, Debug, Clone, Default)]
89pub struct Attribute {
90 r#type: AttributeType,
91 description: Option<String>,
92 required: Option<bool>,
93 optional: Option<bool>,
94 computed: Option<bool>,
95 sensitive: Option<bool>,
96 description_kind: Option<StringKind>,
97 deprecated: Option<bool>,
98}
99
100#[derive(Serialize, Deserialize, Debug, Clone, Default)]
101pub struct NestedBlock {
102 block: Block,
103 nesting_mode: Option<String>,
104 min_items: Option<u8>,
105 max_items: Option<u8>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, Default)]
109struct AttributeType(Value);
110
111pub fn generate_serde(
112 config: &str,
113 out: &mut dyn Write,
114 registry: &Registry,
115) -> std::result::Result<(), Box<dyn std::error::Error>> {
116 let config = CodeGeneratorConfig::new(config.to_string());
117
118 CodeGenerator::new(&config).output(out, ®istry)
119}
120
121pub fn export_schema_to_registry(
122 schema: &TerraformSchemaExport,
123) -> std::result::Result<Registry, Box<dyn std::error::Error>> {
124 let mut r = Registry::new();
125 let mut roots = BTreeMap::new();
126 roots.insert("provider", Vec::<&str>::new());
127 roots.insert("resource", Vec::<&str>::new());
128 roots.insert("data", Vec::<&str>::new());
129
130 for (pn, pv) in &schema.provider_schemas {
131 let pn = pn.split('/').last().unwrap_or(pn);
132 let ps = &pv.provider;
133 export_block(None, &pn, ps.block.clone(), &mut r)?;
134 if let Some(provider) = roots.get_mut("provider") {
135 provider.push(pn);
136 }
137
138 if let Some(rss) = &pv.resource_schemas {
139 for (n, i) in rss {
140 let mut b = i.block.clone();
142 inject_meta_arguments(&mut b);
143
144 export_block(Some("resource".to_owned()), &n, b, &mut r)?;
145 if let Some(resources) = roots.get_mut("resource") {
146 resources.push(n);
147 }
148 }
149 }
150
151 if let Some(dss) = &pv.data_source_schemas {
152 for (n, i) in dss {
153 let b = i.block.clone();
154 export_block(Some("data_source".to_owned()), &n, b, &mut r)?;
155 if let Some(resources) = roots.get_mut("data") {
156 resources.push(n);
157 }
158 }
159 }
160
161 export_roots(&roots, &mut r);
162 generate_config(&roots, &mut r);
163 }
164 Ok(r)
165}
166
167fn generate_config(roots: &BTreeMap<&str, Vec<&str>>, reg: &mut Registry) {
168 let mut target_attrs = Vec::new();
169
170 for root_name in roots.keys() {
171 target_attrs.push(Named {
172 name: root_name.to_string(),
173 value: Format::Option(Box::new(Format::Seq(Box::new(Format::TypeName(format!(
174 "{}_root",
175 root_name
176 )))))),
177 });
178 }
179 reg.insert(
180 (None, "config".to_string()),
181 ContainerFormat::Struct(target_attrs),
182 );
183}
184
185fn export_roots(roots: &BTreeMap<&str, Vec<&str>>, reg: &mut Registry) {
186 for (root_name, root_members) in roots {
187 let mut enumz = BTreeMap::new();
188 for (pos, member) in root_members.iter().enumerate() {
189 let mut variant_type_name = format!("Vec<Map<String, Vec<{}_details>>>", member);
190
191 if root_name.to_string().eq("provider") {
192 variant_type_name = format!("Vec<{}_details>", member);
193 }
194
195 enumz.insert(
196 pos as u32,
197 Named {
198 name: member.to_string(),
199 value: VariantFormat::NewType(Box::new(Format::TypeName(variant_type_name))),
200 },
201 );
202 }
203 reg.insert(
204 (None, format!("{}_root", root_name.to_owned())),
205 ContainerFormat::Enum(enumz),
206 );
207 }
208}
209
210fn export_attributes(
211 attrs: &BTreeMap<String, Attribute>,
212) -> std::result::Result<Option<ContainerFormat>, Box<dyn std::error::Error>> {
213 let mut target_attrs = Vec::new();
214 for (an, at) in attrs {
215 let an = RESERVED_WORDS
216 .iter()
217 .find(|w| an == &w.to_string())
218 .map(|w| format!("r#{}", w))
219 .unwrap_or_else(|| an.to_string());
220
221 let f = match &at.r#type {
222 AttributeType(Value::String(t)) if t == "string" => Format::Str,
223 AttributeType(Value::String(t)) if t == "bool" => Format::Bool,
224 AttributeType(Value::String(t)) if t == "number" => Format::I64,
225 AttributeType(Value::String(t)) if t == "set" || t == "list" => {
226 Format::Seq(Box::new(Format::Str))
227 }
228 AttributeType(Value::String(t)) if t == "map" => Format::Map {
229 key: Box::new(Format::Str),
230 value: Box::new(Format::Str),
231 },
232 AttributeType(Value::String(t)) => {
233 return Err(Box::new(std::io::Error::new(
234 std::io::ErrorKind::Other,
235 format!("Unknown type {}", t),
236 )))
237 }
238 AttributeType(Value::Array(t))
239 if t.first().unwrap() == "set" || t.first().unwrap() == "list" =>
240 {
241 Format::Seq(Box::new(Format::Str))
242 }
243 AttributeType(Value::Array(t)) if t.first().unwrap() == "map" => Format::Map {
245 key: Box::new(Format::Str),
246 value: Box::new(Format::Str),
247 },
248 unknown => {
249 return Err(Box::new(std::io::Error::new(
250 std::io::ErrorKind::Other,
251 format!("Type {:?} not supported", unknown),
252 )))
253 }
254 };
255 let attr_fmt = match (at.optional, at.computed) {
256 (Some(opt), _) if opt => Format::Option(Box::new(f.clone())),
257 (_, Some(cmp)) if cmp => Format::Option(Box::new(f.clone())),
258 _ => f.clone(),
259 };
260
261 target_attrs.push(Named {
262 name: an,
263 value: attr_fmt,
264 });
265 }
266 if !target_attrs.is_empty() {
267 Ok(Some(ContainerFormat::Struct(target_attrs)))
268 } else {
269 Ok(None)
270 }
271}
272
273fn inject_meta_arguments(blk: &mut Block) {
274 let depends_on_attr = Attribute {
275 r#type: AttributeType(serde_json::json!(["set"])),
276 optional: Some(true),
277 ..Default::default()
278 };
279 let count_attr = Attribute {
280 r#type: AttributeType(serde_json::json!("number")),
281 optional: Some(true),
282 ..Default::default()
283 };
284
285 let for_each_attr = Attribute {
286 r#type: AttributeType(serde_json::json!(["set"])),
287 optional: Some(true),
288 ..Default::default()
289 };
290
291 let provider_attr = Attribute {
292 r#type: AttributeType(serde_json::json!("string")),
293 optional: Some(true),
294 ..Default::default()
295 };
296
297 if let Some(attrs) = blk.attributes.as_mut() {
298 attrs.insert("depends_on".to_owned(), depends_on_attr);
299 attrs.insert("count".to_owned(), count_attr);
300 attrs.insert("for_each".to_owned(), for_each_attr);
301 attrs.insert("provider".to_owned(), provider_attr);
302 }
303}
304
305fn export_block(
306 namespace: Option<String>,
307 name: &str,
308 blk: Block,
309 reg: &mut Registry,
310) -> std::result::Result<(), Box<dyn std::error::Error>> {
311 let mut cf1 = export_attributes(&blk.attributes.as_ref().unwrap())?;
312 if let Some(bt) = &blk.block_types {
313 for (block_type_name, nested_block) in bt {
314 export_block_type(
315 namespace.as_ref(),
316 name,
317 block_type_name,
318 nested_block,
319 reg,
320 &mut cf1.as_mut().unwrap(),
321 )?;
322 }
323 }
324
325 reg.insert((None, format!("{}_details", name)), cf1.unwrap());
326
327 Ok(())
328}
329
330fn export_block_type(
331 namespace: Option<&String>,
332 parent_name: &str,
333 name: &str,
334 blk: &NestedBlock,
335 reg: &mut Registry,
336 cf: &mut ContainerFormat,
337) -> std::result::Result<(), Box<dyn std::error::Error>> {
338 let mut inner_block_types = Vec::new();
339 if let Some(attrs) = &blk.block.attributes {
340 let mut nested_cf = export_attributes(attrs)?;
341 let block_type_ns = namespace.clone().map_or_else(
342 || format!("{}_block_type", parent_name),
343 |v| format!("{}_{}_block_type", parent_name, v),
344 );
345 let block_type_fqn = namespace.clone().map_or_else(
346 || format!("{}_block_type_{}", parent_name, name.to_owned()),
347 |v| format!("{}_{}_block_type_{}", parent_name, v, name.to_owned()),
348 );
349
350 if let Some(bt) = &blk.block.block_types {
352 for (block_type_name, nested_block) in bt {
353 export_block_type(
354 namespace,
355 name,
356 block_type_name,
357 nested_block,
358 reg,
359 &mut nested_cf.as_mut().unwrap(),
360 )?;
361 }
362 }
363 reg.insert((Some(block_type_ns), name.to_owned()), nested_cf.unwrap());
364 inner_block_types.push((name, block_type_fqn));
365 }
366
367 if let ContainerFormat::Struct(ref mut attrs) = cf {
368 for (_, (n, fqn)) in inner_block_types.iter().enumerate() {
369 attrs.push(Named {
370 name: n.to_string(),
371 value: Format::Option(Box::new(Format::Seq(Box::new(Format::TypeName(
372 fqn.to_string(),
373 ))))),
374 });
375 }
376 };
377
378 Ok(())
379}
380
381pub fn read_tf_schema_from_file<P: AsRef<Path>>(
382 path: P,
383) -> std::result::Result<TerraformSchemaExport, Box<dyn std::error::Error>> {
384 let file = File::open(path).expect("input file must be readable");
386 let reader = BufReader::new(file);
387 let d: TerraformSchemaExport = serde_json::from_reader(reader)?;
389
390 Ok(d)
392}
393
394#[cfg(test)]
395mod test {
396 use super::*;
397 use crate::test_utils::{config, datasource_root, provider_root, resource_root};
398 use std::fs::File;
399 use std::process::Command;
400 use tempfile::tempdir;
401
402 #[test]
403 fn test_deserialize_example_tf_schema() {
404 let tf_schema = read_tf_schema_from_file("./tests/fixtures/test-provider-schema.json");
405
406 assert!(tf_schema.is_ok());
407 let test_schema = tf_schema
408 .as_ref()
409 .unwrap()
410 .provider_schemas
411 .get("test_provider");
412
413 assert_eq!(tf_schema.as_ref().unwrap().provider_schemas.len(), 1);
414 assert!(test_schema.is_some());
415 assert_eq!(
416 test_schema
417 .unwrap()
418 .data_source_schemas
419 .as_ref()
420 .unwrap()
421 .len(),
422 2
423 );
424 assert_eq!(
425 test_schema.map(|x| x.resource_schemas.is_none()),
426 Some(false)
427 );
428 }
429
430 #[test]
431 fn test_generate_registry_from_schema() {
432 let tf_schema = read_tf_schema_from_file("./tests/fixtures/test-provider-schema.json");
433 let registry = export_schema_to_registry(&tf_schema.as_ref().unwrap());
434
435 assert!(registry.is_ok());
436 assert_eq!(registry.unwrap().len(), 10);
437 }
438
439 #[test]
440 fn test_generate_serde_model_from_registry() {
441 let tf_schema = read_tf_schema_from_file("./tests/fixtures/test-provider-schema.json");
442 let registry = export_schema_to_registry(&tf_schema.as_ref().unwrap());
443 let dir = tempdir().unwrap();
444
445 std::fs::write(
446 dir.path().join("Cargo.toml"),
447 r#"[package]
448 name = "testing"
449 version = "0.1.0"
450 edition = "2018"
451
452 [dependencies]
453 serde = { version = "1.0", features = ["derive"] }
454 serde_bytes = "0.11"
455
456 [workspace]
457 "#,
458 )
459 .unwrap();
460 std::fs::create_dir(dir.path().join("src")).unwrap();
461 let source_path = dir.path().join("src/lib.rs");
462 let mut source = File::create(&source_path).unwrap();
463 generate_serde("test", &mut source, ®istry.unwrap()).unwrap();
464 let target_dir = std::env::current_dir().unwrap().join("../target");
466 let status = Command::new("cargo")
467 .current_dir(dir.path())
468 .arg("build")
469 .arg("--target-dir")
470 .arg(target_dir)
471 .status()
472 .unwrap();
473 assert!(status.success());
474 }
475
476 #[test]
477 fn test_unmarshall_provider() {
478 let res: config =
479 serde_json::from_str(include_str!("../tests/fixtures/provider_test.json")).unwrap();
480 assert_eq!(res.provider.as_ref().map(|x| x.is_empty()), Some(false));
481 assert_eq!(
482 res.provider.as_ref().map(|x| x.get(0).is_none()),
483 Some(false)
484 );
485 let prv = res
486 .provider
487 .as_ref()
488 .and_then(|x| x.get(0))
489 .and_then(|x| match x {
490 provider_root::test_provider(p) => p.get(0),
491 });
492 assert_eq!(prv.is_none(), false);
493 assert_eq!(
494 prv.map(|x| x.api_token.to_owned()),
495 Some("ABC12345".to_owned())
496 );
497 }
498
499 #[test]
500 fn test_unmarshall_resource() {
501 let res: config =
502 serde_json::from_str(include_str!("../tests/fixtures/resource_test.json")).unwrap();
503 assert_eq!(res.resource.as_ref().map(|x| x.is_empty()), Some(false));
504 assert_eq!(
505 res.resource.as_ref().map(|x| x.get(0).is_none()),
506 Some(false)
507 );
508 let res_a = res
509 .resource
510 .as_ref()
511 .and_then(|x| x.get(0))
512 .and_then(|x| match x {
513 resource_root::test_resource_a(r1) => r1.get(0),
514 _ => None,
515 })
516 .and_then(|x| x.get("test"))
517 .and_then(|x| x.first());
518 assert_eq!(res_a.is_none(), false);
519 assert_eq!(
520 res_a.map(|x| x.name.to_owned()),
521 Some("test_resource_a".to_owned())
522 );
523 }
524
525 #[test]
526 fn test_unmarshall_datasource() {
527 let res: config =
528 serde_json::from_str(include_str!("../tests/fixtures/datasource_test.json")).unwrap();
529 assert_eq!(res.data.as_ref().map(|x| x.is_empty()), Some(false));
530 assert_eq!(res.data.as_ref().map(|x| x.get(0).is_none()), Some(false));
531 let res_a = res
532 .data
533 .as_ref()
534 .and_then(|x| x.get(0))
535 .and_then(|x| match x {
536 datasource_root::test_data_source_b(ds1) => ds1.get(0),
537 _ => None,
538 })
539 .and_then(|x| x.get("test"))
540 .and_then(|x| x.first());
541 assert_eq!(res_a.is_none(), false);
542 assert_eq!(
543 res_a.map(|x| x.name.to_owned()),
544 Some("test_datasource_b".to_owned())
545 );
546 }
547
548 #[test]
549 fn test_unmarshall_block_type() {
550 let res: config =
551 serde_json::from_str(include_str!("../tests/fixtures/block_type_test.json")).unwrap();
552 assert_eq!(res.data.as_ref().map(|x| x.is_empty()), Some(false));
553 assert_eq!(res.data.as_ref().map(|x| x.get(0).is_none()), Some(false));
554 let res_a = res
555 .data
556 .as_ref()
557 .and_then(|x| x.get(0))
558 .and_then(|x| match x {
559 datasource_root::test_data_source_a(ds1) => ds1.get(0),
560 _ => None,
561 })
562 .and_then(|x| x.get("test"))
563 .and_then(|x| x.first());
564 assert_eq!(res_a.is_none(), false);
565 assert_eq!(
566 res_a.map(|x| x.name.to_owned()),
567 Some("test_datasource_a".to_owned())
568 );
569 assert_eq!(res_a.map(|x| x.datasource_a_type.is_none()), Some(false));
570 assert_eq!(
571 res_a.and_then(|x| x.datasource_a_type.as_ref().map(|x| x.is_empty())),
572 Some(false)
573 );
574 assert_eq!(
575 res_a.and_then(|x| x
576 .datasource_a_type
577 .as_ref()
578 .unwrap()
579 .first()
580 .unwrap()
581 .filter_type
582 .to_owned()),
583 Some("REGEX".to_owned())
584 );
585 }
586}