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 TryFrom<&str> for RawJson {
94    type Error = serde_json::Error;
95
96    fn try_from(value: &str) -> Result<Self, Self::Error> {
97        Self::new(value)
98    }
99}
100
101impl AsRef<str> for RawJson {
102    fn as_ref(&self) -> &str {
103        self.as_str()
104    }
105}
106
107impl std::ops::Deref for RawJson {
108    type Target = str;
109
110    fn deref(&self) -> &Self::Target {
111        self.as_str()
112    }
113}
114
115impl std::fmt::Display for RawJson {
116    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        formatter.write_str(self.as_str())
118    }
119}
120
121impl Serialize for RawJson {
122    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
123    where
124        S: ::serde::Serializer,
125    {
126        serializer.serialize_str(self.as_str())
127    }
128}
129
130impl<'de> ::serde::Deserialize<'de> for RawJson {
131    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
132    where
133        D: ::serde::Deserializer<'de>,
134    {
135        let value = <String as ::serde::Deserialize>::deserialize(deserializer)?;
136        Self::new(value).map_err(::serde::de::Error::custom)
137    }
138}
139
140/// A typed value encoded as one validated JSON string on the wire.
141#[derive(Clone, Debug, Default, Eq, PartialEq)]
142pub struct Json<T>(T);
143
144impl<T> Json<T> {
145    /// Wraps a typed value for JSON-string wire encoding.
146    pub const fn new(value: T) -> Self {
147        Self(value)
148    }
149
150    /// Borrows the typed value.
151    pub const fn as_inner(&self) -> &T {
152        &self.0
153    }
154
155    /// Returns the owned typed value.
156    pub fn into_inner(self) -> T {
157        self.0
158    }
159}
160
161impl<T> From<T> for Json<T> {
162    fn from(value: T) -> Self {
163        Self::new(value)
164    }
165}
166
167impl<T> std::ops::Deref for Json<T> {
168    type Target = T;
169
170    fn deref(&self) -> &Self::Target {
171        self.as_inner()
172    }
173}
174
175impl<T: Serialize> Serialize for Json<T> {
176    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
177    where
178        S: ::serde::Serializer,
179    {
180        let encoded = encode_portable_json(&self.0).map_err(::serde::ser::Error::custom)?;
181        serializer.serialize_str(&encoded)
182    }
183}
184
185impl<'de, T: DeserializeOwned> ::serde::Deserialize<'de> for Json<T> {
186    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
187    where
188        D: ::serde::Deserializer<'de>,
189    {
190        let encoded = <String as ::serde::Deserialize>::deserialize(deserializer)?;
191        decode_portable_json(&encoded)
192            .map(Self)
193            .map_err(::serde::de::Error::custom)
194    }
195}
196
197/// Shared native bytes encoded as canonical padded Base64 on the wire.
198#[derive(Clone, Debug, Default, Eq, PartialEq)]
199pub struct Bytes(bytes::Bytes);
200
201impl Bytes {
202    /// Wraps an owned or shared byte buffer without copying when possible.
203    pub fn new(value: impl Into<bytes::Bytes>) -> Self {
204        Self(value.into())
205    }
206
207    /// Returns the byte slice.
208    #[must_use]
209    pub fn as_slice(&self) -> &[u8] {
210        &self.0
211    }
212
213    /// Returns the shared byte buffer without copying.
214    #[must_use]
215    pub fn into_shared(self) -> bytes::Bytes {
216        self.0
217    }
218
219    /// Returns an owned vector containing the bytes.
220    #[must_use]
221    pub fn into_vec(self) -> Vec<u8> {
222        self.0.to_vec()
223    }
224}
225
226impl From<Vec<u8>> for Bytes {
227    fn from(value: Vec<u8>) -> Self {
228        Self(value.into())
229    }
230}
231
232impl From<&[u8]> for Bytes {
233    fn from(value: &[u8]) -> Self {
234        Self(bytes::Bytes::copy_from_slice(value))
235    }
236}
237
238impl From<bytes::Bytes> for Bytes {
239    fn from(value: bytes::Bytes) -> Self {
240        Self(value)
241    }
242}
243
244impl From<Bytes> for bytes::Bytes {
245    fn from(value: Bytes) -> Self {
246        value.0
247    }
248}
249
250impl From<Bytes> for Vec<u8> {
251    fn from(value: Bytes) -> Self {
252        value.into_vec()
253    }
254}
255
256impl AsRef<[u8]> for Bytes {
257    fn as_ref(&self) -> &[u8] {
258        self.as_slice()
259    }
260}
261
262impl std::ops::Deref for Bytes {
263    type Target = [u8];
264
265    fn deref(&self) -> &Self::Target {
266        self.as_slice()
267    }
268}
269
270impl Serialize for Bytes {
271    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
272    where
273        S: ::serde::Serializer,
274    {
275        serializer.serialize_str(&STANDARD.encode(self.as_slice()))
276    }
277}
278
279impl<'de> ::serde::Deserialize<'de> for Bytes {
280    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
281    where
282        D: ::serde::Deserializer<'de>,
283    {
284        let encoded = <String as ::serde::Deserialize>::deserialize(deserializer)?;
285        STANDARD
286            .decode(encoded)
287            .map(Self::from)
288            .map_err(|_| ::serde::de::Error::custom("bytes must be canonical padded base64"))
289    }
290}
291
292/// A forward-compatible Capability-defined error unknown to this binding version.
293#[derive(Clone, Debug, PartialEq, Serialize, ::serde::Deserialize)]
294pub struct UnknownDomainError {
295    /// Stable Domain Error code.
296    pub code: String,
297    /// Optional opaque error payload.
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub payload: Option<serde_json::Value>,
300    /// Additional fields preserved for forward compatibility.
301    #[serde(default, flatten)]
302    pub extra: BTreeMap<String, serde_json::Value>,
303}
304
305/// Encodes a typed value after enforcing the portable JSON number profile.
306pub fn encode_portable_json<T: Serialize>(value: &T) -> Result<String, serde_json::Error> {
307    let value = serde_json::to_value(value)?;
308    validate_portable_json_value(&value).map_err(portable_json_error)?;
309    serde_json::to_string(&value)
310}
311
312/// Decodes a typed value after enforcing the portable JSON number profile.
313pub fn decode_portable_json<T: DeserializeOwned>(wire: &str) -> Result<T, serde_json::Error> {
314    let value: serde_json::Value = serde_json::from_str(wire)?;
315    validate_portable_json_value(&value).map_err(portable_json_error)?;
316    serde_json::from_value(value)
317}
318
319/// Validates recursively that ordinary JSON numbers are portable across runtimes.
320pub fn validate_portable_json_value(value: &serde_json::Value) -> Result<(), String> {
321    match value {
322        serde_json::Value::Number(number) => {
323            let safe = number.as_i64().is_some_and(|value| {
324                (-9_007_199_254_740_991..=9_007_199_254_740_991).contains(&value)
325            }) || number
326                .as_u64()
327                .is_some_and(|value| value <= 9_007_199_254_740_991)
328                || (number.is_f64()
329                    && number.as_f64().is_some_and(|value| {
330                        value.is_finite()
331                            && (value.abs() <= 9_007_199_254_740_991.0 || value.fract() != 0.0)
332                    }));
333            if !safe {
334                return Err("wire JSON contains an unsafe number".to_owned());
335            }
336        }
337        serde_json::Value::Array(values) => {
338            for value in values {
339                validate_portable_json_value(value)?;
340            }
341        }
342        serde_json::Value::Object(values) => {
343            for value in values.values() {
344                validate_portable_json_value(value)?;
345            }
346        }
347        serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::String(_) => {}
348    }
349    Ok(())
350}
351
352fn portable_json_error(detail: String) -> serde_json::Error {
353    serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidData, detail))
354}
355
356/// Serde helpers used by generated field attributes.
357pub mod serde {
358    /// Deserializes a required field while allowing generated structs to reject omission.
359    pub fn deserialize_required<'de, D, T>(deserializer: D) -> Result<T, D::Error>
360    where
361        D: ::serde::Deserializer<'de>,
362        T: ::serde::Deserialize<'de>,
363    {
364        <T as ::serde::Deserialize>::deserialize(deserializer)
365    }
366
367    /// Preserves the distinction between a missing field and an explicit `null`.
368    #[allow(clippy::option_option)]
369    pub fn deserialize_optional_value<'de, D, T>(
370        deserializer: D,
371    ) -> Result<Option<Option<T>>, D::Error>
372    where
373        D: ::serde::Deserializer<'de>,
374        T: ::serde::Deserialize<'de>,
375    {
376        Ok(Some(<Option<T> as ::serde::Deserialize>::deserialize(
377            deserializer,
378        )?))
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385
386    #[test]
387    fn bytes_preserve_canonical_padded_base64_wire_behavior() {
388        for length in 0..=258 {
389            let value = Bytes::from(
390                (0..length)
391                    .map(|index| u8::try_from(index % 256).unwrap())
392                    .collect::<Vec<_>>(),
393            );
394            let wire = serde_json::to_string(&value).unwrap();
395            assert_eq!(serde_json::from_str::<Bytes>(&wire).unwrap(), value);
396        }
397        for wire in [r#""not base64""#, r#""AQI""#, r#""AQJ=""#] {
398            assert!(serde_json::from_str::<Bytes>(wire).is_err());
399        }
400    }
401
402    #[test]
403    fn shared_bytes_round_trip_without_copying() {
404        let source = bytes::Bytes::from_static(b"shared");
405        let contract = Bytes::from(source.clone());
406        assert_eq!(contract.as_slice().as_ptr(), source.as_ptr());
407        let restored = contract.into_shared();
408        assert_eq!(restored.as_ptr(), source.as_ptr());
409    }
410
411    #[test]
412    fn portable_json_rejects_unsafe_integer_values() {
413        let error = decode_portable_json::<serde_json::Value>("9007199254740992").unwrap_err();
414        assert!(error.to_string().contains("unsafe number"));
415    }
416
417    #[test]
418    fn raw_json_validates_content_without_changing_the_string_wire_shape() {
419        let raw = RawJson::new(r#"{"ready":true}"#).unwrap();
420        assert_eq!(
421            serde_json::to_string(&raw).unwrap(),
422            r#""{\"ready\":true}""#
423        );
424        assert_eq!(
425            serde_json::from_str::<RawJson>(r#""{\"ready\":true}""#).unwrap(),
426            raw
427        );
428        assert!(RawJson::new("not JSON").is_err());
429        assert!(RawJson::new("9007199254740992").is_err());
430        assert_eq!(raw.as_ref(), r#"{"ready":true}"#);
431        assert_eq!(raw.to_string(), r#"{"ready":true}"#);
432    }
433
434    #[test]
435    fn typed_json_round_trips_as_one_json_string() {
436        let value = Json::new(BTreeMap::from([("answer".to_owned(), 42_i32)]));
437        let wire = serde_json::to_string(&value).unwrap();
438        assert_eq!(wire, r#""{\"answer\":42}""#);
439        assert_eq!(
440            serde_json::from_str::<Json<BTreeMap<String, i32>>>(&wire).unwrap(),
441            value
442        );
443    }
444}