Skip to main content

monty_proto/convert/
mod.rs

1//! Conversions between [`pb`](crate::pb) wire types and monty's public types.
2//!
3//! Direction conventions:
4//!
5//! - **Rust → proto is total** (`From<&T>`): every monty value has a wire
6//!   representation, and borrowing avoids cloning large containers twice.
7//! - **proto → Rust is fallible** (`TryFrom<T>` with [`ProtoConvertError`]):
8//!   wire data comes from the other side of a process boundary and must be
9//!   treated as untrusted — unknown names, out-of-range numbers, and missing
10//!   oneof arms are errors, never panics.
11//!
12//! Values cross as one flat arena per message ([`crate::WireArena`]) with the
13//! message naming its roots by index, so nesting depth is unbounded on the
14//! wire; the conversions here pair each root with its arena and check the
15//! index is in range.
16
17mod exception;
18pub(crate) mod limits;
19mod os_call;
20mod os_policy;
21mod resume;
22mod type_checking;
23
24use std::{error, fmt};
25
26use monty_types::{
27    MontyObject, NamedValues,
28    unstable::{self, MontyGraph, NodeId},
29};
30pub use os_call::{os_call_from_proto, os_call_to_proto};
31pub use resume::{
32    ext_result_from_proto, ext_result_to_proto, future_results_from_proto, future_results_to_proto,
33    resume_call_from_proto,
34};
35
36use crate::{
37    BudgetVec, pb,
38    wire::{WireArena, graph_error},
39};
40
41/// Why a wire value could not be converted into its monty equivalent.
42///
43/// Returned by all `TryFrom<pb::...>` impls in this crate. The variants are
44/// deliberately specific so a parent can log exactly which field a misbehaving
45/// child produced.
46#[derive(Debug)]
47pub enum ProtoConvertError {
48    /// A required message field or oneof was absent.
49    MissingField(&'static str),
50    /// An exception type name that monty does not know.
51    UnknownExcType(String),
52    /// A type name that monty's `MontyType::from_type_name` does not know.
53    UnknownType(String),
54    /// A builtin function name that monty does not know.
55    UnknownBuiltinFunction(String),
56    /// A file handle mode string that is not a supported `open()` mode.
57    InvalidFileMode(String),
58    /// A `time.time` caller name that no `TimeCaller` spells.
59    InvalidTimeCaller(String),
60    /// A field value was out of range or otherwise malformed.
61    InvalidValue {
62        /// The offending field, e.g. `"Date.month"`.
63        field: &'static str,
64        /// Human-readable explanation.
65        reason: String,
66    },
67}
68
69impl fmt::Display for ProtoConvertError {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        match self {
72            Self::MissingField(field) => write!(f, "missing required field {field}"),
73            Self::UnknownExcType(name) => write!(f, "unknown exception type {name:?}"),
74            Self::UnknownType(name) => write!(f, "unknown type name {name:?}"),
75            Self::UnknownBuiltinFunction(name) => write!(f, "unknown builtin function {name:?}"),
76            Self::InvalidFileMode(mode) => write!(f, "invalid file mode {mode:?}"),
77            Self::InvalidTimeCaller(caller) => write!(f, "invalid time caller {caller:?}"),
78            Self::InvalidValue { field, reason } => write!(f, "invalid value for {field}: {reason}"),
79        }
80    }
81}
82
83impl error::Error for ProtoConvertError {}
84
85impl From<MontyObject> for pb::Complete {
86    fn from(value: MontyObject) -> Self {
87        let (graph, root) = unstable::into_graph_parts(value);
88        Self {
89            value: root.0,
90            values: Some(WireArena::new(graph)),
91        }
92    }
93}
94
95impl TryFrom<pb::Complete> for MontyObject {
96    type Error = ProtoConvertError;
97
98    fn try_from(complete: pb::Complete) -> Result<Self, ProtoConvertError> {
99        root_object(complete.values, complete.value, "Complete.values")
100    }
101}
102
103/// Splits named inputs into `NamedRef`s and the arena they index.
104#[must_use]
105pub fn named_values_to_proto(inputs: NamedValues) -> (BudgetVec<pb::NamedRef>, WireArena) {
106    let (graph, names) = unstable::into_named_values_parts(inputs);
107    let refs = names
108        .into_iter()
109        .map(|(name, id)| pb::NamedRef { name, value: id.0 })
110        .collect();
111    (refs, WireArena::new(graph))
112}
113
114/// Validates decoded named inputs against their arena.
115pub fn named_values_from_proto(
116    inputs: impl IntoIterator<Item = pb::NamedRef>,
117    values: Option<WireArena>,
118) -> Result<NamedValues, ProtoConvertError> {
119    let graph = graph_or_empty(values)?;
120    let names = inputs
121        .into_iter()
122        .map(|input| (input.name, NodeId(input.value)))
123        .collect();
124    unstable::named_values_from_parts(graph, names).map_err(|err| graph_error(&err))
125}
126
127/// Pairs a message's arena with the root it names, rejecting an absent arena
128/// (`field` names it) or an out-of-range root.
129pub(crate) fn root_object(
130    values: Option<WireArena>,
131    root: u32,
132    field: &'static str,
133) -> Result<MontyObject, ProtoConvertError> {
134    let graph = values.ok_or(ProtoConvertError::MissingField(field))?.into_graph()?;
135    unstable::object_from_graph(graph, NodeId(root)).map_err(|err| graph_error(&err))
136}
137
138/// A message's arena, or an empty one when the field is absent (a message
139/// with no value-typed fields set never needs one).
140pub(crate) fn graph_or_empty(values: Option<WireArena>) -> Result<MontyGraph, ProtoConvertError> {
141    values.map_or_else(|| Ok(MontyGraph::new()), WireArena::into_graph)
142}