1#![forbid(unsafe_code)]
9#![deny(missing_docs)]
10
11pub mod cookbook;
12
13use sim_codec_bridge::{
14 BridgeBook, BridgePacket, BridgePatchPayload, BridgeReceiptPayload, BridgeReviewPayload,
15 BridgeScore, BridgeVotePayload, expr_to_packet, packet_to_expr,
16};
17use sim_kernel::{Cx, Error, Expr, Result, Symbol};
18use sim_lib_view::codec::reduce_for_caps;
19use sim_lib_view::{Draft, Operation, SurfaceCaps, SurfaceCodec, roundtrip_holds};
20use sim_value::access::field;
21use sim_value::build::{entry, list, map, text};
22
23pub use cookbook::packet_review_demo;
24
25pub const BRIDGE_PACKET_SURFACE_CODEC_ID: &str = "surface:bridge-packet";
27
28#[derive(Clone, Copy, Debug, Default)]
30pub struct BridgePacketSurfaceCodec;
31
32impl BridgePacketSurfaceCodec {
33 pub fn new() -> Self {
35 Self
36 }
37}
38
39impl SurfaceCodec for BridgePacketSurfaceCodec {
40 fn encode(&self, _cx: &mut Cx, value: &Expr, caps: &SurfaceCaps) -> Result<Expr> {
41 let packet = expr_to_packet(value)?;
42 let scene = packet_scene(&packet, caps)?;
43 Ok(reduce_for_caps(&scene, caps))
44 }
45
46 fn decode(&self, _cx: &mut Cx, value: &Expr, intent: &Expr) -> Result<Draft> {
47 sim_lib_intent::validate_intent(intent)
48 .map_err(|error| Error::HostError(format!("invalid intent: {error}")))?;
49 let packet = expr_to_packet(value)?;
50 let path = path_segments(intent)?;
51 let target = required_field(intent, "value")?;
52 if path.is_empty() {
53 return Ok(Draft::clean(value.clone(), target.clone()));
54 }
55 if path.first().map(String::as_str) != Some("bridge-collab") {
56 return Err(Error::Eval(
57 "BRIDGE packet surface only decodes bridge-collab edits".to_owned(),
58 ));
59 }
60 let action = path.get(1).map(String::as_str).ok_or_else(|| {
61 Error::Eval("BRIDGE collaboration edit is missing an action".to_owned())
62 })?;
63 let part = collaboration_part(&packet, action, target)?;
64 Ok(Draft::clean(value.clone(), part))
65 }
66
67 fn commit(&self, _cx: &mut Cx, draft: &Draft) -> Result<Operation> {
68 Ok(Operation {
69 form: map(vec![
70 (
71 "op",
72 Expr::Symbol(Symbol::qualified("bridge", "surface-edit")),
73 ),
74 ("value", draft.proposed.clone()),
75 ]),
76 })
77 }
78}
79
80pub fn bridge_packet_view(cx: &mut Cx, packet: &BridgePacket, caps: &SurfaceCaps) -> Result<Expr> {
82 BridgePacketSurfaceCodec::new().encode(cx, &packet_to_expr(packet), caps)
83}
84
85pub fn bridge_packet_edit(cx: &mut Cx, packet: &BridgePacket, intent: &Expr) -> Result<Expr> {
87 let value = packet_to_expr(packet);
88 let codec = BridgePacketSurfaceCodec::new();
89 debug_assert!(roundtrip_holds(cx, &codec, &value).unwrap_or(false));
90 let draft = codec.decode(cx, &value, intent)?;
91 if !draft.committable {
92 return Err(Error::Eval(
93 "BRIDGE packet edit produced a rejected draft".to_owned(),
94 ));
95 }
96 Ok(draft.proposed)
97}
98
99fn packet_scene(packet: &BridgePacket, caps: &SurfaceCaps) -> Result<Expr> {
100 let profiles = BridgeBook::standard()
101 .profiles
102 .matching_profiles(packet)
103 .into_iter()
104 .map(|profile| profile.as_qualified_str())
105 .collect::<Vec<_>>()
106 .join(", ");
107 let cid = packet.header.cid.as_deref().unwrap_or("unstamped");
108 let mut children = vec![
109 sim_lib_scene::badge("surface", BRIDGE_PACKET_SURFACE_CODEC_ID),
110 sim_lib_scene::build::text_node(format!("cid {cid}")),
111 sim_lib_scene::build::text_node(format!(
112 "move {} from {}",
113 packet.header.move_kind.as_qualified_str(),
114 packet.header.from
115 )),
116 sim_lib_scene::build::text_node(format!(
117 "profiles {}",
118 if profiles.is_empty() {
119 "none".to_owned()
120 } else {
121 profiles
122 }
123 )),
124 sim_lib_scene::build::text_node(format!("surface {}", caps.preset_name())),
125 ];
126 children.extend(packet.body.iter().map(|part| {
127 sim_lib_scene::box_(
128 "part",
129 vec![
130 sim_lib_scene::badge("kind", &part.kind.as_qualified_str()),
131 sim_lib_scene::build::text_node(format!("id {}", part.id.as_qualified_str())),
132 sim_lib_scene::build::text_node(payload_summary(&part.payload)),
133 ],
134 )
135 }));
136 let scene = sim_lib_scene::stack("column", children);
137 sim_lib_scene::validate_scene(&scene)
138 .map_err(|error| Error::HostError(format!("invalid BRIDGE packet scene: {error}")))?;
139 Ok(scene)
140}
141
142fn collaboration_part(packet: &BridgePacket, action: &str, value: &Expr) -> Result<Expr> {
143 match action {
144 "patch" => {
145 let patch = BridgePatchPayload::new(
146 packet_cid(packet)?,
147 required_string(value, "target")?,
148 required_field(value, "replacement")?.clone(),
149 );
150 Ok(part_expr("P1", "Patch", patch.to_expr()))
151 }
152 "review" => {
153 let review = BridgeReviewPayload::new(
154 required_string(value, "target")?,
155 required_string(value, "body")?,
156 );
157 Ok(part_expr("R1", "Review", review.to_expr()))
158 }
159 "vote" => {
160 let vote = BridgeVotePayload::new(required_string(value, "target")?, scores(value)?);
161 Ok(part_expr("V1", "Vote", vote.to_expr()))
162 }
163 "receipt" => {
164 let receipt =
165 BridgeReceiptPayload::new(required_symbol(value, "status")?.clone(), refs(value)?);
166 Ok(part_expr("Rc1", "Receipt", receipt.to_expr()))
167 }
168 other => Err(Error::Eval(format!(
169 "unknown BRIDGE collaboration edit action {other}"
170 ))),
171 }
172}
173
174fn part_expr(id: &str, kind: &str, payload: Expr) -> Expr {
175 Expr::Map(vec![
176 entry("id", Expr::Symbol(Symbol::new(id))),
177 entry("kind", Expr::Symbol(Symbol::qualified("bridge", kind))),
178 entry("payload", payload),
179 ])
180}
181
182fn scores(value: &Expr) -> Result<Vec<BridgeScore>> {
183 let scores = required_vector(value, "scores")?
184 .iter()
185 .map(BridgeScore::from_expr)
186 .collect::<Result<Vec<_>>>()?;
187 if scores.is_empty() {
188 return Err(Error::Eval(
189 "BRIDGE packet vote edit requires at least one score".to_owned(),
190 ));
191 }
192 Ok(scores)
193}
194
195fn refs(value: &Expr) -> Result<Vec<String>> {
196 required_vector(value, "refs")?
197 .iter()
198 .map(|item| match item {
199 Expr::String(value) => Ok(value.clone()),
200 _ => Err(Error::TypeMismatch {
201 expected: "string",
202 found: "non-string",
203 }),
204 })
205 .collect()
206}
207
208fn packet_cid(packet: &BridgePacket) -> Result<String> {
209 packet.header.cid.clone().ok_or_else(|| {
210 Error::Eval("BRIDGE packet surface edits require a stamped packet".to_owned())
211 })
212}
213
214fn path_segments(intent: &Expr) -> Result<Vec<String>> {
215 required_vector(intent, "path")?
216 .iter()
217 .map(|segment| match segment {
218 Expr::String(value) => Ok(value.clone()),
219 Expr::Symbol(symbol) => Ok(symbol.name.to_string()),
220 _ => Err(Error::Eval(
221 "BRIDGE packet edit path segments must be strings or symbols".to_owned(),
222 )),
223 })
224 .collect()
225}
226
227fn required_field<'a>(expr: &'a Expr, name: &str) -> Result<&'a Expr> {
228 field(expr, name).ok_or_else(|| Error::Eval(format!("missing field {name}")))
229}
230
231fn required_string<'a>(expr: &'a Expr, name: &str) -> Result<&'a str> {
232 match required_field(expr, name)? {
233 Expr::String(value) => Ok(value),
234 _ => Err(Error::TypeMismatch {
235 expected: "string",
236 found: "non-string",
237 }),
238 }
239}
240
241fn required_symbol<'a>(expr: &'a Expr, name: &str) -> Result<&'a Symbol> {
242 match required_field(expr, name)? {
243 Expr::Symbol(value) => Ok(value),
244 _ => Err(Error::TypeMismatch {
245 expected: "symbol",
246 found: "non-symbol",
247 }),
248 }
249}
250
251fn required_vector<'a>(expr: &'a Expr, name: &str) -> Result<&'a [Expr]> {
252 match required_field(expr, name)? {
253 Expr::List(items) | Expr::Vector(items) => Ok(items),
254 _ => Err(Error::Eval(format!("field {name} must be a list"))),
255 }
256}
257
258fn payload_summary(payload: &Expr) -> String {
259 let rendered = format!("{payload:?}");
260 if rendered.len() <= 96 {
261 rendered
262 } else {
263 format!("{}...", &rendered[..96])
264 }
265}
266
267pub fn patch_edit_intent(target: &str, replacement: Expr, origin: sim_lib_intent::Origin) -> Expr {
269 sim_lib_intent::intent(
270 "edit-field",
271 origin,
272 vec![
273 ("target", Expr::Symbol(Symbol::new("bridge-packet"))),
274 ("path", list(vec![text("bridge-collab"), text("patch")])),
275 (
276 "value",
277 map(vec![("target", text(target)), ("replacement", replacement)]),
278 ),
279 ],
280 )
281}
282
283pub fn review_edit_intent(target: &str, body: &str, origin: sim_lib_intent::Origin) -> Expr {
285 sim_lib_intent::intent(
286 "edit-field",
287 origin,
288 vec![
289 ("target", Expr::Symbol(Symbol::new("bridge-packet"))),
290 ("path", list(vec![text("bridge-collab"), text("review")])),
291 (
292 "value",
293 map(vec![("target", text(target)), ("body", text(body))]),
294 ),
295 ],
296 )
297}
298
299pub fn vote_edit_intent(
301 target: &str,
302 scores: Vec<BridgeScore>,
303 origin: sim_lib_intent::Origin,
304) -> Expr {
305 sim_lib_intent::intent(
306 "edit-field",
307 origin,
308 vec![
309 ("target", Expr::Symbol(Symbol::new("bridge-packet"))),
310 ("path", list(vec![text("bridge-collab"), text("vote")])),
311 (
312 "value",
313 map(vec![
314 ("target", text(target)),
315 (
316 "scores",
317 Expr::Vector(scores.iter().map(BridgeScore::to_expr).collect()),
318 ),
319 ]),
320 ),
321 ],
322 )
323}
324
325pub fn receipt_edit_intent(
327 status: Symbol,
328 refs: Vec<String>,
329 origin: sim_lib_intent::Origin,
330) -> Expr {
331 sim_lib_intent::intent(
332 "edit-field",
333 origin,
334 vec![
335 ("target", Expr::Symbol(Symbol::new("bridge-packet"))),
336 ("path", list(vec![text("bridge-collab"), text("receipt")])),
337 (
338 "value",
339 map(vec![
340 ("status", Expr::Symbol(status)),
341 ("refs", list(refs.into_iter().map(text).collect::<Vec<_>>())),
342 ]),
343 ),
344 ],
345 )
346}
347
348#[cfg(test)]
349mod tests;