1use std::collections::HashSet;
2
3use frp_plexus::{AtomId, BlockId};
4use serde::{Deserialize, Serialize};
5
6use crate::error::DomainError;
7use crate::meta::Meta;
8use crate::port::{Port, PortDirection};
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct BlockSchema {
18 pub inputs: Vec<Port>,
19 pub outputs: Vec<Port>,
20}
21
22impl BlockSchema {
23 pub fn new(inputs: Vec<Port>, outputs: Vec<Port>) -> Self {
25 Self { inputs, outputs }
26 }
27
28 pub fn validate(&self) -> Result<(), DomainError> {
34 let mut seen_inputs = HashSet::new();
35 for p in &self.inputs {
36 if p.direction != PortDirection::Input {
37 return Err(DomainError::InvalidSchema(format!(
38 "port '{}' listed as input but has direction {}",
39 p.name, p.direction
40 )));
41 }
42 if !seen_inputs.insert(&p.name) {
43 return Err(DomainError::DuplicatePort(p.name.clone()));
44 }
45 }
46
47 let mut seen_outputs = HashSet::new();
48 for p in &self.outputs {
49 if p.direction != PortDirection::Output {
50 return Err(DomainError::InvalidSchema(format!(
51 "port '{}' listed as output but has direction {}",
52 p.name, p.direction
53 )));
54 }
55 if !seen_outputs.insert(&p.name) {
56 return Err(DomainError::DuplicatePort(p.name.clone()));
57 }
58 }
59
60 Ok(())
61 }
62
63 pub fn find_input(&self, name: &str) -> Option<&Port> {
65 self.inputs.iter().find(|p| p.name == name)
66 }
67
68 pub fn find_output(&self, name: &str) -> Option<&Port> {
70 self.outputs.iter().find(|p| p.name == name)
71 }
72
73 pub fn find_port(&self, name: &str) -> Option<&Port> {
75 self.find_input(name).or_else(|| self.find_output(name))
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct Block {
87 pub id: BlockId,
88 pub schema: BlockSchema,
89 pub atoms: Vec<AtomId>,
91 pub meta: Meta,
92}
93
94impl Block {
95 pub fn new(id: BlockId, schema: BlockSchema, atoms: Vec<AtomId>, meta: Meta) -> Self {
97 Self { id, schema, atoms, meta }
98 }
99}
100
101impl frp_loom::memory::HasBlockId for Block {
106 fn block_id(&self) -> BlockId {
107 self.id
108 }
109}
110
111#[derive(Default)]
117pub struct BlockBuilder {
118 id: Option<BlockId>,
119 schema: Option<BlockSchema>,
120 atoms: Vec<AtomId>,
121 meta: Meta,
122}
123
124impl BlockBuilder {
125 pub fn new() -> Self {
126 Self::default()
127 }
128
129 pub fn id(mut self, id: BlockId) -> Self {
130 self.id = Some(id);
131 self
132 }
133
134 pub fn schema(mut self, schema: BlockSchema) -> Self {
135 self.schema = Some(schema);
136 self
137 }
138
139 pub fn atom(mut self, id: AtomId) -> Self {
140 self.atoms.push(id);
141 self
142 }
143
144 pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
145 self.meta = self.meta.with_label(key, value);
146 self
147 }
148
149 pub fn build(self) -> Result<Block, DomainError> {
151 let id = self.id.ok_or_else(|| DomainError::MissingField("id".into()))?;
152 let schema = self.schema.ok_or_else(|| DomainError::MissingField("schema".into()))?;
153 schema.validate()?;
154 Ok(Block::new(id, schema, self.atoms, self.meta))
155 }
156}
157
158#[cfg(test)]
163mod tests {
164 use frp_plexus::{IdGen, TypeSig};
165
166 use super::*;
167 use crate::port::Port;
168
169 fn make_schema(ids: &IdGen) -> BlockSchema {
170 BlockSchema::new(
171 vec![Port::new_input(ids.next_port_id(), "x", TypeSig::Int)],
172 vec![Port::new_output(ids.next_port_id(), "y", TypeSig::Int)],
173 )
174 }
175
176 #[test]
177 fn schema_validate_passes() {
178 let ids = IdGen::new();
179 assert!(make_schema(&ids).validate().is_ok());
180 }
181
182 #[test]
183 fn schema_duplicate_input_name_fails() {
184 let ids = IdGen::new();
185 let schema = BlockSchema::new(
186 vec![
187 Port::new_input(ids.next_port_id(), "x", TypeSig::Int),
188 Port::new_input(ids.next_port_id(), "x", TypeSig::Float),
189 ],
190 vec![],
191 );
192 assert!(matches!(schema.validate(), Err(DomainError::DuplicatePort(_))));
193 }
194
195 #[test]
196 fn block_builder_builds_successfully() {
197 let ids = IdGen::new();
198 let block = BlockBuilder::new()
199 .id(ids.next_block_id())
200 .schema(make_schema(&ids))
201 .label("env", "test")
202 .build()
203 .unwrap();
204 assert_eq!(block.meta.labels["env"], "test");
205 }
206
207 #[test]
208 fn block_builder_missing_id_fails() {
209 let ids = IdGen::new();
210 let err = BlockBuilder::new().schema(make_schema(&ids)).build().unwrap_err();
211 assert!(matches!(err, DomainError::MissingField(_)));
212 }
213
214 #[test]
215 fn schema_find_port() {
216 let ids = IdGen::new();
217 let schema = make_schema(&ids);
218 assert!(schema.find_input("x").is_some());
219 assert!(schema.find_output("y").is_some());
220 assert!(schema.find_port("x").is_some());
221 assert!(schema.find_port("z").is_none());
222 }
223}