floe-core 0.3.7

Core library for Floe, a YAML-driven technical ingestion tool.
Documentation
use serde_json::Value;

use crate::ConfigError;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SelectorToken {
    Field(String),
    Index(usize),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SelectorValue {
    Scalar(String),
    Null,
    NonScalar(Value),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SelectorError {
    pub message: String,
}

impl std::fmt::Display for SelectorError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.message)
    }
}

impl std::error::Error for SelectorError {}

pub fn parse_selector(selector: &str) -> Result<Vec<SelectorToken>, SelectorError> {
    let trimmed = selector.trim();
    if trimmed.is_empty() {
        return Err(SelectorError {
            message: "selector is empty".to_string(),
        });
    }
    let mut tokens = Vec::new();
    let mut buf = String::new();
    let mut chars = trimmed.chars().peekable();
    let mut last_was_index = false;
    while let Some(ch) = chars.next() {
        match ch {
            '.' => {
                if buf.is_empty() {
                    if last_was_index {
                        last_was_index = false;
                        continue;
                    }
                    return Err(SelectorError {
                        message: "selector contains empty field".to_string(),
                    });
                }
                tokens.push(SelectorToken::Field(buf.clone()));
                buf.clear();
                last_was_index = false;
            }
            '[' => {
                if !buf.is_empty() {
                    tokens.push(SelectorToken::Field(buf.clone()));
                    buf.clear();
                }
                let mut index_buf = String::new();
                for next in chars.by_ref() {
                    if next == ']' {
                        break;
                    }
                    index_buf.push(next);
                }
                if index_buf.is_empty() {
                    return Err(SelectorError {
                        message: "selector index is empty".to_string(),
                    });
                }
                if !index_buf.chars().all(|c| c.is_ascii_digit()) {
                    return Err(SelectorError {
                        message: format!("selector index is not numeric: {}", index_buf),
                    });
                }
                let index = index_buf.parse::<usize>().map_err(|_| SelectorError {
                    message: format!("selector index is invalid: {}", index_buf),
                })?;
                tokens.push(SelectorToken::Index(index));
                last_was_index = true;
            }
            ']' => {
                return Err(SelectorError {
                    message: "selector contains unexpected ']'".to_string(),
                });
            }
            _ => {
                buf.push(ch);
                last_was_index = false;
            }
        }
    }
    if !buf.is_empty() {
        tokens.push(SelectorToken::Field(buf));
    }
    Ok(tokens)
}

pub fn evaluate_selector(
    value: &Value,
    tokens: &[SelectorToken],
) -> Result<SelectorValue, SelectorError> {
    if tokens.is_empty() {
        return Err(SelectorError {
            message: "selector has no tokens".to_string(),
        });
    }
    let mut current = value;
    for token in tokens {
        match token {
            SelectorToken::Field(name) => {
                let object = match current.as_object() {
                    Some(object) => object,
                    None => return Ok(SelectorValue::Null),
                };
                match object.get(name) {
                    Some(next) => current = next,
                    None => return Ok(SelectorValue::Null),
                }
            }
            SelectorToken::Index(index) => {
                let array = match current.as_array() {
                    Some(array) => array,
                    None => return Ok(SelectorValue::Null),
                };
                if let Some(next) = array.get(*index) {
                    current = next;
                } else {
                    return Ok(SelectorValue::Null);
                }
            }
        }
    }
    if current.is_null() {
        return Ok(SelectorValue::Null);
    }
    if current.is_object() || current.is_array() {
        return Ok(SelectorValue::NonScalar(current.clone()));
    }
    match current {
        Value::String(value) => Ok(SelectorValue::Scalar(value.clone())),
        Value::Bool(value) => Ok(SelectorValue::Scalar(value.to_string())),
        Value::Number(value) => Ok(SelectorValue::Scalar(value.to_string())),
        Value::Null => Ok(SelectorValue::Null),
        Value::Object(_) | Value::Array(_) => Ok(SelectorValue::NonScalar(current.clone())),
    }
}

pub fn selector_is_top_level(selector: &str) -> bool {
    !selector.contains('.') && !selector.contains('[')
}

pub fn compact_json(value: &Value) -> Result<String, SelectorError> {
    serde_json::to_string(value).map_err(|err| SelectorError {
        message: format!("failed to serialize json value: {err}"),
    })
}

pub fn selector_tokens_or_error(selector: &str) -> Result<Vec<SelectorToken>, ConfigError> {
    parse_selector(selector).map_err(|err| ConfigError(err.message))
}