Skip to main content

act_sdk/
bytes.rs

1//! `Bytes` — a binary field that travels as a CBOR byte string (major type 2),
2//! projected to/from the canonical `{"$bytes":"<base64>"}` envelope on JSON
3//! transports. See the native binary encoding design (§3, §5.1).
4
5/// A binary value. Serializes to a CBOR byte string and deserializes **only**
6/// from a CBOR byte string — i.e. the `{"$bytes":"<base64>"}` envelope on JSON
7/// transports. A bare string is rejected: a string is text, not bytes.
8#[derive(Debug, Clone, PartialEq, Eq, Default)]
9pub struct Bytes(pub Vec<u8>);
10
11impl From<Vec<u8>> for Bytes {
12    fn from(v: Vec<u8>) -> Self {
13        Bytes(v)
14    }
15}
16
17impl From<Bytes> for Vec<u8> {
18    fn from(b: Bytes) -> Self {
19        b.0
20    }
21}
22
23impl AsRef<[u8]> for Bytes {
24    fn as_ref(&self) -> &[u8] {
25        &self.0
26    }
27}
28
29impl serde::Serialize for Bytes {
30    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
31        serializer.serialize_bytes(&self.0)
32    }
33}
34
35impl<'de> serde::Deserialize<'de> for Bytes {
36    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
37        struct BytesVisitor;
38
39        impl serde::de::Visitor<'_> for BytesVisitor {
40            type Value = Bytes;
41
42            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
43                f.write_str("a CBOR byte string (a {\"$bytes\":…} envelope on JSON)")
44            }
45
46            fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Bytes, E> {
47                Ok(Bytes(v.to_vec()))
48            }
49
50            fn visit_byte_buf<E: serde::de::Error>(self, v: Vec<u8>) -> Result<Bytes, E> {
51                Ok(Bytes(v))
52            }
53        }
54
55        // deserialize_any dispatches a byte string to visit_bytes/visit_byte_buf;
56        // any other type (text, number, …) hits the Visitor default and errors.
57        deserializer.deserialize_any(BytesVisitor)
58    }
59}
60
61impl schemars::JsonSchema for Bytes {
62    fn schema_name() -> std::borrow::Cow<'static, str> {
63        "Bytes".into()
64    }
65
66    // Always inline so a `Bytes` field shows the envelope shape directly in the
67    // tool's parameters-schema, rather than a `$ref` into `$defs`.
68    fn inline_schema() -> bool {
69        true
70    }
71
72    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
73        // Binary travels only as the canonical {"$bytes":"<base64>"} envelope.
74        schemars::json_schema!({
75            "type": "object",
76            "description": "Binary value as a base64 byte-string envelope.",
77            "properties": {
78                "$bytes": { "type": "string", "contentEncoding": "base64" }
79            },
80            "required": ["$bytes"],
81            "additionalProperties": false
82        })
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    fn cbor_of(v: &ciborium::value::Value) -> Vec<u8> {
91        let mut buf = Vec::new();
92        ciborium::into_writer(v, &mut buf).unwrap();
93        buf
94    }
95
96    #[test]
97    fn serializes_to_cbor_byte_string() {
98        let mut buf = Vec::new();
99        ciborium::into_writer(&Bytes(b"hello".to_vec()), &mut buf).unwrap();
100        let value: ciborium::value::Value = ciborium::from_reader(&buf[..]).unwrap();
101        assert_eq!(value, ciborium::value::Value::Bytes(b"hello".to_vec()));
102    }
103
104    #[test]
105    fn deserializes_from_cbor_byte_string() {
106        let cbor = cbor_of(&ciborium::value::Value::Bytes(b"hi".to_vec()));
107        let b: Bytes = ciborium::from_reader(&cbor[..]).unwrap();
108        assert_eq!(b.0, b"hi");
109    }
110
111    #[test]
112    fn rejects_text_string() {
113        // A bare string is text, not bytes — Bytes accepts only a byte string.
114        let cbor = cbor_of(&ciborium::value::Value::Text("aGVsbG8=".into()));
115        let r: Result<Bytes, _> = ciborium::from_reader(&cbor[..]);
116        assert!(r.is_err());
117    }
118
119    #[test]
120    fn schema_is_bytes_envelope() {
121        let schema = schemars::schema_for!(Bytes);
122        let v = serde_json::to_value(&schema).unwrap();
123        assert_eq!(v.get("type").and_then(|x| x.as_str()), Some("object"));
124        assert_eq!(
125            v["properties"]["$bytes"]["contentEncoding"].as_str(),
126            Some("base64")
127        );
128        assert_eq!(v["required"][0].as_str(), Some("$bytes"));
129    }
130
131    #[derive(serde::Deserialize, schemars::JsonSchema)]
132    struct Params {
133        data: Bytes,
134    }
135
136    #[test]
137    fn bytes_field_composes_with_derive() {
138        let schema = schemars::schema_for!(Params);
139        let v = serde_json::to_value(&schema).unwrap();
140        // The field's schema is the inlined $bytes envelope (no $ref).
141        assert_eq!(
142            v["properties"]["data"]["properties"]["$bytes"]["contentEncoding"],
143            "base64"
144        );
145
146        // Decode: a CBOR map {"data": h'6869'} → Params { data: Bytes(b"hi") }.
147        let cbor = cbor_of(&ciborium::value::Value::Map(vec![(
148            ciborium::value::Value::Text("data".into()),
149            ciborium::value::Value::Bytes(b"hi".to_vec()),
150        )]));
151        let p: Params = ciborium::from_reader(&cbor[..]).unwrap();
152        assert_eq!(p.data.0, b"hi");
153    }
154}