Skip to main content

lenso_contract_runtime/
lib.rs

1//! Runtime-neutral wire primitives and serde support for generated Lenso contracts.
2
3use std::collections::BTreeMap;
4
5use ::serde::{Serialize, de::DeserializeOwned};
6use base64::{Engine as _, engine::general_purpose::STANDARD};
7use serde_json::value::RawValue;
8
9/// Signed 64-bit integer encoded as a decimal string on the wire.
10pub type Int64 = String;
11/// Unsigned 64-bit integer encoded as a decimal string on the wire.
12pub type Uint64 = String;
13/// RFC 3339 timestamp encoded as a string on the wire.
14pub type Timestamp = String;
15/// ISO 8601 duration encoded as a string on the wire.
16pub type Duration = String;
17/// Distinguishes a missing field from an explicit `null` value.
18pub type OptionalValue<T> = Option<Option<T>>;
19
20/// One validated complete JSON value encoded as a JSON string on the wire.
21///
22/// The wrapper removes string concatenation from contract code while preserving
23/// the existing wire shape used by `*_json` fields.
24pub struct RawJson(Box<RawValue>);
25
26impl RawJson {
27    /// Validates and wraps one complete portable JSON value.
28    pub fn new(value: impl Into<String>) -> Result<Self, serde_json::Error> {
29        let value = value.into();
30        let parsed: serde_json::Value = serde_json::from_str(&value)?;
31        validate_portable_json_value(&parsed).map_err(portable_json_error)?;
32        RawValue::from_string(value).map(Self)
33    }
34
35    /// Returns the exact validated JSON source.
36    #[must_use]
37    pub fn as_str(&self) -> &str {
38        self.0.get()
39    }
40
41    /// Returns the owned validated JSON source.
42    #[must_use]
43    pub fn into_string(self) -> String {
44        self.0.get().to_owned()
45    }
46}
47
48impl Default for RawJson {
49    fn default() -> Self {
50        Self(RawValue::from_string("null".to_owned()).expect("null is valid JSON"))
51    }
52}
53
54impl Clone for RawJson {
55    fn clone(&self) -> Self {
56        Self::new(self.as_str()).expect("RawJson always contains validated JSON")
57    }
58}
59
60impl std::fmt::Debug for RawJson {
61    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        formatter
63            .debug_tuple("RawJson")
64            .field(&self.as_str())
65            .finish()
66    }
67}
68
69impl PartialEq for RawJson {
70    fn eq(&self, other: &Self) -> bool {
71        self.as_str() == other.as_str()
72    }
73}
74
75impl Eq for RawJson {}
76
77impl std::str::FromStr for RawJson {
78    type Err = serde_json::Error;
79
80    fn from_str(value: &str) -> Result<Self, Self::Err> {
81        Self::new(value)
82    }
83}
84
85impl TryFrom<String> for RawJson {
86    type Error = serde_json::Error;
87
88    fn try_from(value: String) -> Result<Self, Self::Error> {
89        Self::new(value)
90    }
91}
92
93impl Serialize for RawJson {
94    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
95    where
96        S: ::serde::Serializer,
97    {
98        serializer.serialize_str(self.as_str())
99    }
100}
101
102impl<'de> ::serde::Deserialize<'de> for RawJson {
103    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
104    where
105        D: ::serde::Deserializer<'de>,
106    {
107        let value = <String as ::serde::Deserialize>::deserialize(deserializer)?;
108        Self::new(value).map_err(::serde::de::Error::custom)
109    }
110}
111
112/// A typed value encoded as one validated JSON string on the wire.
113#[derive(Clone, Debug, Default, Eq, PartialEq)]
114pub struct Json<T>(T);
115
116impl<T> Json<T> {
117    /// Wraps a typed value for JSON-string wire encoding.
118    pub const fn new(value: T) -> Self {
119        Self(value)
120    }
121
122    /// Borrows the typed value.
123    pub const fn as_inner(&self) -> &T {
124        &self.0
125    }
126
127    /// Returns the owned typed value.
128    pub fn into_inner(self) -> T {
129        self.0
130    }
131}
132
133impl<T> From<T> for Json<T> {
134    fn from(value: T) -> Self {
135        Self::new(value)
136    }
137}
138
139impl<T> std::ops::Deref for Json<T> {
140    type Target = T;
141
142    fn deref(&self) -> &Self::Target {
143        self.as_inner()
144    }
145}
146
147impl<T: Serialize> Serialize for Json<T> {
148    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
149    where
150        S: ::serde::Serializer,
151    {
152        let encoded = encode_portable_json(&self.0).map_err(::serde::ser::Error::custom)?;
153        serializer.serialize_str(&encoded)
154    }
155}
156
157impl<'de, T: DeserializeOwned> ::serde::Deserialize<'de> for Json<T> {
158    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
159    where
160        D: ::serde::Deserializer<'de>,
161    {
162        let encoded = <String as ::serde::Deserialize>::deserialize(deserializer)?;
163        decode_portable_json(&encoded)
164            .map(Self)
165            .map_err(::serde::de::Error::custom)
166    }
167}
168
169/// Shared native bytes encoded as canonical padded Base64 on the wire.
170#[derive(Clone, Debug, Default, Eq, PartialEq)]
171pub struct Bytes(bytes::Bytes);
172
173impl Bytes {
174    /// Wraps an owned or shared byte buffer without copying when possible.
175    pub fn new(value: impl Into<bytes::Bytes>) -> Self {
176        Self(value.into())
177    }
178
179    /// Returns the byte slice.
180    #[must_use]
181    pub fn as_slice(&self) -> &[u8] {
182        &self.0
183    }
184
185    /// Returns the shared byte buffer without copying.
186    #[must_use]
187    pub fn into_shared(self) -> bytes::Bytes {
188        self.0
189    }
190
191    /// Returns an owned vector containing the bytes.
192    #[must_use]
193    pub fn into_vec(self) -> Vec<u8> {
194        self.0.to_vec()
195    }
196}
197
198impl From<Vec<u8>> for Bytes {
199    fn from(value: Vec<u8>) -> Self {
200        Self(value.into())
201    }
202}
203
204impl From<&[u8]> for Bytes {
205    fn from(value: &[u8]) -> Self {
206        Self(bytes::Bytes::copy_from_slice(value))
207    }
208}
209
210impl From<bytes::Bytes> for Bytes {
211    fn from(value: bytes::Bytes) -> Self {
212        Self(value)
213    }
214}
215
216impl From<Bytes> for bytes::Bytes {
217    fn from(value: Bytes) -> Self {
218        value.0
219    }
220}
221
222impl From<Bytes> for Vec<u8> {
223    fn from(value: Bytes) -> Self {
224        value.into_vec()
225    }
226}
227
228impl AsRef<[u8]> for Bytes {
229    fn as_ref(&self) -> &[u8] {
230        self.as_slice()
231    }
232}
233
234impl std::ops::Deref for Bytes {
235    type Target = [u8];
236
237    fn deref(&self) -> &Self::Target {
238        self.as_slice()
239    }
240}
241
242impl Serialize for Bytes {
243    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
244    where
245        S: ::serde::Serializer,
246    {
247        serializer.serialize_str(&STANDARD.encode(self.as_slice()))
248    }
249}
250
251impl<'de> ::serde::Deserialize<'de> for Bytes {
252    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
253    where
254        D: ::serde::Deserializer<'de>,
255    {
256        let encoded = <String as ::serde::Deserialize>::deserialize(deserializer)?;
257        STANDARD
258            .decode(encoded)
259            .map(Self::from)
260            .map_err(|_| ::serde::de::Error::custom("bytes must be canonical padded base64"))
261    }
262}
263
264/// A forward-compatible Capability-defined error unknown to this binding version.
265#[derive(Clone, Debug, PartialEq, Serialize, ::serde::Deserialize)]
266pub struct UnknownDomainError {
267    /// Stable Domain Error code.
268    pub code: String,
269    /// Optional opaque error payload.
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub payload: Option<serde_json::Value>,
272    /// Additional fields preserved for forward compatibility.
273    #[serde(default, flatten)]
274    pub extra: BTreeMap<String, serde_json::Value>,
275}
276
277/// Encodes a typed value after enforcing the portable JSON number profile.
278pub fn encode_portable_json<T: Serialize>(value: &T) -> Result<String, serde_json::Error> {
279    let value = serde_json::to_value(value)?;
280    validate_portable_json_value(&value).map_err(portable_json_error)?;
281    serde_json::to_string(&value)
282}
283
284/// Decodes a typed value after enforcing the portable JSON number profile.
285pub fn decode_portable_json<T: DeserializeOwned>(wire: &str) -> Result<T, serde_json::Error> {
286    let value: serde_json::Value = serde_json::from_str(wire)?;
287    validate_portable_json_value(&value).map_err(portable_json_error)?;
288    serde_json::from_value(value)
289}
290
291/// Validates recursively that ordinary JSON numbers are portable across runtimes.
292pub fn validate_portable_json_value(value: &serde_json::Value) -> Result<(), String> {
293    match value {
294        serde_json::Value::Number(number) => {
295            let safe = number.as_i64().is_some_and(|value| {
296                (-9_007_199_254_740_991..=9_007_199_254_740_991).contains(&value)
297            }) || number
298                .as_u64()
299                .is_some_and(|value| value <= 9_007_199_254_740_991)
300                || (number.is_f64()
301                    && number.as_f64().is_some_and(|value| {
302                        value.is_finite()
303                            && (value.abs() <= 9_007_199_254_740_991.0 || value.fract() != 0.0)
304                    }));
305            if !safe {
306                return Err("wire JSON contains an unsafe number".to_owned());
307            }
308        }
309        serde_json::Value::Array(values) => {
310            for value in values {
311                validate_portable_json_value(value)?;
312            }
313        }
314        serde_json::Value::Object(values) => {
315            for value in values.values() {
316                validate_portable_json_value(value)?;
317            }
318        }
319        serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::String(_) => {}
320    }
321    Ok(())
322}
323
324fn portable_json_error(detail: String) -> serde_json::Error {
325    serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidData, detail))
326}
327
328/// Serde helpers used by generated field attributes.
329pub mod serde {
330    /// Deserializes a required field while allowing generated structs to reject omission.
331    pub fn deserialize_required<'de, D, T>(deserializer: D) -> Result<T, D::Error>
332    where
333        D: ::serde::Deserializer<'de>,
334        T: ::serde::Deserialize<'de>,
335    {
336        <T as ::serde::Deserialize>::deserialize(deserializer)
337    }
338
339    /// Preserves the distinction between a missing field and an explicit `null`.
340    #[allow(clippy::option_option)]
341    pub fn deserialize_optional_value<'de, D, T>(
342        deserializer: D,
343    ) -> Result<Option<Option<T>>, D::Error>
344    where
345        D: ::serde::Deserializer<'de>,
346        T: ::serde::Deserialize<'de>,
347    {
348        Ok(Some(<Option<T> as ::serde::Deserialize>::deserialize(
349            deserializer,
350        )?))
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    #[test]
359    fn bytes_preserve_canonical_padded_base64_wire_behavior() {
360        for length in 0..=258 {
361            let value = Bytes::from(
362                (0..length)
363                    .map(|index| u8::try_from(index % 256).unwrap())
364                    .collect::<Vec<_>>(),
365            );
366            let wire = serde_json::to_string(&value).unwrap();
367            assert_eq!(serde_json::from_str::<Bytes>(&wire).unwrap(), value);
368        }
369        for wire in [r#""not base64""#, r#""AQI""#, r#""AQJ=""#] {
370            assert!(serde_json::from_str::<Bytes>(wire).is_err());
371        }
372    }
373
374    #[test]
375    fn shared_bytes_round_trip_without_copying() {
376        let source = bytes::Bytes::from_static(b"shared");
377        let contract = Bytes::from(source.clone());
378        assert_eq!(contract.as_slice().as_ptr(), source.as_ptr());
379        let restored = contract.into_shared();
380        assert_eq!(restored.as_ptr(), source.as_ptr());
381    }
382
383    #[test]
384    fn portable_json_rejects_unsafe_integer_values() {
385        let error = decode_portable_json::<serde_json::Value>("9007199254740992").unwrap_err();
386        assert!(error.to_string().contains("unsafe number"));
387    }
388
389    #[test]
390    fn raw_json_validates_content_without_changing_the_string_wire_shape() {
391        let raw = RawJson::new(r#"{"ready":true}"#).unwrap();
392        assert_eq!(
393            serde_json::to_string(&raw).unwrap(),
394            r#""{\"ready\":true}""#
395        );
396        assert_eq!(
397            serde_json::from_str::<RawJson>(r#""{\"ready\":true}""#).unwrap(),
398            raw
399        );
400        assert!(RawJson::new("not JSON").is_err());
401        assert!(RawJson::new("9007199254740992").is_err());
402    }
403
404    #[test]
405    fn typed_json_round_trips_as_one_json_string() {
406        let value = Json::new(BTreeMap::from([("answer".to_owned(), 42_i32)]));
407        let wire = serde_json::to_string(&value).unwrap();
408        assert_eq!(wire, r#""{\"answer\":42}""#);
409        assert_eq!(
410            serde_json::from_str::<Json<BTreeMap<String, i32>>>(&wire).unwrap(),
411            value
412        );
413    }
414}