Skip to main content

raster_style/
style.rs

1use std::cell::Cell;
2use std::sync::OnceLock;
3
4use serde::{de, de::DeserializeSeed, Deserialize, Deserializer, Serialize};
5use serde_json::Value;
6
7use crate::error::{Error, Result};
8use crate::json::{canonical_value, parse_strict, StrictSeed};
9use crate::semantics::validate_semantics;
10
11const SCHEMA: &str = include_str!("../assets/raster-style-v2.schema.json");
12static VALIDATOR: OnceLock<std::result::Result<jsonschema::Validator, String>> = OnceLock::new();
13
14pub(crate) fn validator() -> Result<&'static jsonschema::Validator> {
15    VALIDATOR
16        .get_or_init(|| {
17            let schema: Value = serde_json::from_str(SCHEMA).map_err(|error| error.to_string())?;
18            jsonschema::validator_for(&schema).map_err(|error| error.to_string())
19        })
20        .as_ref()
21        .map_err(|_| Error::new("E_CONFIG", "Embedded schema could not be compiled"))
22}
23
24/// A validated, normalized document. No mutable reference to its value is exposed.
25#[derive(Debug, Clone, PartialEq, Serialize)]
26#[serde(transparent)]
27pub struct Style(Value);
28
29impl Style {
30    pub fn from_json(text: &str) -> Result<Self> {
31        Self::from_validated_value(parse_strict(text)?)
32    }
33
34    /// Programmatic values take the same binary64, budget and validation path as raw JSON.
35    pub fn from_value(value: Value) -> Result<Self> {
36        Self::from_json(&canonical_value(&value)?)
37    }
38
39    pub(crate) fn from_validated_value(mut value: Value) -> Result<Self> {
40        if !validator()?.is_valid(&value) {
41            return Err(Error::new(
42                "E_SCHEMA",
43                "Raster style does not match the schema",
44            ));
45        }
46        crate::profile::normalize_profile(&mut value)?;
47        validate_semantics(&value)?;
48        normalize_colors(&mut value);
49        canonical_value(&value)?;
50        Ok(Self(value))
51    }
52
53    pub fn as_value(&self) -> &Value {
54        &self.0
55    }
56    pub fn into_value(self) -> Value {
57        self.0
58    }
59    pub fn to_json(&self) -> Result<String> {
60        canonical_value(&self.0)
61    }
62}
63
64impl<'de> Deserialize<'de> for Style {
65    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
66        let nodes = Cell::new(0);
67        let value = StrictSeed::root(&nodes).deserialize(deserializer)?;
68        Self::from_validated_value(value).map_err(de::Error::custom)
69    }
70}
71
72fn normalize_color(value: &mut Value) {
73    if let Some(color) = value.as_str() {
74        let mut normalized = color.to_ascii_lowercase();
75        if normalized.len() == 7 {
76            normalized.push_str("ff");
77        }
78        *value = Value::String(normalized);
79    }
80}
81
82fn normalize_colors(style: &mut Value) {
83    for path in ["/renderer/color", "/image/background"] {
84        if let Some(color) = style.pointer_mut(path) {
85            normalize_color(color);
86        }
87    }
88    let Some(color_map) = style.pointer_mut("/renderer/color_mapping") else {
89        return;
90    };
91    for key in ["under", "over", "outside_color", "fallback_color"] {
92        if let Some(color) = color_map.get_mut(key) {
93            if color.as_str().is_some_and(|color| color.starts_with('#')) {
94                normalize_color(color);
95            }
96        }
97    }
98    for key in ["stops", "entries"] {
99        if let Some(entries) = color_map.get_mut(key).and_then(Value::as_array_mut) {
100            for entry in entries {
101                normalize_color(&mut entry["color"]);
102            }
103        }
104    }
105    if let Some(colors) = color_map.get_mut("colors").and_then(Value::as_array_mut) {
106        for color in colors {
107            normalize_color(color);
108        }
109    }
110}