relay-knowledge 1.1.5

Graph-database-based knowledge graph project.
Documentation
use super::model::{ConfigFact, ConfigImport, ConfigRange, ConfigReference, ConfigValueKind};

pub(super) fn source_lines(content: &str) -> Vec<ConfigLine<'_>> {
    let mut lines = Vec::new();
    let mut byte_start = 0usize;
    for (index, raw_line) in content.split_inclusive('\n').enumerate() {
        let without_lf = raw_line.strip_suffix('\n').unwrap_or(raw_line);
        let text = without_lf.strip_suffix('\r').unwrap_or(without_lf);
        lines.push(ConfigLine {
            number: index + 1,
            byte_start,
            byte_end: byte_start + text.len(),
            text,
        });
        byte_start += raw_line.len();
    }
    if content.is_empty() {
        lines.push(ConfigLine {
            number: 1,
            byte_start: 0,
            byte_end: 0,
            text: "",
        });
    }

    lines
}

pub(super) struct ConfigLine<'a> {
    pub(super) number: usize,
    pub(super) byte_start: usize,
    pub(super) byte_end: usize,
    pub(super) text: &'a str,
}

impl ConfigLine<'_> {
    pub(super) fn range(&self) -> ConfigRange {
        ConfigRange {
            byte_start: self.byte_start,
            byte_end: self.byte_end,
            line_start: self.number,
            line_end: self.number,
        }
    }
}

pub(super) fn push_definition(
    definitions: &mut Vec<ConfigFact>,
    name: impl AsRef<str>,
    kind: &'static str,
    range: ConfigRange,
) {
    push_definition_with_value_kind(definitions, name, kind, ConfigValueKind::Unknown, range);
}

pub(super) fn push_boolean_definition(
    definitions: &mut Vec<ConfigFact>,
    name: impl AsRef<str>,
    kind: &'static str,
    range: ConfigRange,
) {
    push_definition_with_value_kind(definitions, name, kind, ConfigValueKind::Boolean, range);
}

fn push_definition_with_value_kind(
    definitions: &mut Vec<ConfigFact>,
    name: impl AsRef<str>,
    kind: &'static str,
    value_kind: ConfigValueKind,
    range: ConfigRange,
) {
    let name = clean_name(name.as_ref());
    if name.is_empty() {
        return;
    }
    if let Some(existing) = definitions
        .iter_mut()
        .find(|existing| existing.name == name && existing.kind == kind && existing.range == range)
    {
        if existing.value_kind == ConfigValueKind::Unknown {
            existing.value_kind = value_kind;
        }
        return;
    }

    definitions.push(ConfigFact {
        name,
        kind,
        value_kind,
        range,
    });
}

pub(super) fn push_reference(
    references: &mut Vec<ConfigReference>,
    name: impl AsRef<str>,
    kind: &'static str,
    range: ConfigRange,
) {
    let name = clean_name(name.as_ref());
    if !name.is_empty()
        && !references.iter().any(|existing| {
            existing.name == name && existing.kind == kind && existing.range == range
        })
    {
        references.push(ConfigReference { name, kind, range });
    }
}

pub(super) fn push_import(
    imports: &mut Vec<ConfigImport>,
    module: impl AsRef<str>,
    range: ConfigRange,
) {
    let module = clean_name(module.as_ref());
    if !module.is_empty()
        && !imports
            .iter()
            .any(|existing| existing.module == module && existing.range == range)
    {
        imports.push(ConfigImport { module, range });
    }
}

fn clean_name(value: &str) -> String {
    unquote(value)
        .trim()
        .trim_end_matches(',')
        .trim_end_matches(')')
        .trim_end_matches('}')
        .to_owned()
}

pub(super) fn unquote(value: &str) -> &str {
    value
        .trim()
        .trim_matches('"')
        .trim_matches('\'')
        .trim_matches('`')
}

pub(super) fn config_key_prefix(value: &str) -> &str {
    value
        .trim()
        .trim_start_matches('-')
        .trim()
        .trim_start_matches('{')
        .trim_start_matches('[')
        .trim()
        .trim_matches('"')
        .trim_matches('\'')
}

pub(super) fn first_quoted(value: &str) -> Option<&str> {
    let quote = value
        .chars()
        .find(|character| matches!(character, '"' | '\''))?;
    let start = value.find(quote)? + quote.len_utf8();
    let rest = &value[start..];
    let end = rest.find(quote)?;

    Some(&rest[..end])
}

pub(super) fn call_name(value: &str) -> Option<&str> {
    value
        .split('(')
        .next()
        .map(str::trim)
        .filter(|name| valid_config_key(name))
}

pub(super) fn call_args_prefix<'a>(line: &'a str, command: &str) -> Option<&'a str> {
    if !line
        .get(..command.len())
        .is_some_and(|prefix| prefix.eq_ignore_ascii_case(command))
    {
        return None;
    }
    let rest = line.get(command.len()..)?.trim_start();
    rest.strip_prefix('(')
}

pub(super) fn paren_delta(value: &str) -> i32 {
    value.chars().fold(0, |balance, character| {
        balance
            + match character {
                '(' => 1,
                ')' => -1,
                _ => 0,
            }
    })
}

pub(super) fn assignment(line: &str) -> Option<(&str, &str)> {
    for marker in ["?=", "+=", ":=", "="] {
        if let Some((left, right)) = line.split_once(marker) {
            let name = left.trim();
            if valid_config_key(name) {
                return Some((name, right.trim()));
            }
        }
    }

    None
}

pub(super) fn variable_references<'a>(line: &'a str, prefix: &str, suffix: &str) -> Vec<&'a str> {
    let mut values = Vec::new();
    for part in line.split(prefix).skip(1) {
        let value = if suffix == " " {
            part.split(|character: char| !valid_config_character(character))
                .next()
        } else {
            part.split(suffix).next()
        };
        if let Some(value) = value.filter(|value| valid_config_key(value)) {
            values.push(value);
        }
    }

    values
}

pub(super) fn template_variables(line: &str) -> Vec<&str> {
    line.split("{{")
        .skip(1)
        .filter_map(|part| part.split("}}").next())
        .map(str::trim)
        .filter_map(|part| part.split(['|', '.', ' ', '(']).next())
        .map(str::trim)
        .filter(|name| valid_config_key(name))
        .collect()
}

pub(super) fn identifier_prefix(value: &str) -> Option<&str> {
    value
        .split(|character: char| !valid_config_character(character))
        .next()
        .filter(|name| valid_config_key(name))
}

pub(super) fn valid_target(value: &str) -> bool {
    let value = value.trim();
    !value.is_empty()
        && !value.starts_with('.')
        && value
            .chars()
            .all(|character| valid_config_character(character) || matches!(character, '/' | ':'))
}

pub(super) fn valid_config_key(value: &str) -> bool {
    !value.is_empty() && value.chars().all(valid_config_character)
}

pub(super) fn valid_config_character(character: char) -> bool {
    character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.' | '/' | ':')
}

pub(super) fn strip_line_comment(value: &str) -> &str {
    value.split('#').next().unwrap_or(value)
}

pub(super) fn strip_inline_hash_comment(value: &str) -> &str {
    let mut quote = None;
    let mut escaped = false;
    for (index, character) in value.char_indices() {
        if escaped {
            escaped = false;
            continue;
        }
        if character == '\\' && quote.is_some() {
            escaped = true;
            continue;
        }
        if matches!(character, '"' | '\'') {
            if quote == Some(character) {
                quote = None;
            } else if quote.is_none() {
                quote = Some(character);
            }
            continue;
        }
        if character == '#' && quote.is_none() {
            return &value[..index];
        }
    }

    value
}