Skip to main content

wyrd/authoring/
pattern.rs

1//! Reusable graph fragments: validated patterns expand into parent weaves.
2//!
3//! A [`Pattern`] is an immutable inner weave plus named input/output exports.
4//! Include renames knots under `instance_id/` so multiple instances do not
5//! collide. Exported inputs may leave required ports unconnected in the inner
6//! graph; the parent must wire them after expand.
7
8use std::collections::{BTreeMap, BTreeSet};
9use std::string::String;
10use std::vec::Vec;
11
12use crate::foundation::{ports_of, PortDir};
13
14use crate::{KnotDef, PortRefDef, ThreadDef, ValidationError, WeaveDef};
15
16#[cfg(feature = "schema")]
17use schemars::JsonSchema;
18#[cfg(feature = "serde")]
19use serde::{Deserialize, Serialize};
20#[cfg(feature = "schema")]
21use std::borrow::ToOwned;
22
23/// Named export of an inner knot port for parent-weave wiring.
24#[derive(Clone, Debug, PartialEq, Eq)]
25#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26#[cfg_attr(feature = "schema", derive(JsonSchema))]
27pub struct PatternExportDef {
28    /// Export name exposed to the parent weave (`in(export)` / `out(export)`).
29    pub name: String,
30    /// Inner knot port this export aliases.
31    pub port: PortRefDef,
32}
33
34impl PatternExportDef {
35    /// Build an export from a parent-visible name and inner knot port.
36    pub fn new(name: impl Into<String>, knot: impl Into<String>, port: impl Into<String>) -> Self {
37        Self {
38            name: name.into(),
39            port: PortRefDef::new(knot, port),
40        }
41    }
42}
43
44/// Editable pattern definition; convert with [`Pattern::try_from`].
45#[derive(Clone, Debug, PartialEq)]
46#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
47#[cfg_attr(feature = "schema", derive(JsonSchema))]
48pub struct PatternDef {
49    /// Pattern catalog id (non-empty, no `/`).
50    pub id: String,
51    /// Inner weave expanded under `instance_id/` on include.
52    pub inner: WeaveDef,
53    /// Required input ports the parent must wire after include.
54    pub inputs: Vec<PatternExportDef>,
55    /// Output ports available as parent thread sources.
56    pub outputs: Vec<PatternExportDef>,
57}
58
59/// Immutable, validated reusable graph fragment.
60#[derive(Clone, Debug, PartialEq)]
61pub struct Pattern {
62    id: String,
63    inner: WeaveDef,
64    inputs: Vec<PatternExportDef>,
65    outputs: Vec<PatternExportDef>,
66}
67
68impl Pattern {
69    /// Pattern catalog id (no `/` in the id).
70    pub fn id(&self) -> &str {
71        &self.id
72    }
73
74    /// Inner weave definition (knot ids must not contain `/` either).
75    pub fn inner(&self) -> &WeaveDef {
76        &self.inner
77    }
78
79    /// Input exports the parent must wire after include.
80    pub fn inputs(&self) -> &[PatternExportDef] {
81        &self.inputs
82    }
83
84    /// Output exports available as parent sources.
85    pub fn outputs(&self) -> &[PatternExportDef] {
86        &self.outputs
87    }
88
89    /// Clone into the serializable definition form.
90    pub fn to_def(&self) -> PatternDef {
91        PatternDef {
92            id: self.id.clone(),
93            inner: self.inner.clone(),
94            inputs: self.inputs.clone(),
95            outputs: self.outputs.clone(),
96        }
97    }
98}
99
100impl TryFrom<PatternDef> for Pattern {
101    type Error = ValidationError;
102
103    fn try_from(def: PatternDef) -> Result<Self, Self::Error> {
104        if def.id.is_empty() || def.id.contains('/') {
105            return Err(ValidationError::InvalidPatternId {
106                pattern_id: def.id,
107                reason: "must be non-empty and contain no slash",
108            });
109        }
110        if let Some(knot) = def.inner.knots.iter().find(|knot| knot.id.contains('/')) {
111            return Err(ValidationError::InvalidKnotId {
112                knot_id: knot.id.clone(),
113                reason: "pattern inner knot ids must contain no slash",
114            });
115        }
116        let index: BTreeMap<&str, &KnotDef> =
117            def.inner.knots.iter().map(|k| (k.id.as_str(), k)).collect();
118        let mut names = BTreeSet::new();
119        let mut external = BTreeSet::new();
120        let internally_connected: BTreeSet<(String, String)> = def
121            .inner
122            .threads
123            .iter()
124            .map(|thread| (thread.to.knot.clone(), thread.to.port.clone()))
125            .collect();
126        let mut physical_inputs: BTreeMap<(String, String), String> = BTreeMap::new();
127        for export in &def.inputs {
128            if !names.insert(export.name.as_str()) {
129                return Err(ValidationError::DuplicateExport {
130                    export: export.name.clone(),
131                });
132            }
133            check_export(&index, export, PortDir::In)?;
134            let endpoint = (export.port.knot.clone(), export.port.port.clone());
135            if internally_connected.contains(&endpoint) {
136                return Err(ValidationError::PatternInputAlreadyConnected {
137                    export: export.name.clone(),
138                    knot_id: export.port.knot.clone(),
139                    port: export.port.port.clone(),
140                });
141            }
142            if let Some(first_export) =
143                physical_inputs.insert(endpoint.clone(), export.name.clone())
144            {
145                return Err(ValidationError::DuplicatePatternInput {
146                    knot_id: endpoint.0,
147                    port: endpoint.1,
148                    first_export,
149                    duplicate_export: export.name.clone(),
150                });
151            }
152            external.insert(endpoint);
153        }
154        names.clear();
155        for export in &def.outputs {
156            if !names.insert(export.name.as_str()) {
157                return Err(ValidationError::DuplicateExport {
158                    export: export.name.clone(),
159                });
160            }
161            check_export(&index, export, PortDir::Out)?;
162        }
163        crate::authoring::validate::validate_def_with_external_inputs(&def.inner, &external)?;
164        Ok(Self {
165            id: def.id,
166            inner: def.inner,
167            inputs: def.inputs,
168            outputs: def.outputs,
169        })
170    }
171}
172
173impl From<Pattern> for PatternDef {
174    fn from(pattern: Pattern) -> Self {
175        Self {
176            id: pattern.id,
177            inner: pattern.inner,
178            inputs: pattern.inputs,
179            outputs: pattern.outputs,
180        }
181    }
182}
183
184fn check_export(
185    index: &BTreeMap<&str, &KnotDef>,
186    export: &PatternExportDef,
187    expected: PortDir,
188) -> Result<(), ValidationError> {
189    let knot =
190        index
191            .get(export.port.knot.as_str())
192            .ok_or_else(|| ValidationError::UnknownKnot {
193                knot_id: export.port.knot.clone(),
194            })?;
195    let ports = ports_of(&knot.kind);
196    let info = ports
197        .iter()
198        .find(|p| p.name == export.port.port)
199        .ok_or_else(|| ValidationError::UnknownPort {
200            knot_id: knot.id.clone(),
201            port: export.port.port.clone(),
202            expected: ports.iter().map(|p| String::from(p.name)).collect(),
203        })?;
204    if info.dir != expected {
205        return Err(ValidationError::WrongPortDirection {
206            knot_id: knot.id.clone(),
207            port: export.port.port.clone(),
208            expected,
209            actual: info.dir,
210        });
211    }
212    Ok(())
213}
214
215pub(crate) struct ExpandedPattern {
216    pub knots: Vec<KnotDef>,
217    pub threads: Vec<ThreadDef>,
218    pub inputs: BTreeMap<String, PortRefDef>,
219    pub outputs: BTreeMap<String, PortRefDef>,
220}
221
222pub(crate) fn expand(
223    instance_id: &str,
224    pattern: &Pattern,
225) -> Result<ExpandedPattern, crate::BuildError> {
226    if instance_id.is_empty() || instance_id.contains('/') {
227        return Err(crate::BuildError::InvalidId {
228            id: String::from(instance_id),
229            reason: "pattern instance ids must be non-empty and contain no slash",
230        });
231    }
232    let prefix = format_prefix(instance_id);
233    let knots = pattern
234        .inner
235        .knots
236        .iter()
237        .map(|knot| KnotDef {
238            id: prefixed(&prefix, &knot.id),
239            kind: knot.kind.clone(),
240        })
241        .collect();
242    let threads = pattern
243        .inner
244        .threads
245        .iter()
246        .map(|thread| ThreadDef {
247            from: PortRefDef::new(
248                prefixed(&prefix, &thread.from.knot),
249                thread.from.port.clone(),
250            ),
251            to: PortRefDef::new(prefixed(&prefix, &thread.to.knot), thread.to.port.clone()),
252        })
253        .collect();
254    let inputs = pattern
255        .inputs
256        .iter()
257        .map(|export| {
258            (
259                export.name.clone(),
260                PortRefDef::new(
261                    prefixed(&prefix, &export.port.knot),
262                    export.port.port.clone(),
263                ),
264            )
265        })
266        .collect();
267    let outputs = pattern
268        .outputs
269        .iter()
270        .map(|export| {
271            (
272                export.name.clone(),
273                PortRefDef::new(
274                    prefixed(&prefix, &export.port.knot),
275                    export.port.port.clone(),
276                ),
277            )
278        })
279        .collect();
280    Ok(ExpandedPattern {
281        knots,
282        threads,
283        inputs,
284        outputs,
285    })
286}
287
288fn format_prefix(instance_id: &str) -> String {
289    let mut value = String::from(instance_id);
290    value.push('/');
291    value
292}
293fn prefixed(prefix: &str, knot: &str) -> String {
294    let mut value = String::from(prefix);
295    value.push_str(knot);
296    value
297}