1use std::collections::HashMap;
13
14use ezu_style as spec;
15
16use crate::node::Node;
17
18#[derive(Debug, Clone)]
20pub struct Connection {
21 pub port: String,
23 pub src: String,
25}
26
27pub struct BuiltNode {
31 pub node: Box<dyn Node>,
32 pub connections: Vec<Connection>,
33}
34
35pub struct FactoryCtx<'a> {
38 pub params: &'a indexmap::IndexMap<String, spec::ParamDecl>,
39 pub sources: &'a indexmap::IndexMap<String, spec::SourceDecl>,
40}
41
42#[derive(Debug, thiserror::Error)]
43pub enum FactoryError {
44 #[error("missing required field `{0}`")]
45 MissingField(String),
46 #[error("field `{field}` has wrong type: {msg}")]
47 BadField { field: String, msg: String },
48 #[error("unknown param reference `${0}`")]
49 UnknownParam(String),
50 #[error("unknown asset reference `@{0}`")]
51 UnknownAsset(String),
52 #[error("{0}")]
53 Custom(String),
54}
55
56pub trait NodeFactory: Send + Sync {
62 fn op_name(&self) -> &'static str;
64
65 fn build(
66 &self,
67 fields: &serde_json::Map<String, serde_json::Value>,
68 ctx: &FactoryCtx<'_>,
69 ) -> Result<BuiltNode, FactoryError>;
70
71 fn schema(&self) -> serde_json::Value {
78 serde_json::json!({})
79 }
80}
81
82pub struct StaticOp(pub &'static dyn NodeFactory);
86
87inventory::collect!(StaticOp);
88
89#[macro_export]
98macro_rules! submit_node {
99 ($factory:ident) => {
100 $crate::inventory::submit! {
101 $crate::StaticOp(&$factory)
102 }
103 };
104}
105
106#[derive(Default)]
108pub struct NodeRegistry {
109 ops: HashMap<&'static str, &'static dyn NodeFactory>,
110}
111
112impl NodeRegistry {
113 pub fn new() -> Self {
114 Self::default()
115 }
116
117 pub fn from_inventory() -> Self {
120 let mut r = Self::default();
121 for StaticOp(f) in inventory::iter::<StaticOp> {
122 r.register_static(*f);
123 }
124 r
125 }
126
127 pub fn register(&mut self, factory: impl NodeFactory + 'static) {
131 self.register_static(Box::leak(Box::new(factory)));
132 }
133
134 pub fn register_static(&mut self, factory: &'static dyn NodeFactory) {
137 self.ops.insert(factory.op_name(), factory);
138 }
139
140 pub fn get(&self, op_name: &str) -> Option<&dyn NodeFactory> {
141 self.ops.get(op_name).copied()
142 }
143
144 pub fn op_names(&self) -> Vec<&'static str> {
146 let mut names: Vec<_> = self.ops.keys().copied().collect();
147 names.sort_unstable();
148 names
149 }
150
151 pub fn document_schema(&self) -> serde_json::Value {
157 use serde_json::{json, Value};
158 let mut variants: Vec<Value> = Vec::with_capacity(self.ops.len());
159 for op in self.op_names() {
160 let factory = self
161 .ops
162 .get(op)
163 .expect("op_names yields keys present in self.ops");
164 let mut schema = factory.schema();
165 if !schema.is_object() {
166 schema = json!({});
167 }
168 let obj = schema
169 .as_object_mut()
170 .expect("schema was just normalized to an object");
171 obj.entry("type").or_insert_with(|| json!("object"));
172 let props = obj
174 .entry("properties")
175 .or_insert_with(|| json!({}))
176 .as_object_mut()
177 .expect("`properties` was just inserted as a JSON object");
178 props.insert(
179 "op".to_string(),
180 json!({ "const": op, "description": format!("Selects the `{op}` operation.") }),
181 );
182 let required = obj
184 .entry("required")
185 .or_insert_with(|| json!([]))
186 .as_array_mut()
187 .expect("`required` was just inserted as a JSON array");
188 if !required.iter().any(|v| v.as_str() == Some("op")) {
189 required.insert(0, json!("op"));
190 }
191 obj.insert("title".to_string(), json!(format!("op: {op}")));
192 variants.push(schema);
193 }
194
195 json!({
196 "$schema": "https://json-schema.org/draft/2020-12/schema",
197 "title": "Ezu Style Spec",
198 "type": "object",
199 "required": ["name", "nodes", "output"],
200 "properties": {
201 "name": { "type": "string" },
202 "version": { "type": "string" },
203 "tile-size": { "type": "integer", "minimum": 1 },
204 "pad": { "type": "integer", "minimum": 0 },
205 "params": {
206 "type": "object",
207 "additionalProperties": {
208 "type": "object",
209 "required": ["type", "default"],
210 "properties": {
211 "type": { "enum": ["color", "number", "bool"] },
212 "default": {},
213 "min": { "type": "number" },
214 "max": { "type": "number" },
215 "description": { "type": "string" }
216 }
217 }
218 },
219 "sources": {
220 "type": "object",
221 "additionalProperties": {
222 "oneOf": [
223 {
224 "type": "object",
225 "required": ["type", "src"],
226 "properties": {
227 "type": { "enum": ["brush", "image"] },
228 "src": { "type": "string" }
229 }
230 },
231 {
232 "type": "object",
233 "required": ["type", "url"],
234 "properties": {
235 "type": { "enum": ["mvt", "pmtiles"] },
236 "url": { "type": "string" }
237 }
238 },
239 {
240 "type": "object",
241 "required": ["type", "url", "encoding"],
242 "properties": {
243 "type": { "const": "dem" },
244 "url": { "type": "string" },
245 "encoding": { "enum": ["terrarium", "mapbox-rgb"] },
246 "tile-size": { "type": "integer", "minimum": 1 },
247 "max-zoom": { "type": "integer", "minimum": 0 },
248 "neighbor-fetch": { "type": "boolean" },
249 "elevation-offset": { "type": "number" }
250 }
251 }
252 ]
253 }
254 },
255 "nodes": {
256 "type": "object",
257 "additionalProperties": { "oneOf": variants }
258 },
259 "output": {
260 "type": "string",
261 "description": "Node id of the final raster (with or without `@`)."
262 }
263 }
264 })
265 }
266}
267
268pub mod schema_frag {
271 use serde_json::{json, Value};
272
273 pub fn node_ref() -> Value {
275 json!({
276 "type": "string",
277 "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
278 "description": "Reference to another node (`@name`)."
279 })
280 }
281
282 pub fn asset_ref() -> Value {
284 json!({
285 "type": "string",
286 "description": "Asset reference (`@name`) or literal path."
287 })
288 }
289
290 pub fn color() -> Value {
292 json!({
293 "type": "string",
294 "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|\\$[A-Za-z_][A-Za-z0-9_-]*)$",
295 "description": "sRGB hex color, or `$param` reference."
296 })
297 }
298
299 pub fn unit_number() -> Value {
301 json!({ "type": "number", "minimum": 0.0, "maximum": 1.0 })
302 }
303
304 pub fn px_number() -> Value {
306 json!({ "type": "number", "minimum": 0.0 })
307 }
308}
309
310pub fn take_input_ref(
315 fields: &serde_json::Map<String, serde_json::Value>,
316 name: &str,
317) -> Result<String, FactoryError> {
318 let v = fields
319 .get(name)
320 .ok_or_else(|| FactoryError::MissingField(name.to_string()))?;
321 let s = v.as_str().ok_or_else(|| FactoryError::BadField {
322 field: name.to_string(),
323 msg: "expected string node reference".into(),
324 })?;
325 match spec::FieldRef::classify(s) {
326 spec::FieldRef::Node(id) => Ok(id.to_string()),
327 _ => Err(FactoryError::BadField {
328 field: name.to_string(),
329 msg: format!("expected `@node-ref`, got `{s}`"),
330 }),
331 }
332}
333
334pub fn take_optional_input_ref(
337 fields: &serde_json::Map<String, serde_json::Value>,
338 name: &str,
339) -> Result<Option<String>, FactoryError> {
340 match fields.get(name) {
341 None => Ok(None),
342 Some(v) if v.is_null() => Ok(None),
343 Some(_) => Ok(Some(take_input_ref(fields, name)?)),
344 }
345}