fog_human_json/
lib.rs

1//! This crate provides functions to go back and forth between fog-pack and JSON, 
2//! making it relatively easy for users to view pretty-printed fog-pack values and 
3//! edit them with existing JSON tooling. A common complaint with binary data 
4//! formats like fog-pack is that reading them is painful, and lowering that pain 
5//! with JSON is exactly what this crate is for.
6//! 
7//! This is *not* a crate for turning regular JSON into fog-pack data. It uses a 
8//! number of special string prefixes to encode fog-pack types in JSON, which can 
9//! interfere with arbitrary JSON-to-fog conversions.
10//! 
11//! So, what does this actually do for conversion? Well, it takes each fog-pack type 
12//! and either directly converts it to a corresponding JSON type, or it specially 
13//! encodes it in a string that starts with `$fog-`. So a 32-bit floating point 
14//! value could be specifically encoded as `$fog-F32: 1.23`. The full list of types 
15//! is:
16//! 
17//! - Str: A regular string. This is just prepended so fog-pack strings that start 
18//!   with `$fog-` won't get caught by the parser.
19//! - Bin: Encodes the binary data as Base64 using the "standard" encoding (bonus 
20//!   symbols of `+/`, no padding used, padding is accepted when parsing).
21//! - F32Hex / F64Hex: Encodes a binary32/64 IEEE floating-point value in big-endian hex. 
22//!   The fog-to-json process should only do this when writing out a NaN or 
23//!   Infinity.
24//! - F32 / F64 / Int: Prints a standard JSON Number, but includes the type 
25//!   information. This done by telling the converter to do it specifically, by a 
26//!   user adding type information, or by the converter for any F32 value (as 
27//!   `serde_json` will always use F64 for floating-point).
28//! - Time: Encodes the time as a RFC 3339 formatted string.
29//! - Hash / Identity / StreamId / LockId: Encodes the corresponding primitive as a 
30//!   base58 string (in the Bitcoin base58 style).
31//! - DataLockbox / IdentityLockbox / StreamLockbox / LockLockbox: Encodes the 
32//!   corresponding lockbox as Base64 data, just like with the "Bin" type.
33//! 
34//! That covers conversion between fog-pack Values and JSON values, but not 
35//! Documents and Entries. Those are converted into JSON objects with the following 
36//! key-value pairs:
37//! 
38//! - Documents:
39//!   - "schema": If present, a `$fog-Hash:HASH` with the schema.
40//!   - "signer": If present, a `$fog-Identity:IDENTITY` with the signer's 
41//!     Identity. 
42//!   - "compression": If not present, uses default compression. If present and 
43//!     null, no compression is used. If set to a number between 0-255, uses that 
44//!     as the compression level.
45//!   - "data": The document content. Must be present.
46//! - Entries:
47//!   - "parent": Parent document's hash.
48//!   - "key": Entry's string key.
49//!   - "signer": If present, holds the signer's Identity.
50//!   - "compression": If not present, uses default compression. If present and 
51//!     null, no compression is used. If set to a number between 0 & 255, uses that 
52//!     as the compression level.
53//!   - "data": The entry content. Must be present.
54//! 
55//! When going from JSON to a Document or Entry, if there's a "signer" specified, an intermediate 
56//! struct will be provided that must be signed by a 
57//! [`IdentityKey`][fog_crypto::identity::IdentityKey] that matches the signer.
58//!
59//! As an example, let's take a struct that looks the one below, put it into a document, and look 
60//! at the resulting JSON:
61//!
62//! ```
63//! # use std::collections::BTreeMap;
64//! # use fog_crypto::identity::IdentityKey;
65//! # use serde::{Serialize, Deserialize};
66//! use fog_pack::{types::*, schema::NoSchema};
67//! use fog_human_json::*;
68//!
69//! #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
70//! struct Test {
71//!     boolean: bool,
72//!     int: i64,
73//!     float32: f32,
74//!     float64: f64,
75//!     #[serde(with = "serde_bytes")]
76//!     bin: Vec<u8>,
77//!     string: String,
78//!     array: Vec<u32>,
79//!     map: BTreeMap<String, u32>,
80//!     time: Timestamp,
81//!     hash: Hash,
82//!     id: Identity,
83//! }
84//! 
85//! // ... Fill it up with some data ...
86//! # let bin = vec![0u8,1,2,3,4];
87//! # let hash = Hash::new(&bin);
88//! # let mut map = BTreeMap::new();
89//! # map.insert(String::from("a"), 1u32);
90//! # map.insert(String::from("b"), 2u32);
91//! # map.insert(String::from("c"), 3u32);
92//! # let time = Timestamp::now().unwrap();
93//! #
94//! # let mut rng = rand::thread_rng();
95//! # let id_key = IdentityKey::new_temp(&mut rng);
96//! # let id = id_key.id().clone();
97//! #
98//! # let test = Test {
99//! #     boolean: false,
100//! #     int: -12345,
101//! #     float32: 0.0f32,
102//! #     float64: 0.0f64,
103//! #     bin,
104//! #     string: "hello".into(),
105//! #     array: vec![0,1,2,3,4],
106//! #     map,
107//! #     time,
108//! #     hash,
109//! #     id,
110//! # };
111//! #
112//! let test: Test = test;
113//!
114//! let doc = fog_pack::document::NewDocument::new(None, &test).unwrap();
115//! let doc = NoSchema::validate_new_doc(doc).unwrap();
116//!
117//! let json_val = doc_to_json(&doc);
118//! let json_raw = serde_json::to_string_pretty(&json_val).expect("JSON Value to raw string");
119//! ```
120//! 
121//! The resulting JSON could look something like:
122//!
123//! ```text
124//! {
125//!   "data": {
126//!     "array": [ 0, 1, 2, 3, 4 ],
127//!     "bin": "$fog-Bin:AAECAwQ",
128//!     "boolean": false,
129//!     "float32": "$fog-F32:0.0",
130//!     "float64": 0.0,
131//!     "hash": "$fog-Hash:R7KEBd4fxeYgDtoivjDUK97HwEcL7k7hm3qjPhZFEhzL",
132//!     "id": "$fog-Identity:T4MvqAy6RVR2J8efJzgQW9xN9Z8avJBFEmefuSnBMWQP",
133//!     "int": -12345,
134//!     "lock": "$fog-LockId:ME7DmA9ADSYE6sq8SRvQ2ncd1kosQZoZqG7XiCFX55Uz",
135//!     "map": {
136//!       "a": 1,
137//!       "b": 2,
138//!       "c": 3
139//!     },
140//!     "stream_id": "$fog-StreamId:U4sLqPrtAgzKbUVnr47PcgPT2Rq3D9kBqrvhZ9NvCTvq",
141//!     "string": "hello",
142//!     "time": "$fog-Time:2023-07-12T17:33:13.454466675Z"
143//!   }
144//! }
145//! ```
146//!
147
148use thiserror::Error;
149
150type FogValue = fog_pack::types::Value;
151type FogValueRef<'a> = fog_pack::types::ValueRef<'a>;
152type JsonValue = serde_json::Value;
153type JsonNumber = serde_json::Number;
154type JsonMap = serde_json::Map<String, JsonValue>;
155const FOG_PREFIX: &str = "$fog-";
156
157mod enc;
158mod dec;
159mod doc;
160mod entry;
161mod query;
162
163use std::collections::BTreeMap;
164
165pub use enc::{fog_to_json, fogref_to_json};
166pub use dec::{json_to_fog, DecodeError};
167pub use doc::*;
168pub use entry::*;
169pub use query::*;
170
171/// An error that occurred while converting from JSON to a fog-pack object, like a Document or 
172/// Entry.
173#[derive(Clone, Debug, Error)]
174pub enum ObjectError {
175    /// Data conversion failed for a particular key-value pair in the object
176    #[error("Data conversion failed for key {key}")]
177    Decode {
178        key: &'static str,
179        #[source]
180        src: DecodeError,
181    },
182    /// Expected a different data type
183    #[error("Wrong data type for key \"{0}\"")]
184    WrongDataType(&'static str),
185    /// The root JSON value wasn't an Object as expected
186    #[error("Expected a root Object for Doc/Entry/Query conversion")]
187    NotAnObject,
188    /// The object contained an unexpected key-value pair
189    #[error("Unrecognized key (\"{0}\") while parsing fog-pack Object")]
190    UnrecognizedKey(String),
191    /// Missing one of the required key-value pairs for this fog-pack object
192    #[error("Missing required key \"{0}\" for root object")]
193    MissingKey(&'static str),
194    /// Couldn't form the final result for some fog-pack specific reason
195    #[error("Failed to form the fog-pack result")]
196    FogPack(#[from] fog_pack::error::Error),
197    /// The provided key was incorrect
198    #[error("Incorrect Identity Key for signing, needed {0}")]
199    IncorrectIdentityKey(Box<fog_pack::types::Identity>),
200}
201
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use fog_crypto::{identity::IdentityKey, stream::StreamKey, lock::LockKey};
207    use fog_pack::{types::*, schema::NoSchema};
208    use serde::{Deserialize, Serialize};
209
210    #[test]
211    fn back_and_forth() {
212
213        #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
214        struct Test {
215            boolean: bool,
216            int: i64,
217            float32: f32,
218            float64: f64,
219            #[serde(with = "serde_bytes")]
220            bin: Vec<u8>,
221            string: String,
222            array: Vec<u32>,
223            map: BTreeMap<String, u32>,
224            time: Timestamp,
225            hash: Hash,
226            id: Identity,
227            stream_id: StreamId,
228            lock: LockId,
229            databox: DataLockbox
230        }
231
232        
233        let bin = vec![0u8,1,2,3,4];
234        let hash = Hash::new(&bin);
235        let mut map = BTreeMap::new();
236        map.insert(String::from("a"), 1u32);
237        map.insert(String::from("b"), 2u32);
238        map.insert(String::from("c"), 3u32);
239        let time = Timestamp::now().unwrap();
240
241        let mut rng = rand::thread_rng();
242        let id_key = IdentityKey::new_temp(&mut rng);
243        let id = id_key.id().clone();
244        let stream_key = StreamKey::new_temp(&mut rng);
245        let stream_id = stream_key.id().clone();
246        let lock_key = LockKey::new_temp(&mut rng);
247        let lock = lock_key.id().clone();
248        let databox = lock.encrypt_data(&mut rng, &[0u8, 1, 2, 3]);
249
250        let test = Test {
251            boolean: false,
252            int: -12345,
253            float32: 0.0f32,
254            float64: 0.0f64,
255            bin,
256            string: "hello".into(),
257            array: vec![0,1,2,3,4],
258            map,
259            time,
260            hash,
261            id,
262            stream_id,
263            lock,
264            databox,
265        };
266
267        let doc = fog_pack::document::NewDocument::new(None, &test).unwrap();
268        let doc = NoSchema::validate_new_doc(doc).unwrap();
269
270        let json_val = doc_to_json(&doc);
271        let json_raw = serde_json::to_string(&json_val).expect("JSON Value to raw string");
272        let parsed_json: JsonValue = serde_json::from_str(&json_raw).expect("Parsed JSON");
273        assert_eq!( json_val, parsed_json);
274
275        let parsed_doc = json_to_doc(&parsed_json).expect("JSON to document");
276        let MaybeDocument::NewDocument(parsed_doc) = parsed_doc else {
277            panic!("Document shouldn't have needed signing")
278        };
279        assert_eq!(parsed_doc.hash(), doc.hash());
280
281        let parsed_doc = NoSchema::validate_new_doc(parsed_doc).expect("Validated document");
282        let roundtrip_test: Test = parsed_doc.deserialize().expect("Deserialized correctly");
283
284        assert!(roundtrip_test == test);
285    }
286}