use std::collections::HashMap;
use ezu_style as spec;
use crate::node::Node;
#[derive(Debug, Clone)]
pub struct Connection {
pub port: String,
pub src: String,
}
pub struct BuiltNode {
pub node: Box<dyn Node>,
pub connections: Vec<Connection>,
}
pub struct FactoryCtx<'a> {
pub params: &'a indexmap::IndexMap<String, spec::ParamDecl>,
pub assets: &'a indexmap::IndexMap<String, spec::AssetDecl>,
}
#[derive(Debug, thiserror::Error)]
pub enum FactoryError {
#[error("missing required field `{0}`")]
MissingField(String),
#[error("field `{field}` has wrong type: {msg}")]
BadField { field: String, msg: String },
#[error("unknown param reference `${0}`")]
UnknownParam(String),
#[error("unknown asset reference `@{0}`")]
UnknownAsset(String),
#[error("{0}")]
Custom(String),
}
pub trait NodeFactory: Send + Sync {
fn op_name(&self) -> &'static str;
fn build(
&self,
fields: &serde_json::Map<String, serde_json::Value>,
ctx: &FactoryCtx<'_>,
) -> Result<BuiltNode, FactoryError>;
fn schema(&self) -> serde_json::Value {
serde_json::json!({})
}
}
pub struct StaticOp(pub &'static dyn NodeFactory);
inventory::collect!(StaticOp);
#[macro_export]
macro_rules! submit_node {
($factory:ident) => {
$crate::inventory::submit! {
$crate::StaticOp(&$factory)
}
};
}
#[derive(Default)]
pub struct NodeRegistry {
ops: HashMap<&'static str, &'static dyn NodeFactory>,
}
impl NodeRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn from_inventory() -> Self {
let mut r = Self::default();
for StaticOp(f) in inventory::iter::<StaticOp> {
r.register_static(*f);
}
r
}
pub fn register(&mut self, factory: impl NodeFactory + 'static) {
self.register_static(Box::leak(Box::new(factory)));
}
pub fn register_static(&mut self, factory: &'static dyn NodeFactory) {
self.ops.insert(factory.op_name(), factory);
}
pub fn get(&self, op_name: &str) -> Option<&dyn NodeFactory> {
self.ops.get(op_name).copied()
}
pub fn op_names(&self) -> Vec<&'static str> {
let mut names: Vec<_> = self.ops.keys().copied().collect();
names.sort_unstable();
names
}
pub fn document_schema(&self) -> serde_json::Value {
use serde_json::{json, Value};
let mut variants: Vec<Value> = Vec::with_capacity(self.ops.len());
for op in self.op_names() {
let factory = self
.ops
.get(op)
.expect("op_names yields keys present in self.ops");
let mut schema = factory.schema();
if !schema.is_object() {
schema = json!({});
}
let obj = schema
.as_object_mut()
.expect("schema was just normalized to an object");
obj.entry("type").or_insert_with(|| json!("object"));
let props = obj
.entry("properties")
.or_insert_with(|| json!({}))
.as_object_mut()
.expect("`properties` was just inserted as a JSON object");
props.insert(
"op".to_string(),
json!({ "const": op, "description": format!("Selects the `{op}` operation.") }),
);
let required = obj
.entry("required")
.or_insert_with(|| json!([]))
.as_array_mut()
.expect("`required` was just inserted as a JSON array");
if !required.iter().any(|v| v.as_str() == Some("op")) {
required.insert(0, json!("op"));
}
obj.insert("title".to_string(), json!(format!("op: {op}")));
variants.push(schema);
}
json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Ezu Style Spec",
"type": "object",
"required": ["name", "nodes", "output"],
"properties": {
"name": { "type": "string" },
"version": { "type": "string" },
"tile-size": { "type": "integer", "minimum": 1 },
"pad": { "type": "integer", "minimum": 0 },
"params": {
"type": "object",
"additionalProperties": {
"type": "object",
"required": ["type", "default"],
"properties": {
"type": { "enum": ["color", "number", "bool"] },
"default": {},
"min": { "type": "number" },
"max": { "type": "number" },
"description": { "type": "string" }
}
}
},
"assets": {
"type": "object",
"additionalProperties": {
"type": "object",
"required": ["type", "src"],
"properties": {
"type": { "enum": ["brush", "image", "mask-image", "gradient"] },
"src": { "type": "string" }
}
}
},
"nodes": {
"type": "object",
"additionalProperties": { "oneOf": variants }
},
"output": {
"type": "string",
"description": "Node id of the final raster (with or without `@`)."
}
}
})
}
}
pub mod schema_frag {
use serde_json::{json, Value};
pub fn node_ref() -> Value {
json!({
"type": "string",
"pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
"description": "Reference to another node (`@name`)."
})
}
pub fn asset_ref() -> Value {
json!({
"type": "string",
"description": "Asset reference (`@name`) or literal path."
})
}
pub fn color() -> Value {
json!({
"type": "string",
"pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|\\$[A-Za-z_][A-Za-z0-9_-]*)$",
"description": "sRGB hex color, or `$param` reference."
})
}
pub fn unit_number() -> Value {
json!({ "type": "number", "minimum": 0.0, "maximum": 1.0 })
}
pub fn px_number() -> Value {
json!({ "type": "number", "minimum": 0.0 })
}
}
pub fn take_input_ref(
fields: &serde_json::Map<String, serde_json::Value>,
name: &str,
) -> Result<String, FactoryError> {
let v = fields
.get(name)
.ok_or_else(|| FactoryError::MissingField(name.to_string()))?;
let s = v.as_str().ok_or_else(|| FactoryError::BadField {
field: name.to_string(),
msg: "expected string node reference".into(),
})?;
match spec::FieldRef::classify(s) {
spec::FieldRef::Node(id) => Ok(id.to_string()),
_ => Err(FactoryError::BadField {
field: name.to_string(),
msg: format!("expected `@node-ref`, got `{s}`"),
}),
}
}
pub fn take_optional_input_ref(
fields: &serde_json::Map<String, serde_json::Value>,
name: &str,
) -> Result<Option<String>, FactoryError> {
match fields.get(name) {
None => Ok(None),
Some(v) if v.is_null() => Ok(None),
Some(_) => Ok(Some(take_input_ref(fields, name)?)),
}
}