1#![deny(unsafe_code)]
6#![warn(missing_docs)]
7
8mod document;
9mod hash;
10mod value;
11
12pub use document::{PirBinding, PirDocument, PirRoot};
13pub use hash::content_hash;
14pub use value::{PirInteger, PirMapEntry, PirValue};
15
16use parallax_core::{ErrorCode, ParallaxError, PIR_SCHEMA_VERSION};
17
18pub const SCHEMA_VERSION: u32 = PIR_SCHEMA_VERSION;
20
21pub type Result<T> = parallax_core::Result<T>;
23
24pub fn to_json_bytes(doc: &PirDocument) -> Result<Vec<u8>> {
26 serde_json::to_vec_pretty(doc).map_err(|e| {
27 ParallaxError::new(ErrorCode::SerializationFailure, e.to_string())
28 .with_source("parallax-ir")
29 .with_operation("to_json_bytes")
30 })
31}
32
33pub fn to_json_bytes_compact(doc: &PirDocument) -> Result<Vec<u8>> {
35 serde_json::to_vec(doc).map_err(|e| {
36 ParallaxError::new(ErrorCode::SerializationFailure, e.to_string())
37 .with_source("parallax-ir")
38 .with_operation("to_json_bytes_compact")
39 })
40}
41
42pub fn from_json_bytes(bytes: &[u8]) -> Result<PirDocument> {
44 let doc: PirDocument = serde_json::from_slice(bytes).map_err(|e| {
45 ParallaxError::new(ErrorCode::SerializationFailure, e.to_string())
46 .with_source("parallax-ir")
47 .with_operation("from_json_bytes")
48 })?;
49 doc.validate()?;
50 Ok(doc)
51}
52
53pub fn value_from_json(v: &serde_json::Value) -> Result<PirValue> {
55 PirValue::from_json(v)
56}
57
58pub fn value_to_json(v: &PirValue) -> serde_json::Value {
60 v.to_json()
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use indexmap::IndexMap;
67 use parallax_core::{ErrorCode, ObjectId};
68
69 #[test]
70 fn rejects_future_schema_on_deserialize() {
71 let raw = br#"{"schema":99,"bindings":{},"objects":{},"roots":[],"metadata":{}}"#;
72 let err = from_json_bytes(raw).unwrap_err();
73 assert_eq!(err.code, ErrorCode::InvalidSnapshot);
74 assert!(err.message.contains("unsupported PIR schema"));
75 }
76
77 #[test]
78 fn rejects_zero_schema() {
79 let mut doc = PirDocument::new();
80 doc.schema = 0;
81 assert!(doc.validate().is_err());
82 }
83
84 #[test]
85 fn round_trip_demo_state() {
86 let mut fields: IndexMap<String, PirValue> = IndexMap::new();
87 fields.insert("username".into(), PirValue::string("Ada"));
88 fields.insert("score".into(), PirValue::int_i64(42));
89 fields.insert(
90 "projects".into(),
91 PirValue::list(vec![
92 PirValue::string("compiler"),
93 PirValue::string("runtime"),
94 PirValue::string("vm"),
95 ]),
96 );
97 let state = PirValue::Map {
98 entries: fields
99 .into_iter()
100 .map(|(k, v)| PirMapEntry {
101 key: PirValue::string(k),
102 value: v,
103 })
104 .collect(),
105 };
106 let mut doc = PirDocument::new();
107 doc.set_binding("state", state);
108 let bytes = to_json_bytes(&doc).unwrap();
109 let restored = from_json_bytes(&bytes).unwrap();
110 assert_eq!(doc.bindings.len(), restored.bindings.len());
111 let s = restored.binding("state").unwrap();
112 match s {
113 PirValue::Map { entries } => {
114 assert_eq!(entries.len(), 3);
115 }
116 other => panic!("expected map, got {other:?}"),
117 }
118 }
119
120 #[test]
121 fn non_finite_float_round_trips_without_becoming_null() {
122 for (v, token) in [
123 (f64::NAN, "NaN"),
124 (f64::INFINITY, "Infinity"),
125 (f64::NEG_INFINITY, "-Infinity"),
126 ] {
127 let pir = PirValue::Float { v };
128 let json = pir.to_json();
129 assert_eq!(json["t"], "float");
130 assert_eq!(json["v"], token);
131 let restored = PirValue::from_json(&json).unwrap();
132 match restored {
133 PirValue::Float { v: got } => {
134 if v.is_nan() {
135 assert!(got.is_nan());
136 } else {
137 assert_eq!(got, v);
138 }
139 }
140 other => panic!("expected float, got {other:?}"),
141 }
142 }
143 }
144
145 #[test]
146 fn summary_handles_multibyte_prefix_without_panic() {
147 let s = "café".repeat(20);
149 let summary = PirValue::string(s).summary();
150 assert!(summary.starts_with('"'));
151 assert!(summary.ends_with('"') || summary.contains('…'));
152 }
153
154 #[test]
155 fn validate_rejects_dangling_refs() {
156 let mut doc = PirDocument::new();
157 doc.set_binding(
158 "x",
159 PirValue::Ref {
160 id: ObjectId::new(99),
161 },
162 );
163 let err = doc.validate().unwrap_err();
164 assert_eq!(err.code, ErrorCode::InvalidSnapshot);
165 }
166}