Skip to main content

frp_weave/
template.rs

1//! Serialisable block templates that can be converted to [`Archetype`]s.
2
3use serde::{Deserialize, Serialize};
4
5use frp_domain::{AtomKind, BlockSchema, Port, port::PortDirection};
6use frp_plexus::{BlockId, PortId, TypeSig};
7
8use crate::archetype::Archetype;
9use crate::error::WeaveError;
10
11/// A serialisable description of a single port in a template.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct PortDef {
14    /// Port name.
15    pub name: String,
16    /// `"input"` or `"output"`.
17    pub direction: String,
18    /// Human-readable type signature string (e.g. `"Any"`, `"Int"`, `"Bool"`).
19    pub type_sig: String,
20}
21
22impl PortDef {
23    /// Parse direction + type_sig strings into a [`Port`].
24    ///
25    /// Returns `WeaveError::TemplateError` for unknown direction / type values.
26    pub fn to_port(&self, id: PortId) -> Result<Port, WeaveError> {
27        let type_sig = parse_type_sig(&self.type_sig)?;
28        match self.direction.to_lowercase().as_str() {
29            "input" => Ok(Port::new_input(id, self.name.clone(), type_sig)),
30            "output" => Ok(Port::new_output(id, self.name.clone(), type_sig)),
31            other => Err(WeaveError::TemplateError(format!(
32                "unknown direction: '{other}'"
33            ))),
34        }
35    }
36}
37
38/// A serialisable template that can be turned into an [`Archetype`] or used to
39/// instantiate a [`frp_domain::Block`] directly.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct BlockTemplate {
42    /// Archetype name.
43    pub name: String,
44    /// Version.
45    pub version: u32,
46    /// Port definitions.
47    pub ports: Vec<PortDef>,
48    /// Required atom kind names (e.g. `["Transform", "Source"]`).
49    pub required_atoms: Vec<String>,
50}
51
52impl BlockTemplate {
53    /// Convert the template to an [`Archetype`].
54    ///
55    /// Port ids are assigned sequentially starting from 1.
56    pub fn to_archetype(&self) -> Result<Archetype, WeaveError> {
57        let mut inputs = Vec::new();
58        let mut outputs = Vec::new();
59
60        for (i, def) in self.ports.iter().enumerate() {
61            let port = def.to_port(PortId::new((i + 1) as u64))?;
62            match port.direction {
63                PortDirection::Input => inputs.push(port),
64                PortDirection::Output => outputs.push(port),
65            }
66        }
67
68        let schema = BlockSchema::new(inputs, outputs);
69        schema
70            .validate()
71            .map_err(|e| WeaveError::ValidationFailed(e.to_string()))?;
72
73        let required = self
74            .required_atoms
75            .iter()
76            .map(|s| parse_atom_kind(s))
77            .collect::<Result<Vec<_>, _>>()?;
78
79        Ok(Archetype::new(
80            self.name.clone(),
81            self.version,
82            schema,
83            required,
84        ))
85    }
86
87    /// Shorthand — convert to archetype and immediately instantiate a block.
88    pub fn instantiate(&self, block_id: BlockId) -> Result<frp_domain::Block, WeaveError> {
89        self.to_archetype()?.instantiate(block_id)
90    }
91}
92
93// ── helpers ──────────────────────────────────────────────────────────────────
94
95fn parse_type_sig(s: &str) -> Result<TypeSig, WeaveError> {
96    match s.to_lowercase().as_str() {
97        "any" => Ok(TypeSig::Any),
98        "null" => Ok(TypeSig::Null),
99        "bool" => Ok(TypeSig::Bool),
100        "int" => Ok(TypeSig::Int),
101        "float" => Ok(TypeSig::Float),
102        "string" | "str" => Ok(TypeSig::String),
103        "bytes" => Ok(TypeSig::Bytes),
104        other => Err(WeaveError::TemplateError(format!(
105            "unknown type sig: '{other}'"
106        ))),
107    }
108}
109
110fn parse_atom_kind(s: &str) -> Result<AtomKind, WeaveError> {
111    match s.to_lowercase().as_str() {
112        "source" => Ok(AtomKind::Source),
113        "sink" => Ok(AtomKind::Sink),
114        "transform" => Ok(AtomKind::Transform),
115        "state" => Ok(AtomKind::State),
116        "trigger" => Ok(AtomKind::Trigger),
117        other => Err(WeaveError::TemplateError(format!(
118            "unknown atom kind: '{other}'"
119        ))),
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use frp_plexus::BlockId;
127
128    fn basic_template() -> BlockTemplate {
129        BlockTemplate {
130            name: "my_block".to_string(),
131            version: 1,
132            ports: vec![
133                PortDef {
134                    name: "in".to_string(),
135                    direction: "input".to_string(),
136                    type_sig: "Any".to_string(),
137                },
138                PortDef {
139                    name: "out".to_string(),
140                    direction: "output".to_string(),
141                    type_sig: "Int".to_string(),
142                },
143            ],
144            required_atoms: vec!["Transform".to_string()],
145        }
146    }
147
148    #[test]
149    fn to_archetype_success() {
150        let arch = basic_template().to_archetype().unwrap();
151        assert_eq!(arch.id, "my_block");
152        assert_eq!(arch.version, 1);
153        assert_eq!(arch.required_atoms, vec![AtomKind::Transform]);
154    }
155
156    #[test]
157    fn instantiate_success() {
158        let block = basic_template().instantiate(BlockId::new(5)).unwrap();
159        assert_eq!(block.id, BlockId::new(5));
160    }
161
162    #[test]
163    fn unknown_direction_fails() {
164        let mut t = basic_template();
165        t.ports[0].direction = "sideways".to_string();
166        let err = t.to_archetype().unwrap_err();
167        assert!(matches!(err, WeaveError::TemplateError(_)));
168    }
169
170    #[test]
171    fn unknown_type_sig_fails() {
172        let mut t = basic_template();
173        t.ports[0].type_sig = "Turbo".to_string();
174        let err = t.to_archetype().unwrap_err();
175        assert!(matches!(err, WeaveError::TemplateError(_)));
176    }
177
178    #[test]
179    fn unknown_atom_kind_fails() {
180        let mut t = basic_template();
181        t.required_atoms = vec!["Gadget".to_string()];
182        let err = t.to_archetype().unwrap_err();
183        assert!(matches!(err, WeaveError::TemplateError(_)));
184    }
185
186    #[test]
187    fn round_trip_serde() {
188        let t = basic_template();
189        let json = serde_json::to_string(&t).unwrap();
190        let back: BlockTemplate = serde_json::from_str(&json).unwrap();
191        assert_eq!(back.name, t.name);
192        assert_eq!(back.ports.len(), t.ports.len());
193    }
194}