Skip to main content

ezu_graph/
input.rs

1//! `In<T>` — a scalar node field that accepts an inline literal, a
2//! `$param` reference (resolved against [`EvalCtx::params`] at eval
3//! time), or a `@node` connection on a [`PortKind::Scalar`] input port.
4//!
5//! Factories read fields through an [`InReader`], which classifies each
6//! value and accumulates the scalar [`PortSpec`]s / [`Connection`]s /
7//! param names the node must expose:
8//!
9//! ```ignore
10//! let mut r = InReader::new(fields, ctx, FIXED_PORTS.len());
11//! let sigma = r.number(\"sigma\")?;          // In<f64>
12//! let color = r.color_or(\"color\", WHITE)?; // In<[f32; 4]>
13//! let parts = r.finish();
14//! // node stores `sigma`, `color`, `parts.ports` (appended after the
15//! // fixed ports), returns `parts.param_refs` from `Node::param_refs`,
16//! // and the factory returns `parts.connections` alongside any fixed
17//! // ones.
18//! ```
19//!
20//! Cache correctness: `In::param_hash` feeds the *static* identity
21//! (literal value / param name / port index) into `Node::param_hash`;
22//! the runtime value of a `$param` is folded into the cache key by the
23//! evaluator via [`Node::param_refs`](crate::node::Node::param_refs),
24//! and a port-fed value arrives through the upstream node's hash.
25
26use ezu_style as spec;
27use serde_json::Value;
28use xxhash_rust::xxh3::Xxh3;
29
30use crate::eval::{EvalCtx, EvalError};
31use crate::port::{PortKind, PortSpec};
32use crate::registry::{Connection, FactoryCtx, FactoryError};
33use crate::value::{PortValue, ScalarValue};
34
35/// Accepts-list for a scalar input port.
36pub const ACCEPTS_SCALAR: &[PortKind] = &[PortKind::Scalar];
37
38/// A scalar field type readable through [`In<T>`] / [`InReader`].
39pub trait ScalarType: Copy {
40    /// Human-readable type name for error messages.
41    const NAME: &'static str;
42    /// Whether a param declaration of `kind` supplies this type.
43    fn matches_kind(kind: spec::ParamKind) -> bool;
44    /// Extract from a runtime scalar value.
45    fn from_scalar(v: ScalarValue) -> Option<Self>;
46    /// Parse from a JSON literal (field value or param default).
47    fn from_json(v: &Value) -> Option<Self>;
48    /// Feed into a cache-key hasher.
49    fn hash_into(&self, h: &mut Xxh3);
50    /// Clamp to a declaration's `min` / `max`. Identity for non-numbers.
51    fn clamp_decl(self, _min: Option<f64>, _max: Option<f64>) -> Self {
52        self
53    }
54}
55
56impl ScalarType for f64 {
57    const NAME: &'static str = "number";
58    fn matches_kind(kind: spec::ParamKind) -> bool {
59        kind == spec::ParamKind::Number
60    }
61    fn from_scalar(v: ScalarValue) -> Option<Self> {
62        v.as_number()
63    }
64    fn from_json(v: &Value) -> Option<Self> {
65        v.as_f64()
66    }
67    fn hash_into(&self, h: &mut Xxh3) {
68        h.update(&self.to_le_bytes());
69    }
70    fn clamp_decl(self, min: Option<f64>, max: Option<f64>) -> Self {
71        let mut v = self;
72        if let Some(m) = min {
73            v = v.max(m);
74        }
75        if let Some(m) = max {
76            v = v.min(m);
77        }
78        v
79    }
80}
81
82/// Straight (non-premultiplied) sRGB-encoded RGBA in `[0, 1]` — the
83/// same convention as a parsed `#rrggbb[aa]` literal.
84impl ScalarType for [f32; 4] {
85    const NAME: &'static str = "color";
86    fn matches_kind(kind: spec::ParamKind) -> bool {
87        kind == spec::ParamKind::Color
88    }
89    fn from_scalar(v: ScalarValue) -> Option<Self> {
90        v.as_color()
91    }
92    fn from_json(v: &Value) -> Option<Self> {
93        spec::parse_hex_color(v.as_str()?)
94    }
95    fn hash_into(&self, h: &mut Xxh3) {
96        for c in self {
97            h.update(&c.to_le_bytes());
98        }
99    }
100}
101
102impl ScalarType for bool {
103    const NAME: &'static str = "bool";
104    fn matches_kind(kind: spec::ParamKind) -> bool {
105        kind == spec::ParamKind::Bool
106    }
107    fn from_scalar(v: ScalarValue) -> Option<Self> {
108        v.as_bool()
109    }
110    fn from_json(v: &Value) -> Option<Self> {
111        v.as_bool()
112    }
113    fn hash_into(&self, h: &mut Xxh3) {
114        h.update(&[*self as u8]);
115    }
116}
117
118/// A node field whose value is an inline literal, a `$param` reference,
119/// or a scalar input port. Resolve with [`In::get`] inside `Node::eval`.
120#[derive(Debug, Clone)]
121pub enum In<T> {
122    /// Inline literal, baked at build time.
123    Const(T),
124    /// `$param` reference: read `EvalCtx::params` at eval time, fall
125    /// back to the declaration default, clamp numbers to the
126    /// declaration's `min` / `max`.
127    Param {
128        name: String,
129        fallback: T,
130        min: Option<f64>,
131        max: Option<f64>,
132    },
133    /// `@node` connection on a scalar input port.
134    Port {
135        /// Positional index into `Node::inputs()`.
136        ix: usize,
137        /// Field name, for error messages.
138        name: &'static str,
139    },
140}
141
142impl<T: ScalarType> In<T> {
143    /// Resolve the field's value for one eval.
144    pub fn get(&self, ctx: &EvalCtx<'_>, inputs: &[Option<PortValue>]) -> Result<T, EvalError> {
145        match self {
146            In::Const(v) => Ok(*v),
147            In::Param {
148                name,
149                fallback,
150                min,
151                max,
152            } => {
153                let v = match ctx.params.get(name) {
154                    None => *fallback,
155                    Some(sv) => T::from_scalar(sv).ok_or_else(|| {
156                        EvalError::Other(format!(
157                            "param `${name}`: expected {}, got {}",
158                            T::NAME,
159                            sv.kind_name()
160                        ))
161                    })?,
162                };
163                Ok(v.clamp_decl(*min, *max))
164            }
165            In::Port { ix, name } => {
166                let v = inputs
167                    .get(*ix)
168                    .and_then(|o| o.as_ref())
169                    .ok_or_else(|| EvalError::MissingInput((*name).into()))?;
170                let PortValue::Scalar(sv) = v else {
171                    return Err(EvalError::Other(format!(
172                        "port `{name}`: expected a scalar, got {}",
173                        v.kind()
174                    )));
175                };
176                T::from_scalar(*sv).ok_or_else(|| {
177                    EvalError::Other(format!(
178                        "port `{name}`: expected {}, got {}",
179                        T::NAME,
180                        sv.kind_name()
181                    ))
182                })
183            }
184        }
185    }
186
187    /// Feed the field's *static* identity into `Node::param_hash`.
188    /// Runtime `$param` values are keyed via `Node::param_refs`; a
189    /// port-fed value is keyed via the upstream node's hash.
190    pub fn param_hash(&self, h: &mut Xxh3) {
191        match self {
192            In::Const(v) => {
193                h.update(b"c");
194                v.hash_into(h);
195            }
196            In::Param { name, fallback, .. } => {
197                h.update(b"p");
198                h.update(name.as_bytes());
199                fallback.hash_into(h);
200            }
201            In::Port { ix, .. } => {
202                h.update(b"@");
203                h.update(&(*ix as u64).to_le_bytes());
204            }
205        }
206    }
207
208    /// Static upper bound usable for build-time decisions (pad
209    /// propagation): the literal value, or a `$param`'s declared `max`.
210    /// `None` for ports and unbounded params — pad-affecting fields
211    /// must reject those at build time.
212    pub fn static_bound(&self) -> Option<f64>
213    where
214        T: Into<f64>,
215    {
216        match self {
217            In::Const(v) => Some((*v).into()),
218            In::Param { max, .. } => *max,
219            In::Port { .. } => None,
220        }
221    }
222}
223
224/// Parse a caller-supplied parameter assignment (CLI `--param k=v`,
225/// server query string) against the document's declarations. Unknown
226/// names, type mismatches, and out-of-range numbers are errors — hosts
227/// report them up front instead of silently clamping at eval time.
228pub fn parse_param_value(
229    decls: &indexmap::IndexMap<String, spec::ParamDecl>,
230    name: &str,
231    raw: &str,
232) -> Result<ScalarValue, String> {
233    let decl = decls
234        .get(name)
235        .ok_or_else(|| format!("unknown param `{name}`"))?;
236    match decl.kind {
237        spec::ParamKind::Number => {
238            let v: f64 = raw
239                .parse()
240                .map_err(|_| format!("param `{name}`: `{raw}` is not a number"))?;
241            if let Some(m) = decl.min {
242                if v < m {
243                    return Err(format!("param `{name}`: {v} is below min {m}"));
244                }
245            }
246            if let Some(m) = decl.max {
247                if v > m {
248                    return Err(format!("param `{name}`: {v} is above max {m}"));
249                }
250            }
251            Ok(ScalarValue::Number(v))
252        }
253        spec::ParamKind::Bool => match raw {
254            "true" | "1" => Ok(ScalarValue::Bool(true)),
255            "false" | "0" => Ok(ScalarValue::Bool(false)),
256            _ => Err(format!("param `{name}`: expected true/false, got `{raw}`")),
257        },
258        spec::ParamKind::Color => spec::parse_hex_color(raw)
259            .map(ScalarValue::Color)
260            .ok_or_else(|| format!("param `{name}`: `{raw}` is not a `#rrggbb[aa]` color")),
261    }
262}
263
264/// What an [`InReader`] accumulated: scalar input ports (to append
265/// after the node's fixed ports), their connections, and the names of
266/// `$param` references (returned from `Node::param_refs`).
267#[derive(Debug, Default)]
268pub struct InParts {
269    pub ports: Vec<PortSpec>,
270    pub connections: Vec<Connection>,
271    pub param_refs: Vec<String>,
272}
273
274/// Field reader used by node factories: classifies each field as
275/// literal / `$param` / `@node` and accumulates the node's scalar
276/// port plumbing. See the module docs for the usage pattern.
277pub struct InReader<'a, 'c> {
278    fields: &'a serde_json::Map<String, Value>,
279    ctx: &'a FactoryCtx<'c>,
280    parts: InParts,
281    next_port: usize,
282}
283
284impl<'a, 'c> InReader<'a, 'c> {
285    /// `fixed_ports` is the number of input ports the node declares
286    /// before any scalar ports (e.g. 1 for `blur`'s `input`). Scalar
287    /// port indices start there.
288    pub fn new(
289        fields: &'a serde_json::Map<String, Value>,
290        ctx: &'a FactoryCtx<'c>,
291        fixed_ports: usize,
292    ) -> Self {
293        Self {
294            fields,
295            ctx,
296            parts: InParts::default(),
297            next_port: fixed_ports,
298        }
299    }
300
301    /// Required number field.
302    pub fn number(&mut self, name: &'static str) -> Result<In<f64>, FactoryError> {
303        self.read(name, None)
304    }
305
306    /// Optional number field with a default.
307    pub fn number_or(&mut self, name: &'static str, default: f64) -> Result<In<f64>, FactoryError> {
308        self.read(name, Some(default))
309    }
310
311    /// Required color field (`#rrggbb[aa]`, `$param`, or `@node`).
312    pub fn color(&mut self, name: &'static str) -> Result<In<[f32; 4]>, FactoryError> {
313        self.read(name, None)
314    }
315
316    /// Optional color field with a default.
317    pub fn color_or(
318        &mut self,
319        name: &'static str,
320        default: [f32; 4],
321    ) -> Result<In<[f32; 4]>, FactoryError> {
322        self.read(name, Some(default))
323    }
324
325    /// Optional color field; `None` when absent.
326    pub fn color_opt(&mut self, name: &'static str) -> Result<Option<In<[f32; 4]>>, FactoryError> {
327        if !self.fields.contains_key(name) {
328            return Ok(None);
329        }
330        Ok(Some(self.read(name, None)?))
331    }
332
333    /// Optional bool field with a default.
334    pub fn bool_or(&mut self, name: &'static str, default: bool) -> Result<In<bool>, FactoryError> {
335        self.read(name, Some(default))
336    }
337
338    fn read<T: ScalarType>(
339        &mut self,
340        name: &'static str,
341        default: Option<T>,
342    ) -> Result<In<T>, FactoryError> {
343        let Some(v) = self.fields.get(name) else {
344            return default
345                .map(In::Const)
346                .ok_or_else(|| FactoryError::MissingField(name.to_string()));
347        };
348        if let Some(s) = v.as_str() {
349            match spec::FieldRef::classify(s) {
350                spec::FieldRef::Node(id) => {
351                    let ix = self.next_port;
352                    self.next_port += 1;
353                    self.parts.ports.push(PortSpec {
354                        name,
355                        accepts: ACCEPTS_SCALAR,
356                        optional: false,
357                    });
358                    self.parts.connections.push(Connection {
359                        port: name.to_string(),
360                        src: id.to_string(),
361                    });
362                    return Ok(In::Port { ix, name });
363                }
364                spec::FieldRef::Param(p) => {
365                    let decl = self
366                        .ctx
367                        .params
368                        .get(p)
369                        .ok_or_else(|| FactoryError::UnknownParam(p.to_string()))?;
370                    if !T::matches_kind(decl.kind) {
371                        return Err(FactoryError::BadField {
372                            field: name.into(),
373                            msg: format!(
374                                "param `${p}` is declared `{:?}`, but this field needs a {}",
375                                decl.kind,
376                                T::NAME
377                            ),
378                        });
379                    }
380                    let fallback =
381                        T::from_json(&decl.default).ok_or_else(|| FactoryError::BadField {
382                            field: name.into(),
383                            msg: format!("param `${p}` default is not a valid {}", T::NAME),
384                        })?;
385                    self.parts.param_refs.push(p.to_string());
386                    return Ok(In::Param {
387                        name: p.to_string(),
388                        fallback,
389                        min: decl.min,
390                        max: decl.max,
391                    });
392                }
393                spec::FieldRef::Literal(_) => {} // fall through
394            }
395        }
396        T::from_json(v)
397            .map(In::Const)
398            .ok_or_else(|| FactoryError::BadField {
399                field: name.into(),
400                msg: format!("expected {} literal, `$param`, or `@node`", T::NAME),
401            })
402    }
403
404    /// Hand back the accumulated ports / connections / param refs.
405    pub fn finish(self) -> InParts {
406        self.parts
407    }
408}