pipa-lang 1.0.0-alpha.1

A tiny template language
Documentation
// SPDX-FileCopyrightText: Copyright 2026 olav@occy.org
// SPDX-License-Identifier: MPL-2.0

//! Parse values from JSON and YAML.

use crate::value::Value;
use crate::value::error::Error;
use crate::value::keys::Keys;
use crate::value::list::List;
use crate::value::map::Map;
use crate::value::span::Span;
use crate::value::tracer::Segment;
use crate::value::tracer::Tracer;
use saphyr::AnnotatedMapping;
use saphyr::AnnotatedSequence;
use saphyr::LoadableYamlNode;
use saphyr::MarkedYaml;
use saphyr::Scalar;
use saphyr::YamlData;

pub(crate) fn usize(value: &[u8]) -> Result<usize, Error> {
    str(value)?
        .parse::<usize>()
        .map_err(|e| Error::Parse(e.to_string()))
}

pub(crate) fn str(value: &[u8]) -> Result<&str, Error> {
    core::str::from_utf8(value)
        .map_err(|_| Error::Parse(String::from_utf8_lossy(value).into_owned()))
}

pub fn json_map(data: &[u8]) -> Result<Map, Error> {
    let data = parse_bytes(data)?;

    let Ok(json) = serde_json::from_str(data) else {
        return Err(Error::Parse(display_bytes(data)));
    };

    match json {
        serde_json::Value::Object(v) => Ok(json_object(v)),
        _ => Err(Error::Parse(display_bytes(data))),
    }
}

fn json_value(json: serde_json::Value) -> Value {
    match json {
        serde_json::Value::Null => Value::default(),
        serde_json::Value::Bool(v) => v.into(),
        serde_json::Value::Number(v) => v.to_string().into(),
        serde_json::Value::String(v) => v.into(),
        serde_json::Value::Array(v) => json_array(v).into(),
        serde_json::Value::Object(v) => json_object(v).into(),
    }
}

fn json_object(json: serde_json::Map<String, serde_json::Value>) -> Map {
    json.into_iter().map(|(k, v)| (k, json_value(v))).collect()
}

fn json_array(json: Vec<serde_json::Value>) -> List {
    json.into_iter().map(json_value).collect()
}

pub fn yaml_map(source: String, span: Span, segment: Segment, data: &[u8]) -> Result<Map, Error> {
    let data = parse_bytes(data)?;
    let keys = Keys::default();

    if data.trim().is_empty() {
        return Ok(Map::default());
    }

    let yaml = MarkedYaml::load_from_str(data)?;

    if yaml.len() != 1 {
        return Err(Error::Parse(display_bytes(data)));
    }

    let Some(yaml) = yaml.into_iter().next() else {
        return Err(Error::Parse(display_bytes(data)));
    };

    let Ok(Value::Map(mut map)) = yaml_value(&source, span, segment, data, &keys, yaml) else {
        return Err(Error::Parse(display_bytes(data)));
    };

    map.tracer = Tracer::default()
        .with_source(source)
        .with_span(span)
        .with_segment(segment);

    Ok(map)
}

fn yaml_value(
    source: &str,
    span: Span,
    segment: Segment,
    data: &str,
    keys: &Keys,
    yaml: MarkedYaml<'_>,
) -> Result<Value, ()> {
    let i0 = yaml.span.start.index();
    let i1 = yaml.span.end.index();

    let yaml_span = match &yaml.data {
        YamlData::Sequence(_) if data.chars().nth(i0) == Some('[') => {
            span.zoom(Span::new(i0.saturating_add(1), i1))
        }
        _ => span.zoom(Span::new(i0, i1)),
    };

    let value = match yaml.data {
        YamlData::Sequence(d) => yaml_sequence(source, span, segment, data, keys, d)?,
        YamlData::Mapping(d) => yaml_mapping(source, span, segment, data, keys, d)?,
        YamlData::Value(Scalar::Null) => Value::default(),
        YamlData::Value(Scalar::Boolean(d)) => d.into(),
        YamlData::Value(Scalar::FloatingPoint(d)) => Value::from(d.to_string()),
        YamlData::Value(Scalar::Integer(d)) => Value::from(d.to_string()),
        YamlData::Value(Scalar::String(d)) => Value::from(d.into_owned()),
        YamlData::Representation(..) => return Err(()),
        YamlData::Tagged(..) => return Err(()),
        YamlData::Alias(_) => return Err(()),
        YamlData::BadValue => return Err(()),
    };

    let tracer = Tracer::default()
        .with_source(source.to_string())
        .with_span(yaml_span)
        .with_segment(segment)
        .with_keys(keys.clone());

    Ok(value.with_tracer(tracer))
}

fn yaml_sequence(
    source: &str,
    span: Span,
    segment: Segment,
    data: &str,
    keys: &Keys,
    yaml: AnnotatedSequence<MarkedYaml<'_>>,
) -> Result<Value, ()> {
    Ok(Value::List(
        yaml.into_iter()
            .enumerate()
            .map(|(i, v)| yaml_value(source, span, segment, data, &keys.clone().fold(i), v))
            .collect::<Result<List, _>>()?,
    ))
}

fn yaml_mapping(
    source: &str,
    span: Span,
    segment: Segment,
    data: &str,
    keys: &Keys,
    yaml: AnnotatedMapping<'_, MarkedYaml<'_>>,
) -> Result<Value, ()> {
    Ok(Value::Map(
        yaml.into_iter()
            .map(|(k, v)| yaml_entry(source, span, segment, data, keys, k, v))
            .collect::<Result<Map, _>>()?,
    ))
}

fn yaml_entry(
    source: &str,
    span: Span,
    segment: Segment,
    data: &str,
    keys: &Keys,
    k: MarkedYaml<'_>,
    v: MarkedYaml<'_>,
) -> Result<(String, Value), ()> {
    yaml_value(source, span, segment, data, keys, k)
        .and_then(|k| String::from_utf8(k.join()).map_err(|_| ()))
        .map(|k| (k.clone(), keys.clone().fold(k)))
        .and_then(|(k, keys)| yaml_value(source, span, segment, data, &keys, v).map(|v| (k, v)))
}

#[cfg(test)]
mod test {
    use crate::value::Value;
    use crate::value::equivalent::Equivalent;
    use crate::value::keys::Keys;
    use crate::value::list::List;
    use crate::value::map::Map;
    use crate::value::parse::json_map;
    use crate::value::parse::yaml_map;
    use crate::value::span::Span;
    use crate::value::tracer::Segment;
    use crate::value::tracer::Tracer;

    #[test]
    fn test_json_map() {
        let parsed = json_map(br#"{ "a": [1,2,"b"] }"#).unwrap();
        let list = List::default().with("1").with("2").with("b");
        let map = Map::default().with("a", list);

        assert!(parsed.equivalent(&map), "{parsed}");
    }

    #[test]
    fn test_yaml_map() {
        let value = Value::from(String::from("value")).with_tracer(
            Tracer::default()
                .with_source(String::from("index.html"))
                .with_span(Span::new(1016, 1021))
                .with_keys(Keys::parse(b"outer.inner")),
        );

        let inner = Value::Map(Map::default().with("inner", value)).with_tracer(
            Tracer::default()
                .with_source(String::from("index.html"))
                .with_span(Span::new(1000, 1023))
                .with_keys(Keys::parse(b"outer")),
        );

        let outer = Value::Map(Map::default().with("outer", inner)).with_tracer(
            Tracer::default()
                .with_source("index.html".to_string())
                .with_span(Span::new(1000, 1023)),
        );

        let parsed = yaml_map(
            String::from("index.html"),
            Span::new(1000, 1023),
            Segment::Field,
            b"outer: { inner: value }",
        );

        assert!(parsed.unwrap().equivalent(&outer.try_into().unwrap()));
    }
}

fn parse_bytes(data: &[u8]) -> Result<&str, Error> {
    str::from_utf8(data).map_err(|_| Error::Parse(display_bytes(data)))
}

fn display_bytes<T: AsRef<[u8]>>(t: T) -> String {
    String::from_utf8_lossy(t.as_ref()).into_owned()
}