1use sim_kernel::{Error, Expr, NumberLiteral, Result, Symbol};
2use sim_value::build::entry;
3
4#[derive(Clone, Debug, PartialEq, Eq)]
6pub struct BridgeScore {
7 pub axis: Symbol,
9 pub value: i64,
11 pub reason: String,
13}
14
15impl BridgeScore {
16 pub fn new(axis: Symbol, value: i64, reason: impl Into<String>) -> Self {
18 Self {
19 axis,
20 value,
21 reason: reason.into(),
22 }
23 }
24
25 pub fn from_expr(expr: &Expr) -> Result<Self> {
27 let fields = map_fields(expr, "bridge/Score")?;
28 reject_unknown(fields, &["axis", "value", "reason"])?;
29 Ok(Self::new(
30 required_symbol(fields, "axis")?.clone(),
31 required_i64(fields, "value")?,
32 required_string(fields, "reason")?.to_owned(),
33 ))
34 }
35
36 pub fn to_expr(&self) -> Expr {
38 Expr::Map(vec![
39 entry("axis", Expr::Symbol(self.axis.clone())),
40 entry("value", i64_expr(self.value)),
41 entry("reason", Expr::String(self.reason.clone())),
42 ])
43 }
44}
45
46#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct BridgeReviewPayload {
49 pub target: String,
51 pub body: String,
53}
54
55impl BridgeReviewPayload {
56 pub fn new(target: impl Into<String>, body: impl Into<String>) -> Self {
58 Self {
59 target: target.into(),
60 body: body.into(),
61 }
62 }
63
64 pub fn from_expr(expr: &Expr) -> Result<Self> {
66 let fields = map_fields(expr, "bridge/Review")?;
67 reject_unknown(fields, &["target", "body"])?;
68 Ok(Self::new(
69 required_string(fields, "target")?,
70 required_string(fields, "body")?,
71 ))
72 }
73
74 pub fn to_expr(&self) -> Expr {
76 Expr::Map(vec![
77 entry("target", Expr::String(self.target.clone())),
78 entry("body", Expr::String(self.body.clone())),
79 ])
80 }
81}
82
83#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct BridgeVotePayload {
86 pub target: String,
88 pub scores: Vec<BridgeScore>,
90}
91
92impl BridgeVotePayload {
93 pub fn new(target: impl Into<String>, scores: Vec<BridgeScore>) -> Self {
95 Self {
96 target: target.into(),
97 scores,
98 }
99 }
100
101 pub fn from_expr(expr: &Expr) -> Result<Self> {
103 let fields = map_fields(expr, "bridge/Vote")?;
104 reject_unknown(fields, &["target", "scores"])?;
105 let scores = required_vector(fields, "scores")?
106 .iter()
107 .map(BridgeScore::from_expr)
108 .collect::<Result<Vec<_>>>()?;
109 if scores.is_empty() {
110 return Err(Error::Eval(
111 "BRIDGE vote payload must carry at least one score".to_owned(),
112 ));
113 }
114 Ok(Self::new(required_string(fields, "target")?, scores))
115 }
116
117 pub fn to_expr(&self) -> Expr {
119 Expr::Map(vec![
120 entry("target", Expr::String(self.target.clone())),
121 entry(
122 "scores",
123 Expr::Vector(self.scores.iter().map(BridgeScore::to_expr).collect()),
124 ),
125 ])
126 }
127}
128
129#[derive(Clone, Debug, PartialEq, Eq)]
131pub struct BridgePatchPayload {
132 pub parent_cid: String,
134 pub target: String,
136 pub replacement: Expr,
138}
139
140impl BridgePatchPayload {
141 pub fn new(
143 parent_cid: impl Into<String>,
144 target: impl Into<String>,
145 replacement: Expr,
146 ) -> Self {
147 Self {
148 parent_cid: parent_cid.into(),
149 target: target.into(),
150 replacement,
151 }
152 }
153
154 pub fn from_expr(expr: &Expr) -> Result<Self> {
156 let fields = map_fields(expr, "bridge/Patch")?;
157 reject_unknown(fields, &["parent-cid", "target", "replacement"])?;
158 Ok(Self::new(
159 required_string(fields, "parent-cid")?,
160 required_string(fields, "target")?,
161 required_field(fields, "replacement")?.clone(),
162 ))
163 }
164
165 pub fn to_expr(&self) -> Expr {
167 Expr::Map(vec![
168 entry("parent-cid", Expr::String(self.parent_cid.clone())),
169 entry("target", Expr::String(self.target.clone())),
170 entry("replacement", self.replacement.clone()),
171 ])
172 }
173}
174
175#[derive(Clone, Debug, PartialEq, Eq)]
177pub struct BridgeEvidencePayload {
178 pub source: String,
180 pub body: String,
182}
183
184impl BridgeEvidencePayload {
185 pub fn new(source: impl Into<String>, body: impl Into<String>) -> Self {
187 Self {
188 source: source.into(),
189 body: body.into(),
190 }
191 }
192
193 pub fn from_expr(expr: &Expr) -> Result<Self> {
195 let fields = map_fields(expr, "bridge/Evidence")?;
196 reject_unknown(fields, &["source", "body"])?;
197 Ok(Self::new(
198 required_string(fields, "source")?,
199 required_string(fields, "body")?,
200 ))
201 }
202
203 pub fn to_expr(&self) -> Expr {
205 Expr::Map(vec![
206 entry("source", Expr::String(self.source.clone())),
207 entry("body", Expr::String(self.body.clone())),
208 ])
209 }
210}
211
212#[derive(Clone, Debug, PartialEq, Eq)]
214pub struct BridgeReceiptPayload {
215 pub status: Symbol,
217 pub refs: Vec<String>,
219}
220
221impl BridgeReceiptPayload {
222 pub fn new(status: Symbol, refs: Vec<String>) -> Self {
224 Self { status, refs }
225 }
226
227 pub fn from_expr(expr: &Expr) -> Result<Self> {
229 let fields = map_fields(expr, "bridge/Receipt")?;
230 reject_unknown(fields, &["status", "refs"])?;
231 Ok(Self::new(
232 required_symbol(fields, "status")?.clone(),
233 string_vector(fields, "refs")?,
234 ))
235 }
236
237 pub fn to_expr(&self) -> Expr {
239 Expr::Map(vec![
240 entry("status", Expr::Symbol(self.status.clone())),
241 entry(
242 "refs",
243 Expr::Vector(self.refs.iter().cloned().map(Expr::String).collect()),
244 ),
245 ])
246 }
247}
248
249#[derive(Clone, Debug, PartialEq, Eq)]
251pub struct BridgeAttestPayload {
252 pub subject: String,
254 pub claim: String,
256 pub evidence: Vec<String>,
258}
259
260impl BridgeAttestPayload {
261 pub fn new(
263 subject: impl Into<String>,
264 claim: impl Into<String>,
265 evidence: Vec<String>,
266 ) -> Self {
267 Self {
268 subject: subject.into(),
269 claim: claim.into(),
270 evidence,
271 }
272 }
273
274 pub fn from_expr(expr: &Expr) -> Result<Self> {
276 let fields = map_fields(expr, "bridge/Attest")?;
277 reject_unknown(fields, &["subject", "claim", "evidence"])?;
278 Ok(Self::new(
279 required_string(fields, "subject")?,
280 required_string(fields, "claim")?,
281 string_vector(fields, "evidence")?,
282 ))
283 }
284
285 pub fn to_expr(&self) -> Expr {
287 Expr::Map(vec![
288 entry("subject", Expr::String(self.subject.clone())),
289 entry("claim", Expr::String(self.claim.clone())),
290 entry(
291 "evidence",
292 Expr::Vector(self.evidence.iter().cloned().map(Expr::String).collect()),
293 ),
294 ])
295 }
296}
297
298pub fn validate_collab_payload(kind: &Symbol, payload: &Expr) -> Result<()> {
300 if kind == &part("Review") {
301 BridgeReviewPayload::from_expr(payload).map(drop)
302 } else if kind == &part("Vote") {
303 BridgeVotePayload::from_expr(payload).map(drop)
304 } else if kind == &part("Patch") {
305 BridgePatchPayload::from_expr(payload).map(drop)
306 } else if kind == &part("Evidence") {
307 BridgeEvidencePayload::from_expr(payload).map(drop)
308 } else if kind == &part("Receipt") {
309 BridgeReceiptPayload::from_expr(payload).map(drop)
310 } else if kind == &part("Attest") {
311 BridgeAttestPayload::from_expr(payload).map(drop)
312 } else {
313 Ok(())
314 }
315}
316
317fn map_fields<'a>(expr: &'a Expr, label: &str) -> Result<&'a [(Expr, Expr)]> {
318 match expr {
319 Expr::Map(fields) => Ok(fields),
320 _ => Err(Error::Eval(format!("{label} payload must be a map"))),
321 }
322}
323
324fn reject_unknown(fields: &[(Expr, Expr)], allowed: &[&str]) -> Result<()> {
325 for (key, _) in fields {
326 let Some(name) = field_name(key) else {
327 return Err(Error::Eval(
328 "BRIDGE collaboration field keys must be symbols".to_owned(),
329 ));
330 };
331 if !allowed.contains(&name.as_str()) {
332 return Err(Error::Eval(format!(
333 "unknown BRIDGE collaboration field {name}"
334 )));
335 }
336 }
337 Ok(())
338}
339
340fn required_field<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
341 fields
342 .iter()
343 .find_map(|(key, value)| (field_name(key).as_deref() == Some(name)).then_some(value))
344 .ok_or_else(|| Error::Eval(format!("BRIDGE collaboration record is missing {name}")))
345}
346
347fn required_symbol<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a Symbol> {
348 match required_field(fields, name)? {
349 Expr::Symbol(symbol) => Ok(symbol),
350 _ => Err(Error::TypeMismatch {
351 expected: "symbol",
352 found: "non-symbol",
353 }),
354 }
355}
356
357fn required_string<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a str> {
358 match required_field(fields, name)? {
359 Expr::String(value) => Ok(value),
360 _ => Err(Error::TypeMismatch {
361 expected: "string",
362 found: "non-string",
363 }),
364 }
365}
366
367fn required_i64(fields: &[(Expr, Expr)], name: &str) -> Result<i64> {
368 match required_field(fields, name)? {
369 Expr::Number(number) => number.canonical.parse().map_err(|_| {
370 Error::Eval(format!(
371 "BRIDGE collaboration field {name} must be an i64 literal"
372 ))
373 }),
374 _ => Err(Error::TypeMismatch {
375 expected: "number",
376 found: "non-number",
377 }),
378 }
379}
380
381fn required_vector<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a [Expr]> {
382 match required_field(fields, name)? {
383 Expr::Vector(items) | Expr::List(items) => Ok(items),
384 _ => Err(Error::Eval(format!(
385 "BRIDGE collaboration field {name} must be a vector"
386 ))),
387 }
388}
389
390fn string_vector(fields: &[(Expr, Expr)], name: &str) -> Result<Vec<String>> {
391 required_vector(fields, name)?
392 .iter()
393 .map(|item| match item {
394 Expr::String(value) => Ok(value.clone()),
395 _ => Err(Error::TypeMismatch {
396 expected: "string",
397 found: "non-string",
398 }),
399 })
400 .collect()
401}
402
403fn i64_expr(value: i64) -> Expr {
404 Expr::Number(NumberLiteral {
405 domain: Symbol::qualified("numbers", "i64"),
406 canonical: value.to_string(),
407 })
408}
409
410fn field_name(expr: &Expr) -> Option<String> {
411 match expr {
412 Expr::Symbol(symbol) => Some(symbol.name.to_string()),
413 Expr::String(value) => Some(value.clone()),
414 _ => None,
415 }
416}
417
418fn part(name: &str) -> Symbol {
419 Symbol::qualified("bridge", name)
420}