Skip to main content

raster_style/
style.rs

1use std::cell::Cell;
2
3use serde::{de, de::DeserializeSeed, Deserialize, Deserializer, Serialize};
4use serde_json::Value;
5
6use crate::error::{Error, Result};
7use crate::json::{canonical_value, parse_strict, StrictSeed};
8use crate::schema::{
9    default_schemas, has_extension, SchemaRegistry, ValidationReport, TILE_SCHEMA_URL,
10};
11use crate::semantics::validate_semantics;
12
13/// A normalized v3 document. Unknown declared extensions are preserved; inspect validation before execution.
14#[derive(Debug, Clone, PartialEq, Serialize)]
15#[serde(transparent)]
16pub struct Style {
17    value: Value,
18    #[serde(skip)]
19    validation: ValidationReport,
20}
21
22impl Style {
23    /// Parse with the embedded registry, preserving unknown declarations as incomplete.
24    ///
25    /// # Errors
26    /// Returns JSON, budget, schema and static-semantic errors; use a custom
27    /// [`SchemaRegistry`] to register third-party schema resources.
28    pub fn from_json(text: &str) -> Result<Self> {
29        Self::from_validated_value(parse_strict(text)?)
30    }
31
32    /// Programmatic values take the same binary64, budget and validation path as raw JSON.
33    ///
34    /// # Errors
35    /// Returns the serialization errors or the validation errors of [`Self::from_json`].
36    pub fn from_value(value: Value) -> Result<Self> {
37        Self::from_json(&canonical_value(&value)?)
38    }
39
40    pub(crate) fn from_validated_value(value: Value) -> Result<Self> {
41        Self::from_value_in_registry(value, default_schemas()?)
42    }
43
44    pub(crate) fn from_value_in_registry(
45        mut value: Value,
46        schemas: &SchemaRegistry,
47    ) -> Result<Self> {
48        schemas.check(&value)?;
49        crate::profile::normalize_profile(&mut value)?;
50        validate_semantics(&value)?;
51        normalize_colors(&mut value);
52        canonical_value(&value)?;
53        let validation = schemas.check(&value)?;
54        Ok(Self { value, validation })
55    }
56
57    /// Borrow the report snapshot captured when this immutable style was constructed.
58    /// Later schema registrations do not update it; parse again to obtain a fresh report.
59    pub fn validation(&self) -> &ValidationReport {
60        &self.validation
61    }
62
63    /// Execution support is independent of schema validity and uses exact versioned URLs.
64    ///
65    /// # Errors
66    /// Returns `E_SCHEMA_UNRESOLVED` for incomplete validation and
67    /// `E_UNSUPPORTED_EXTENSION` when the executor's list omits a declaration.
68    pub fn require_extension_support(&self, supported: &[&str]) -> Result<()> {
69        if !self.validation.complete {
70            return Err(Error::new(
71                "E_SCHEMA_UNRESOLVED",
72                "Complete validation is required before execution",
73            ));
74        }
75        if let Some(declarations) = self.value["extensions"].as_array() {
76            for declaration in declarations {
77                let url = declaration.as_str().expect("core-validated schema URL");
78                if !supported.contains(&url) {
79                    return Err(Error::new(
80                        "E_UNSUPPORTED_EXTENSION",
81                        "Executor does not support this exact extension version",
82                    )
83                    .at(url));
84                }
85            }
86        }
87        Ok(())
88    }
89
90    /// Borrow the normalized document without permitting mutation.
91    pub fn as_value(&self) -> &Value {
92        &self.value
93    }
94    /// Transfer ownership of the normalized document, discarding the report snapshot.
95    pub fn into_value(self) -> Value {
96        self.value
97    }
98    /// Serialize the normalized document as RFC 8785 canonical JSON.
99    ///
100    /// # Errors
101    /// Returns `E_JSON` on serialization failure or `E_LIMIT` if the byte budget is exceeded.
102    pub fn to_json(&self) -> Result<String> {
103        canonical_value(&self.value)
104    }
105}
106
107impl<'de> Deserialize<'de> for Style {
108    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
109        let nodes = Cell::new(0);
110        let value = StrictSeed::root(&nodes).deserialize(deserializer)?;
111        Self::from_validated_value(value).map_err(de::Error::custom)
112    }
113}
114
115fn normalize_color(value: &mut Value) {
116    if let Some(color) = value.as_str() {
117        let mut normalized = color.to_ascii_lowercase();
118        if normalized.len() == 7 {
119            normalized.push_str("ff");
120        }
121        *value = Value::String(normalized);
122    }
123}
124
125fn normalize_colors(style: &mut Value) {
126    let mut paths = vec!["/renderer/color"];
127    if has_extension(style, TILE_SCHEMA_URL) {
128        paths.push("/raster:tile/background");
129    }
130    for path in paths {
131        if let Some(color) = style.pointer_mut(path) {
132            normalize_color(color);
133        }
134    }
135    let Some(color_map) = style.pointer_mut("/renderer/color_mapping") else {
136        return;
137    };
138    for key in ["under", "over", "outside_color", "fallback_color"] {
139        if let Some(color) = color_map.get_mut(key) {
140            if color.as_str().is_some_and(|color| color.starts_with('#')) {
141                normalize_color(color);
142            }
143        }
144    }
145    for key in ["stops", "entries"] {
146        if let Some(entries) = color_map.get_mut(key).and_then(Value::as_array_mut) {
147            for entry in entries {
148                normalize_color(&mut entry["color"]);
149            }
150        }
151    }
152    if let Some(colors) = color_map.get_mut("colors").and_then(Value::as_array_mut) {
153        for color in colors {
154            normalize_color(color);
155        }
156    }
157}