maplibre-expr 0.2.0

Pure-Rust parser and evaluator for MapLibre GL style expressions
Documentation
//! Runtime values produced by evaluating an expression.

use std::collections::BTreeMap;
use std::fmt;

use crate::color::Color;

/// A value in the MapLibre expression type system.
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
    Null,
    Bool(bool),
    Number(f64),
    String(String),
    Color(Color),
    Array(Vec<Value>),
    Object(BTreeMap<String, Value>),
    /// A resolved image reference (the `image` operator).
    Image {
        name: String,
        available: bool,
    },
    /// Formatted text (the `format` operator): a list of styled sections.
    Formatted(Vec<FormatSection>),
    /// A `numberArray` value.
    NumberArray(Vec<f64>),
    /// A `colorArray` value.
    ColorArray(Vec<Color>),
    /// A `padding` value: `[top, right, bottom, left]`.
    Padding([f64; 4]),
    /// A `projectionDefinition`: a named projection or a transition between two.
    Projection(Projection),
    /// A locale-aware string collator (the `collator` operator).
    Collator {
        case_sensitive: bool,
        diacritic_sensitive: bool,
        locale: Option<String>,
    },
}

/// A projection definition value.
#[derive(Debug, Clone, PartialEq)]
pub enum Projection {
    Named(String),
    Transition {
        from: String,
        to: String,
        transition: f64,
    },
}

/// One styled section of a [`Value::Formatted`] value.
#[derive(Debug, Clone, PartialEq)]
pub struct FormatSection {
    pub text: String,
    /// `(name, available)` for an image section.
    pub image: Option<(String, bool)>,
    pub scale: Option<f64>,
    pub font_stack: Option<String>,
    pub text_color: Option<Color>,
    pub vertical_align: Option<String>,
}

impl Value {
    /// The MapLibre type name of this value (`"number"`, `"string"`, ...).
    pub fn type_name(&self) -> &'static str {
        match self {
            Value::Null => "null",
            Value::Bool(_) => "boolean",
            Value::Number(_) => "number",
            Value::String(_) => "string",
            Value::Color(_) => "color",
            Value::Array(_) => "array",
            Value::Object(_) => "object",
            Value::Image { .. } => "resolvedImage",
            Value::Formatted(_) => "formatted",
            Value::NumberArray(_) => "numberArray",
            Value::ColorArray(_) => "colorArray",
            Value::Padding(_) => "padding",
            Value::Projection(_) => "projectionDefinition",
            Value::Collator { .. } => "collator",
        }
    }

    pub fn as_number(&self) -> Option<f64> {
        match self {
            Value::Number(n) => Some(*n),
            _ => None,
        }
    }

    pub fn as_bool(&self) -> Option<bool> {
        match self {
            Value::Bool(b) => Some(*b),
            _ => None,
        }
    }

    pub fn as_str(&self) -> Option<&str> {
        match self {
            Value::String(s) => Some(s),
            _ => None,
        }
    }

    /// Truthiness per the MapLibre `to-boolean` rules.
    pub fn is_truthy(&self) -> bool {
        match self {
            Value::Null => false,
            Value::Bool(b) => *b,
            Value::Number(n) => *n != 0.0 && !n.is_nan(),
            Value::String(s) => !s.is_empty(),
            _ => true,
        }
    }

    /// Build a literal [`Value`] from raw JSON (used by the `literal` operator
    /// and by bare literals in an expression).
    pub fn from_json(json: &serde_json::Value) -> Value {
        match json {
            serde_json::Value::Null => Value::Null,
            serde_json::Value::Bool(b) => Value::Bool(*b),
            serde_json::Value::Number(n) => Value::Number(n.as_f64().unwrap_or(f64::NAN)),
            serde_json::Value::String(s) => Value::String(s.clone()),
            serde_json::Value::Array(a) => Value::Array(a.iter().map(Value::from_json).collect()),
            serde_json::Value::Object(o) => Value::Object(
                o.iter()
                    .map(|(k, v)| (k.clone(), Value::from_json(v)))
                    .collect(),
            ),
        }
    }
}

impl fmt::Display for Value {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Value::Null => write!(f, ""),
            Value::Bool(b) => write!(f, "{b}"),
            Value::Number(n) => write!(f, "{}", format_number(*n)),
            Value::String(s) => write!(f, "{s}"),
            Value::Color(c) => write!(f, "{c}"),
            Value::Array(a) => {
                let parts: Vec<String> = a.iter().map(|v| v.to_string()).collect();
                write!(f, "{}", parts.join(","))
            }
            Value::Object(_) => write!(f, "{self:?}"),
            Value::Image { name, .. } => write!(f, "{name}"),
            Value::Formatted(sections) => {
                for s in sections {
                    write!(f, "{}", s.text)?;
                }
                Ok(())
            }
            Value::NumberArray(v) => {
                let parts: Vec<String> = v.iter().map(|n| format_number(*n)).collect();
                write!(f, "{}", parts.join(","))
            }
            Value::ColorArray(v) => {
                let parts: Vec<String> = v.iter().map(|c| c.to_string()).collect();
                write!(f, "{}", parts.join(","))
            }
            Value::Padding(v) => {
                let parts: Vec<String> = v.iter().map(|n| format_number(*n)).collect();
                write!(f, "{}", parts.join(","))
            }
            Value::Projection(Projection::Named(s)) => write!(f, "{s}"),
            Value::Projection(_) => write!(f, "{self:?}"),
            Value::Collator { .. } => write!(f, "collator"),
        }
    }
}

/// Format a number the way JavaScript's `String(n)` would (no trailing `.0`).
pub fn format_number(n: f64) -> String {
    if n == n.trunc() && n.is_finite() && n.abs() < 1e21 {
        format!("{}", n as i64)
    } else {
        format!("{n}")
    }
}