Skip to main content

rustev_core/
builder.rs

1//! The typed Rust builder (R-02). It produces the same
2//! [`Definition`] value the JSON parser produces, and both go through one
3//! compiler, so there is one source of behavior. Operator arguments are the
4//! typed [`OpArgs`] structs; nothing is defaulted: every step, input and
5//! policy item is stated in full.
6
7use rustev_contract::decimal::Decimal;
8use rustev_contract::definition::{
9    Adjustment, Cond, Definition, Expr, FieldDecl, Freshness, Handler, HandlerAction, InputDecl,
10    LimitsDecl, Literal, OutcomeDecl, ParamDecl, ParamSource, PolicyDecl, Predicate,
11    ProvenanceClass, ReasonSet, Rule, SemanticDecl, StepBody, StepDecl, TypeDecl,
12};
13use rustev_contract::definition::{CmpOp, ExactDecl};
14use rustev_contract::schema;
15
16use crate::ops::OpArgs;
17
18/// Builds a definition. `build` requires limits and at least one rule.
19#[derive(Debug, Clone)]
20pub struct DefinitionBuilder {
21    name: String,
22    version: String,
23    package: String,
24    inputs: Vec<InputDecl>,
25    steps: Vec<StepDecl>,
26    policy: PolicyDecl,
27    limits: Option<LimitsDecl>,
28}
29
30/// `build` was called without limits.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct MissingLimits;
33
34impl DefinitionBuilder {
35    pub fn new(name: &str, version: &str, package: &str) -> Self {
36        Self {
37            name: name.into(),
38            version: version.into(),
39            package: package.into(),
40            inputs: vec![],
41            steps: vec![],
42            policy: PolicyDecl {
43                on_unresolved: vec![],
44                rules: vec![],
45                adjustments: vec![],
46            },
47            limits: None,
48        }
49    }
50
51    pub fn input(
52        mut self,
53        name: &str,
54        ty: TypeDecl,
55        provenance: &[ProvenanceClass],
56        freshness: Freshness,
57    ) -> Self {
58        self.inputs.push(InputDecl {
59            name: name.into(),
60            ty,
61            provenance: provenance.to_vec(),
62            freshness,
63            evidence: None,
64            on_disagreement: None,
65        });
66        self
67    }
68
69    pub fn exact(mut self, id: &str, args: OpArgs) -> Self {
70        let (op, version) = args.op();
71        self.steps.push(StepDecl {
72            id: id.into(),
73            body: StepBody::Exact(ExactDecl {
74                op: op.into(),
75                version,
76                args: args.to_json(),
77            }),
78        });
79        self
80    }
81
82    pub fn semantic(mut self, id: &str, decl: SemanticDecl) -> Self {
83        self.steps.push(StepDecl {
84            id: id.into(),
85            body: StepBody::Semantic(decl),
86        });
87        self
88    }
89
90    pub fn on_unresolved(
91        mut self,
92        target: &str,
93        reasons: ReasonSet,
94        action: HandlerAction,
95    ) -> Self {
96        self.policy.on_unresolved.push(Handler {
97            target: target.into(),
98            reasons,
99            action,
100        });
101        self
102    }
103
104    pub fn rule(mut self, id: &str, when: Cond, then: OutcomeDecl) -> Self {
105        self.policy.rules.push(Rule {
106            id: id.into(),
107            when,
108            then,
109        });
110        self
111    }
112
113    pub fn adjustment(mut self, id: &str, when: Cond, param: &str, scale: &[&str]) -> Self {
114        self.policy.adjustments.push(Adjustment {
115            id: id.into(),
116            when,
117            param: param.into(),
118            scale: scale.iter().map(|s| s.to_string()).collect(),
119        });
120        self
121    }
122
123    pub fn limits(mut self, limits: LimitsDecl) -> Self {
124        self.limits = Some(limits);
125        self
126    }
127
128    pub fn build(self) -> Result<Definition, MissingLimits> {
129        Ok(Definition {
130            schema: schema::DEFINITION.into(),
131            name: self.name,
132            version: self.version,
133            package: self.package,
134            inputs: self.inputs,
135            steps: self.steps,
136            policy: self.policy,
137            limits: self.limits.ok_or(MissingLimits)?,
138            actionability: None,
139            screening: None,
140        })
141    }
142}
143
144/// Type helpers.
145pub mod ty {
146    use super::*;
147
148    pub fn text(max_bytes: u64) -> TypeDecl {
149        TypeDecl::Text { max_bytes }
150    }
151    pub fn enumeration(options: &[&str]) -> TypeDecl {
152        TypeDecl::Enum {
153            options: options.iter().map(|s| s.to_string()).collect(),
154        }
155    }
156    pub fn list(item: TypeDecl, max_items: u64) -> TypeDecl {
157        TypeDecl::List {
158            item: Box::new(item),
159            max_items,
160        }
161    }
162    pub fn record(fields: &[(&str, TypeDecl)]) -> TypeDecl {
163        TypeDecl::Record {
164            fields: fields
165                .iter()
166                .map(|(n, t)| FieldDecl {
167                    name: n.to_string(),
168                    ty: t.clone(),
169                })
170                .collect(),
171            present_when: None,
172        }
173    }
174}
175
176/// Expression and predicate helpers.
177pub mod e {
178    use super::*;
179
180    pub fn r(reference: &str) -> Expr {
181        Expr::Ref(reference.into())
182    }
183    pub fn int(v: i64) -> Expr {
184        Expr::Lit(Literal::Integer(v))
185    }
186    /// A decimal literal; panics on an invalid literal, so use it with
187    /// constants only.
188    pub fn dec(v: &str) -> Expr {
189        Expr::Lit(Literal::Decimal(decimal(v)))
190    }
191    pub fn enum_lit(v: &str) -> Expr {
192        Expr::Lit(Literal::Enum(v.into()))
193    }
194    pub fn text(v: &str) -> Expr {
195        Expr::Lit(Literal::Text(v.into()))
196    }
197    pub fn boolean(v: bool) -> Expr {
198        Expr::Lit(Literal::Bool(v))
199    }
200    pub fn duration_ms(v: u64) -> Expr {
201        Expr::Lit(Literal::DurationMs(v))
202    }
203    pub fn sub(a: Expr, b: Expr) -> Expr {
204        Expr::Sub {
205            left: Box::new(a),
206            right: Box::new(b),
207        }
208    }
209    pub fn fx(amount: Expr, from: Expr, to: Expr, rates: &str, scale: u32) -> Expr {
210        Expr::Fx {
211            amount: Box::new(amount),
212            from: Box::new(from),
213            to: Box::new(to),
214            rates: rates.into(),
215            scale,
216        }
217    }
218    pub fn cmp(a: Expr, op: CmpOp, b: Expr) -> Predicate {
219        Predicate::Cmp {
220            left: a,
221            op,
222            right: b,
223        }
224    }
225    pub fn all(ps: Vec<Predicate>) -> Predicate {
226        Predicate::All(ps)
227    }
228    pub fn any(ps: Vec<Predicate>) -> Predicate {
229        Predicate::Any(ps)
230    }
231    pub fn not(p: Predicate) -> Predicate {
232        Predicate::Not(Box::new(p))
233    }
234    /// A decimal constant; panics on an invalid literal.
235    pub fn decimal(v: &str) -> Decimal {
236        Decimal::parse(v).expect("decimal literal")
237    }
238}
239
240/// Outcome helpers.
241pub mod out {
242    use super::*;
243
244    pub fn propose(action: &str, params: &[(&str, ParamSource)]) -> OutcomeDecl {
245        OutcomeDecl::Propose {
246            action: action.into(),
247            params: params
248                .iter()
249                .map(|(n, v)| ParamDecl {
250                    name: n.to_string(),
251                    value: v.clone(),
252                })
253                .collect(),
254        }
255    }
256    pub fn lit_enum(v: &str) -> ParamSource {
257        ParamSource::Lit(Literal::Enum(v.into()))
258    }
259    pub fn from(reference: &str) -> ParamSource {
260        ParamSource::Ref(reference.into())
261    }
262    pub fn escalate(reason: &str) -> OutcomeDecl {
263        OutcomeDecl::Escalate {
264            reason: reason.into(),
265        }
266    }
267}
268
269/// Adds spec 034 declarations and reference sites to a typed definition builder.
270/// `build` validates stages 1 and 2 using the same path as parsed JSON.
271#[cfg(feature = "parameterized")]
272#[derive(Debug, Clone)]
273pub struct TemplateBuilder {
274    document: serde_json::Value,
275    error: Option<String>,
276}
277#[cfg(feature = "parameterized")]
278impl TemplateBuilder {
279    pub fn new(builder: DefinitionBuilder) -> Result<Self, MissingLimits> {
280        Ok(Self::from_definition(builder.build()?))
281    }
282    pub fn from_definition(definition: Definition) -> Self {
283        let mut document = serde_json::to_value(definition).expect("definition is serializable");
284        document["schema"] = serde_json::json!(schema::DEFINITION_V2);
285        Self {
286            document,
287            error: None,
288        }
289    }
290    pub fn parameter(mut self, declaration: rustev_contract::parameter::ParameterDecl) -> Self {
291        let array = self
292            .document
293            .as_object_mut()
294            .expect("definition object")
295            .entry("parameters")
296            .or_insert_with(|| serde_json::json!([]));
297        array
298            .as_array_mut()
299            .expect("parameter array")
300            .push(serde_json::json!(declaration));
301        self
302    }
303    pub fn variants(mut self, names: &[&str]) -> Self {
304        self.document["variants"] = serde_json::json!(names);
305        self
306    }
307    pub fn only_in(self, item_pointer: &str, names: &[&str]) -> Self {
308        self.item_member(item_pointer, "only_in", serde_json::json!(names))
309    }
310    pub fn enabled(self, item_pointer: &str, enabled: rustev_contract::parameter::Enabled) -> Self {
311        self.item_member(item_pointer, "enabled", serde_json::json!(enabled))
312    }
313    fn item_member(mut self, pointer: &str, key: &str, value: serde_json::Value) -> Self {
314        if let Some(item) = self
315            .document
316            .pointer_mut(pointer)
317            .and_then(serde_json::Value::as_object_mut)
318        {
319            item.insert(key.into(), value);
320        } else if self.error.is_none() {
321            self.error = Some(format!("no item at {pointer}"));
322        }
323        self
324    }
325    /// Replace an existing JSON Pointer site with a parameter reference.
326    pub fn reference(mut self, pointer: &str, parameter: &str) -> Self {
327        if let Some(site) = self.document.pointer_mut(pointer) {
328            *site = serde_json::json!({"param": parameter});
329        } else if self.error.is_none() {
330            self.error = Some(format!("no reference site at {pointer}"));
331        }
332        self
333    }
334    pub fn build(self) -> Result<Vec<u8>, crate::instantiate::InstantiationRefusal> {
335        use crate::instantiate::{
336            InstantiationCode, InstantiationRefusal, check_declarations, parse_template,
337        };
338        let invalid = |detail: String| InstantiationRefusal {
339            stage: 1,
340            code: InstantiationCode::InvalidDefinitionDocument,
341            subject: format!(
342                "definition:{}@{}",
343                self.document["name"].as_str().unwrap_or("?"),
344                self.document["version"].as_str().unwrap_or("?")
345            ),
346            detail,
347        };
348        if let Some(e) = self.error {
349            return Err(invalid(e));
350        }
351        let bytes = rustev_contract::canonical::canonical_bytes(&self.document)
352            .map_err(|e| invalid(e.to_string()))?;
353        let template = parse_template(&bytes)?;
354        check_declarations(&template)?;
355        Ok(template.canonical)
356    }
357}