dbschema 0.1.2

Define database schema's as HCL files, and generate idempotent SQL migrations
Documentation
use std::collections::HashMap;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result, bail};
use hcl::Body;
use hcl::value::Map;

use crate::Loader;
use crate::frontend::core::get_attr_string;
use crate::frontend::env::EnvVars;
use crate::prisma::{
    self, BlockAttribute, ConfigBlock, DefaultValue, FieldAttribute, Model, Schema, View,
};

/// Load all `data` blocks in the current body and populate the evaluation environment.
pub fn load_data_sources(
    loader: &dyn Loader,
    base: &Path,
    body: &Body,
    env: &mut EnvVars,
) -> Result<()> {
    for blk in body.blocks().filter(|b| b.identifier() == "data") {
        let dtype = blk
            .labels()
            .get(0)
            .ok_or_else(|| anyhow::anyhow!("data block missing type label"))?
            .as_str()
            .to_string();
        let name = blk
            .labels()
            .get(1)
            .ok_or_else(|| anyhow::anyhow!("data block missing name label"))?
            .as_str()
            .to_string();

        let value = match dtype.as_str() {
            "prisma_schema" => load_prisma_schema(loader, base, blk.body(), env)?,
            other => bail!("unsupported data source type '{other}'"),
        };

        env.data
            .entry(dtype)
            .or_insert_with(HashMap::new)
            .insert(name, value);
    }
    Ok(())
}

fn load_prisma_schema(
    loader: &dyn Loader,
    base: &Path,
    body: &Body,
    env: &EnvVars,
) -> Result<hcl::Value> {
    let file = get_attr_string(body, "file", env)?
        .context("prisma_schema data source requires 'file' attribute")?;
    let path = resolve_relative(base, &file);
    let contents = loader
        .load(&path)
        .with_context(|| format!("reading Prisma schema from {}", path.display()))?;
    let schema = prisma::parse_schema_str(&contents)?;
    Ok(schema_to_value(schema))
}

fn resolve_relative(base: &Path, value: &str) -> PathBuf {
    let p = Path::new(value);
    if p.is_absolute() {
        p.to_path_buf()
    } else {
        base.join(p)
    }
}

fn schema_to_value(schema: Schema) -> hcl::Value {
    let mut root = Map::<String, hcl::Value>::new();
    root.insert("models".into(), models_to_value(&schema.models));
    root.insert("views".into(), views_to_value(&schema.views));
    root.insert(
        "composite_types".into(),
        composite_types_to_value(&schema.composite_types),
    );
    root.insert(
        "type_aliases".into(),
        type_aliases_to_value(&schema.type_aliases),
    );
    root.insert(
        "custom_blocks".into(),
        custom_blocks_to_value(&schema.custom_blocks),
    );
    root.insert("enums".into(), enums_to_value(&schema.enums));
    root.insert(
        "datasources".into(),
        config_blocks_to_value(&schema.datasources),
    );
    root.insert(
        "generators".into(),
        config_blocks_to_value(&schema.generators),
    );
    hcl::Value::Object(root)
}

fn models_to_value(models: &[Model]) -> hcl::Value {
    let mut map = Map::<String, hcl::Value>::new();
    for model in models {
        map.insert(
            model.name.to_string(),
            model_like_to_value(&model.name, &model.fields, &model.attributes),
        );
    }
    hcl::Value::Object(map)
}

fn views_to_value(views: &[View]) -> hcl::Value {
    let mut map = Map::<String, hcl::Value>::new();
    for view in views {
        map.insert(
            view.name.to_string(),
            model_like_to_value(&view.name, &view.fields, &view.attributes),
        );
    }
    hcl::Value::Object(map)
}

fn model_like_to_value(
    name: &prisma::Identifier,
    fields: &[prisma::Field],
    attributes: &[BlockAttribute],
) -> hcl::Value {
    let mut model_map = Map::new();
    model_map.insert("name".into(), hcl::Value::String(name.to_string()));
    model_map.insert("fields".into(), fields_to_value(fields));
    model_map.insert("attributes".into(), block_attributes_to_value(attributes));
    hcl::Value::Object(model_map)
}

fn composite_types_to_value(types: &[prisma::CompositeType]) -> hcl::Value {
    let mut map = Map::<String, hcl::Value>::new();
    for ct in types {
        let mut ct_map = Map::new();
        ct_map.insert("name".into(), hcl::Value::String(ct.name.to_string()));
        ct_map.insert("fields".into(), fields_to_value(&ct.fields));
        map.insert(ct.name.to_string(), hcl::Value::Object(ct_map));
    }
    hcl::Value::Object(map)
}

fn type_aliases_to_value(aliases: &[prisma::TypeAlias]) -> hcl::Value {
    let mut map = Map::<String, hcl::Value>::new();
    for alias in aliases {
        let mut alias_map = Map::new();
        alias_map.insert("name".into(), hcl::Value::String(alias.name.to_string()));
        alias_map.insert("target".into(), type_to_value(&alias.target));
        alias_map.insert(
            "attributes".into(),
            field_attributes_to_value(&alias.attributes),
        );
        if let Some(doc) = &alias.documentation {
            alias_map.insert("documentation".into(), hcl::Value::String(doc.clone()));
        }
        map.insert(alias.name.to_string(), hcl::Value::Object(alias_map));
    }
    hcl::Value::Object(map)
}

fn custom_blocks_to_value(blocks: &[prisma::CustomBlock]) -> hcl::Value {
    let mut map = Map::<String, hcl::Value>::new();
    for block in blocks {
        let mut block_map = Map::new();
        block_map.insert("name".into(), hcl::Value::String(block.name.to_string()));
        block_map.insert(
            "contents".into(),
            hcl::Value::String(block.contents.clone()),
        );
        if let Some(doc) = &block.documentation {
            block_map.insert("documentation".into(), hcl::Value::String(doc.clone()));
        }
        map.insert(block.name.to_string(), hcl::Value::Object(block_map));
    }
    hcl::Value::Object(map)
}

fn fields_to_value(fields: &[prisma::Field]) -> hcl::Value {
    let mut map = Map::<String, hcl::Value>::new();
    for field in fields {
        let mut field_map = Map::new();
        field_map.insert("name".into(), hcl::Value::String(field.name.to_string()));
        field_map.insert("type".into(), type_to_value(&field.r#type));
        field_map.insert(
            "attributes".into(),
            field_attributes_to_value(&field.attributes),
        );
        map.insert(field.name.to_string(), hcl::Value::Object(field_map));
    }
    hcl::Value::Object(map)
}

fn enums_to_value(enums: &[prisma::Enum]) -> hcl::Value {
    let mut map = Map::<String, hcl::Value>::new();
    for enm in enums {
        let mut enum_map = Map::new();
        enum_map.insert("name".into(), hcl::Value::String(enm.name.to_string()));
        enum_map.insert("values".into(), enum_values_to_value(&enm.values));
        enum_map.insert(
            "attributes".into(),
            block_attributes_to_value(&enm.attributes),
        );
        map.insert(enm.name.to_string(), hcl::Value::Object(enum_map));
    }
    hcl::Value::Object(map)
}

fn enum_values_to_value(values: &[prisma::EnumValue]) -> hcl::Value {
    let mut map = Map::<String, hcl::Value>::new();
    for value in values {
        let mut value_map = Map::new();
        value_map.insert("name".into(), hcl::Value::String(value.name.to_string()));
        if let Some(mapped) = &value.mapped_name {
            value_map.insert("mapped_name".into(), hcl::Value::String(mapped.clone()));
        }
        map.insert(value.name.to_string(), hcl::Value::Object(value_map));
    }
    hcl::Value::Object(map)
}

fn config_blocks_to_value(blocks: &[ConfigBlock]) -> hcl::Value {
    let mut map = Map::<String, hcl::Value>::new();
    for block in blocks {
        let mut block_map = Map::new();
        block_map.insert("name".into(), hcl::Value::String(block.name.to_string()));
        if let Some(doc) = &block.documentation {
            block_map.insert("documentation".into(), hcl::Value::String(doc.clone()));
        }
        block_map.insert(
            "properties".into(),
            config_properties_to_value(&block.properties),
        );
        map.insert(block.name.to_string(), hcl::Value::Object(block_map));
    }
    hcl::Value::Object(map)
}

fn config_properties_to_value(properties: &[prisma::ConfigProperty]) -> hcl::Value {
    let mut map = Map::<String, hcl::Value>::new();
    for prop in properties {
        let mut prop_map = Map::new();
        prop_map.insert("name".into(), hcl::Value::String(prop.name.to_string()));
        if let Some(value) = &prop.value {
            prop_map.insert("value".into(), hcl::Value::String(value.clone()));
        }
        map.insert(prop.name.to_string(), hcl::Value::Object(prop_map));
    }
    hcl::Value::Object(map)
}

fn type_to_value(ty: &prisma::Type) -> hcl::Value {
    let mut map = Map::new();
    map.insert("name".into(), hcl::Value::String(ty.name.clone()));
    map.insert("optional".into(), hcl::Value::Bool(ty.optional));
    map.insert("list".into(), hcl::Value::Bool(ty.list));
    hcl::Value::Object(map)
}

fn field_attributes_to_value(attrs: &[FieldAttribute]) -> hcl::Value {
    let mut map = Map::<String, hcl::Value>::new();
    let raw: Vec<hcl::Value> = attrs
        .iter()
        .map(|a| hcl::Value::String(format!("{}", a)))
        .collect();
    map.insert("raw".into(), hcl::Value::Array(raw));

    if attrs.iter().any(|a| {
        matches!(a, FieldAttribute::Id)
            || matches!(a, FieldAttribute::Raw(raw) if raw_field_attribute_name(raw) == Some("id"))
    }) {
        map.insert("id".into(), hcl::Value::Bool(true));
    }
    if attrs.iter().any(|a| {
        matches!(a, FieldAttribute::Unique)
            || matches!(a, FieldAttribute::Raw(raw) if raw_field_attribute_name(raw) == Some("unique"))
    }) {
        map.insert("unique".into(), hcl::Value::Bool(true));
    }
    if let Some(default) = attrs.iter().find_map(|a| match a {
        FieldAttribute::Default(d) => Some(d),
        _ => None,
    }) {
        map.insert("default".into(), default_to_value(default));
    }
    if let Some(map_attr) = attrs.iter().find_map(|a| match a {
        FieldAttribute::Map(m) => Some(m.clone()),
        _ => None,
    }) {
        map.insert("map".into(), hcl::Value::String(map_attr));
    }
    if let Some(db) = attrs.iter().find_map(|a| match a {
        FieldAttribute::DbNative(db) => Some(db.clone()),
        _ => None,
    }) {
        map.insert("db_native".into(), hcl::Value::String(db));
    }
    if let Some(relation) = attrs.iter().find_map(|a| match a {
        FieldAttribute::Relation(rel) => Some(rel),
        _ => None,
    }) {
        map.insert("relation".into(), relation_to_value(relation));
    }

    hcl::Value::Object(map)
}

fn block_attributes_to_value(attrs: &[BlockAttribute]) -> hcl::Value {
    let mut map = Map::<String, hcl::Value>::new();
    let raw: Vec<hcl::Value> = attrs
        .iter()
        .map(|a| hcl::Value::String(format!("{}", a)))
        .collect();
    map.insert("raw".into(), hcl::Value::Array(raw));

    if let Some(name) = attrs.iter().find_map(|a| match a {
        BlockAttribute::Map(m) => Some(m.clone()),
        _ => None,
    }) {
        map.insert("map".into(), hcl::Value::String(name));
    }

    hcl::Value::Object(map)
}

fn relation_to_value(relation: &prisma::RelationAttribute) -> hcl::Value {
    let mut map = Map::<String, hcl::Value>::new();
    if let Some(name) = &relation.name {
        map.insert("name".into(), hcl::Value::String(name.clone()));
    }
    map.insert("fields".into(), identifiers_to_array(&relation.fields));
    map.insert(
        "references".into(),
        identifiers_to_array(&relation.references),
    );
    if let Some(map_name) = &relation.map {
        map.insert("map".into(), hcl::Value::String(map_name.clone()));
    }
    if let Some(on_delete) = &relation.on_delete {
        map.insert("on_delete".into(), hcl::Value::String(on_delete.clone()));
    }
    if let Some(on_update) = &relation.on_update {
        map.insert("on_update".into(), hcl::Value::String(on_update.clone()));
    }

    hcl::Value::Object(map)
}

fn default_to_value(value: &DefaultValue) -> hcl::Value {
    hcl::Value::String(format!("{}", value))
}

fn identifiers_to_array(values: &[prisma::Identifier]) -> hcl::Value {
    hcl::Value::Array(
        values
            .iter()
            .map(|value| hcl::Value::String(value.to_string()))
            .collect(),
    )
}

fn raw_field_attribute_name(raw: &str) -> Option<&str> {
    let trimmed = raw.trim_start();
    let stripped = trimmed.strip_prefix('@')?;
    let end = stripped
        .find(|c: char| c == '(' || c.is_whitespace())
        .unwrap_or(stripped.len());
    Some(&stripped[..end])
}