feat(ariadne): deserialize Clew AST JSON
This commit was merged in pull request #42.
This commit is contained in:
@@ -95,6 +95,9 @@
|
|||||||
committing a top-down rewrite.
|
committing a top-down rewrite.
|
||||||
- `rules_rust` `rust_test` targets cannot set both `crate` and `srcs`.
|
- `rules_rust` `rust_test` targets cannot set both `crate` and `srcs`.
|
||||||
Use one input style per test target.
|
Use one input style per test target.
|
||||||
|
- Rust tests in this monorepo live in separate `*_test.rs` files, not inline `#[cfg(test)]` modules.
|
||||||
|
- Ariadne Rust tests are declared in Bazel `BUILD` files; do not add `[[test]]` entries to
|
||||||
|
`src/ariadne/Cargo.toml` unless explicitly requested.
|
||||||
- In this monorepo, "dotfiles" refers to the dotfiles/NixOS configuration under `src/dotfiles/` and `configs/nixos/`.
|
- In this monorepo, "dotfiles" refers to the dotfiles/NixOS configuration under `src/dotfiles/` and `configs/nixos/`.
|
||||||
- The flake does not currently define `formatter.x86_64-linux`.
|
- The flake does not currently define `formatter.x86_64-linux`.
|
||||||
Do not use `nix fmt` as a validation command until a formatter is added.
|
Do not use `nix fmt` as a validation command until a formatter is added.
|
||||||
|
|||||||
@@ -4,9 +4,14 @@ rust_library(
|
|||||||
name = "ariadne_lib",
|
name = "ariadne_lib",
|
||||||
crate_name = "ariadne",
|
crate_name = "ariadne",
|
||||||
srcs = [
|
srcs = [
|
||||||
|
"clew.rs",
|
||||||
"lib.rs",
|
"lib.rs",
|
||||||
"pipeline.rs",
|
"pipeline.rs",
|
||||||
],
|
],
|
||||||
|
deps = [
|
||||||
|
"//third_party/crates:serde",
|
||||||
|
"//third_party/crates:serde_json",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
rust_binary(
|
rust_binary(
|
||||||
@@ -19,6 +24,15 @@ rust_binary(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
rust_test(
|
||||||
|
name = "clew_test",
|
||||||
|
srcs = ["clew_test.rs"],
|
||||||
|
deps = [
|
||||||
|
":ariadne_lib",
|
||||||
|
"//third_party/crates:serde_json",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
rust_test(
|
rust_test(
|
||||||
name = "pipeline_test",
|
name = "pipeline_test",
|
||||||
srcs = ["pipeline_test.rs"],
|
srcs = ["pipeline_test.rs"],
|
||||||
|
|||||||
171
src/ariadne/clew.rs
Normal file
171
src/ariadne/clew.rs
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::error;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::Number;
|
||||||
|
|
||||||
|
const SCHEMA: &str = "clew-renpy-ast-v1";
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Json(serde_json::Error),
|
||||||
|
SchemaMismatch {
|
||||||
|
expected: &'static str,
|
||||||
|
actual: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::Json(error) => write!(formatter, "invalid Clew JSON: {error}"),
|
||||||
|
Error::SchemaMismatch { expected, actual } => write!(
|
||||||
|
formatter,
|
||||||
|
"unsupported Clew schema: expected {expected:?}, got {actual:?}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl error::Error for Error {
|
||||||
|
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Error::Json(error) => Some(error),
|
||||||
|
Error::SchemaMismatch { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for Error {
|
||||||
|
fn from(error: serde_json::Error) -> Self {
|
||||||
|
Error::Json(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, PartialEq)]
|
||||||
|
pub struct Document {
|
||||||
|
pub schema: String,
|
||||||
|
pub source: String,
|
||||||
|
pub game_root: String,
|
||||||
|
pub slot: u64,
|
||||||
|
pub python_version: String,
|
||||||
|
pub renpy_version: Option<String>,
|
||||||
|
pub metadata: Value,
|
||||||
|
pub roots: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum Value {
|
||||||
|
Null,
|
||||||
|
Bool(bool),
|
||||||
|
Number(Number),
|
||||||
|
String(String),
|
||||||
|
Tagged(TaggedValue),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 key: Value,
|
||||||
|
pub value: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
|
pub struct Function {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub module: Option<String>,
|
||||||
|
pub filename: Option<String>,
|
||||||
|
pub firstlineno: Option<Number>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
|
pub struct Method {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub module: Option<String>,
|
||||||
|
pub filename: Option<String>,
|
||||||
|
pub firstlineno: Option<Number>,
|
||||||
|
pub receiver_class: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
|
pub struct PythonType {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub module: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bytes(bytes: &[u8]) -> Result<Document, Error> {
|
||||||
|
let document: Document = serde_json::from_slice(bytes)?;
|
||||||
|
if document.schema != SCHEMA {
|
||||||
|
return Err(Error::SchemaMismatch {
|
||||||
|
expected: SCHEMA,
|
||||||
|
actual: document.schema,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(document)
|
||||||
|
}
|
||||||
176
src/ariadne/clew_test.rs
Normal file
176
src/ariadne/clew_test.rs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
use ariadne::clew::{self, DictEntry, Error, TaggedValue, Value};
|
||||||
|
use serde_json::Number;
|
||||||
|
|
||||||
|
fn document_with(roots: &str) -> String {
|
||||||
|
format!(
|
||||||
|
r#"{{
|
||||||
|
"schema":"clew-renpy-ast-v1",
|
||||||
|
"source":"/tmp/game/script.rpyc",
|
||||||
|
"game_root":"/tmp/game",
|
||||||
|
"slot":2,
|
||||||
|
"python_version":"2.7.18",
|
||||||
|
"renpy_version":"8.4.0",
|
||||||
|
"metadata":null,
|
||||||
|
"roots":{roots}
|
||||||
|
}}"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_minimal_document() {
|
||||||
|
let document = clew::from_bytes(document_with("null").as_bytes()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(document.schema, "clew-renpy-ast-v1");
|
||||||
|
assert_eq!(document.slot, 2);
|
||||||
|
assert_eq!(document.roots, Value::Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_wrong_schema() {
|
||||||
|
let json = br#"{
|
||||||
|
"schema":"other",
|
||||||
|
"source":"/tmp/game/script.rpyc",
|
||||||
|
"game_root":"/tmp/game",
|
||||||
|
"slot":2,
|
||||||
|
"python_version":"2.7.18",
|
||||||
|
"renpy_version":null,
|
||||||
|
"metadata":null,
|
||||||
|
"roots":null
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
clew::from_bytes(json).unwrap_err(),
|
||||||
|
Error::SchemaMismatch {
|
||||||
|
expected: "clew-renpy-ast-v1",
|
||||||
|
actual
|
||||||
|
} if actual == "other"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_primitive_values() {
|
||||||
|
let document = clew::from_bytes(
|
||||||
|
document_with(r#"{"type":"list","items":[null,true,7,1.5,"text"]}"#).as_bytes(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
document.roots,
|
||||||
|
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()),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_tuple_and_dict_values() {
|
||||||
|
let document = clew::from_bytes(
|
||||||
|
document_with(r#"{"type":"dict","items":[{"key":"names","value":{"type":"tuple","items":["a","b"]}}]}"#)
|
||||||
|
.as_bytes(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
document.roots,
|
||||||
|
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())
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_object_and_reference_values() {
|
||||||
|
let document = clew::from_bytes(
|
||||||
|
document_with(
|
||||||
|
r#"{"type":"list","items":[{"type":"object","id":1,"class":"renpy.ast.Label","attributes":{"name":"start","next":{"type":"ref","id":1}}},{"type":"ref","id":1}]}"#,
|
||||||
|
)
|
||||||
|
.as_bytes(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
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::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!(*id, 1);
|
||||||
|
assert_eq!(class_name, "renpy.ast.Label");
|
||||||
|
assert_eq!(attributes["name"], Value::String("start".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_special_tag_values() {
|
||||||
|
let document = clew::from_bytes(
|
||||||
|
document_with(
|
||||||
|
r#"{"type":"list","items":[{"type":"function","name":"f","module":"m","filename":"file.py","firstlineno":9},{"type":"method","name":"g","module":"m","receiver_class":"C"},{"type":"python-type","name":"Label","module":"renpy.ast"},{"type":"repr","class":"C","repr":"<C>"},{"type":"bytes","repr":"b'abc'"},{"type":"truncated","class":"C","repr":"<C>"},{"type":"recursion","class":"list"},{"type":"attribute-error","class":"Exception","repr":"Exception('x')"}]}"#,
|
||||||
|
)
|
||||||
|
.as_bytes(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
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::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]
|
||||||
|
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("data did not match any variant")),
|
||||||
|
Error::SchemaMismatch { .. } => panic!("unknown tags should be JSON shape errors"),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use crate::pipeline::Stage;
|
use crate::pipeline::Stage;
|
||||||
|
|
||||||
|
pub mod clew;
|
||||||
pub mod pipeline;
|
pub mod pipeline;
|
||||||
|
|
||||||
const OUTPUT_GRAPH_DIR: &str = "graph";
|
const OUTPUT_GRAPH_DIR: &str = "graph";
|
||||||
@@ -31,7 +32,10 @@ fn get_output_dir(config: &Config, stage: Stage) -> Result<PathBuf> {
|
|||||||
if output_dir.exists() && !output_dir.is_dir() {
|
if output_dir.exists() && !output_dir.is_dir() {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::NotADirectory,
|
io::ErrorKind::NotADirectory,
|
||||||
format!("output path is not a directory: {path}", path = output_dir.display()),
|
format!(
|
||||||
|
"output path is not a directory: {path}",
|
||||||
|
path = output_dir.display()
|
||||||
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user