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}