1use wasm_bindgen::prelude::*;
9use serde::{Deserialize, Serialize};
10use json_atomic::canonize;
11use std::collections::BTreeMap;
12
13#[derive(Debug, Clone, Deserialize, Serialize)]
18pub struct ChipInput {
19 pub did: String,
20 pub cid: String,
21 pub logic: LogicExpr,
22}
23
24#[derive(Debug, Clone, Deserialize, Serialize)]
31#[serde(untagged)]
32pub enum LogicExpr {
33 Bool(bool),
34 OpMap(BTreeMap<String, serde_json::Value>), }
36
37#[derive(Debug, Clone)]
38pub enum Op {
39 Gte([Operand; 2]),
40 Lte([Operand; 2]),
41 Gt([Operand; 2]),
42 Lt([Operand; 2]),
43 Eq([Operand; 2]),
44 And(Vec<LogicExprStrict>),
45 Or(Vec<LogicExprStrict>),
46 Not(Box<LogicExprStrict>),
47}
48
49#[derive(Debug, Clone)]
50pub enum LogicExprStrict {
51 Bool(bool),
52 Op(Op),
53}
54
55#[derive(Debug, Clone, Deserialize, Serialize)]
56#[serde(untagged)]
57pub enum Operand {
58 LiteralNumber(f64),
59 LiteralBool(bool),
60 LiteralString(String),
61 Ref(RefOperand),
62}
63
64#[derive(Debug, Clone, Deserialize, Serialize)]
65pub struct RefOperand {
66 #[serde(rename = "$ref")]
67 pub path: String,
68}
69
70#[derive(Debug, Clone, Deserialize, Serialize)]
72pub struct Context {
73 #[serde(flatten)]
74 pub fields: BTreeMap<String, ContextValue>,
75}
76
77#[derive(Debug, Clone, Deserialize, Serialize)]
78#[serde(untagged)]
79pub enum ContextValue {
80 Null,
81 Bool(bool),
82 Number(f64),
83 String(String),
84 Array(Vec<ContextValue>),
85 Object(BTreeMap<String, ContextValue>),
86}
87
88#[derive(Debug, Clone, Serialize)]
89pub struct DecisionOutput {
90 pub reason: String,
91 pub result: bool,
92}
93
94#[derive(Debug, Clone, Serialize)]
95pub struct ChipResult {
96 pub chip_cid: String,
97 pub decision_cid: String,
98 pub did: String,
99 pub reason: String,
100 pub result: bool,
101}
102
103#[wasm_bindgen]
108pub fn blake3_hex(data: &[u8]) -> String {
109 blake3::hash(data).to_hex().to_string()
110}
111
112#[wasm_bindgen]
113pub fn canonicalize_context(ctx_json: &str) -> Result<String, JsValue> {
114 let ctx: Context = serde_json::from_str(ctx_json)
116 .map_err(|e| JsValue::from_str(&format!("Parse error: {}", e)))?;
117
118 let bytes = canonize(&ctx.fields)
120 .map_err(|e| JsValue::from_str(&format!("Canonize error: {}", e)))?;
121
122 String::from_utf8(bytes)
123 .map_err(|e| JsValue::from_str(&format!("UTF-8 error: {}", e)))
124}
125
126#[wasm_bindgen]
127pub fn decision_cid(
128 did: &str,
129 chip_cid: &str,
130 ctx_json: &str,
131 result: bool,
132 reason: &str,
133) -> Result<String, JsValue> {
134 let ctx: Context = serde_json::from_str(ctx_json)
135 .map_err(|e| JsValue::from_str(&format!("Parse error: {}", e)))?;
136
137 let ctx_canon = canonize(&ctx.fields)
138 .map_err(|e| JsValue::from_str(&format!("Canonize error: {}", e)))?;
139
140 let out = DecisionOutput {
141 reason: reason.to_string(),
142 result,
143 };
144
145 let out_canon = canonize(&out)
146 .map_err(|e| JsValue::from_str(&format!("Canonize error: {}", e)))?;
147
148 let mut preimage = Vec::new();
150 preimage.extend_from_slice(b"UBL_DECISION_V1\0");
151 preimage.extend_from_slice(did.as_bytes());
152 preimage.push(0);
153 preimage.extend_from_slice(chip_cid.as_bytes());
154 preimage.push(0);
155 preimage.extend_from_slice(&ctx_canon);
156 preimage.push(0);
157 preimage.extend_from_slice(&out_canon);
158
159 Ok(blake3_hex(&preimage))
160}
161
162#[wasm_bindgen]
163pub fn evaluate_chip(chip_json: &str, ctx_json: &str) -> Result<String, JsValue> {
164 let chip: ChipInput = serde_json::from_str(chip_json)
166 .map_err(|e| JsValue::from_str(&format!("Parse chip error: {}", e)))?;
167
168 let ctx: Context = serde_json::from_str(ctx_json)
170 .map_err(|e| JsValue::from_str(&format!("Parse ctx error: {}", e)))?;
171
172 let strict = to_strict(&chip.logic)
174 .map_err(|e| JsValue::from_str(&format!("Logic parse error: {}", e)))?;
175
176 let (result, reason) = eval_logic(&strict, &ctx);
177
178 let cid = decision_cid(&chip.did, &chip.cid, ctx_json, result, &reason)?;
179
180 let out = ChipResult {
181 chip_cid: chip.cid.clone(),
182 decision_cid: cid,
183 did: chip.did.clone(),
184 reason,
185 result,
186 };
187
188 let bytes = canonize(&out)
189 .map_err(|e| JsValue::from_str(&format!("Canonize error: {}", e)))?;
190
191 String::from_utf8(bytes)
192 .map_err(|e| JsValue::from_str(&format!("UTF-8 error: {}", e)))
193}
194
195fn to_strict(expr: &LogicExpr) -> Result<LogicExprStrict, String> {
200 match expr {
201 LogicExpr::Bool(b) => Ok(LogicExprStrict::Bool(*b)),
202 LogicExpr::OpMap(map) => {
203 if map.len() != 1 {
204 return Err("Logic op map must have exactly 1 key".to_string());
205 }
206 let (k, v) = map.iter().next().unwrap();
207 let op = parse_op(k, v)?;
208 Ok(LogicExprStrict::Op(op))
209 }
210 }
211}
212
213fn parse_op(op: &str, v: &serde_json::Value) -> Result<Op, String> {
214 match op {
215 ">=" => Ok(Op::Gte(parse_two_operands(v)?)),
216 "<=" => Ok(Op::Lte(parse_two_operands(v)?)),
217 ">" => Ok(Op::Gt(parse_two_operands(v)?)),
218 "<" => Ok(Op::Lt(parse_two_operands(v)?)),
219 "==" => Ok(Op::Eq(parse_two_operands(v)?)),
220 "and" => {
221 let arr = v.as_array().ok_or("and expects array")?;
222 let mut items = Vec::new();
223 for item in arr {
224 let e: LogicExpr = serde_json::from_value(item.clone()).map_err(|e| e.to_string())?;
225 items.push(to_strict(&e)?);
226 }
227 Ok(Op::And(items))
228 }
229 "or" => {
230 let arr = v.as_array().ok_or("or expects array")?;
231 let mut items = Vec::new();
232 for item in arr {
233 let e: LogicExpr = serde_json::from_value(item.clone()).map_err(|e| e.to_string())?;
234 items.push(to_strict(&e)?);
235 }
236 Ok(Op::Or(items))
237 }
238 "not" => {
239 let e: LogicExpr = serde_json::from_value(v.clone()).map_err(|e| e.to_string())?;
240 Ok(Op::Not(Box::new(to_strict(&e)?)))
241 }
242 _ => Err(format!("Unknown operator: {}", op)),
243 }
244}
245
246fn parse_two_operands(v: &serde_json::Value) -> Result<[Operand; 2], String> {
247 let arr = v.as_array().ok_or("comparison expects array")?;
248 if arr.len() != 2 {
249 return Err("comparison expects exactly 2 operands".to_string());
250 }
251 let a: Operand = serde_json::from_value(arr[0].clone()).map_err(|e| e.to_string())?;
252 let b: Operand = serde_json::from_value(arr[1].clone()).map_err(|e| e.to_string())?;
253 Ok([a, b])
254}
255
256fn eval_logic(expr: &LogicExprStrict, ctx: &Context) -> (bool, String) {
261 match expr {
262 LogicExprStrict::Bool(b) => (*b, format!("{}", b)),
263 LogicExprStrict::Op(op) => match op {
264 Op::Gte([a,b]) => eval_cmp(a,b,ctx,">=", |x,y| x>=y),
265 Op::Lte([a,b]) => eval_cmp(a,b,ctx,"<=", |x,y| x<=y),
266 Op::Gt([a,b]) => eval_cmp(a,b,ctx,">", |x,y| x>y),
267 Op::Lt([a,b]) => eval_cmp(a,b,ctx,"<", |x,y| x<y),
268 Op::Eq([a,b]) => eval_eq(a,b,ctx),
269 Op::And(items) => {
270 let mut reasons = Vec::new();
271 for it in items {
272 let (r, rs) = eval_logic(it, ctx);
273 reasons.push(rs);
274 if !r {
275 return (false, format!("and(false): {}", reasons.join(" ∧ ")));
276 }
277 }
278 (true, format!("and(true): {}", reasons.join(" ∧ ")))
279 }
280 Op::Or(items) => {
281 let mut reasons = Vec::new();
282 for it in items {
283 let (r, rs) = eval_logic(it, ctx);
284 reasons.push(rs);
285 if r {
286 return (true, format!("or(true): {}", reasons.join(" ∨ ")));
287 }
288 }
289 (false, format!("or(false): {}", reasons.join(" ∨ ")))
290 }
291 Op::Not(inner) => {
292 let (r, rs) = eval_logic(inner, ctx);
293 (!r, format!("not({})", rs))
294 }
295 }
296 }
297}
298
299fn eval_cmp<F>(a: &Operand, b: &Operand, ctx: &Context, op: &str, f: F) -> (bool, String)
300where
301 F: Fn(f64, f64) -> bool,
302{
303 let la = resolve_number(a, ctx);
304 let lb = resolve_number(b, ctx);
305 match (la, lb) {
306 (Some(x), Some(y)) => {
307 let r = f(x, y);
308 (r, format!("{} {} {} = {}", x, op, y, r))
309 }
310 _ => (false, format!("invalid {} operands", op)),
311 }
312}
313
314fn eval_eq(a: &Operand, b: &Operand, ctx: &Context) -> (bool, String) {
315 let ra = resolve_any(a, ctx);
316 let rb = resolve_any(b, ctx);
317 let r = ra == rb;
318 (r, format!("{:?} == {:?} = {}", ra, rb, r))
319}
320
321#[derive(Debug, Clone, PartialEq)]
322enum AnyVal {
323 Null,
324 Bool(bool),
325 Num(f64),
326 Str(String),
327}
328
329fn resolve_any(op: &Operand, ctx: &Context) -> AnyVal {
330 match op {
331 Operand::LiteralNumber(n) => AnyVal::Num(*n),
332 Operand::LiteralBool(b) => AnyVal::Bool(*b),
333 Operand::LiteralString(s) => AnyVal::Str(s.clone()),
334 Operand::Ref(r) => {
335 match get_ctx_value(ctx, &r.path) {
336 Some(ContextValue::Null) => AnyVal::Null,
337 Some(ContextValue::Bool(b)) => AnyVal::Bool(*b),
338 Some(ContextValue::Number(n)) => AnyVal::Num(*n),
339 Some(ContextValue::String(s)) => AnyVal::Str(s.clone()),
340 _ => AnyVal::Null,
341 }
342 }
343 }
344}
345
346fn resolve_number(op: &Operand, ctx: &Context) -> Option<f64> {
347 match op {
348 Operand::LiteralNumber(n) => Some(*n),
349 Operand::Ref(r) => match get_ctx_value(ctx, &r.path) {
350 Some(ContextValue::Number(n)) => Some(*n),
351 _ => None,
352 },
353 _ => None,
354 }
355}
356
357fn get_ctx_value<'a>(ctx: &'a Context, path: &str) -> Option<&'a ContextValue> {
358 let parts: Vec<&str> = path.split('.').collect();
359 let mut cur = &ctx.fields;
360
361 for (i, part) in parts.iter().enumerate() {
362 let v = cur.get(*part)?;
363 if i == parts.len() - 1 {
364 return Some(v);
365 }
366 match v {
367 ContextValue::Object(obj) => cur = obj,
368 _ => return None,
369 }
370 }
371 None
372}