Skip to main content

ezu_graph/
build.rs

1//! Drive [`GraphBuilder`] from a parsed [`spec::Document`] using a
2//! [`NodeRegistry`].
3
4use ezu_style as spec;
5
6use crate::graph::{BuildError, Graph, GraphBuilder};
7use crate::port::PortKind;
8use crate::registry::{FactoryCtx, FactoryError, NodeRegistry};
9
10#[derive(Debug, thiserror::Error)]
11pub enum BuildGraphError {
12    #[error("unknown op `{op}` on node `{node}`")]
13    UnknownOp { node: String, op: String },
14
15    #[error("factory error on node `{node}`: {source}")]
16    Factory {
17        node: String,
18        #[source]
19        source: FactoryError,
20    },
21
22    #[error(transparent)]
23    Expand(#[from] spec::ExpandError),
24
25    #[error(
26        "call `{call}` of `{func}`: input `{input}` expects {expected}, but `@{src}` produces {got}"
27    )]
28    FuncInputKind {
29        call: String,
30        func: String,
31        input: String,
32        expected: PortKind,
33        src: String,
34        got: PortKind,
35    },
36
37    #[error("call `{call}` of `{func}`: declared output-kind is {declared}, but the body produces {got}")]
38    FuncOutputKind {
39        call: String,
40        func: String,
41        declared: PortKind,
42        got: PortKind,
43    },
44
45    #[error("legend entry `{label}` names `@{src}`, which is not a node in this style")]
46    LegendUnknownNode { label: String, src: String },
47
48    #[error("legend entry `{label}` names `@{src}`, which produces {got} — a legend entry must name a node that draws something ({expected})")]
49    LegendNodeKind {
50        label: String,
51        src: String,
52        expected: PortKind,
53        got: PortKind,
54    },
55
56    #[error(transparent)]
57    Graph(#[from] BuildError),
58}
59
60fn port_kind(k: spec::FuncKind) -> PortKind {
61    match k {
62        spec::FuncKind::Features => PortKind::Features,
63        spec::FuncKind::Raster => PortKind::Raster,
64        spec::FuncKind::Sprite => PortKind::Sprite,
65        spec::FuncKind::Brush => PortKind::Brush,
66        spec::FuncKind::Scalar => PortKind::Scalar,
67        spec::FuncKind::ScalarField => PortKind::ScalarField,
68    }
69}
70
71/// Build a typed [`Graph`] from a parsed document and a registry of
72/// node factories. Documents with a `functions` block are expanded
73/// inline first; declared input/output kinds are verified against the
74/// built graph's resolved port kinds.
75pub fn build_graph(
76    doc: &spec::Document,
77    registry: &NodeRegistry,
78) -> Result<Graph, BuildGraphError> {
79    let expanded = spec::expand_functions(doc)?;
80    let (doc, kind_checks) = match &expanded {
81        Some(e) => (&e.doc, e.kind_checks.as_slice()),
82        None => (doc, &[][..]),
83    };
84
85    let ctx = FactoryCtx {
86        params: &doc.params,
87        sources: &doc.sources,
88    };
89
90    let mut gb = GraphBuilder::new();
91    let mut pending: Vec<(String, Vec<crate::registry::Connection>)> = Vec::new();
92
93    for (id, spec) in &doc.nodes {
94        let factory = registry
95            .get(&spec.op)
96            .ok_or_else(|| BuildGraphError::UnknownOp {
97                node: id.clone(),
98                op: spec.op.clone(),
99            })?;
100
101        let built = factory
102            .build(&spec.fields, &ctx)
103            .map_err(|e| BuildGraphError::Factory {
104                node: id.clone(),
105                source: e,
106            })?;
107
108        gb.add_node(id.clone(), built.node);
109        pending.push((id.clone(), built.connections));
110    }
111
112    for (dst, conns) in pending {
113        for c in conns {
114            gb.connect(c.src, dst.clone(), c.port);
115        }
116    }
117
118    gb.set_output(doc.output.as_str().to_string());
119    let graph = gb.build()?;
120
121    // Verify each call site's declared kinds against the resolved port
122    // kinds. Argument sources and the call's output node are plain
123    // graph nodes after expansion, so this is a pure lookup.
124    for check in kind_checks {
125        let Some(ix) = graph.index_of(&check.node) else {
126            // The referenced node failed to resolve — the builder has
127            // already reported the real error path; skip.
128            continue;
129        };
130        let got = graph.output_kind(ix);
131        let expected = port_kind(check.declared);
132        if got != expected {
133            return Err(match &check.input {
134                Some(input) => BuildGraphError::FuncInputKind {
135                    call: check.call.clone(),
136                    func: check.func.clone(),
137                    input: input.clone(),
138                    expected,
139                    src: check.node.clone(),
140                    got,
141                },
142                None => BuildGraphError::FuncOutputKind {
143                    call: check.call.clone(),
144                    func: check.func.clone(),
145                    declared: expected,
146                    got,
147                },
148            });
149        }
150    }
151
152    // The legend is not part of the graph, but its entries point into
153    // it: each one names the node that draws the symbol it explains. A
154    // dangling or non-drawing reference is a broken legend, and a broken
155    // legend is worse than none — check it here, where every caller
156    // already passes.
157    if let Some(legend) = &doc.legend {
158        for entry in &legend.entries {
159            let src = entry.from.as_str();
160            let Some(ix) = graph.index_of(src) else {
161                return Err(BuildGraphError::LegendUnknownNode {
162                    label: entry.label.clone(),
163                    src: src.to_string(),
164                });
165            };
166            let got = graph.output_kind(ix);
167            if got != PortKind::Raster {
168                return Err(BuildGraphError::LegendNodeKind {
169                    label: entry.label.clone(),
170                    src: src.to_string(),
171                    expected: PortKind::Raster,
172                    got,
173                });
174            }
175        }
176    }
177    Ok(graph)
178}