knave-expand 0.14.21

The reading, scoping and expansion stages of knave
Documentation
use std::rc::Rc;

use super::problems::ProjectProblems;
use crate::core::Keyword;
use crate::diagnostic::{Diagnostics, SimilarName};
use crate::reader::{Form, FormKind, NumberValue};
use crate::source::Span;

pub struct Field<'f> {
    pub key: &'f str,
    pub keyword: &'f Form,
    pub value: &'f Form,
}

pub struct ScannedForms<'f> {
    pub fields: Fields<'f>,
    pub others: Vec<&'f Form>,
}

pub struct Fields<'f> {
    fields: Vec<Field<'f>>,
}

pub struct FieldScanner<'p> {
    known_keys: &'static [&'static str],
    problems: &'p mut Diagnostics,
}

impl<'p> FieldScanner<'p> {
    pub fn new(known_keys: &'static [&'static str], problems: &'p mut Diagnostics) -> Self {
        Self {
            known_keys,
            problems,
        }
    }

    pub fn scan<'f>(mut self, forms: &'f [Form]) -> ScannedForms<'f> {
        let mut scanned = ScannedForms {
            fields: Fields { fields: Vec::new() },
            others: Vec::new(),
        };
        let mut remaining = forms.iter().peekable();
        while let Some(form) = remaining.next() {
            match Self::key_of(form) {
                Some(key) => {
                    let value = remaining.next_if(|value| Self::key_of(value).is_none());
                    self.add_field(&mut scanned.fields, key, form, value);
                }
                None => scanned.others.push(form),
            }
        }
        scanned
    }

    pub fn key_of(form: &Form) -> Option<&str> {
        Keyword::field_name(form.as_symbol()?.name())
    }

    fn add_field<'f>(
        &mut self,
        fields: &mut Fields<'f>,
        key: &'f str,
        keyword: &'f Form,
        value: Option<&'f Form>,
    ) {
        if !self.known_keys.contains(&key) {
            return self.report_unknown(key, keyword.span());
        }
        let Some(value) = value else {
            return self
                .problems
                .push(ProjectProblems::missing_value(key, keyword.span()));
        };
        if let Some(first) = fields.get(key) {
            return self.problems.push(ProjectProblems::duplicate_entry(
                &format!(":{key}"),
                keyword.span(),
                first.keyword.span(),
            ));
        }
        fields.fields.push(Field {
            key,
            keyword,
            value,
        });
    }

    fn report_unknown(&mut self, key: &str, span: Span) {
        let candidates: Vec<Rc<str>> = self.known_keys.iter().map(|&key| Rc::from(key)).collect();
        let closest = SimilarName::closest(key, &candidates);
        self.problems
            .push(ProjectProblems::unknown_key(key, span, closest.as_deref()));
    }
}

impl<'f> Fields<'f> {
    const STRING: &'static str = "a string";
    const STRINGS: &'static str = "a list of strings";
    const WHOLE_NUMBER: &'static str = "a whole number";

    pub fn get(&self, key: &str) -> Option<&Field<'f>> {
        self.fields.iter().find(|field| field.key == key)
    }

    pub fn string(&self, key: &str, problems: &mut Diagnostics) -> Option<String> {
        let field = self.get(key)?;
        match field.value.kind() {
            FormKind::String(text) => Some(text.to_string()),
            _ => field.report_mismatch(Self::STRING, problems),
        }
    }

    pub fn whole_number(&self, key: &str, problems: &mut Diagnostics) -> Option<(i128, Span)> {
        let field = self.get(key)?;
        match Self::integer_of(field.value) {
            Some(value) => Some((value, field.value.span())),
            None => field.report_mismatch(Self::WHOLE_NUMBER, problems),
        }
    }

    fn integer_of(form: &Form) -> Option<i128> {
        match form.kind() {
            FormKind::Number(number) => match number.value() {
                NumberValue::Integer(value) => Some(*value),
                NumberValue::Float(_) => None,
            },
            _ => None,
        }
    }

    pub fn strings(&self, key: &str, problems: &mut Diagnostics) -> Vec<String> {
        let Some(field) = self.get(key) else {
            return Vec::new();
        };
        let Some(items) = field.value.as_list() else {
            return field
                .report_mismatch(Self::STRINGS, problems)
                .unwrap_or_default();
        };
        items
            .iter()
            .filter_map(|item| Self::quoted(item, problems))
            .collect()
    }

    fn quoted(item: &Form, problems: &mut Diagnostics) -> Option<String> {
        match item.kind() {
            FormKind::String(text) => Some(text.to_string()),
            _ => {
                problems.push(ProjectProblems::unquoted_feature(
                    &item.to_string(),
                    item.span(),
                ));
                None
            }
        }
    }
}

impl Field<'_> {
    fn report_mismatch<T>(&self, expected: &str, problems: &mut Diagnostics) -> Option<T> {
        problems.push(ProjectProblems::mismatched_value(
            self.key,
            expected,
            self.value.span(),
        ));
        None
    }
}