#![cfg(feature = "schema")]
use newt_interaction::{InteractionDefinition, InteractionInstance, Response};
use std::path::{Path, PathBuf};
fn schema_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("schema")
}
fn published(name: &str, generated: &str) {
let path = schema_dir().join(format!("{name}.schema.json"));
if std::env::var("NEWT_GOLDEN_UPDATE").is_ok() {
std::fs::create_dir_all(schema_dir()).expect("schema dir");
std::fs::write(&path, generated).expect("write schema");
return;
}
let committed = std::fs::read_to_string(&path).unwrap_or_else(|e| {
panic!(
"{} is missing ({e}). Re-baseline deliberately with \
NEWT_GOLDEN_UPDATE=1.",
path.display()
)
});
assert_eq!(
committed, generated,
"the published schema for `{name}` no longer matches the type. If \
that is intended, re-baseline with NEWT_GOLDEN_UPDATE=1 and say in \
the PR what changed — non-Rust consumers read these files."
);
}
fn render<T: schemars::JsonSchema>() -> String {
let schema = schemars::schema_for!(T);
let mut text = serde_json::to_string_pretty(&schema).expect("render schema");
text.push('\n');
text
}
#[test]
fn every_record_type_has_a_published_schema() {
published("definition", &render::<InteractionDefinition>());
published("instance", &render::<InteractionInstance>());
published("response", &render::<Response>());
}
#[test]
fn each_schema_names_the_fields_of_its_record() {
for (name, expected) in [
(
"definition",
vec![
"schema", "kind", "revision", "markdown", "controls", "features",
],
),
(
"instance",
vec![
"schema",
"nonce",
"definition",
"revision",
"ttl_ticks",
"scope",
"responder_policy",
"provenance",
],
),
(
"response",
vec![
"schema",
"definition",
"instance",
"revision",
"values",
"idempotency_key",
"responder_provenance",
],
),
] {
let text = std::fs::read_to_string(schema_dir().join(format!("{name}.schema.json")))
.expect("schema exists");
let parsed: serde_json::Value = serde_json::from_str(&text).expect("schema parses");
let required = parsed["required"]
.as_array()
.unwrap_or_else(|| panic!("`{name}` schema has no required list"));
let listed: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
for field in expected {
assert!(
listed.contains(&field),
"`{name}` schema does not require `{field}`: {listed:?}"
);
}
}
}
#[test]
fn every_object_schema_forbids_additional_properties() {
for name in ["definition", "instance", "response"] {
let text = std::fs::read_to_string(schema_dir().join(format!("{name}.schema.json")))
.expect("schema exists");
let parsed: serde_json::Value = serde_json::from_str(&text).expect("schema parses");
assert_eq!(
parsed["additionalProperties"],
serde_json::Value::Bool(false),
"the `{name}` schema does not forbid additional properties, but \
the decoder does"
);
let mut open = Vec::new();
if let Some(defs) = parsed.get("definitions").and_then(|d| d.as_object()) {
for (def_name, def) in defs {
if def.get("type").and_then(|t| t.as_str()) != Some("object") {
continue;
}
if def.get("additionalProperties") != Some(&serde_json::Value::Bool(false)) {
open.push(def_name.clone());
}
}
}
assert!(
open.is_empty(),
"nested objects in the `{name}` schema still allow additional \
properties: {open:?}"
);
}
}
#[test]
fn each_schema_pins_exactly_its_own_version_tag() {
for (name, tag) in [
("definition", newt_interaction::DEFINITION_SCHEMA_V1),
("instance", newt_interaction::INSTANCE_SCHEMA_V1),
("response", newt_interaction::RESPONSE_SCHEMA_V1),
] {
let text = std::fs::read_to_string(schema_dir().join(format!("{name}.schema.json")))
.expect("schema exists");
let parsed: serde_json::Value = serde_json::from_str(&text).expect("schema parses");
let property = &parsed["properties"]["schema"];
let reference = property.get("$ref").and_then(|r| r.as_str()).or_else(|| {
property
.get("allOf")
.and_then(|a| a.as_array())
.and_then(|a| a.first())
.and_then(|first| first.get("$ref"))
.and_then(|r| r.as_str())
});
let resolved = match reference {
Some(reference) => {
let key = reference.rsplit('/').next().expect("a $ref name");
&parsed["definitions"][key]
}
None => property,
};
let pinned = resolved
.get("const")
.and_then(|c| c.as_str())
.or_else(|| {
resolved
.get("enum")
.and_then(|e| e.as_array())
.filter(|values| values.len() == 1)
.and_then(|values| values[0].as_str())
})
.unwrap_or_else(|| {
panic!("the `{name}` schema does not pin its version tag: {resolved}")
});
assert_eq!(pinned, tag, "`{name}` pins the wrong tag");
assert_ne!(pinned, tag.replace("/v1", "/v2"));
for (other, other_tag) in [
("definition", newt_interaction::DEFINITION_SCHEMA_V1),
("instance", newt_interaction::INSTANCE_SCHEMA_V1),
("response", newt_interaction::RESPONSE_SCHEMA_V1),
] {
if other != name {
assert_ne!(
pinned, other_tag,
"the `{name}` schema accepts the `{other}` tag"
);
}
}
}
}
#[test]
fn validated_scalars_publish_their_constraints() {
let definition: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(schema_dir().join("definition.schema.json")).unwrap(),
)
.unwrap();
let response: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(schema_dir().join("response.schema.json")).unwrap(),
)
.unwrap();
for (schema, name) in [(&definition, "ControlId"), (&definition, "OptionId")] {
let def = &schema["definitions"][name];
assert_eq!(def["minLength"], 1, "{name} may be empty on the wire");
assert_eq!(
def["pattern"], "^[A-Za-z0-9_-]+$",
"{name} does not publish its charset"
);
}
for (schema, name) in [
(&definition, "SurfaceFeature"),
(&response, "SecretRef"),
(&response, "IdempotencyKey"),
] {
assert_eq!(
schema["definitions"][name]["minLength"], 1,
"{name} may be empty on the wire"
);
}
}