achitekfile 0.2.0

A tree-sitter backed semantic parser for the Achitekfile DSL
Documentation
use super::syntax::{named_children, text, text_range_for_node};
use crate::model::{
    AchitekFile, Blueprint, ComparisonOperator, Dependency, Prompt, PromptType, Spanned,
    Validation, Value,
};
use tree_sitter::{Node, Tree};

pub(super) fn build_file(tree: &Tree, source: &str) -> AchitekFile {
    let root = tree.root_node();
    let mut cursor = root.walk();
    let mut blueprint = Blueprint::default();
    let mut prompts = Vec::new();

    for child in root.named_children(&mut cursor) {
        match child.kind() {
            "blueprint_block" => {
                blueprint = parse_blueprint(child, source);
            }
            "prompt_block" => {
                if let Some(prompt) = parse_prompt(child, source) {
                    prompts.push(prompt);
                }
            }
            _ => {}
        }
    }

    AchitekFile::new(blueprint, prompts)
}

fn parse_blueprint(node: Node<'_>, source: &str) -> Blueprint {
    let mut blueprint = Blueprint {
        range: Some(text_range_for_node(node)),
        ..Blueprint::default()
    };
    for child in named_children(node) {
        if child.kind() != "blueprint_attribute" {
            continue;
        }

        let Some(key_node) = child.child_by_field_name("key") else {
            continue;
        };

        let Some(value_node) = child.child_by_field_name("value") else {
            continue;
        };

        let key = text(key_node, source);
        let Some(value) = parse_string_literal(value_node, source) else {
            continue;
        };
        let spanned = Spanned {
            value,
            range: text_range_for_node(child),
        };

        match key {
            "version" => blueprint.version = Some(spanned),
            "name" => blueprint.name = Some(spanned),
            "description" => blueprint.description = Some(spanned),
            "author" => blueprint.author = Some(spanned),
            "min_achitek_version" => blueprint.min_achitek_version = Some(spanned),
            _ => {}
        }
    }

    blueprint
}

fn parse_prompt(node: Node<'_>, source: &str) -> Option<Spanned<Prompt>> {
    let name_node = node.child_by_field_name("name")?;
    let name = parse_string_literal(name_node, source)?;
    let mut choices: Vec<Value> = Vec::new();
    let mut choices_declared = false;
    let mut prompt_type = None;
    let mut help = None;
    let mut default = None;
    let mut required = None;
    let mut depends_on = None;
    let mut validation = Validation::default();

    for child in named_children(node) {
        match child.kind() {
            "question_attribute" => {
                let Some(attribute) = named_children(child).next() else {
                    continue;
                };
                let Some(value_node) = attribute.child_by_field_name("value") else {
                    continue;
                };

                match attribute.kind() {
                    "type_attribute" => prompt_type = parse_prompt_type(value_node, source),
                    "help_attribute" => help = parse_string_literal(value_node, source),
                    "choices_attribute" => {
                        choices_declared = true;
                        choices = parse_array(value_node, source);
                    }
                    "default_attribute" => default = parse_value(value_node, source),
                    "required_attribute" => required = parse_bool(value_node, source),
                    "depends_on_attribute" => depends_on = parse_dependency(value_node, source),
                    _ => {}
                }
            }
            "validate_block" => parse_validation(child, source, &mut validation),
            _ => {}
        }
    }

    Some(Spanned {
        value: Prompt {
            name,
            prompt_type,
            help,
            choices,
            choices_declared,
            default,
            required,
            depends_on,
            validation,
        },
        range: text_range_for_node(node),
    })
}

fn parse_validation(node: Node<'_>, source: &str, validation: &mut Validation) {
    for item in named_children(node).filter(|node| node.kind() == "validate_attribute") {
        let Some(attribute) = named_children(item).next() else {
            continue;
        };
        let Some(value_node) = attribute.child_by_field_name("value") else {
            continue;
        };

        match attribute.kind() {
            "regex_attribute" => validation.regex = parse_string_literal(value_node, source),
            "min_length_attribute" => validation.min_length = parse_integer(value_node, source),
            "max_length_attribute" => validation.max_length = parse_integer(value_node, source),
            "min_selections_attribute" => {
                validation.min_selections = parse_integer(value_node, source)
            }
            "max_selections_attribute" => {
                validation.max_selections = parse_integer(value_node, source)
            }
            _ => {}
        }
    }
}

fn parse_prompt_type(node: Node<'_>, source: &str) -> Option<PromptType> {
    match text(node, source) {
        "string" => Some(PromptType::String),
        "paragraph" => Some(PromptType::Paragraph),
        "bool" => Some(PromptType::Bool),
        "select" => Some(PromptType::Select),
        "multiselect" => Some(PromptType::MultiSelect),
        _ => None,
    }
}

fn parse_string_literal(node: Node<'_>, source: &str) -> Option<String> {
    let text = text(node, source);
    let without_open = text.strip_prefix('"')?;
    let inner = without_open.strip_suffix('"')?;

    let mut parsed = String::new();
    let mut chars = inner.chars();
    while let Some(ch) = chars.next() {
        if ch != '\\' {
            parsed.push(ch);
            continue;
        }

        match chars.next()? {
            'n' => parsed.push('\n'),
            't' => parsed.push('\t'),
            'r' => parsed.push('\r'),
            '"' => parsed.push('"'),
            '\\' => parsed.push('\\'),
            _ => return None,
        }
    }

    Some(parsed)
}

fn parse_array(node: Node<'_>, source: &str) -> Vec<Value> {
    let Some(value_list) = named_children(node).find(|node| node.kind() == "value_list") else {
        return Vec::new();
    };

    named_children(value_list)
        .filter(|node| node.kind() == "value")
        .filter_map(|node| parse_value(node, source))
        .collect()
}

fn parse_value(node: Node<'_>, source: &str) -> Option<Value> {
    let inner = if node.kind() == "value" || node.kind() == "literal_value" {
        named_children(node).next()?
    } else {
        node
    };

    match inner.kind() {
        "string_literal" => parse_string_literal(inner, source).map(Value::String),
        "boolean" => match text(inner, source) {
            "true" => Some(Value::Bool(true)),
            "false" => Some(Value::Bool(false)),
            _ => None,
        },
        "integer" => text(inner, source).parse::<u64>().ok().map(Value::Integer),
        "identifier" => Some(Value::Identifier(text(inner, source).to_owned())),
        "array" => Some(Value::Array(parse_array(inner, source))),
        _ => None,
    }
}

fn parse_bool(node: Node<'_>, source: &str) -> Option<bool> {
    match text(node, source) {
        "true" => Some(true),
        "false" => Some(false),
        _ => None,
    }
}

fn parse_integer(node: Node<'_>, source: &str) -> Option<u64> {
    text(node, source).parse::<u64>().ok()
}

fn parse_dependency(node: Node<'_>, source: &str) -> Option<Dependency> {
    let inner = if node.kind() == "dependency_expr" {
        named_children(node).next()?
    } else {
        node
    };

    match inner.kind() {
        "simple_dependency" => {
            let reference = inner.child_by_field_name("reference")?;
            Some(Dependency::Reference(text(reference, source).to_owned()))
        }
        "comparison_dependency" => {
            let left = inner.child_by_field_name("left")?;
            let right = inner.child_by_field_name("right")?;
            Some(Dependency::Comparison {
                left: text(left, source).to_owned(),
                operator: parse_comparison_operator(inner, source)?,
                right: parse_value(right, source)?,
            })
        }
        "method_call_dependency" => {
            let receiver = inner.child_by_field_name("receiver")?;
            let method = inner.child_by_field_name("method")?;
            let argument = inner.child_by_field_name("argument")?;

            if text(method, source) != "contains" {
                return None;
            }

            Some(Dependency::Contains {
                receiver: text(receiver, source).to_owned(),
                argument: parse_value(argument, source)?,
            })
        }
        "combinator_dependency" => {
            let name = inner.child_by_field_name("name")?;
            let arguments = inner.child_by_field_name("arguments")?;
            let dependencies = named_children(arguments)
                .filter(|node| node.kind() == "dependency_expr")
                .filter_map(|node| parse_dependency(node, source))
                .collect::<Vec<_>>();

            match text(name, source) {
                "all" => Some(Dependency::All(dependencies)),
                "any" => Some(Dependency::Any(dependencies)),
                _ => None,
            }
        }
        _ => None,
    }
}

fn parse_comparison_operator(node: Node<'_>, source: &str) -> Option<ComparisonOperator> {
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        match text(child, source) {
            "==" => return Some(ComparisonOperator::Equal),
            "!=" => return Some(ComparisonOperator::NotEqual),
            _ => {}
        }
    }

    None
}