refactor(ariadne): derive Clew AST deserialization
This commit is contained in:
@@ -3,7 +3,7 @@ use std::error;
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{Map, Number};
|
use serde_json::Number;
|
||||||
|
|
||||||
const SCHEMA: &str = "clew-renpy-ast-v1";
|
const SCHEMA: &str = "clew-renpy-ast-v1";
|
||||||
|
|
||||||
@@ -55,41 +55,88 @@ pub struct Document {
|
|||||||
pub roots: Value,
|
pub roots: Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
|
#[serde(untagged)]
|
||||||
pub enum Value {
|
pub enum Value {
|
||||||
Null,
|
Null,
|
||||||
Bool(bool),
|
Bool(bool),
|
||||||
Number(Number),
|
Number(Number),
|
||||||
String(String),
|
String(String),
|
||||||
List(Vec<Value>),
|
Tagged(TaggedValue),
|
||||||
Tuple(Vec<Value>),
|
|
||||||
Dict(Vec<DictEntry>),
|
|
||||||
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 },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum TaggedValue {
|
||||||
|
#[serde(rename = "list")]
|
||||||
|
List { items: Vec<Value> },
|
||||||
|
|
||||||
|
#[serde(rename = "tuple")]
|
||||||
|
Tuple { items: Vec<Value> },
|
||||||
|
|
||||||
|
#[serde(rename = "dict")]
|
||||||
|
Dict { items: Vec<DictEntry> },
|
||||||
|
|
||||||
|
#[serde(rename = "object")]
|
||||||
|
Object {
|
||||||
|
id: u64,
|
||||||
|
|
||||||
|
#[serde(rename = "class")]
|
||||||
|
class_name: String,
|
||||||
|
|
||||||
|
attributes: BTreeMap<String, Value>,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[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 struct DictEntry {
|
||||||
pub key: Value,
|
pub key: Value,
|
||||||
pub value: Value,
|
pub value: Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
pub struct Object {
|
|
||||||
pub id: u64,
|
|
||||||
pub class_name: String,
|
|
||||||
pub attributes: BTreeMap<String, Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct Function {
|
pub struct Function {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub module: Option<String>,
|
pub module: Option<String>,
|
||||||
@@ -97,7 +144,7 @@ pub struct Function {
|
|||||||
pub firstlineno: Option<Number>,
|
pub firstlineno: Option<Number>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
pub struct Method {
|
pub struct Method {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub module: Option<String>,
|
pub module: Option<String>,
|
||||||
@@ -106,7 +153,7 @@ pub struct Method {
|
|||||||
pub receiver_class: Option<String>,
|
pub receiver_class: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
pub struct PythonType {
|
pub struct PythonType {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub module: Option<String>,
|
pub module: Option<String>,
|
||||||
@@ -122,160 +169,3 @@ pub fn from_bytes(bytes: &[u8]) -> Result<Document, Error> {
|
|||||||
}
|
}
|
||||||
Ok(document)
|
Ok(document)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Value {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
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<Value, String> {
|
|
||||||
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<String, serde_json::Value>) -> Result<Value, String> {
|
|
||||||
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<String, serde_json::Value>) -> Result<Vec<Value>, 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<String, serde_json::Value>) -> Result<Vec<DictEntry>, 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<DictEntry, String> {
|
|
||||||
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<String, serde_json::Value>,
|
|
||||||
) -> Result<BTreeMap<String, Value>, 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<String, serde_json::Value>, field: &str) -> Result<String, String> {
|
|
||||||
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<String, serde_json::Value>,
|
|
||||||
field: &str,
|
|
||||||
) -> Result<Option<String>, 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<String, serde_json::Value>,
|
|
||||||
field: &str,
|
|
||||||
) -> Result<Option<Number>, 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<String, serde_json::Value>, field: &str) -> Result<u64, String> {
|
|
||||||
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")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use ariadne::clew::{self, DictEntry, Error, Value};
|
use ariadne::clew::{self, DictEntry, Error, TaggedValue, Value};
|
||||||
use serde_json::Number;
|
use serde_json::Number;
|
||||||
|
|
||||||
fn document_with(roots: &str) -> String {
|
fn document_with(roots: &str) -> String {
|
||||||
@@ -56,13 +56,15 @@ fn parses_primitive_values() {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
document.roots,
|
document.roots,
|
||||||
Value::List(vec![
|
Value::Tagged(TaggedValue::List {
|
||||||
Value::Null,
|
items: vec![
|
||||||
Value::Bool(true),
|
Value::Null,
|
||||||
Value::Number(Number::from(7)),
|
Value::Bool(true),
|
||||||
Value::Number(Number::from_f64(1.5).unwrap()),
|
Value::Number(Number::from(7)),
|
||||||
Value::String("text".to_string()),
|
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!(
|
assert_eq!(
|
||||||
document.roots,
|
document.roots,
|
||||||
Value::Dict(vec![DictEntry {
|
Value::Tagged(TaggedValue::Dict {
|
||||||
key: Value::String("names".to_string()),
|
items: vec![DictEntry {
|
||||||
value: Value::Tuple(vec![
|
key: Value::String("names".to_string()),
|
||||||
Value::String("a".to_string()),
|
value: Value::Tagged(TaggedValue::Tuple {
|
||||||
Value::String("b".to_string())
|
items: vec![
|
||||||
]),
|
Value::String("a".to_string()),
|
||||||
}])
|
Value::String("b".to_string())
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
}]
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,20 +102,22 @@ fn parses_object_and_reference_values() {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let Value::List(items) = document.roots else {
|
let Value::Tagged(TaggedValue::List { items }) = document.roots else {
|
||||||
panic!("roots should be a list");
|
panic!("roots should be a list");
|
||||||
};
|
};
|
||||||
assert_eq!(items.len(), 2);
|
assert_eq!(items.len(), 2);
|
||||||
assert_eq!(items[1], Value::Ref { id: 1 });
|
assert_eq!(items[1], Value::Tagged(TaggedValue::Ref { id: 1 }));
|
||||||
let Value::Object(object) = &items[0] else {
|
let Value::Tagged(TaggedValue::Object {
|
||||||
|
id,
|
||||||
|
class_name,
|
||||||
|
attributes,
|
||||||
|
}) = &items[0]
|
||||||
|
else {
|
||||||
panic!("first item should be an object");
|
panic!("first item should be an object");
|
||||||
};
|
};
|
||||||
assert_eq!(object.id, 1);
|
assert_eq!(*id, 1);
|
||||||
assert_eq!(object.class_name, "renpy.ast.Label");
|
assert_eq!(class_name, "renpy.ast.Label");
|
||||||
assert_eq!(
|
assert_eq!(attributes["name"], Value::String("start".to_string()));
|
||||||
object.attributes["name"],
|
|
||||||
Value::String("start".to_string())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -122,18 +130,39 @@ fn parses_special_tag_values() {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let Value::List(items) = document.roots else {
|
let Value::Tagged(TaggedValue::List { items }) = document.roots else {
|
||||||
panic!("roots should be a list");
|
panic!("roots should be a list");
|
||||||
};
|
};
|
||||||
assert_eq!(items.len(), 8);
|
assert_eq!(items.len(), 8);
|
||||||
assert!(matches!(items[0], Value::Function(_)));
|
assert!(matches!(
|
||||||
assert!(matches!(items[1], Value::Method(_)));
|
items[0],
|
||||||
assert!(matches!(items[2], Value::PythonType(_)));
|
Value::Tagged(TaggedValue::Function(_))
|
||||||
assert!(matches!(items[3], Value::Repr { .. }));
|
));
|
||||||
assert!(matches!(items[4], Value::Bytes { .. }));
|
assert!(matches!(items[1], Value::Tagged(TaggedValue::Method(_))));
|
||||||
assert!(matches!(items[5], Value::Truncated { .. }));
|
assert!(matches!(
|
||||||
assert!(matches!(items[6], Value::Recursion { .. }));
|
items[2],
|
||||||
assert!(matches!(items[7], Value::AttributeError { .. }));
|
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]
|
#[test]
|
||||||
@@ -141,11 +170,7 @@ fn rejects_unknown_tags() {
|
|||||||
let error = clew::from_bytes(document_with(r#"{"type":"future"}"#).as_bytes()).unwrap_err();
|
let error = clew::from_bytes(document_with(r#"{"type":"future"}"#).as_bytes()).unwrap_err();
|
||||||
|
|
||||||
match error {
|
match error {
|
||||||
Error::Json(error) => assert!(
|
Error::Json(error) => assert!(error.to_string().contains("data did not match any variant")),
|
||||||
error
|
|
||||||
.to_string()
|
|
||||||
.contains("unknown Clew value type: future")
|
|
||||||
),
|
|
||||||
Error::SchemaMismatch { .. } => panic!("unknown tags should be JSON shape errors"),
|
Error::SchemaMismatch { .. } => panic!("unknown tags should be JSON shape errors"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user