diff --git a/src/ariadne/clew.rs b/src/ariadne/clew.rs index 94bb6f7..3831715 100644 --- a/src/ariadne/clew.rs +++ b/src/ariadne/clew.rs @@ -3,7 +3,7 @@ use std::error; use std::fmt; use serde::Deserialize; -use serde_json::{Map, Number}; +use serde_json::Number; const SCHEMA: &str = "clew-renpy-ast-v1"; @@ -55,41 +55,88 @@ pub struct Document { pub roots: Value, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(untagged)] pub enum Value { Null, Bool(bool), Number(Number), String(String), - List(Vec), - Tuple(Vec), - Dict(Vec), - Object(Object), - Ref { id: u64 }, - Function(Function), - Method(Method), - PythonType(PythonType), - Repr { class_name: String, repr: String }, - Bytes { repr: String }, - Truncated { class_name: String, repr: String }, - Recursion { class_name: String }, - AttributeError { class_name: String, repr: String }, + Tagged(TaggedValue), } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(tag = "type")] +pub enum TaggedValue { + #[serde(rename = "list")] + List { items: Vec }, + + #[serde(rename = "tuple")] + Tuple { items: Vec }, + + #[serde(rename = "dict")] + Dict { items: Vec }, + + #[serde(rename = "object")] + Object { + id: u64, + + #[serde(rename = "class")] + class_name: String, + + attributes: BTreeMap, + }, + + #[serde(rename = "ref")] + Ref { id: u64 }, + + #[serde(rename = "function")] + Function(Function), + + #[serde(rename = "method")] + Method(Method), + + #[serde(rename = "python-type")] + PythonType(PythonType), + + #[serde(rename = "repr")] + Repr { + #[serde(rename = "class")] + class_name: String, + repr: String, + }, + + #[serde(rename = "bytes")] + Bytes { repr: String }, + + #[serde(rename = "truncated")] + Truncated { + #[serde(rename = "class")] + class_name: String, + repr: String, + }, + + #[serde(rename = "recursion")] + Recursion { + #[serde(rename = "class")] + class_name: String, + }, + + #[serde(rename = "attribute-error")] + AttributeError { + #[serde(rename = "class")] + class_name: String, + repr: String, + }, +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] pub struct DictEntry { pub key: Value, pub value: Value, } -#[derive(Debug, Clone, PartialEq)] -pub struct Object { - pub id: u64, - pub class_name: String, - pub attributes: BTreeMap, -} - -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Deserialize, PartialEq)] pub struct Function { pub name: Option, pub module: Option, @@ -97,7 +144,7 @@ pub struct Function { pub firstlineno: Option, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Deserialize, PartialEq)] pub struct Method { pub name: Option, pub module: Option, @@ -106,7 +153,7 @@ pub struct Method { pub receiver_class: Option, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Deserialize, PartialEq)] pub struct PythonType { pub name: Option, pub module: Option, @@ -122,160 +169,3 @@ pub fn from_bytes(bytes: &[u8]) -> Result { } Ok(document) } - -impl<'de> Deserialize<'de> for Value { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let value = serde_json::Value::deserialize(deserializer)?; - parse_value(value).map_err(serde::de::Error::custom) - } -} - -fn parse_value(value: serde_json::Value) -> Result { - match value { - serde_json::Value::Null => Ok(Value::Null), - serde_json::Value::Bool(value) => Ok(Value::Bool(value)), - serde_json::Value::Number(value) => Ok(Value::Number(value)), - serde_json::Value::String(value) => Ok(Value::String(value)), - serde_json::Value::Array(_) => Err("Clew values do not use untagged arrays".to_string()), - serde_json::Value::Object(mut object) => parse_tagged_object(&mut object), - } -} - -fn parse_tagged_object(object: &mut Map) -> Result { - let tag = take_string(object, "type")?; - match tag.as_str() { - "list" => Ok(Value::List(take_items(object)?)), - "tuple" => Ok(Value::Tuple(take_items(object)?)), - "dict" => Ok(Value::Dict(take_dict_items(object)?)), - "object" => Ok(Value::Object(Object { - id: take_u64(object, "id")?, - class_name: take_string(object, "class")?, - attributes: take_attributes(object)?, - })), - "ref" => Ok(Value::Ref { - id: take_u64(object, "id")?, - }), - "function" => Ok(Value::Function(Function { - name: take_optional_string(object, "name")?, - module: take_optional_string(object, "module")?, - filename: take_optional_string(object, "filename")?, - firstlineno: take_optional_number(object, "firstlineno")?, - })), - "method" => Ok(Value::Method(Method { - name: take_optional_string(object, "name")?, - module: take_optional_string(object, "module")?, - filename: take_optional_string(object, "filename")?, - firstlineno: take_optional_number(object, "firstlineno")?, - receiver_class: take_optional_string(object, "receiver_class")?, - })), - "python-type" => Ok(Value::PythonType(PythonType { - name: take_optional_string(object, "name")?, - module: take_optional_string(object, "module")?, - })), - "repr" => Ok(Value::Repr { - class_name: take_string(object, "class")?, - repr: take_string(object, "repr")?, - }), - "bytes" => Ok(Value::Bytes { - repr: take_string(object, "repr")?, - }), - "truncated" => Ok(Value::Truncated { - class_name: take_string(object, "class")?, - repr: take_string(object, "repr")?, - }), - "recursion" => Ok(Value::Recursion { - class_name: take_string(object, "class")?, - }), - "attribute-error" => Ok(Value::AttributeError { - class_name: take_string(object, "class")?, - repr: take_string(object, "repr")?, - }), - _ => Err(format!("unknown Clew value type: {tag}")), - } -} - -fn take_items(object: &mut Map) -> Result, String> { - match object.remove("items") { - Some(serde_json::Value::Array(items)) => items.into_iter().map(parse_value).collect(), - Some(_) => Err("Clew items field must be an array".to_string()), - None => Err("missing Clew items field".to_string()), - } -} - -fn take_dict_items(object: &mut Map) -> Result, String> { - match object.remove("items") { - Some(serde_json::Value::Array(items)) => items.into_iter().map(parse_dict_entry).collect(), - Some(_) => Err("Clew dict items field must be an array".to_string()), - None => Err("missing Clew dict items field".to_string()), - } -} - -fn parse_dict_entry(value: serde_json::Value) -> Result { - let serde_json::Value::Object(mut object) = value else { - return Err("Clew dict entry must be an object".to_string()); - }; - let key = object.remove("key").ok_or("missing Clew dict entry key")?; - let value = object - .remove("value") - .ok_or("missing Clew dict entry value")?; - Ok(DictEntry { - key: parse_value(key)?, - value: parse_value(value)?, - }) -} - -fn take_attributes( - object: &mut Map, -) -> Result, String> { - match object.remove("attributes") { - Some(serde_json::Value::Object(attributes)) => attributes - .into_iter() - .map(|(key, value)| parse_value(value).map(|value| (key, value))) - .collect(), - Some(_) => Err("Clew object attributes field must be an object".to_string()), - None => Err("missing Clew object attributes field".to_string()), - } -} - -fn take_string(object: &mut Map, field: &str) -> Result { - match object.remove(field) { - Some(serde_json::Value::String(value)) => Ok(value), - Some(_) => Err(format!("Clew {field} field must be a string")), - None => Err(format!("missing Clew {field} field")), - } -} - -fn take_optional_string( - object: &mut Map, - field: &str, -) -> Result, String> { - match object.remove(field) { - Some(serde_json::Value::String(value)) => Ok(Some(value)), - Some(serde_json::Value::Null) | None => Ok(None), - Some(_) => Err(format!("Clew {field} field must be a string or null")), - } -} - -fn take_optional_number( - object: &mut Map, - field: &str, -) -> Result, String> { - match object.remove(field) { - Some(serde_json::Value::Number(value)) => Ok(Some(value)), - Some(serde_json::Value::Null) | None => Ok(None), - Some(_) => Err(format!("Clew {field} field must be a number or null")), - } -} - -fn take_u64(object: &mut Map, field: &str) -> Result { - match object.remove(field) { - Some(serde_json::Value::Number(value)) => value - .as_u64() - .ok_or_else(|| format!("Clew {field} field must be a non-negative integer")), - Some(_) => Err(format!("Clew {field} field must be a number")), - None => Err(format!("missing Clew {field} field")), - } -} diff --git a/src/ariadne/clew_test.rs b/src/ariadne/clew_test.rs index 9462859..1d67377 100644 --- a/src/ariadne/clew_test.rs +++ b/src/ariadne/clew_test.rs @@ -1,4 +1,4 @@ -use ariadne::clew::{self, DictEntry, Error, Value}; +use ariadne::clew::{self, DictEntry, Error, TaggedValue, Value}; use serde_json::Number; fn document_with(roots: &str) -> String { @@ -56,13 +56,15 @@ fn parses_primitive_values() { assert_eq!( document.roots, - Value::List(vec![ - Value::Null, - Value::Bool(true), - Value::Number(Number::from(7)), - Value::Number(Number::from_f64(1.5).unwrap()), - Value::String("text".to_string()), - ]) + Value::Tagged(TaggedValue::List { + items: vec![ + Value::Null, + Value::Bool(true), + Value::Number(Number::from(7)), + Value::Number(Number::from_f64(1.5).unwrap()), + Value::String("text".to_string()), + ] + }) ); } @@ -76,13 +78,17 @@ fn parses_tuple_and_dict_values() { assert_eq!( document.roots, - Value::Dict(vec![DictEntry { - key: Value::String("names".to_string()), - value: Value::Tuple(vec![ - Value::String("a".to_string()), - Value::String("b".to_string()) - ]), - }]) + Value::Tagged(TaggedValue::Dict { + items: vec![DictEntry { + key: Value::String("names".to_string()), + value: Value::Tagged(TaggedValue::Tuple { + items: vec![ + Value::String("a".to_string()), + Value::String("b".to_string()) + ] + }), + }] + }) ); } @@ -96,20 +102,22 @@ fn parses_object_and_reference_values() { ) .unwrap(); - let Value::List(items) = document.roots else { + let Value::Tagged(TaggedValue::List { items }) = document.roots else { panic!("roots should be a list"); }; assert_eq!(items.len(), 2); - assert_eq!(items[1], Value::Ref { id: 1 }); - let Value::Object(object) = &items[0] else { + assert_eq!(items[1], Value::Tagged(TaggedValue::Ref { id: 1 })); + let Value::Tagged(TaggedValue::Object { + id, + class_name, + attributes, + }) = &items[0] + else { panic!("first item should be an object"); }; - assert_eq!(object.id, 1); - assert_eq!(object.class_name, "renpy.ast.Label"); - assert_eq!( - object.attributes["name"], - Value::String("start".to_string()) - ); + assert_eq!(*id, 1); + assert_eq!(class_name, "renpy.ast.Label"); + assert_eq!(attributes["name"], Value::String("start".to_string())); } #[test] @@ -122,18 +130,39 @@ fn parses_special_tag_values() { ) .unwrap(); - let Value::List(items) = document.roots else { + let Value::Tagged(TaggedValue::List { items }) = document.roots else { panic!("roots should be a list"); }; assert_eq!(items.len(), 8); - assert!(matches!(items[0], Value::Function(_))); - assert!(matches!(items[1], Value::Method(_))); - assert!(matches!(items[2], Value::PythonType(_))); - assert!(matches!(items[3], Value::Repr { .. })); - assert!(matches!(items[4], Value::Bytes { .. })); - assert!(matches!(items[5], Value::Truncated { .. })); - assert!(matches!(items[6], Value::Recursion { .. })); - assert!(matches!(items[7], Value::AttributeError { .. })); + assert!(matches!( + items[0], + Value::Tagged(TaggedValue::Function(_)) + )); + assert!(matches!(items[1], Value::Tagged(TaggedValue::Method(_)))); + assert!(matches!( + items[2], + Value::Tagged(TaggedValue::PythonType(_)) + )); + assert!(matches!( + items[3], + Value::Tagged(TaggedValue::Repr { .. }) + )); + assert!(matches!( + items[4], + Value::Tagged(TaggedValue::Bytes { .. }) + )); + assert!(matches!( + items[5], + Value::Tagged(TaggedValue::Truncated { .. }) + )); + assert!(matches!( + items[6], + Value::Tagged(TaggedValue::Recursion { .. }) + )); + assert!(matches!( + items[7], + Value::Tagged(TaggedValue::AttributeError { .. }) + )); } #[test] @@ -141,11 +170,7 @@ fn rejects_unknown_tags() { let error = clew::from_bytes(document_with(r#"{"type":"future"}"#).as_bytes()).unwrap_err(); match error { - Error::Json(error) => assert!( - error - .to_string() - .contains("unknown Clew value type: future") - ), + Error::Json(error) => assert!(error.to_string().contains("data did not match any variant")), Error::SchemaMismatch { .. } => panic!("unknown tags should be JSON shape errors"), } }