Skip to main content

sim_codec_bridge/
line.rs

1use std::collections::BTreeMap;
2
3use serde_json::Value as JsonValue;
4use sim_codec::{DecodeBudget, DecodeLimits};
5use sim_kernel::{CodecId, Error, Expr, Result, Symbol};
6
7use crate::identity::content_id_string;
8use crate::warrant::parse_content_id_string;
9use crate::{BridgeBook, BridgeHeader, BridgePacket, BridgePart, BridgeProvenance, BridgeWarrant};
10
11/// Encodes a BRIDGE packet to the strict `BRIDGE/1` line face.
12pub fn encode_bridge_text(packet: &BridgePacket, book: &BridgeBook) -> Result<String> {
13    for part in &packet.body {
14        book.parts.require_registered(&part.kind)?;
15        match &part.kind {
16            kind if *kind == Symbol::qualified("bridge", "Frame") => {
17                book.frames.validate_payload(&part.payload)?;
18            }
19            kind if *kind == Symbol::qualified("bridge", "Call") => {
20                crate::validate_call_payload(&part.payload)?;
21            }
22            kind if *kind == Symbol::qualified("bridge", "Weave") => {
23                crate::validate_weave_payload(&part.payload)?;
24            }
25            kind if collab_part(kind) => {
26                crate::validate_collab_payload(kind, &part.payload)?;
27            }
28            _ => {}
29        }
30    }
31    let mut lines = vec![
32        "BRIDGE/1".to_owned(),
33        format!("CID {}", packet.header.cid.as_deref().unwrap_or("nil")),
34        format!("MOVE {}", symbol_text(&packet.header.move_kind)),
35        format!("FROM {}", checked_token("FROM", &packet.header.from)?),
36        format!("TO {}", string_list(&packet.header.to)?),
37        format!("ROLE {}", symbol_text(&packet.header.role)),
38        format!("PARENTS {}", string_list(&packet.header.parents)?),
39        format!("TASK {}", symbol_text(&packet.header.task)),
40        format!("OUTPUT {}", symbol_text(&packet.header.output)),
41        format!("CEIL {}", symbol_list(&packet.header.ceiling)),
42        format!("CONTEXT {}", symbol_list(&packet.header.context)),
43        format!(
44            "PROV author={} card={}",
45            symbol_text(&packet.header.provenance.author),
46            packet.header.provenance.card.as_deref().unwrap_or("nil")
47        ),
48    ];
49    if let Some(warrant) = &packet.warrant {
50        lines.push(format!("WARRANT {}", warrant_text(warrant)));
51    }
52    lines.push("BODY".to_owned());
53    for part in &packet.body {
54        lines.push(format!(
55            "{} {} payload={}",
56            part_keyword(&part.kind),
57            symbol_text(&part.id),
58            payload_text(&part.payload)?
59        ));
60    }
61    lines.push("END".to_owned());
62    Ok(format!("{}\n", lines.join("\n")))
63}
64
65/// Decodes the strict `BRIDGE/1` line face to a BRIDGE packet.
66pub fn decode_bridge_text(text: &str, book: &BridgeBook) -> Result<BridgePacket> {
67    let mut lines = text.lines();
68    match lines.next() {
69        Some("BRIDGE/1") => {}
70        _ => {
71            return Err(Error::Eval(
72                "BRIDGE packet must start with BRIDGE/1".to_owned(),
73            ));
74        }
75    }
76
77    let mut headers = BTreeMap::new();
78    let mut body_lines = Vec::new();
79    let mut in_body = false;
80    let mut ended = false;
81    for line in lines {
82        if ended {
83            if !line.trim().is_empty() {
84                return Err(Error::Eval("BRIDGE packet has text after END".to_owned()));
85            }
86            continue;
87        }
88        if line == "BODY" {
89            if in_body {
90                return Err(Error::Eval("duplicate BRIDGE BODY marker".to_owned()));
91            }
92            in_body = true;
93            continue;
94        }
95        if line == "END" {
96            if !in_body {
97                return Err(Error::Eval("BRIDGE END before BODY".to_owned()));
98            }
99            ended = true;
100            continue;
101        }
102        if in_body {
103            body_lines.push(line.to_owned());
104        } else {
105            let (key, value) = split_header(line)?;
106            if !is_known_header(key) {
107                return Err(Error::Eval(format!("unknown BRIDGE header {key}")));
108            }
109            if headers.insert(key.to_owned(), value.to_owned()).is_some() {
110                return Err(Error::Eval(format!("duplicate BRIDGE header {key}")));
111            }
112        }
113    }
114    if !ended {
115        return Err(Error::Eval("BRIDGE packet is missing END".to_owned()));
116    }
117    let packet = BridgePacket {
118        header: BridgeHeader {
119            cid: header(&headers, "CID").and_then(parse_cid)?,
120            move_kind: parse_symbol(header(&headers, "MOVE")?),
121            from: header(&headers, "FROM")?.to_owned(),
122            to: parse_string_list(header(&headers, "TO")?)?,
123            role: parse_symbol(header(&headers, "ROLE")?),
124            parents: parse_string_list(header(&headers, "PARENTS")?)?,
125            task: parse_symbol(header(&headers, "TASK")?),
126            output: parse_symbol(header(&headers, "OUTPUT")?),
127            ceiling: parse_symbol_list(header(&headers, "CEIL")?)?,
128            context: parse_symbol_list(header(&headers, "CONTEXT")?)?,
129            provenance: parse_provenance(header(&headers, "PROV")?)?,
130        },
131        body: body_lines
132            .iter()
133            .map(|line| parse_part(line, book))
134            .collect::<Result<Vec<_>>>()?,
135        warrant: match headers.get("WARRANT") {
136            Some(value) => Some(parse_warrant(value)?),
137            None => None,
138        },
139    };
140    Ok(packet)
141}
142
143fn split_header(line: &str) -> Result<(&str, &str)> {
144    line.split_once(' ')
145        .ok_or_else(|| Error::Eval(format!("malformed BRIDGE header line {line:?}")))
146}
147
148fn is_known_header(header: &str) -> bool {
149    matches!(
150        header,
151        "CID"
152            | "MOVE"
153            | "FROM"
154            | "TO"
155            | "ROLE"
156            | "PARENTS"
157            | "TASK"
158            | "OUTPUT"
159            | "CEIL"
160            | "CONTEXT"
161            | "PROV"
162            | "WARRANT"
163    )
164}
165
166fn header<'a>(headers: &'a BTreeMap<String, String>, name: &str) -> Result<&'a str> {
167    headers
168        .get(name)
169        .map(String::as_str)
170        .ok_or_else(|| Error::Eval(format!("BRIDGE packet is missing {name} header")))
171}
172
173fn parse_cid(value: &str) -> Result<Option<String>> {
174    if value == "nil" {
175        Ok(None)
176    } else {
177        Ok(Some(value.to_owned()))
178    }
179}
180
181fn parse_provenance(value: &str) -> Result<BridgeProvenance> {
182    let mut author = None;
183    let mut card = None;
184    for item in value.split(' ') {
185        let Some((key, value)) = item.split_once('=') else {
186            return Err(Error::Eval(format!("malformed BRIDGE provenance {item}")));
187        };
188        match key {
189            "author" => author = Some(parse_symbol(value)),
190            "card" => {
191                card = Some(if value == "nil" {
192                    None
193                } else {
194                    Some(value.to_owned())
195                })
196            }
197            _ => {
198                return Err(Error::Eval(format!(
199                    "unknown BRIDGE provenance field {key}"
200                )));
201            }
202        }
203    }
204    Ok(BridgeProvenance {
205        author: author.ok_or_else(|| Error::Eval("BRIDGE provenance missing author".to_owned()))?,
206        card: card.ok_or_else(|| Error::Eval("BRIDGE provenance missing card".to_owned()))?,
207    })
208}
209
210fn warrant_text(warrant: &BridgeWarrant) -> String {
211    format!(
212        "moves={} frames={} parts={}",
213        content_id_string(&warrant.moves),
214        content_id_string(&warrant.frames),
215        warrant_parts_text(&warrant.parts)
216    )
217}
218
219fn warrant_parts_text(parts: &[(Symbol, sim_kernel::ContentId)]) -> String {
220    format!(
221        "[{}]",
222        parts
223            .iter()
224            .map(|(kind, id)| format!("{}={}", symbol_text(kind), content_id_string(id)))
225            .collect::<Vec<_>>()
226            .join(",")
227    )
228}
229
230fn parse_warrant(value: &str) -> Result<BridgeWarrant> {
231    let mut moves = None;
232    let mut frames = None;
233    let mut parts = None;
234    for item in value.split(' ') {
235        let Some((key, value)) = item.split_once('=') else {
236            return Err(Error::Eval(format!("malformed BRIDGE warrant {item}")));
237        };
238        match key {
239            "moves" => moves = Some(parse_content_id_string(value)?),
240            "frames" => frames = Some(parse_content_id_string(value)?),
241            "parts" => parts = Some(parse_warrant_parts(value)?),
242            _ => return Err(Error::Eval(format!("unknown BRIDGE warrant field {key}"))),
243        }
244    }
245    Ok(BridgeWarrant {
246        moves: moves.ok_or_else(|| Error::Eval("BRIDGE warrant missing moves".to_owned()))?,
247        frames: frames.ok_or_else(|| Error::Eval("BRIDGE warrant missing frames".to_owned()))?,
248        parts: parts.ok_or_else(|| Error::Eval("BRIDGE warrant missing parts".to_owned()))?,
249    })
250}
251
252fn parse_warrant_parts(text: &str) -> Result<Vec<(Symbol, sim_kernel::ContentId)>> {
253    parse_list(text)?
254        .into_iter()
255        .map(|item| {
256            let (kind, cid) = item
257                .split_once('=')
258                .ok_or_else(|| Error::Eval(format!("malformed BRIDGE warrant part {item}")))?;
259            Ok((parse_symbol(kind), parse_content_id_string(cid)?))
260        })
261        .collect()
262}
263
264fn parse_part(line: &str, book: &BridgeBook) -> Result<BridgePart> {
265    let mut fields = line.splitn(3, ' ');
266    let keyword = fields
267        .next()
268        .ok_or_else(|| Error::Eval("empty BRIDGE part line".to_owned()))?;
269    let id = fields
270        .next()
271        .ok_or_else(|| Error::Eval(format!("BRIDGE part {keyword} missing id")))?;
272    let rest = fields
273        .next()
274        .ok_or_else(|| Error::Eval(format!("BRIDGE part {keyword} missing payload")))?;
275    let payload = rest
276        .strip_prefix("payload=")
277        .ok_or_else(|| Error::Eval(format!("BRIDGE part {keyword} has unknown field")))?;
278    let kind = kind_from_keyword(keyword);
279    book.parts.require_registered(&kind)?;
280    let payload = parse_payload(payload)?;
281    match &kind {
282        kind if *kind == Symbol::qualified("bridge", "Frame") => {
283            book.frames.validate_payload(&payload)?;
284        }
285        kind if *kind == Symbol::qualified("bridge", "Call") => {
286            crate::validate_call_payload(&payload)?;
287        }
288        kind if *kind == Symbol::qualified("bridge", "Weave") => {
289            crate::validate_weave_payload(&payload)?;
290        }
291        kind if collab_part(kind) => {
292            crate::validate_collab_payload(kind, &payload)?;
293        }
294        _ => {}
295    }
296    Ok(BridgePart {
297        id: parse_symbol(id),
298        kind,
299        payload,
300    })
301}
302
303fn payload_text(expr: &Expr) -> Result<String> {
304    serde_json::to_string(&sim_codec_json::expr_to_json(expr))
305        .map_err(|err| Error::Eval(format!("encode BRIDGE payload JSON: {err}")))
306}
307
308fn parse_payload(text: &str) -> Result<Expr> {
309    let value = serde_json::from_str::<JsonValue>(text)
310        .map_err(|err| Error::Eval(format!("parse BRIDGE payload JSON: {err}")))?;
311    let mut budget = DecodeBudget::new(DecodeLimits::default());
312    sim_codec_json::json_to_expr(CodecId(0), &value, &mut budget, 0)
313}
314
315fn symbol_text(symbol: &Symbol) -> String {
316    symbol.as_qualified_str()
317}
318
319fn parse_symbol(text: &str) -> Symbol {
320    match text.split_once('/') {
321        Some((namespace, name)) if !namespace.is_empty() && !name.is_empty() => {
322            Symbol::qualified(namespace.to_owned(), name.to_owned())
323        }
324        _ => Symbol::new(text.to_owned()),
325    }
326}
327
328fn string_list(items: &[String]) -> Result<String> {
329    let tokens = items
330        .iter()
331        .map(|item| checked_token("list item", item))
332        .collect::<Result<Vec<_>>>()?;
333    Ok(format!("[{}]", tokens.join(",")))
334}
335
336fn symbol_list(items: &[Symbol]) -> String {
337    format!(
338        "[{}]",
339        items.iter().map(symbol_text).collect::<Vec<_>>().join(",")
340    )
341}
342
343fn parse_string_list(text: &str) -> Result<Vec<String>> {
344    parse_list(text).map(|items| items.into_iter().map(str::to_owned).collect())
345}
346
347fn parse_symbol_list(text: &str) -> Result<Vec<Symbol>> {
348    parse_list(text).map(|items| items.into_iter().map(parse_symbol).collect())
349}
350
351fn parse_list(text: &str) -> Result<Vec<&str>> {
352    let inner = text
353        .strip_prefix('[')
354        .and_then(|text| text.strip_suffix(']'))
355        .ok_or_else(|| Error::Eval(format!("BRIDGE list must use brackets: {text}")))?;
356    if inner.is_empty() {
357        Ok(Vec::new())
358    } else {
359        Ok(inner.split(',').collect())
360    }
361}
362
363fn checked_token<'a>(label: &str, value: &'a str) -> Result<&'a str> {
364    if value.is_empty()
365        || value
366            .chars()
367            .any(|ch| ch.is_whitespace() || matches!(ch, '[' | ']' | ','))
368    {
369        Err(Error::Eval(format!(
370            "BRIDGE {label} must be a non-empty token"
371        )))
372    } else {
373        Ok(value)
374    }
375}
376
377fn part_keyword(kind: &Symbol) -> String {
378    kind.name.to_ascii_uppercase()
379}
380
381fn kind_from_keyword(keyword: &str) -> Symbol {
382    let mut chars = keyword.chars();
383    let name = match chars.next() {
384        Some(first) => format!(
385            "{}{}",
386            first.to_ascii_uppercase(),
387            chars.as_str().to_ascii_lowercase()
388        ),
389        None => String::new(),
390    };
391    Symbol::qualified("bridge", name)
392}
393
394fn collab_part(kind: &Symbol) -> bool {
395    matches!(
396        kind.name.as_ref(),
397        "Review" | "Vote" | "Patch" | "Evidence" | "Receipt" | "Attest"
398    )
399}