rototo 0.1.0-alpha.3

Control plane for runtime configuration of your application.
Documentation
use std::collections::BTreeMap;

use toml_span::Value as TomlValue;
use toml_span::value::Table;

use crate::diagnostics::DiagnosticLocation;

use super::super::index::*;
use super::super::source::SourceDocument;
use super::super::syntax::{ParsedToml, item_location, value_location};
use super::fields::{integer_field, json_from_toml_value, optional_string_field, string_field};

pub(crate) fn project_variable(
    document: &SourceDocument,
    toml: &ParsedToml,
    id: &str,
) -> VariableNode {
    let root = toml.root_table();
    let location = document.document_location();
    let schema_version = root
        .map(|root| integer_field(document, root, "schema_version", location.clone()))
        .unwrap_or_else(|| ProjectField::Missing {
            location: location.clone(),
        });
    let description = root.and_then(|root| optional_string_field(document, root, "description"));
    let type_source = project_type_source(document, root, location.clone());
    let values = project_values(document, root, id);
    let resolve = project_resolve(document, root, id);

    VariableNode {
        doc: document.id,
        id: id.to_owned(),
        location,
        schema_version,
        description,
        type_source,
        values,
        resolve,
    }
}

fn project_type_source(
    document: &SourceDocument,
    root: Option<&Table<'_>>,
    location: DiagnosticLocation,
) -> TypeSourceNode {
    let Some(root) = root else {
        return TypeSourceNode::Missing { location };
    };
    let type_item = root.get("type");
    let schema_item = root.get("schema");
    match (type_item, schema_item) {
        (None, None) => TypeSourceNode::Missing { location },
        (Some(_type_item), Some(schema_item)) => TypeSourceNode::Conflict {
            location: item_location(document, schema_item),
        },
        (Some(item), None) => match item.as_str() {
            Some(type_name) => {
                let location = item_location(document, item);
                if let Some(resource_id) = type_name.strip_prefix("resource:") {
                    TypeSourceNode::Resource(Spanned {
                        value: resource_id.to_owned(),
                        location,
                    })
                } else {
                    TypeSourceNode::Primitive(Spanned {
                        value: type_name.to_owned(),
                        location,
                    })
                }
            }
            None => TypeSourceNode::Invalid {
                location: item_location(document, item),
            },
        },
        (None, Some(item)) => match item.as_str() {
            Some(schema) => TypeSourceNode::Schema(Spanned {
                value: schema.to_owned(),
                location: item_location(document, item),
            }),
            None => TypeSourceNode::Invalid {
                location: item_location(document, item),
            },
        },
    }
}

fn project_values(document: &SourceDocument, root: Option<&Table<'_>>, id: &str) -> ValuesNode {
    let Some(root) = root else {
        return ValuesNode {
            location: document.document_location(),
            inline_values: BTreeMap::new(),
            invalid_shape: false,
        };
    };
    let Some(item) = root.get("values") else {
        return ValuesNode {
            location: document.document_location(),
            inline_values: BTreeMap::new(),
            invalid_shape: false,
        };
    };
    let location = item_location(document, item);
    let Some(table) = item.as_table() else {
        return ValuesNode {
            location,
            inline_values: BTreeMap::new(),
            invalid_shape: true,
        };
    };

    let inline_values = project_inline_values(document, id, table);
    ValuesNode {
        location,
        inline_values,
        invalid_shape: false,
    }
}

fn project_inline_values(
    document: &SourceDocument,
    variable_id: &str,
    table: &Table<'_>,
) -> BTreeMap<String, ValueNode> {
    table
        .iter()
        .map(|(key, value)| {
            let key = key.name.to_string();
            (
                key.clone(),
                ValueNode {
                    variable_id: variable_id.to_owned(),
                    key,
                    location: item_location(document, value),
                    value: json_from_toml_value(value),
                    origin: ValueOrigin::Inline {
                        variable_doc: document.id,
                    },
                },
            )
        })
        .collect()
}

fn project_resolve(
    document: &SourceDocument,
    root: Option<&Table<'_>>,
    variable_id: &str,
) -> ResolveNode {
    let Some(root) = root else {
        return ResolveNode::Missing {
            location: document.document_location(),
        };
    };
    let Some(item) = root.get("resolve") else {
        return ResolveNode::Missing {
            location: document.document_location(),
        };
    };
    let location = item_location(document, item);
    let Some(table) = item.as_table() else {
        return ResolveNode::Invalid { location };
    };

    ResolveNode::Resolve {
        location: location.clone(),
        default: Box::new(string_field(document, table, "default", location.clone())),
        rules: project_rules(document, variable_id, table),
    }
}

fn project_rules(
    document: &SourceDocument,
    variable_id: &str,
    table: &Table<'_>,
) -> RuleCollection {
    let Some(item) = table.get("rule") else {
        return RuleCollection::Rules(Vec::new());
    };

    if let Some(array) = item.as_array() {
        return RuleCollection::Rules(
            array
                .iter()
                .enumerate()
                .map(|(index, value)| project_rule_from_value(document, variable_id, index, value))
                .collect(),
        );
    }

    RuleCollection::Invalid {
        location: item_location(document, item),
    }
}

fn project_rule_from_value(
    document: &SourceDocument,
    variable_id: &str,
    index: usize,
    value: &TomlValue<'_>,
) -> VariableRuleNode {
    let location = value_location(document, value);
    let Some(table) = value.as_table() else {
        return VariableRuleNode {
            index,
            location: location.clone(),
            qualifier: ProjectField::Invalid {
                location: location.clone(),
            },
            value: ProjectField::Invalid { location },
            invalid_shape: true,
        };
    };
    project_rule_from_table_like(document, variable_id, index, table, location, false)
}

fn project_rule_from_table_like(
    document: &SourceDocument,
    _variable_id: &str,
    index: usize,
    table: &Table<'_>,
    location: DiagnosticLocation,
    invalid_shape: bool,
) -> VariableRuleNode {
    VariableRuleNode {
        index,
        location: location.clone(),
        qualifier: string_field(document, table, "qualifier", location.clone()),
        value: string_field(document, table, "value", location.clone()),
        invalid_shape,
    }
}