use std::collections::HashMap;
use indexmap::IndexMap;
use serde::Deserialize;
use crate::StyleError;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Document {
pub name: String,
#[serde(default = "default_version")]
pub version: String,
#[serde(default = "default_tile_size")]
pub tile_size: u32,
#[serde(default)]
pub pad: u32,
#[serde(default)]
pub params: IndexMap<String, ParamDecl>,
#[serde(default)]
pub assets: IndexMap<String, AssetDecl>,
#[serde(default)]
pub sources: IndexMap<String, SourceDecl>,
pub nodes: IndexMap<String, NodeSpec>,
pub output: NodeRef,
}
impl Document {
pub fn from_json(s: &str) -> Result<Self, StyleError> {
Ok(serde_json::from_str(s)?)
}
}
fn default_version() -> String {
"1".to_string()
}
fn default_tile_size() -> u32 {
512
}
#[derive(Debug, Deserialize)]
pub struct NodeSpec {
pub op: String,
#[serde(flatten)]
pub fields: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct ParamDecl {
#[serde(rename = "type")]
pub kind: ParamKind,
pub default: serde_json::Value,
#[serde(default)]
pub min: Option<f64>,
#[serde(default)]
pub max: Option<f64>,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy)]
#[serde(rename_all = "kebab-case")]
pub enum ParamKind {
Color,
Number,
Bool,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct AssetDecl {
#[serde(rename = "type")]
pub kind: AssetKind,
pub src: String,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy)]
#[serde(rename_all = "kebab-case")]
pub enum AssetKind {
Brush,
Image,
MaskImage,
Gradient,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "kebab-case", deny_unknown_fields)]
pub enum SourceDecl {
Dem(DemSource),
Mvt(MvtSource),
Pmtiles(PmtilesSource),
}
#[derive(Debug, Deserialize, Clone)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct MvtSource {
pub url: String,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct PmtilesSource {
pub url: String,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct DemSource {
pub url: String,
pub encoding: DemEncoding,
#[serde(default = "default_dem_tile_size")]
pub tile_size: u32,
#[serde(default)]
pub max_zoom: Option<u8>,
#[serde(default = "default_true")]
pub neighbor_fetch: bool,
#[serde(default)]
pub elevation_offset: f32,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy)]
#[serde(rename_all = "kebab-case")]
pub enum DemEncoding {
Terrarium,
MapboxRgb,
}
fn default_dem_tile_size() -> u32 {
256
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NodeRef(pub String);
impl NodeRef {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl<'de> Deserialize<'de> for NodeRef {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Ok(NodeRef(s.strip_prefix('@').unwrap_or(&s).to_string()))
}
}
pub enum FieldRef<'a> {
Node(&'a str),
Param(&'a str),
Literal(&'a str),
}
impl<'a> FieldRef<'a> {
pub fn classify(s: &'a str) -> Self {
if let Some(rest) = s.strip_prefix('@') {
FieldRef::Node(rest)
} else if let Some(rest) = s.strip_prefix('$') {
FieldRef::Param(rest)
} else {
FieldRef::Literal(s)
}
}
}
pub type FeatureFilter = HashMap<String, FilterMatch>;
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum FilterMatch {
One(FilterAtom),
Any(Vec<FilterAtom>),
Not(NotMatch),
}
#[derive(Debug, Clone, Deserialize)]
pub struct NotMatch {
pub not: NotInner,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum NotInner {
One(FilterAtom),
Any(Vec<FilterAtom>),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum FilterAtom {
Bool(bool),
Int(i64),
Float(f64),
Str(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_minimal_document() {
let json = r##"{
"name": "demo",
"nodes": {
"src": { "op": "image", "src": "assets/bg.png" },
"blur": { "op": "blur", "input": "@src", "sigma": 3 }
},
"output": "@blur"
}"##;
let doc = Document::from_json(json).unwrap();
assert_eq!(doc.name, "demo");
assert_eq!(doc.nodes.len(), 2);
assert_eq!(doc.output.as_str(), "blur");
assert_eq!(doc.nodes["blur"].op, "blur");
assert_eq!(doc.nodes["blur"].fields["input"], "@src");
}
#[test]
fn parses_output_without_at_prefix() {
let json = r##"{
"name": "demo",
"nodes": { "a": { "op": "image", "src": "x.png" } },
"output": "a"
}"##;
let doc = Document::from_json(json).unwrap();
assert_eq!(doc.output.as_str(), "a");
}
#[test]
fn parses_params_and_assets() {
let json = r##"{
"name": "demo",
"params": {
"ink": { "type": "color", "default": "#000000" },
"k": { "type": "number", "default": 0.5, "min": 0, "max": 1 }
},
"assets": {
"brush": { "type": "brush", "src": "assets/wet.myb" }
},
"nodes": { "out": { "op": "solid", "color": "$ink" } },
"output": "@out"
}"##;
let doc = Document::from_json(json).unwrap();
assert_eq!(doc.params["k"].kind, ParamKind::Number);
assert_eq!(doc.assets["brush"].kind, AssetKind::Brush);
assert_eq!(doc.params["k"].max, Some(1.0));
}
#[test]
fn rejects_unknown_top_level_field() {
let json = r##"{
"name": "demo",
"nodes": {},
"output": "@x",
"junk": 1
}"##;
assert!(Document::from_json(json).is_err());
}
#[test]
fn parses_filter_variants() {
let json = r##"{
"kind": "ocean",
"kinds": ["a", "b"],
"kind_detail": { "not": "canal" },
"kind_many": { "not": ["x", "y"] }
}"##;
let f: FeatureFilter = serde_json::from_str(json).unwrap();
assert!(matches!(f["kind"], FilterMatch::One(_)));
assert!(matches!(f["kinds"], FilterMatch::Any(_)));
let FilterMatch::Not(n) = &f["kind_detail"] else {
panic!("expected Not")
};
assert!(matches!(n.not, NotInner::One(_)));
let FilterMatch::Not(n) = &f["kind_many"] else {
panic!("expected Not")
};
assert!(matches!(n.not, NotInner::Any(_)));
}
#[test]
fn classify_field_refs() {
assert!(matches!(FieldRef::classify("@foo"), FieldRef::Node("foo")));
assert!(matches!(FieldRef::classify("$bar"), FieldRef::Param("bar")));
assert!(matches!(
FieldRef::classify("plain"),
FieldRef::Literal("plain")
));
}
}