Skip to main content

sim_codec_bridge/
call.rs

1use sim_kernel::{Error, Expr, Result, Symbol};
2use sim_value::build::entry;
3
4/// Media kind carried by a packed ASK call argument.
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum CallArgumentMedia {
7    /// The declared codec produced text.
8    Text,
9    /// The declared codec produced bytes that were rendered as text inside a fence.
10    Bytes,
11}
12
13impl CallArgumentMedia {
14    /// Returns the stable media symbol.
15    pub fn symbol(self) -> Symbol {
16        match self {
17            Self::Text => Symbol::qualified("bridge", "Text"),
18            Self::Bytes => Symbol::qualified("bridge", "Bytes"),
19        }
20    }
21
22    fn from_symbol(symbol: &Symbol) -> Result<Self> {
23        if symbol == &Symbol::qualified("bridge", "Text") {
24            Ok(Self::Text)
25        } else if symbol == &Symbol::qualified("bridge", "Bytes") {
26            Ok(Self::Bytes)
27        } else {
28            Err(Error::Eval(format!(
29                "unknown BRIDGE call argument media {symbol}"
30            )))
31        }
32    }
33}
34
35/// One packed argument in a `bridge/Call` payload.
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct BridgeCallArgument {
38    /// Argument name.
39    pub name: Symbol,
40    /// Codec used to encode the original value at data position.
41    pub codec: Symbol,
42    /// Encoded media kind.
43    pub media: CallArgumentMedia,
44    /// Content id of the pre-fence encoded datum.
45    pub content_id: String,
46    /// Fence-wrapped encoded argument text shown to the model.
47    pub fenced: String,
48}
49
50impl BridgeCallArgument {
51    /// Builds a packed call argument.
52    pub fn new(
53        name: Symbol,
54        codec: Symbol,
55        media: CallArgumentMedia,
56        content_id: String,
57        fenced: String,
58    ) -> Self {
59        Self {
60            name,
61            codec,
62            media,
63            content_id,
64            fenced,
65        }
66    }
67
68    /// Decodes a call argument expression.
69    pub fn from_expr(expr: &Expr) -> Result<Self> {
70        let fields = map_fields(expr, "bridge/Call argument")?;
71        reject_unknown(fields, &["name", "codec", "media", "content-id", "fenced"])?;
72        let media = CallArgumentMedia::from_symbol(required_symbol(fields, "media")?)?;
73        Ok(Self::new(
74            required_symbol(fields, "name")?.clone(),
75            required_symbol(fields, "codec")?.clone(),
76            media,
77            required_string(fields, "content-id")?.to_owned(),
78            required_string(fields, "fenced")?.to_owned(),
79        ))
80    }
81
82    /// Encodes this call argument as a canonical expression map.
83    pub fn to_expr(&self) -> Expr {
84        Expr::Map(vec![
85            entry("name", Expr::Symbol(self.name.clone())),
86            entry("codec", Expr::Symbol(self.codec.clone())),
87            entry("media", Expr::Symbol(self.media.symbol())),
88            entry("content-id", Expr::String(self.content_id.clone())),
89            entry("fenced", Expr::String(self.fenced.clone())),
90        ])
91    }
92}
93
94/// Typed payload for a `bridge/Call` part.
95#[derive(Clone, Debug, PartialEq, Eq)]
96pub struct BridgeCallPayload {
97    /// Call name.
98    pub name: Symbol,
99    /// Packed arguments.
100    pub args: Vec<BridgeCallArgument>,
101    /// Model parameters covered by the replay key.
102    pub model_params: Vec<(Symbol, Expr)>,
103}
104
105impl BridgeCallPayload {
106    /// Builds an empty call payload for `name`.
107    pub fn new(name: Symbol) -> Self {
108        Self {
109            name,
110            args: Vec::new(),
111            model_params: Vec::new(),
112        }
113    }
114
115    /// Adds a packed argument.
116    pub fn with_arg(mut self, arg: BridgeCallArgument) -> Self {
117        self.args.push(arg);
118        self
119    }
120
121    /// Adds a model parameter.
122    pub fn with_model_param(mut self, name: Symbol, value: Expr) -> Self {
123        self.model_params.push((name, value));
124        self
125    }
126
127    /// Decodes a call payload expression.
128    pub fn from_expr(expr: &Expr) -> Result<Self> {
129        let fields = map_fields(expr, "bridge/Call payload")?;
130        reject_unknown(fields, &["name", "args", "model-params"])?;
131        Ok(Self {
132            name: required_symbol(fields, "name")?.clone(),
133            args: required_vector(fields, "args")?
134                .iter()
135                .map(BridgeCallArgument::from_expr)
136                .collect::<Result<Vec<_>>>()?,
137            model_params: optional_symbol_map(fields, "model-params")?,
138        })
139    }
140
141    /// Encodes this payload as a canonical expression map.
142    pub fn to_expr(&self) -> Expr {
143        let mut fields = vec![
144            entry("name", Expr::Symbol(self.name.clone())),
145            entry(
146                "args",
147                Expr::Vector(self.args.iter().map(BridgeCallArgument::to_expr).collect()),
148            ),
149        ];
150        if !self.model_params.is_empty() {
151            fields.push(entry(
152                "model-params",
153                Expr::Map(
154                    self.model_params
155                        .iter()
156                        .map(|(name, value)| (Expr::Symbol(name.clone()), value.clone()))
157                        .collect(),
158                ),
159            ));
160        }
161        Expr::Map(fields)
162    }
163}
164
165/// Parses and validates a `bridge/Call` payload.
166pub fn validate_call_payload(payload: &Expr) -> Result<BridgeCallPayload> {
167    let payload = BridgeCallPayload::from_expr(payload)?;
168    for arg in &payload.args {
169        validate_arg(arg)?;
170    }
171    Ok(payload)
172}
173
174fn validate_arg(arg: &BridgeCallArgument) -> Result<()> {
175    if arg.content_id.trim().is_empty() {
176        return Err(Error::Eval(format!(
177            "BRIDGE call argument {} is missing a content id",
178            arg.name
179        )));
180    }
181    if !arg.fenced.contains("<sim-data-") || !arg.fenced.contains("</sim-data-") {
182        return Err(Error::Eval(format!(
183            "BRIDGE call argument {} must be fence-wrapped",
184            arg.name
185        )));
186    }
187    Ok(())
188}
189
190fn map_fields<'a>(expr: &'a Expr, label: &str) -> Result<&'a [(Expr, Expr)]> {
191    match expr {
192        Expr::Map(fields) => Ok(fields),
193        _ => Err(Error::Eval(format!("{label} must be a map"))),
194    }
195}
196
197fn reject_unknown(fields: &[(Expr, Expr)], allowed: &[&str]) -> Result<()> {
198    for (key, _) in fields {
199        let Some(name) = field_name(key) else {
200            return Err(Error::Eval(
201                "BRIDGE call field keys must be symbols".to_owned(),
202            ));
203        };
204        if !allowed.contains(&name.as_str()) {
205            return Err(Error::Eval(format!("unknown BRIDGE call field {name}")));
206        }
207    }
208    Ok(())
209}
210
211fn required_field<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
212    fields
213        .iter()
214        .find_map(|(key, value)| (field_name(key).as_deref() == Some(name)).then_some(value))
215        .ok_or_else(|| Error::Eval(format!("BRIDGE call record is missing {name}")))
216}
217
218fn required_symbol<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a Symbol> {
219    match required_field(fields, name)? {
220        Expr::Symbol(symbol) => Ok(symbol),
221        _ => Err(Error::TypeMismatch {
222            expected: "symbol",
223            found: "non-symbol",
224        }),
225    }
226}
227
228fn required_string<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a str> {
229    match required_field(fields, name)? {
230        Expr::String(value) => Ok(value),
231        _ => Err(Error::TypeMismatch {
232            expected: "string",
233            found: "non-string",
234        }),
235    }
236}
237
238fn required_vector<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a [Expr]> {
239    match required_field(fields, name)? {
240        Expr::Vector(items) | Expr::List(items) => Ok(items),
241        _ => Err(Error::Eval(format!(
242            "BRIDGE call {name} field must be a vector"
243        ))),
244    }
245}
246
247fn optional_symbol_map(fields: &[(Expr, Expr)], name: &str) -> Result<Vec<(Symbol, Expr)>> {
248    let Some(value) = fields
249        .iter()
250        .find_map(|(key, value)| (field_name(key).as_deref() == Some(name)).then_some(value))
251    else {
252        return Ok(Vec::new());
253    };
254    let Expr::Map(entries) = value else {
255        return Err(Error::Eval(format!(
256            "BRIDGE call {name} field must be a map"
257        )));
258    };
259    entries
260        .iter()
261        .map(|(key, value)| match key {
262            Expr::Symbol(symbol) => Ok((symbol.clone(), value.clone())),
263            _ => Err(Error::Eval(
264                "BRIDGE call model parameter keys must be symbols".to_owned(),
265            )),
266        })
267        .collect()
268}
269
270fn field_name(expr: &Expr) -> Option<String> {
271    match expr {
272        Expr::Symbol(symbol) => Some(symbol.name.to_string()),
273        Expr::String(value) => Some(value.clone()),
274        _ => None,
275    }
276}