Skip to main content

pflow_dsl/
builder.rs

1//! Fluent builder API for token model schemas via DSL.
2
3use pflow_tokenmodel::Schema;
4
5use crate::ast::*;
6use crate::interpret;
7use crate::sexpr;
8
9/// Fluent builder for constructing token model schemas.
10pub struct Builder {
11    node: SchemaNode,
12    current_state: Option<usize>,
13    current_action: Option<usize>,
14    current_arc: Option<usize>,
15}
16
17impl Builder {
18    /// Creates a new schema builder with the given name.
19    pub fn new(name: &str) -> Self {
20        Self {
21            node: SchemaNode {
22                name: name.into(),
23                version: "v1.0.0".into(),
24                states: Vec::new(),
25                actions: Vec::new(),
26                arcs: Vec::new(),
27                constraints: Vec::new(),
28            },
29            current_state: None,
30            current_action: None,
31            current_arc: None,
32        }
33    }
34
35    /// Sets the schema version.
36    pub fn version(mut self, v: &str) -> Self {
37        self.node.version = v.into();
38        self
39    }
40
41    /// Adds a data state.
42    pub fn data(mut self, id: &str, typ: &str) -> Self {
43        self.clear_current();
44        let idx = self.node.states.len();
45        self.node.states.push(StateNode {
46            id: id.into(),
47            typ: typ.into(),
48            kind: "data".into(),
49            initial: None,
50            exported: false,
51        });
52        self.current_state = Some(idx);
53        self
54    }
55
56    /// Adds a token-counting state.
57    pub fn token(mut self, id: &str, initial: Option<i64>) -> Self {
58        self.clear_current();
59        let idx = self.node.states.len();
60        self.node.states.push(StateNode {
61            id: id.into(),
62            typ: "int".into(),
63            kind: "token".into(),
64            initial: initial.map(InitialValue::Int),
65            exported: false,
66        });
67        self.current_state = Some(idx);
68        self
69    }
70
71    /// Marks the current state as exported.
72    pub fn exported(mut self) -> Self {
73        if let Some(idx) = self.current_state {
74            self.node.states[idx].exported = true;
75        }
76        self
77    }
78
79    /// Sets the initial value for the current state.
80    pub fn initial(mut self, value: i64) -> Self {
81        if let Some(idx) = self.current_state {
82            self.node.states[idx].initial = Some(InitialValue::Int(value));
83        }
84        self
85    }
86
87    /// Adds an action.
88    pub fn action(mut self, id: &str) -> Self {
89        self.clear_current();
90        let idx = self.node.actions.len();
91        self.node.actions.push(ActionNode {
92            id: id.into(),
93            guard: String::new(),
94        });
95        self.current_action = Some(idx);
96        self
97    }
98
99    /// Sets the guard expression for the current action.
100    pub fn guard(mut self, expr: &str) -> Self {
101        if let Some(idx) = self.current_action {
102            self.node.actions[idx].guard = expr.into();
103        }
104        self
105    }
106
107    /// Adds an arc from source to target.
108    pub fn flow(mut self, source: &str, target: &str) -> Self {
109        self.clear_current();
110        let idx = self.node.arcs.len();
111        self.node.arcs.push(ArcNode {
112            source: source.into(),
113            target: target.into(),
114            keys: Vec::new(),
115            value: String::new(),
116        });
117        self.current_arc = Some(idx);
118        self
119    }
120
121    /// Alias for flow.
122    pub fn arc(self, source: &str, target: &str) -> Self {
123        self.flow(source, target)
124    }
125
126    /// Sets the map access keys for the current arc.
127    pub fn keys(mut self, keys: &[&str]) -> Self {
128        if let Some(idx) = self.current_arc {
129            self.node.arcs[idx].keys = keys.iter().map(|s| s.to_string()).collect();
130        }
131        self
132    }
133
134    /// Sets the value binding name for the current arc.
135    pub fn value(mut self, v: &str) -> Self {
136        if let Some(idx) = self.current_arc {
137            self.node.arcs[idx].value = v.into();
138        }
139        self
140    }
141
142    /// Adds a constraint.
143    pub fn constraint(mut self, id: &str, expr: &str) -> Self {
144        self.clear_current();
145        self.node.constraints.push(ConstraintNode {
146            id: id.into(),
147            expr: expr.into(),
148        });
149        self
150    }
151
152    fn clear_current(&mut self) {
153        self.current_state = None;
154        self.current_action = None;
155        self.current_arc = None;
156    }
157
158    /// Returns the underlying AST node.
159    pub fn ast(&self) -> &SchemaNode {
160        &self.node
161    }
162
163    /// Builds and returns the tokenmodel Schema.
164    pub fn schema(self) -> Result<Schema, String> {
165        interpret::interpret(&self.node)
166    }
167
168    /// Builds and returns the tokenmodel Schema. Panics on error.
169    pub fn must_schema(self) -> Schema {
170        self.schema().expect("schema validation failed")
171    }
172
173    /// Generates the S-expression DSL representation.
174    pub fn to_string(&self) -> String {
175        sexpr::to_sexpr(&self.node)
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_builder_basic() {
185        let schema = Builder::new("ERC-020")
186            .data("balances", "map[address]uint256")
187            .exported()
188            .data("totalSupply", "uint256")
189            .action("transfer")
190            .guard("balances[from] >= amount")
191            .flow("balances", "transfer")
192            .keys(&["from"])
193            .flow("transfer", "balances")
194            .keys(&["to"])
195            .constraint("conservation", "sum(balances) == totalSupply")
196            .must_schema();
197
198        assert_eq!(schema.name, "ERC-020");
199        assert_eq!(schema.states.len(), 2);
200        assert_eq!(schema.actions.len(), 1);
201        assert_eq!(schema.arcs.len(), 2);
202        assert_eq!(schema.constraints.len(), 1);
203        assert!(schema.states[0].exported);
204    }
205
206    #[test]
207    fn test_builder_token() {
208        let schema = Builder::new("counter")
209            .token("count", Some(5))
210            .action("inc")
211            .flow("inc", "count")
212            .must_schema();
213
214        assert_eq!(schema.states[0].initial_tokens(), 5);
215        assert!(schema.states[0].is_token());
216    }
217
218    #[test]
219    fn test_builder_to_string() {
220        let b = Builder::new("test")
221            .data("balances", "map[address]uint256")
222            .action("transfer")
223            .flow("balances", "transfer")
224            .keys(&["from"]);
225
226        let sexpr = b.to_string();
227        assert!(sexpr.contains("schema test"));
228        assert!(sexpr.contains("balances"));
229        assert!(sexpr.contains("transfer"));
230    }
231}