Skip to main content

maplibre_expr/
value.rs

1//! Runtime values produced by evaluating an expression.
2
3use std::collections::BTreeMap;
4use std::fmt;
5
6use crate::color::Color;
7
8/// A value in the MapLibre expression type system.
9#[derive(Debug, Clone, PartialEq)]
10pub enum Value {
11    Null,
12    Bool(bool),
13    Number(f64),
14    String(String),
15    Color(Color),
16    Array(Vec<Value>),
17    Object(BTreeMap<String, Value>),
18    /// A resolved image reference (the `image` operator).
19    Image {
20        name: String,
21        available: bool,
22    },
23    /// Formatted text (the `format` operator): a list of styled sections.
24    Formatted(Vec<FormatSection>),
25    /// A `numberArray` value.
26    NumberArray(Vec<f64>),
27    /// A `colorArray` value.
28    ColorArray(Vec<Color>),
29    /// A `padding` value: `[top, right, bottom, left]`.
30    Padding([f64; 4]),
31    /// A `projectionDefinition`: a named projection or a transition between two.
32    Projection(Projection),
33    /// A locale-aware string collator (the `collator` operator).
34    Collator {
35        case_sensitive: bool,
36        diacritic_sensitive: bool,
37        locale: Option<String>,
38    },
39}
40
41/// A projection definition value.
42#[derive(Debug, Clone, PartialEq)]
43pub enum Projection {
44    Named(String),
45    Transition {
46        from: String,
47        to: String,
48        transition: f64,
49    },
50}
51
52/// One styled section of a [`Value::Formatted`] value.
53#[derive(Debug, Clone, PartialEq)]
54pub struct FormatSection {
55    pub text: String,
56    /// `(name, available)` for an image section.
57    pub image: Option<(String, bool)>,
58    pub scale: Option<f64>,
59    pub font_stack: Option<String>,
60    pub text_color: Option<Color>,
61    pub vertical_align: Option<String>,
62}
63
64impl Value {
65    /// The MapLibre type name of this value (`"number"`, `"string"`, ...).
66    pub fn type_name(&self) -> &'static str {
67        match self {
68            Value::Null => "null",
69            Value::Bool(_) => "boolean",
70            Value::Number(_) => "number",
71            Value::String(_) => "string",
72            Value::Color(_) => "color",
73            Value::Array(_) => "array",
74            Value::Object(_) => "object",
75            Value::Image { .. } => "resolvedImage",
76            Value::Formatted(_) => "formatted",
77            Value::NumberArray(_) => "numberArray",
78            Value::ColorArray(_) => "colorArray",
79            Value::Padding(_) => "padding",
80            Value::Projection(_) => "projectionDefinition",
81            Value::Collator { .. } => "collator",
82        }
83    }
84
85    pub fn as_number(&self) -> Option<f64> {
86        match self {
87            Value::Number(n) => Some(*n),
88            _ => None,
89        }
90    }
91
92    pub fn as_bool(&self) -> Option<bool> {
93        match self {
94            Value::Bool(b) => Some(*b),
95            _ => None,
96        }
97    }
98
99    pub fn as_str(&self) -> Option<&str> {
100        match self {
101            Value::String(s) => Some(s),
102            _ => None,
103        }
104    }
105
106    /// Truthiness per the MapLibre `to-boolean` rules.
107    pub fn is_truthy(&self) -> bool {
108        match self {
109            Value::Null => false,
110            Value::Bool(b) => *b,
111            Value::Number(n) => *n != 0.0 && !n.is_nan(),
112            Value::String(s) => !s.is_empty(),
113            _ => true,
114        }
115    }
116
117    /// Build a literal [`Value`] from raw JSON (used by the `literal` operator
118    /// and by bare literals in an expression).
119    pub fn from_json(json: &serde_json::Value) -> Value {
120        match json {
121            serde_json::Value::Null => Value::Null,
122            serde_json::Value::Bool(b) => Value::Bool(*b),
123            serde_json::Value::Number(n) => Value::Number(n.as_f64().unwrap_or(f64::NAN)),
124            serde_json::Value::String(s) => Value::String(s.clone()),
125            serde_json::Value::Array(a) => Value::Array(a.iter().map(Value::from_json).collect()),
126            serde_json::Value::Object(o) => Value::Object(
127                o.iter()
128                    .map(|(k, v)| (k.clone(), Value::from_json(v)))
129                    .collect(),
130            ),
131        }
132    }
133}
134
135impl fmt::Display for Value {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        match self {
138            Value::Null => write!(f, ""),
139            Value::Bool(b) => write!(f, "{b}"),
140            Value::Number(n) => write!(f, "{}", format_number(*n)),
141            Value::String(s) => write!(f, "{s}"),
142            Value::Color(c) => write!(f, "{c}"),
143            Value::Array(a) => {
144                let parts: Vec<String> = a.iter().map(|v| v.to_string()).collect();
145                write!(f, "{}", parts.join(","))
146            }
147            Value::Object(_) => write!(f, "{self:?}"),
148            Value::Image { name, .. } => write!(f, "{name}"),
149            Value::Formatted(sections) => {
150                for s in sections {
151                    write!(f, "{}", s.text)?;
152                }
153                Ok(())
154            }
155            Value::NumberArray(v) => {
156                let parts: Vec<String> = v.iter().map(|n| format_number(*n)).collect();
157                write!(f, "{}", parts.join(","))
158            }
159            Value::ColorArray(v) => {
160                let parts: Vec<String> = v.iter().map(|c| c.to_string()).collect();
161                write!(f, "{}", parts.join(","))
162            }
163            Value::Padding(v) => {
164                let parts: Vec<String> = v.iter().map(|n| format_number(*n)).collect();
165                write!(f, "{}", parts.join(","))
166            }
167            Value::Projection(Projection::Named(s)) => write!(f, "{s}"),
168            Value::Projection(_) => write!(f, "{self:?}"),
169            Value::Collator { .. } => write!(f, "collator"),
170        }
171    }
172}
173
174/// Format a number the way JavaScript's `String(n)` would (no trailing `.0`).
175pub fn format_number(n: f64) -> String {
176    if n == n.trunc() && n.is_finite() && n.abs() < 1e21 {
177        format!("{}", n as i64)
178    } else {
179        format!("{n}")
180    }
181}