tf-bindgen 0.1.0

Automatically generates Rust bindings for Terraform providers.
Documentation
use std::collections::HashMap;

use heck::ToUpperCamelCase;
use itertools::Itertools;
use semver::{Comparator, Op, VersionReq};
use tf_bindgen_schema::provider::v1_0::{Attribute, Block, BlockType, Type};
use tf_bindgen_schema::provider::Schema;

use crate::codegen::type_info::TypeInfo;

use self::field_info::FieldInfo;
use self::path::Path;
use self::struct_info::{StructInfo, StructType};
use self::type_info::Wrapper;

pub mod field_info;
pub mod path;
pub mod struct_info;
pub mod type_info;

pub use tf_bindgen_codegen::resource;
pub use tf_bindgen_codegen::Construct;

pub struct Generator {
    pub providers: Vec<Provider>,
}

#[derive(Debug)]
pub struct Provider {
    pub provider: StructInfo,
    pub resources: Vec<StructInfo>,
    pub data_sources: Vec<StructInfo>,
}

pub struct Nested(Vec<StructInfo>);
pub struct Constructs(Vec<StructInfo>);
pub struct Fields(Vec<FieldInfo>);

impl Generator {
    pub fn from_schema(schema: Schema, versions: HashMap<String, VersionReq>) -> Self {
        let schemas = match schema {
            Schema::V1_0 { provider_schemas } => provider_schemas
                .iter()
                .map(|(url, schema)| {
                    let name = url.split('/').last().unwrap();
                    let version = versions
                        .iter()
                        .find(|(n, _)| n.split('/').last().unwrap() == name)
                        .unwrap()
                        .1
                        .comparators
                        .iter()
                        .map(|comp| cargo_simplify_version(comp.clone()))
                        .join(",");
                    let provider =
                        StructInfo::from_provider(name, version, url, &schema.provider.block);
                    let resources = schema
                        .resource_schemas
                        .iter()
                        .map(|(name, schema)| {
                            let path = Path::empty();
                            let this_path = Path::new(vec![name.to_string()]);
                            let fields = Fields::from_schema(&this_path, &schema.block).0;
                            let nested = Nested::from_schema(&this_path, &schema.block).0;
                            let ty = StructType::Construct {
                                ty: name.clone(),
                                nested,
                            };
                            StructInfo::builder()
                                .ty(ty)
                                .name(name.clone())
                                .path(path)
                                .fields(fields)
                                .build()
                                .unwrap()
                        })
                        .collect();
                    let data_sources = schema
                        .data_source_schemas
                        .iter()
                        .map(|(name, schema)| {
                            let path = Path::new(vec!["data".to_string()]);
                            let this_path = Path::new(vec!["data".to_string(), name.to_string()]);
                            let nested = Nested::from_schema(&this_path, &schema.block).0;
                            let ty = StructType::Construct {
                                ty: name.clone(),
                                nested,
                            };
                            let fields = Fields::from_schema(&this_path, &schema.block).0;
                            StructInfo::builder()
                                .ty(ty)
                                .name(name.clone())
                                .path(path)
                                .fields(fields)
                                .build()
                                .unwrap()
                        })
                        .collect();
                    Provider {
                        provider,
                        resources,
                        data_sources,
                    }
                })
                .collect(),
            Schema::Unknown => unimplemented!("unsupported provider version"),
        };
        Generator { providers: schemas }
    }
}

impl StructInfo {
    pub fn from_provider(
        name: impl Into<String>,
        version: impl Into<String>,
        url: impl Into<String>,
        schema: &Block,
    ) -> Self {
        let name = name.into();
        let path = Path::empty();
        let nested = Nested::from_schema(&path, schema);
        let ty = StructType::Provider {
            ty: url.into(),
            ver: version.into(),
            nested: nested.0,
        };
        let fields = Fields::from_schema(&path, schema);
        StructInfo::builder()
            .ty(ty)
            .path(path)
            .name(name)
            .fields(fields.0)
            .build()
            .unwrap()
    }
}

fn get_fields(ty: &BlockType) -> Option<&HashMap<String, BlockType>> {
    match ty {
        BlockType::Set(inner) | BlockType::Map(inner) | BlockType::List(inner) => get_fields(inner),
        BlockType::Object(fields) => Some(fields),
        _ => None,
    }
}

impl Nested {
    pub fn from_schema(path: &Path, block: &Block) -> Self {
        let iter = block
            .block_types
            .iter()
            .flatten()
            .flat_map(|(name, field)| {
                let mut this_path = path.clone();
                this_path.push(name);
                let schema = match field {
                    Type::Single { block } => block,
                    Type::List { block, .. } => block,
                };
                let this = StructInfo::from_type(path, name, field);
                Nested::from_schema(&this_path, schema)
                    .0
                    .into_iter()
                    .chain(vec![this])
                    .collect::<Vec<_>>()
            });
        let nested = block
            .attributes
            .iter()
            .flatten()
            .flat_map(|(name, field)| {
                let mut this_path = path.clone();
                this_path.push(name);
                let fields = get_fields(&field.r#type);
                let this = fields
                    .iter()
                    .map(|fields| StructInfo::from_fields(path, name, fields));
                fields
                    .iter()
                    .flat_map(move |fields| Nested::from_fields(&this_path, fields).0)
                    .chain(this)
                    .collect::<Vec<_>>()
            })
            .chain(iter)
            .collect();
        Nested(nested)
    }

    pub fn from_fields(path: &Path, fields: &HashMap<String, BlockType>) -> Self {
        let nested = fields
            .iter()
            .filter_map(|(name, ty)| Some((name, get_fields(ty)?)))
            .flat_map(|(name, fields)| {
                let this = StructInfo::from_fields(path, name, fields);
                let mut this_path = path.clone();
                this_path.push(name);
                Nested::from_fields(&this_path, fields)
                    .0
                    .into_iter()
                    .chain(vec![this])
                    .collect::<Vec<_>>()
            })
            .collect();
        Nested(nested)
    }
}

impl Fields {
    pub fn from_schema(path: &Path, schema: &Block) -> Self {
        let attr_fields = schema
            .attributes
            .iter()
            .flatten()
            .map(|(name, field)| FieldInfo::from_field(path, name, field));
        let fields = schema
            .block_types
            .iter()
            .flatten()
            .map(|(name, field)| FieldInfo::from_type(path, name, field))
            .chain(attr_fields)
            .collect();
        Fields(fields)
    }

    pub fn from_fields(path: &Path, fields: &HashMap<String, BlockType>) -> Self {
        let fields = fields
            .iter()
            .map(|(name, ty)| {
                FieldInfo::builder()
                    .path(path.clone())
                    .name(name.clone())
                    .type_info(TypeInfo::from_schema(path, name, ty))
                    .optional(true)
                    .computed(false)
                    .description(None)
                    .build()
                    .unwrap()
            })
            .collect();
        Fields(fields)
    }
}

impl StructInfo {
    pub fn from_type(path: &Path, name: impl Into<String>, ty: &Type) -> Self {
        let name = name.into();
        let mut this_path = path.clone();
        this_path.push(&name);
        let ty: &Block = match ty {
            Type::Single { block } => block,
            Type::List { block, .. } => block,
        };
        let fields = Fields::from_schema(&this_path, ty).0;
        StructInfo::builder()
            .ty(StructType::Nested)
            .path(path.clone())
            .name(name)
            .fields(fields)
            .build()
            .unwrap()
    }

    pub fn from_fields(
        path: &Path,
        name: impl Into<String>,
        fields: &HashMap<String, BlockType>,
    ) -> Self {
        let name = name.into();
        let mut this_path = path.clone();
        this_path.push(&name);
        let fields = Fields::from_fields(&this_path, fields);
        StructInfo::builder()
            .ty(StructType::Nested)
            .path(path.clone())
            .name(name)
            .fields(fields.0)
            .build()
            .unwrap()
    }
}

impl FieldInfo {
    pub fn from_field(path: &Path, name: impl Into<String>, field: &Attribute) -> Self {
        let opt = field.optional.unwrap_or(false);
        let comp = field.computed.unwrap_or(false);
        let req = field.required.unwrap_or(comp && !opt);
        assert_ne!(opt, req, "Field must be optional and required not both.");
        let name = name.into();
        let type_info = TypeInfo::from_schema(path, &name, &field.r#type);
        FieldInfo::builder()
            .path(path.clone())
            .name(name)
            .type_info(type_info)
            .description(field.description.clone())
            .optional(opt)
            .computed(comp)
            .build()
            .unwrap()
    }

    pub fn from_type(path: &Path, name: impl Into<String>, field: &Type) -> Self {
        let name = name.into();
        let req = match field {
            Type::Single { .. } => false,
            Type::List {
                min_items: Some(1),
                max_items: Some(1),
                ..
            } => true,
            Type::List { .. } => false,
        };
        let type_name = path.type_name() + &name.to_upper_camel_case();
        let type_wrapper = match field {
            Type::Single { .. } => Wrapper::Type,
            Type::List { .. } => Wrapper::List,
        };
        FieldInfo::builder()
            .path(path.clone())
            .name(name)
            .type_info(TypeInfo::new(type_wrapper, type_name))
            .optional(!req)
            .computed(false)
            .description(None)
            .build()
            .unwrap()
    }
}

pub fn cargo_simplify_version(constraint: Comparator) -> String {
    let major = constraint.major;
    let minor = constraint.minor.unwrap_or(0);
    let patch = constraint.patch.unwrap_or(0);
    assert!(
        constraint.pre.is_empty(),
        "pre release constraints are not supported"
    );
    match constraint.op {
        Op::Tilde if constraint.minor.is_some() => {
            format!(">={major}{minor}{patch},<{major}{}.0", minor + 1)
        }
        Op::Caret if major == 0 && constraint.minor.is_none() => ">=0.0.0,<1.0.0".to_string(),
        Op::Caret if major == 0 && minor == 0 && constraint.patch.is_some() => {
            format!(">=0.0.{patch},<0.0.{}", patch + 1)
        }
        Op::Caret if major == 0 => {
            format!(">=0.{minor}.0,<0.{}.0", minor + 1)
        }
        Op::Wildcard if constraint.minor.is_some() => {
            format!(">={major}.{minor}.0,<{major}.{}.0", minor + 1)
        }
        Op::Tilde | Op::Caret | Op::Wildcard => {
            format!(">={major}.{minor}.{patch},<{}.0.0", major + 1)
        }
        Op::Exact => format!("={major}.{minor}.{patch}"),
        Op::Greater => format!(">{major}.{minor}.{patch}"),
        Op::GreaterEq => format!(">={major}.{minor}.{patch}"),
        Op::Less => format!("<{major}.{minor}.{patch}"),
        Op::LessEq => format!("<={major}.{minor}.{patch}"),
        _ => unimplemented!(),
    }
}