#![allow(clippy::for_kv_map)]
use std::{collections::HashMap, fs, path::PathBuf};
use jsonschema::JSONSchema;
use serde::Deserialize;
use serde_json::Value;
#[derive(Deserialize)]
struct ParityFile {
vectors: Vec<ParityEntry>,
}
#[derive(Deserialize, Debug)]
struct ParityEntry {
schema: String,
fixture: String,
expect: String,
}
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
}
fn yaml_to_json(yaml: &str) -> Value {
let v: serde_json::Value = tf_types::yaml::from_str(yaml).expect("parse yaml");
serde_json::to_value(v).expect("yaml->json")
}
fn compile_schemas() -> HashMap<String, JSONSchema> {
let schemas_dir = repo_root().join("schemas");
let mut registry: HashMap<String, Value> = HashMap::new();
for entry in fs::read_dir(&schemas_dir).expect("read schemas/") {
let e = entry.expect("dirent");
let path = e.path();
if path.extension().and_then(|s| s.to_str()) != Some("json") {
continue;
}
let name = path.file_name().unwrap().to_string_lossy().to_string();
if !name.ends_with(".schema.json") {
continue;
}
let raw = fs::read_to_string(&path).expect("read schema");
let v: Value = serde_json::from_str(&raw).expect("parse schema");
registry.insert(name, v);
}
let mut out = HashMap::new();
for (name, _schema) in ®istry {
if !name.ends_with(".schema.json") || name == "_common.schema.json" {
continue;
}
let schema_name = name.trim_end_matches(".schema.json").to_string();
let bundled = bundle(&schema_name, ®istry);
let compiled = JSONSchema::options()
.compile(&bundled)
.unwrap_or_else(|e| panic!("compile {}: {}", schema_name, e));
out.insert(schema_name, compiled);
}
out
}
fn bundle(root_name: &str, registry: &HashMap<String, Value>) -> Value {
let mut bundled_defs = serde_json::Map::new();
let root = ®istry[&format!("{}.schema.json", root_name)];
let resolved = resolve(root, root_name, root_name, registry, &mut bundled_defs);
let mut obj = resolved.as_object().cloned().unwrap_or_default();
let mut defs = obj
.remove("$defs")
.and_then(|v| v.as_object().cloned())
.unwrap_or_default();
for (k, v) in bundled_defs {
defs.entry(k).or_insert(v);
}
if !defs.is_empty() {
obj.insert("$defs".to_string(), Value::Object(defs));
}
Value::Object(obj)
}
fn resolve(
node: &Value,
current_file: &str,
root_name: &str,
registry: &HashMap<String, Value>,
bundled: &mut serde_json::Map<String, Value>,
) -> Value {
match node {
Value::Array(xs) => Value::Array(
xs.iter()
.map(|v| resolve(v, current_file, root_name, registry, bundled))
.collect(),
),
Value::Object(map) => {
if let Some(r) = map.get("$ref").and_then(|v| v.as_str()) {
let (file_part, fragment) = match r.split_once('#') {
Some((f, frag)) => (f, frag),
None => (r, ""),
};
if file_part.is_empty() {
if current_file == root_name {
return Value::Object(map.clone());
}
let def_name = fragment.strip_prefix("/$defs/").unwrap_or(fragment);
let key = format!("{}_{}", to_pascal(current_file), def_name);
ensure_def(current_file, def_name, &key, registry, bundled);
let mut out = serde_json::Map::new();
out.insert(
"$ref".to_string(),
Value::String(format!("#/$defs/{}", key)),
);
return Value::Object(out);
}
let schema_name_json = file_part;
let schema_name = schema_name_json.trim_end_matches(".schema.json");
if let Some(def_name) = fragment.strip_prefix("/$defs/") {
let key = format!("{}_{}", to_pascal(schema_name), def_name);
ensure_def(schema_name, def_name, &key, registry, bundled);
let mut out = serde_json::Map::new();
out.insert(
"$ref".to_string(),
Value::String(format!("#/$defs/{}", key)),
);
return Value::Object(out);
}
let root_key = format!("{}_root", to_pascal(schema_name));
ensure_root(schema_name, &root_key, registry, bundled);
let mut out = serde_json::Map::new();
out.insert(
"$ref".to_string(),
Value::String(format!("#/$defs/{}", root_key)),
);
return Value::Object(out);
}
let mut out = serde_json::Map::new();
for (k, v) in map {
if k == "$id" || k == "$schema" {
out.insert(k.clone(), v.clone());
continue;
}
out.insert(
k.clone(),
resolve(v, current_file, root_name, registry, bundled),
);
}
Value::Object(out)
}
_ => node.clone(),
}
}
fn ensure_def(
schema_name: &str,
def_name: &str,
key: &str,
registry: &HashMap<String, Value>,
bundled: &mut serde_json::Map<String, Value>,
) {
if bundled.contains_key(key) {
return;
}
let target_doc = registry
.get(&format!("{}.schema.json", schema_name))
.unwrap_or_else(|| panic!("unknown schema ref target: {}", schema_name));
let defs = target_doc.get("$defs").and_then(|v| v.as_object());
let def = defs
.and_then(|m| m.get(def_name))
.unwrap_or_else(|| panic!("unknown $def: {}#/$defs/{}", schema_name, def_name));
bundled.insert(key.to_string(), Value::Null);
let resolved = resolve(def, schema_name, "__root_placeholder__", registry, bundled);
bundled.insert(key.to_string(), resolved);
}
fn ensure_root(
schema_name: &str,
root_key: &str,
registry: &HashMap<String, Value>,
bundled: &mut serde_json::Map<String, Value>,
) {
if bundled.contains_key(root_key) {
return;
}
let target_doc = registry
.get(&format!("{}.schema.json", schema_name))
.unwrap_or_else(|| panic!("unknown schema ref target: {}", schema_name));
bundled.insert(root_key.to_string(), Value::Null);
let mut copy = target_doc.as_object().cloned().unwrap_or_default();
copy.remove("$id");
copy.remove("$schema");
let defs_map = copy
.remove("$defs")
.and_then(|v| v.as_object().cloned())
.unwrap_or_default();
let resolved = resolve(
&Value::Object(copy),
schema_name,
"__root_placeholder__",
registry,
bundled,
);
bundled.insert(root_key.to_string(), resolved);
for (def_name, def) in defs_map {
let key = format!("{}_{}", to_pascal(schema_name), def_name);
if !bundled.contains_key(&key) {
bundled.insert(key.clone(), Value::Null);
let resolved = resolve(&def, schema_name, "__root_placeholder__", registry, bundled);
bundled.insert(key, resolved);
}
}
}
fn to_pascal(s: &str) -> String {
s.split(|c: char| !c.is_alphanumeric())
.filter(|p| !p.is_empty())
.map(|p| {
let mut chars = p.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
})
.collect()
}
#[test]
fn parity_against_ts() {
let parity_path = repo_root().join("conformance").join("parity.yaml");
let raw = fs::read_to_string(&parity_path)
.unwrap_or_else(|e| panic!("read {}: {}", parity_path.display(), e));
let file: ParityFile = tf_types::yaml::from_str(&raw).expect("parse parity.yaml");
let validators = compile_schemas();
let mut mismatches: Vec<String> = Vec::new();
for v in &file.vectors {
let validator = validators
.get(&v.schema)
.unwrap_or_else(|| panic!("no validator for schema {}", v.schema));
let fixture_path = repo_root().join(&v.fixture);
let yaml = fs::read_to_string(&fixture_path)
.unwrap_or_else(|e| panic!("read {}: {}", fixture_path.display(), e));
let doc = yaml_to_json(&yaml);
let verdict = if validator.is_valid(&doc) {
"valid"
} else {
"invalid"
};
if verdict != v.expect {
let detail = if verdict == "invalid" {
let errors: Vec<String> = validator
.validate(&doc)
.err()
.into_iter()
.flat_map(|errs| {
errs.map(|e| format!("{}@{}", e, e.instance_path))
.collect::<Vec<_>>()
})
.collect();
format!(" [{}]", errors.join("; "))
} else {
String::new()
};
mismatches.push(format!(
"{} ({}): expected {}, got {}{}",
v.fixture, v.schema, v.expect, verdict, detail
));
}
}
if !mismatches.is_empty() {
panic!(
"{} parity mismatches:\n {}",
mismatches.len(),
mismatches.join("\n ")
);
}
}