tfschema_bindgen/
binding.rs

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, &registry)
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                // add terraform meta-tags to block
141                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            /* TODO: It will assume a map of strings even if the specified type is of a different kind (e.g. map of object) */
244            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        // export inner block types
351        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    // Open the file in read-only mode with buffer.
385    let file = File::open(path).expect("input file must be readable");
386    let reader = BufReader::new(file);
387    // Read the JSON contents of the file as an instance of `User`.
388    let d: TerraformSchemaExport = serde_json::from_reader(reader)?;
389
390    // Return the `Diagram`.
391    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, &registry.unwrap()).unwrap();
464        // Use a stable `target` dir to avoid downloading and recompiling crates everytime.
465        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}