1use serde_json::Value as JsonValue;
27
28use crate::descriptor::{
29 FieldDesc, IgnoredDefault, Strategy, TypeBody, TypeDesc, TypeId, TypeShape, VariantDesc,
30};
31use crate::error::{PanicKind, SoilError};
32use crate::json;
33use crate::Runtime;
34
35fn err(message: impl std::fmt::Display) -> SoilError {
36 SoilError::new(
37 PanicKind::DecodeError,
38 format!("descriptor JSON: {message}"),
39 )
40}
41
42pub fn load_descriptors(runtime: &mut Runtime, json: &str) -> Result<Vec<TypeId>, SoilError> {
49 let root: JsonValue =
50 serde_json::from_str(json).map_err(|e| err(format!("invalid JSON: {e}")))?;
51 let JsonValue::Array(entries) = root else {
52 return Err(err("expected a top-level array"));
53 };
54
55 let mut ids = Vec::with_capacity(entries.len());
57 for entry in &entries {
58 let name = get_str(entry, "name")?;
59 ids.push(runtime.registry.declare(name)?);
60 }
61
62 for (entry, id) in entries.iter().zip(&ids) {
64 let desc = parse_desc(runtime, entry)?;
65 runtime.registry.define(*id, desc)?;
66 }
67 Ok(ids)
68}
69
70pub fn descriptors_to_json(runtime: &Runtime, ids: &[TypeId]) -> Result<String, SoilError> {
73 let mut out = String::from("[");
74 for (i, id) in ids.iter().enumerate() {
75 if i > 0 {
76 out.push(',');
77 }
78 let desc = runtime.registry.get(*id)?;
79 out.push_str("{\"name\":");
80 push_str(&desc.name, &mut out);
81 out.push_str(",\"strategy\":{\"tag\":");
82 push_str(
83 match desc.strategy {
84 Strategy::Structural => "Structural",
85 Strategy::Opaque => "Opaque",
86 },
87 &mut out,
88 );
89 out.push_str("},\"body\":");
90 match &desc.body {
91 TypeBody::Opaque => out.push_str("{\"tag\":\"Opaque\"}"),
92 TypeBody::Record(fields) => {
93 out.push_str("{\"tag\":\"Record\",\"value\":[");
94 for (j, field) in fields.iter().enumerate() {
95 if j > 0 {
96 out.push(',');
97 }
98 out.push_str("{\"name\":");
99 push_str(&field.name, &mut out);
100 out.push_str(",\"shape\":");
101 push_shape(runtime, &field.shape, &mut out)?;
102 out.push_str(",\"ignored\":");
103 match &field.ignored {
104 None => out.push_str("null"),
105 Some(IgnoredDefault::Const(value)) => {
106 out.push_str("{\"tag\":\"Const\",\"value\":");
107 out.push_str(&json::encode(runtime, value)?);
108 out.push('}');
109 }
110 Some(IgnoredDefault::CopyField { name, .. }) => {
111 out.push_str("{\"tag\":\"CopyField\",\"value\":");
112 push_str(name, &mut out);
113 out.push('}');
114 }
115 Some(IgnoredDefault::Native(_)) => {
116 return Err(err(format!(
117 "field {} of {} has a native default, which is not serializable",
118 field.name, desc.name
119 )));
120 }
121 }
122 out.push('}');
123 }
124 out.push_str("]}");
125 }
126 TypeBody::Sum(variants) => {
127 out.push_str("{\"tag\":\"Sum\",\"value\":[");
128 for (j, variant) in variants.iter().enumerate() {
129 if j > 0 {
130 out.push(',');
131 }
132 out.push_str("{\"name\":");
133 push_str(&variant.name, &mut out);
134 out.push_str(",\"payload\":");
135 match &variant.payload {
136 None => out.push_str("null"),
137 Some(shape) => push_shape(runtime, shape, &mut out)?,
138 }
139 out.push('}');
140 }
141 out.push_str("]}");
142 }
143 }
144 out.push('}');
145 }
146 out.push(']');
147 Ok(out)
148}
149
150pub fn parse_shape_json(runtime: &Runtime, json: &str) -> Result<TypeShape, SoilError> {
154 let tree: JsonValue =
155 serde_json::from_str(json).map_err(|e| err(format!("invalid JSON: {e}")))?;
156 parse_shape(runtime, &tree)
157}
158
159fn push_str(s: &str, out: &mut String) {
160 json::encode::push_string(s, out);
161}
162
163fn push_shape(runtime: &Runtime, shape: &TypeShape, out: &mut String) -> Result<(), SoilError> {
164 let scalar = |tag: &str, out: &mut String| {
165 out.push_str("{\"tag\":\"");
166 out.push_str(tag);
167 out.push_str("\"}");
168 };
169 match shape {
170 TypeShape::I64 => scalar("I64", out),
171 TypeShape::U64 => scalar("U64", out),
172 TypeShape::I32 => scalar("I32", out),
173 TypeShape::U32 => scalar("U32", out),
174 TypeShape::I16 => scalar("I16", out),
175 TypeShape::U16 => scalar("U16", out),
176 TypeShape::I8 => scalar("I8", out),
177 TypeShape::U8 => scalar("U8", out),
178 TypeShape::BigInt => scalar("BigInt", out),
179 TypeShape::F64 => scalar("F64", out),
180 TypeShape::Utf8 => scalar("Utf8", out),
181 TypeShape::Bytes => scalar("Bytes", out),
182 TypeShape::Unit => scalar("Unit", out),
183 TypeShape::Closure => scalar("Closure", out),
184 TypeShape::List(elem) => {
185 out.push_str("{\"tag\":\"List\",\"value\":");
186 push_shape(runtime, elem, out)?;
187 out.push('}');
188 }
189 TypeShape::Map(key, value) => {
190 out.push_str("{\"tag\":\"Map\",\"value\":{\"key\":");
191 push_shape(runtime, key, out)?;
192 out.push_str(",\"value\":");
193 push_shape(runtime, value, out)?;
194 out.push_str("}}");
195 }
196 TypeShape::Named(id) => {
197 let name = match runtime.registry.get(*id) {
199 Ok(desc) => desc.name.clone(),
200 Err(e) => return Err(e),
201 };
202 out.push_str("{\"tag\":\"Named\",\"value\":");
203 push_str(&name, out);
204 out.push('}');
205 }
206 }
207 Ok(())
208}
209
210fn get_str<'a>(value: &'a JsonValue, key: &str) -> Result<&'a str, SoilError> {
211 value
212 .get(key)
213 .and_then(JsonValue::as_str)
214 .ok_or_else(|| err(format!("missing string {key:?}")))
215}
216
217fn get_tag(value: &JsonValue) -> Result<&str, SoilError> {
218 value
219 .get("tag")
220 .and_then(JsonValue::as_str)
221 .ok_or_else(|| err("missing \"tag\""))
222}
223
224fn parse_desc(runtime: &Runtime, entry: &JsonValue) -> Result<TypeDesc, SoilError> {
225 let name = get_str(entry, "name")?.to_string();
226 let strategy = match get_tag(
227 entry
228 .get("strategy")
229 .ok_or_else(|| err("missing strategy"))?,
230 )? {
231 "Structural" => Strategy::Structural,
232 "Opaque" => Strategy::Opaque,
233 other => return Err(err(format!("unknown strategy {other:?}"))),
234 };
235 let body_json = entry.get("body").ok_or_else(|| err("missing body"))?;
236 let body = match get_tag(body_json)? {
237 "Opaque" => TypeBody::Opaque,
238 "Record" => {
239 let JsonValue::Array(field_entries) = body_json
240 .get("value")
241 .ok_or_else(|| err("record body needs a value"))?
242 else {
243 return Err(err("record body value must be an array"));
244 };
245 let mut fields = Vec::with_capacity(field_entries.len());
246 for field_entry in field_entries {
247 let field_name = get_str(field_entry, "name")?.to_string();
248 let shape = parse_shape(
249 runtime,
250 field_entry
251 .get("shape")
252 .ok_or_else(|| err(format!("field {field_name} needs a shape")))?,
253 )?;
254 let ignored = match field_entry.get("ignored") {
255 None | Some(JsonValue::Null) => None,
256 Some(spec) => Some(parse_ignored(runtime, spec, &field_name, &shape)?),
257 };
258 fields.push(FieldDesc {
259 name: field_name,
260 shape,
261 ignored,
262 });
263 }
264 resolve_copy_indices(&mut fields)?;
265 TypeBody::Record(fields)
266 }
267 "Sum" => {
268 let JsonValue::Array(variant_entries) = body_json
269 .get("value")
270 .ok_or_else(|| err("sum body needs a value"))?
271 else {
272 return Err(err("sum body value must be an array"));
273 };
274 let mut variants = Vec::with_capacity(variant_entries.len());
275 for variant_entry in variant_entries {
276 let variant_name = get_str(variant_entry, "name")?.to_string();
277 let payload = match variant_entry.get("payload") {
278 None | Some(JsonValue::Null) => None,
279 Some(shape_json) => Some(parse_shape(runtime, shape_json)?),
280 };
281 variants.push(VariantDesc {
282 name: variant_name,
283 payload,
284 });
285 }
286 TypeBody::Sum(variants)
287 }
288 other => return Err(err(format!("unknown body tag {other:?}"))),
289 };
290 Ok(TypeDesc {
291 name,
292 strategy,
293 body,
294 })
295}
296
297fn parse_shape(runtime: &Runtime, shape_json: &JsonValue) -> Result<TypeShape, SoilError> {
298 let tag = get_tag(shape_json)?;
299 Ok(match tag {
300 "I64" => TypeShape::I64,
301 "U64" => TypeShape::U64,
302 "I32" => TypeShape::I32,
303 "U32" => TypeShape::U32,
304 "I16" => TypeShape::I16,
305 "U16" => TypeShape::U16,
306 "I8" => TypeShape::I8,
307 "U8" => TypeShape::U8,
308 "BigInt" => TypeShape::BigInt,
309 "F64" => TypeShape::F64,
310 "Utf8" => TypeShape::Utf8,
311 "Bytes" => TypeShape::Bytes,
312 "Unit" => TypeShape::Unit,
313 "Closure" => TypeShape::Closure,
314 "List" => TypeShape::List(Box::new(parse_shape(
315 runtime,
316 shape_json
317 .get("value")
318 .ok_or_else(|| err("List shape needs a value"))?,
319 )?)),
320 "Map" => {
321 let value = shape_json
322 .get("value")
323 .ok_or_else(|| err("Map shape needs a value"))?;
324 TypeShape::Map(
325 Box::new(parse_shape(
326 runtime,
327 value.get("key").ok_or_else(|| err("Map needs a key"))?,
328 )?),
329 Box::new(parse_shape(
330 runtime,
331 value.get("value").ok_or_else(|| err("Map needs a value"))?,
332 )?),
333 )
334 }
335 "Named" => {
336 let name = shape_json
337 .get("value")
338 .and_then(JsonValue::as_str)
339 .ok_or_else(|| err("Named shape needs a name"))?;
340 let id = runtime
341 .registry
342 .lookup(name)
343 .ok_or_else(|| err(format!("unknown type {name:?}")))?;
344 TypeShape::Named(id)
345 }
346 other => return Err(err(format!("unknown shape tag {other:?}"))),
347 })
348}
349
350fn parse_ignored(
351 runtime: &Runtime,
352 spec: &JsonValue,
353 field_name: &str,
354 shape: &TypeShape,
355) -> Result<IgnoredDefault, SoilError> {
356 match get_tag(spec)? {
357 "Const" => {
358 if !is_scalar(shape) {
359 return Err(err(format!(
360 "field {field_name}: Const defaults are limited to scalar shapes"
361 )));
362 }
363 let raw = spec
364 .get("value")
365 .ok_or_else(|| err(format!("field {field_name}: Const needs a value")))?
366 .to_string();
367 let value = json::decode(runtime, shape, &raw)?;
368 Ok(IgnoredDefault::Const(value))
369 }
370 "CopyField" => {
371 let target = spec
372 .get("value")
373 .and_then(JsonValue::as_str)
374 .ok_or_else(|| err(format!("field {field_name}: CopyField needs a field name")))?;
375 Ok(IgnoredDefault::CopyField {
376 name: target.to_string(),
377 index: usize::MAX, })
379 }
380 other => Err(err(format!(
381 "field {field_name}: unknown ignored-default tag {other:?}"
382 ))),
383 }
384}
385
386fn resolve_copy_indices(fields: &mut [FieldDesc]) -> Result<(), SoilError> {
387 let non_ignored: Vec<String> = fields
388 .iter()
389 .filter(|f| f.ignored.is_none())
390 .map(|f| f.name.clone())
391 .collect();
392 for field in fields.iter_mut() {
393 if let Some(IgnoredDefault::CopyField { name, index }) = &mut field.ignored {
394 *index = non_ignored.iter().position(|n| n == name).ok_or_else(|| {
395 err(format!(
396 "CopyField target {name:?} is not a non-ignored field"
397 ))
398 })?;
399 }
400 }
401 Ok(())
402}
403
404fn is_scalar(shape: &TypeShape) -> bool {
405 !matches!(
406 shape,
407 TypeShape::List(_) | TypeShape::Map(..) | TypeShape::Named(_) | TypeShape::Closure
408 )
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414 use crate::json::{decode, encode};
415 use crate::ops;
416 use crate::value::Value;
417
418 const SAMPLE: &str = r#"[
419 {"name": "Tree",
420 "strategy": {"tag": "Structural"},
421 "body": {"tag": "Sum", "value": [
422 {"name": "Leaf", "payload": {"tag": "I64"}},
423 {"name": "Node", "payload": {"tag": "List", "value": {"tag": "Named", "value": "Tree"}}}]}},
424 {"name": "Doc",
425 "strategy": {"tag": "Structural"},
426 "body": {"tag": "Record", "value": [
427 {"name": "text", "shape": {"tag": "Utf8"}, "ignored": null},
428 {"name": "copy", "shape": {"tag": "Utf8"}, "ignored": {"tag": "CopyField", "value": "text"}},
429 {"name": "version", "shape": {"tag": "U64"}, "ignored": {"tag": "Const", "value": 1}}]}},
430 {"name": "PyHandle",
431 "strategy": {"tag": "Opaque"},
432 "body": {"tag": "Opaque"}}
433 ]"#;
434
435 #[test]
436 fn load_recursive_and_round_trip() {
437 let mut rt = Runtime::new();
438 let ids = load_descriptors(&mut rt, SAMPLE).unwrap();
439 assert_eq!(ids.len(), 3);
440 assert_eq!(rt.registry.get(ids[0]).unwrap().name, "Tree");
441
442 let serialized = descriptors_to_json(&rt, &ids).unwrap();
443 let mut rt2 = Runtime::new();
446 let ids2 = load_descriptors(&mut rt2, &serialized).unwrap();
447 assert_eq!(descriptors_to_json(&rt2, &ids2).unwrap(), serialized);
448 }
449
450 #[test]
451 fn loaded_defaults_refill_on_decode() {
452 let mut rt = Runtime::new();
453 let ids = load_descriptors(&mut rt, SAMPLE).unwrap();
454 let doc_shape = TypeShape::Named(ids[1]);
455 let value = decode(&rt, &doc_shape, "{\"text\":\"hi\"}").unwrap();
456 let Value::Record(record) = &value else {
457 panic!()
458 };
459 assert!(ops::eq(&rt, &record.fields[1], &record.fields[0]).unwrap());
461 assert!(matches!(record.fields[2], Value::U64(1)));
462 assert_eq!(encode(&rt, &value).unwrap(), "{\"text\":\"hi\"}");
463 }
464
465 #[test]
466 fn unknown_named_type_rejected() {
467 let mut rt = Runtime::new();
468 let bad = r#"[{"name": "A", "strategy": {"tag": "Structural"},
469 "body": {"tag": "Record", "value": [
470 {"name": "x", "shape": {"tag": "Named", "value": "Missing"}, "ignored": null}]}}]"#;
471 assert!(load_descriptors(&mut rt, bad).is_err());
472 }
473
474 #[test]
475 fn const_defaults_must_be_scalar() {
476 let mut rt = Runtime::new();
477 let bad = r#"[{"name": "A", "strategy": {"tag": "Structural"},
478 "body": {"tag": "Record", "value": [
479 {"name": "xs", "shape": {"tag": "List", "value": {"tag": "I64"}},
480 "ignored": {"tag": "Const", "value": []}}]}}]"#;
481 assert!(load_descriptors(&mut rt, bad).is_err());
482 }
483
484 #[test]
485 fn native_defaults_do_not_serialize() {
486 let mut rt = Runtime::new();
487 let id = rt
488 .registry
489 .register(TypeDesc {
490 name: "N".to_string(),
491 strategy: Strategy::Structural,
492 body: TypeBody::Record(vec![FieldDesc {
493 name: "x".to_string(),
494 shape: TypeShape::U64,
495 ignored: Some(IgnoredDefault::Native(crate::value::Closure::native(
496 |_| Ok(Value::U64(0)),
497 ))),
498 }]),
499 })
500 .unwrap();
501 assert!(descriptors_to_json(&rt, &[id]).is_err());
502 }
503}