use serde_json::Value;
pub const X_GTS_TRAITS_SCHEMA: &str = "x-gts-traits-schema";
pub const X_GTS_TRAITS: &str = "x-gts-traits";
const MAX_RECURSION_DEPTH: usize = 64;
pub(crate) struct EffectiveTraits {
pub schema: Value,
pub values: Value,
pub(crate) resolved_trait_schemas: Vec<Value>,
pub(crate) merged_traits: Value,
}
impl EffectiveTraits {
fn has_schema(&self) -> bool {
!self.resolved_trait_schemas.is_empty()
}
fn has_explicit_values(&self) -> bool {
self.merged_traits
.as_object()
.is_some_and(|m| !m.is_empty())
}
pub(crate) fn validate(&self, check_unresolved: bool) -> Result<(), Vec<String>> {
validate_trait_schema_integrity(&self.resolved_trait_schemas)?;
validate_trait_schema_compatibility(&self.resolved_trait_schemas)?;
if !self.has_schema() {
if self.has_explicit_values() {
return Err(vec![format!(
"{X_GTS_TRAITS} values provided but no {X_GTS_TRAITS_SCHEMA} is defined in the \
inheritance chain"
)]);
}
return Ok(());
}
if effective_schema_is_false(&self.schema) {
if self.has_explicit_values() {
return Err(vec![format!(
"{X_GTS_TRAITS_SCHEMA} resolves to `false` in the chain — \
{X_GTS_TRAITS} values are prohibited"
)]);
}
return Ok(());
}
validate_trait_values(&self.schema, &self.values, check_unresolved)
}
}
pub trait GtsTraitsSchema: schemars::JsonSchema {}
#[must_use]
#[allow(clippy::expect_used)]
pub fn inline_traits_schema_of<T: schemars::JsonSchema>() -> Value {
let mut generator = schemars::generate::SchemaSettings::default()
.with(|s| s.inline_subschemas = true)
.into_generator();
let schema = <T as schemars::JsonSchema>::json_schema(&mut generator);
let mut value = serde_json::to_value(&schema)
.expect("schemars JsonSchema serialization to a JSON value is infallible for valid types");
if let Some(obj) = value.as_object_mut() {
obj.remove("$schema");
}
value
}
#[cfg(test)]
pub fn validate_traits_chain(chain_schemas: &[(String, Value)]) -> Result<(), Vec<String>> {
let mut trait_schemas = Vec::new();
let mut merged = serde_json::Map::new();
for (_id, content) in chain_schemas {
collect_trait_schema_from_value(content, &mut trait_schemas);
collect_traits_from_value(content, &mut merged);
}
let dialect = chain_schemas
.last()
.and_then(|(_, content)| content.get("$schema").and_then(Value::as_str));
build_effective_traits(&trait_schemas, &Value::Object(merged), dialect).validate(true)
}
fn validate_trait_schema_integrity(resolved_trait_schemas: &[Value]) -> Result<(), Vec<String>> {
for (i, ts) in resolved_trait_schemas.iter().enumerate() {
match ts {
Value::Bool(_) => {}
Value::Object(_) => {
if let Err(e) = jsonschema::validator_for(ts) {
return Err(vec![format!(
"x-gts-traits-schema[{i}] is not a valid JSON Schema: {e}"
)]);
}
}
_ => {
return Err(vec![format!(
"x-gts-traits-schema[{i}] must be an object subschema or a boolean; got {ts}"
)]);
}
}
}
Ok(())
}
fn validate_trait_schema_compatibility(
resolved_trait_schemas: &[Value],
) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
for i in 1..resolved_trait_schemas.len() {
let ancestor_schema = build_effective_traits_schema(&resolved_trait_schemas[..i]);
let descendant_schema = build_effective_traits_schema(&resolved_trait_schemas[..=i]);
let ancestor = crate::schema_compat::extract_effective_schema(&ancestor_schema);
let descendant = crate::schema_compat::extract_effective_schema(&descendant_schema);
let pair_errors = crate::schema_compat::validate_effective_schema_compatibility(
&ancestor,
&descendant,
"ancestor trait schema",
"descendant trait schema",
);
errors.extend(pair_errors.into_iter().map(|err| {
format!("x-gts-traits-schema[{i}] is incompatible with ancestor trait schema: {err}")
}));
let branch_errors = crate::schema_compat::validate_closed_descendant_branches(
&ancestor_schema,
&resolved_trait_schemas[i],
"ancestor trait schema",
"descendant trait schema",
);
errors.extend(branch_errors.into_iter().map(|err| {
format!("x-gts-traits-schema[{i}] is incompatible with ancestor trait schema: {err}")
}));
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub(crate) fn build_effective_traits(
resolved_trait_schemas: &[Value],
merged_traits: &Value,
dialect: Option<&str>,
) -> EffectiveTraits {
let mut effective_traits_schema = build_effective_traits_schema(resolved_trait_schemas);
if let Some(dialect) = dialect
&& let Some(obj) = effective_traits_schema.as_object_mut()
{
obj.insert("$schema".to_owned(), Value::String(dialect.to_owned()));
}
let values = materialize_traits(&effective_traits_schema, merged_traits);
EffectiveTraits {
schema: effective_traits_schema,
values,
resolved_trait_schemas: resolved_trait_schemas.to_vec(),
merged_traits: merged_traits.clone(),
}
}
fn validate_trait_values(
effective_traits_schema: &Value,
effective_traits: &Value,
check_unresolved: bool,
) -> Result<(), Vec<String>> {
let mut errors = match validate_traits_against_schema(
effective_traits_schema,
effective_traits,
check_unresolved,
) {
Ok(()) => Vec::new(),
Err(e) => e,
};
let xref = crate::x_gts_ref::XGtsRefValidator::new();
for err in xref.validate_instance(effective_traits, effective_traits_schema, "") {
errors.push(format!("trait x-gts-ref: {err}"));
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn effective_schema_is_false(schema: &Value) -> bool {
effective_schema_is_false_recursive(schema, 0)
}
fn effective_schema_is_false_recursive(schema: &Value, depth: usize) -> bool {
if depth >= MAX_RECURSION_DEPTH {
return false;
}
match schema {
Value::Bool(false) => true,
Value::Object(obj) => {
if let Some(Value::Array(items)) = obj.get("allOf") {
items
.iter()
.any(|item| effective_schema_is_false_recursive(item, depth + 1))
} else {
false
}
}
_ => false,
}
}
pub(crate) fn inline_local_pointers(fragment: &Value, root: &Value) -> Value {
inline_local_pointers_recursive(fragment, root, 0)
}
fn inline_local_pointers_recursive(value: &Value, root: &Value, depth: usize) -> Value {
if depth >= MAX_RECURSION_DEPTH {
return value.clone();
}
match value {
Value::Object(map) => {
if let Some(Value::String(r)) = map.get("$ref")
&& let Some(ptr) = r.strip_prefix("#/")
&& let Some(target) = root.pointer(&format!("/{ptr}"))
{
let mut resolved = inline_local_pointers_recursive(target, root, depth + 1);
if map.len() > 1
&& let Value::Object(resolved_map) = &mut resolved
{
for (k, v) in map {
if k != "$ref" {
resolved_map.insert(
k.clone(),
inline_local_pointers_recursive(v, root, depth + 1),
);
}
}
}
return resolved;
}
let mut out = serde_json::Map::with_capacity(map.len());
for (k, v) in map {
out.insert(
k.clone(),
inline_local_pointers_recursive(v, root, depth + 1),
);
}
Value::Object(out)
}
Value::Array(arr) => Value::Array(
arr.iter()
.map(|v| inline_local_pointers_recursive(v, root, depth + 1))
.collect(),
),
_ => value.clone(),
}
}
pub(crate) fn collect_trait_schema_from_value(value: &Value, out: &mut Vec<Value>) {
collect_trait_schema_recursive(value, out, 0);
}
fn collect_trait_schema_recursive(value: &Value, out: &mut Vec<Value>, depth: usize) {
if depth >= MAX_RECURSION_DEPTH {
return;
}
let Some(obj) = value.as_object() else {
return;
};
if let Some(ts) = obj.get(X_GTS_TRAITS_SCHEMA) {
out.push(ts.clone());
}
if let Some(Value::Array(all_of)) = obj.get("allOf") {
for item in all_of {
collect_trait_schema_recursive(item, out, depth + 1);
}
}
}
pub(crate) fn collect_traits_from_value(
value: &Value,
merged: &mut serde_json::Map<String, Value>,
) {
collect_traits_recursive(value, merged, 0);
}
fn collect_traits_recursive(
value: &Value,
merged: &mut serde_json::Map<String, Value>,
depth: usize,
) {
if depth >= MAX_RECURSION_DEPTH {
return;
}
let Some(obj) = value.as_object() else {
return;
};
if let Some(Value::Object(traits)) = obj.get(X_GTS_TRAITS) {
for (k, v) in traits {
merged.insert(k.clone(), v.clone());
}
}
if let Some(Value::Array(all_of)) = obj.get("allOf") {
for item in all_of {
collect_traits_recursive(item, merged, depth + 1);
}
}
}
pub(crate) fn merge_rfc7396_into(
target: &mut serde_json::Map<String, Value>,
patch: &serde_json::Map<String, Value>,
) {
merge_rfc7396_recursive(target, patch, 0);
}
fn merge_rfc7396_recursive(
target: &mut serde_json::Map<String, Value>,
patch: &serde_json::Map<String, Value>,
depth: usize,
) {
if depth >= MAX_RECURSION_DEPTH {
return;
}
for (k, v) in patch {
match v {
Value::Null => {
target.remove(k);
}
Value::Object(patch_obj) => {
if let Some(Value::Object(existing)) = target.get_mut(k) {
merge_rfc7396_recursive(existing, patch_obj, depth + 1);
} else {
let mut fresh = serde_json::Map::new();
merge_rfc7396_recursive(&mut fresh, patch_obj, depth + 1);
target.insert(k.clone(), Value::Object(fresh));
}
}
other => {
target.insert(k.clone(), other.clone());
}
}
}
}
pub(crate) fn build_effective_traits_schema(schemas: &[Value]) -> Value {
match schemas.len() {
0 => Value::Object(serde_json::Map::new()),
1 => schemas[0].clone(),
_ => {
let mut wrapper = serde_json::Map::new();
wrapper.insert("type".to_owned(), Value::String("object".to_owned()));
wrapper.insert("allOf".to_owned(), Value::Array(schemas.to_vec()));
Value::Object(wrapper)
}
}
}
fn materialize_traits(trait_schema: &Value, traits: &Value) -> Value {
materialize_traits_recursive(trait_schema, traits, 0)
}
type PropResolution = (Value, Option<Value>, Option<Value>);
fn materialize_traits_recursive(trait_schema: &Value, traits: &Value, depth: usize) -> Value {
if depth >= MAX_RECURSION_DEPTH {
return traits.clone();
}
let mut result = match traits {
Value::Object(m) => m.clone(),
_ => serde_json::Map::new(),
};
let mut all_props: Vec<(String, Value)> = Vec::new();
collect_props_recursive(trait_schema, &mut all_props, 0);
let mut order: Vec<String> = Vec::new();
let mut resolved: std::collections::HashMap<String, PropResolution> =
std::collections::HashMap::new();
for (name, sch) in all_props.iter().rev() {
let obj = sch.as_object();
let entry = resolved.entry(name.clone()).or_insert_with(|| {
order.push(name.clone());
(sch.clone(), None, None)
});
if entry.1.is_none()
&& let Some(const_val) = obj.and_then(|o| o.get("const"))
{
entry.1 = Some(const_val.clone());
}
if entry.2.is_none()
&& let Some(default_val) = obj.and_then(|o| o.get("default"))
{
entry.2 = Some(default_val.clone());
}
}
for name in &order {
let (prop_schema, nearest_const, nearest_default) = &resolved[name];
if !result.contains_key(name.as_str()) {
if let Some(const_val) = nearest_const {
result.insert(name.clone(), const_val.clone());
} else if let Some(default_val) = nearest_default {
result.insert(name.clone(), default_val.clone());
}
} else if result.get(name.as_str()).is_some_and(Value::is_object)
&& prop_schema.as_object().is_some_and(|o| {
o.get("type") == Some(&Value::String("object".to_owned()))
&& o.contains_key("properties")
})
{
let nested = materialize_traits_recursive(
prop_schema,
result.get(name.as_str()).unwrap_or(&Value::Null),
depth + 1,
);
result.insert(name.clone(), nested);
}
}
Value::Object(result)
}
fn collect_all_properties(schema: &Value) -> Vec<(String, Value)> {
let mut props = Vec::new();
collect_props_recursive(schema, &mut props, 0);
let mut seen = std::collections::HashSet::new();
let mut deduped = Vec::with_capacity(props.len());
for (name, schema) in props.into_iter().rev() {
if seen.insert(name.clone()) {
deduped.push((name, schema));
}
}
deduped.reverse();
deduped
}
fn collect_props_recursive(schema: &Value, props: &mut Vec<(String, Value)>, depth: usize) {
if depth >= MAX_RECURSION_DEPTH {
return;
}
let Some(obj) = schema.as_object() else {
return;
};
if let Some(Value::Object(p)) = obj.get("properties") {
for (k, v) in p {
props.push((k.clone(), v.clone()));
}
}
if let Some(Value::Array(all_of)) = obj.get("allOf") {
for item in all_of {
collect_props_recursive(item, props, depth + 1);
}
}
}
fn collect_all_required(schema: &Value) -> std::collections::HashSet<String> {
let mut req = std::collections::HashSet::new();
collect_required_recursive(schema, &mut req, 0);
req
}
fn collect_required_recursive(
schema: &Value,
req: &mut std::collections::HashSet<String>,
depth: usize,
) {
if depth >= MAX_RECURSION_DEPTH {
return;
}
let Some(obj) = schema.as_object() else {
return;
};
if let Some(Value::Array(required)) = obj.get("required") {
for item in required {
if let Some(name) = item.as_str() {
req.insert(name.to_owned());
}
}
}
if let Some(Value::Array(all_of)) = obj.get("allOf") {
for item in all_of {
collect_required_recursive(item, req, depth + 1);
}
}
}
fn schema_without_required(schema: &Value) -> Value {
let mut out = schema.clone();
strip_required_recursive(&mut out, 0);
out
}
fn strip_required_recursive(schema: &mut Value, depth: usize) {
if depth >= MAX_RECURSION_DEPTH {
return;
}
let Some(obj) = schema.as_object_mut() else {
return;
};
obj.remove("required");
if let Some(Value::Array(all_of)) = obj.get_mut("allOf") {
for item in all_of {
strip_required_recursive(item, depth + 1);
}
}
}
fn validate_traits_against_schema(
trait_schema: &Value,
effective_traits: &Value,
check_unresolved: bool,
) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
let stripped;
let validation_schema: &Value = if check_unresolved {
trait_schema
} else {
stripped = schema_without_required(trait_schema);
&stripped
};
match jsonschema::validator_for(validation_schema) {
Ok(validator) => {
for error in validator.iter_errors(effective_traits) {
errors.push(format!("trait validation: {error}"));
}
}
Err(e) => {
errors.push(format!("failed to compile trait schema: {e}"));
}
}
if !check_unresolved {
return if errors.is_empty() {
Ok(())
} else {
Err(errors)
};
}
let all_props = collect_all_properties(trait_schema);
let required = collect_all_required(trait_schema);
let traits_obj = effective_traits.as_object();
for (prop_name, prop_schema) in &all_props {
if !required.contains(prop_name.as_str()) {
continue;
}
let has_value = traits_obj.is_some_and(|m| m.contains_key(prop_name.as_str()));
let has_default_or_const = prop_schema
.as_object()
.is_some_and(|m| m.contains_key("default") || m.contains_key("const"));
if !has_value && !has_default_or_const {
let expected_type = prop_schema
.as_object()
.and_then(|m| m.get("type"))
.and_then(Value::as_str)
.unwrap_or("any");
errors.push(format!(
"trait property '{prop_name}' (type: {expected_type}) is not resolved: \
no value provided and no default or const defined in the trait schema. \
All traits must be resolved (via a {X_GTS_TRAITS} value in the chain \
or a `default`/`const` in the trait schema) on non-abstract types; \
otherwise mark the type abstract (x-gts-abstract: true)"
));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_trait_schema_integrity_rejects_non_object_non_boolean() {
let schemas = vec![json!([1, 2])];
let err = validate_trait_schema_integrity(&schemas).unwrap_err();
assert!(
err.iter()
.any(|m| m.contains("must be an object subschema or a boolean")),
"expected the non-object/non-boolean arm, got: {err:?}"
);
}
#[test]
fn test_inline_local_pointers_ref_with_siblings_overlay() {
let root = json!({
"$defs": {"X": {"type": "string", "minLength": 1}}
});
let fragment = json!({"$ref": "#/$defs/X", "description": "extra"});
let inlined = inline_local_pointers(&fragment, &root);
assert_eq!(inlined["type"], json!("string"));
assert_eq!(inlined["minLength"], json!(1));
assert_eq!(inlined["description"], json!("extra"));
assert!(inlined.get("$ref").is_none());
}
#[test]
fn test_no_traits_schema_passes() {
let chain = vec![(
"gts.x.test.base.v1~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": {"id": {"type": "string"}}}),
)];
assert!(validate_traits_chain(&chain).is_ok());
}
#[test]
fn test_traits_without_schema_in_derived_fails() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": {"id": {"type": "string"}}}),
),
(
"derived~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {"retention": "P30D"}
}),
),
];
let err = validate_traits_chain(&chain).unwrap_err();
assert!(
err.iter().any(|e| e.contains("no x-gts-traits-schema")),
"should fail when traits provided without schema: {err:?}"
);
}
#[test]
fn test_traits_without_schema_in_base_fails() {
let chain = vec![(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {"retention": "P30D"},
"properties": {"id": {"type": "string"}}
}),
)];
let err = validate_traits_chain(&chain).unwrap_err();
assert!(
err.iter().any(|e| e.contains("no x-gts-traits-schema")),
"should fail when base has traits but no schema: {err:?}"
);
}
#[test]
fn test_all_traits_resolved() {
let chain = vec![
(
"gts.x.test.base.v1~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"retention": {"type": "string"},
"topicRef": {"type": "string"}
}
}
}),
),
(
"gts.x.test.base.v1~x.test._.derived.v1~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {
"retention": "P90D",
"topicRef": "gts.x.core.events.topic.v1~x.test._.orders.v1"
}
}),
),
];
assert!(validate_traits_chain(&chain).is_ok());
}
#[test]
fn test_defaults_fill_traits() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"retention": {"type": "string", "default": "P30D"},
"topicRef": {"type": "string", "default": "default_topic"}
}
}
}),
),
(
"derived~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#", "type": "object"}),
),
];
assert!(validate_traits_chain(&chain).is_ok());
}
#[test]
fn test_const_only_required_trait_resolves_and_materializes() {
let schemas = vec![json!({
"type": "object",
"additionalProperties": false,
"properties": {
"channel": {"type": "string", "const": "audit"}
},
"required": ["channel"]
})];
let traits = build_effective_traits(
&schemas,
&json!({}),
Some("http://json-schema.org/draft-07/schema#"),
);
assert_eq!(
traits.values["channel"], "audit",
"const must be materialized into effective traits values"
);
assert!(
traits.validate(true).is_ok(),
"a const-locked required trait is fully resolved: {:?}",
traits.validate(true)
);
}
#[test]
fn test_const_takes_priority_over_default_in_materialization() {
let schemas = vec![json!({
"type": "object",
"properties": {
"mode": {"type": "string", "const": "locked", "default": "open"}
}
})];
let traits = build_effective_traits(&schemas, &json!({}), None);
assert_eq!(
traits.values["mode"], "locked",
"const wins over default when the value is absent"
);
}
#[test]
fn test_explicit_value_conflicting_with_const_fails() {
let chain = vec![(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {"channel": {"type": "string", "const": "audit"}}
},
"x-gts-traits": {"channel": "events"}
}),
)];
let err = validate_traits_chain(&chain).unwrap_err();
assert!(
!err.is_empty(),
"explicit value conflicting with const must fail validation"
);
}
#[test]
fn test_ancestor_default_ripples_when_descendant_redeclares_without_default() {
let schemas = vec![
json!({
"type": "object",
"properties": {"retention": {"type": "string", "default": "P30D"}},
"required": ["retention"]
}),
json!({
"type": "object",
"properties": {"retention": {"type": "string"}}
}),
];
let traits = build_effective_traits(
&schemas,
&json!({}),
Some("http://json-schema.org/draft-07/schema#"),
);
assert_eq!(
traits.values["retention"], "P30D",
"ancestor default must ripple when a descendant redeclares without a default"
);
assert!(
traits.validate(true).is_ok(),
"the rippled default resolves the required trait: {:?}",
traits.validate(true)
);
}
#[test]
fn test_missing_required_trait_fails() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"topicRef": {"type": "string"},
"retention": {"type": "string", "default": "P30D"}
},
"required": ["topicRef"]
}
}),
),
(
"derived~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {
"retention": "P90D"
}
}),
),
];
let err = validate_traits_chain(&chain).unwrap_err();
assert!(
err.iter().any(|e| e.contains("topicRef")),
"should mention missing topicRef: {err:?}"
);
}
#[test]
fn test_optional_unresolved_trait_passes() {
let chain = vec![(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"topicRef": {"type": "string"},
"note": {"type": "string"}
},
"required": ["topicRef"]
},
"x-gts-traits": {"topicRef": "events.orders"}
}),
)];
assert!(
validate_traits_chain(&chain).is_ok(),
"optional unresolved trait property must not fail completeness"
);
}
#[test]
fn test_wrong_type_fails() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"maxRetries": {"type": "integer", "minimum": 0, "default": 3}
}
}
}),
),
(
"derived~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {
"maxRetries": "not_a_number"
}
}),
),
];
let err = validate_traits_chain(&chain).unwrap_err();
assert!(!err.is_empty(), "wrong type should fail");
}
#[test]
fn test_unknown_property_fails() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"retention": {"type": "string", "default": "P30D"}
}
}
}),
),
(
"derived~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {
"retention": "P90D",
"unknownTrait": "some_value"
}
}),
),
];
let err = validate_traits_chain(&chain).unwrap_err();
assert!(
err.iter()
.any(|e| e.contains("additional") || e.contains("unknownTrait")),
"unknown property should fail: {err:?}"
);
}
#[test]
fn test_closed_ancestor_blocks_descendant_trait_schema_property_without_values() {
let schemas = vec![
json!({
"type": "object",
"additionalProperties": false,
"properties": {
"retention": {"type": "string"}
}
}),
json!({
"type": "object",
"properties": {
"topicRef": {"type": "string"}
}
}),
];
let traits = build_effective_traits(&schemas, &json!({}), None);
let err = traits
.validate(false)
.expect_err("abstract types must still reject incompatible trait schemas");
assert!(
err.iter()
.any(|e| e.contains("topicRef") || e.contains("additionalProperties")),
"closed ancestor should reject descendant trait property: {err:?}"
);
}
#[test]
fn test_trait_schema_type_conflict_fails_without_values() {
let schemas = vec![
json!({
"type": "object",
"properties": {
"retention": {"type": "string"}
}
}),
json!({
"type": "object",
"properties": {
"retention": {"type": "integer"}
}
}),
];
let traits = build_effective_traits(&schemas, &json!({}), None);
let err = traits
.validate(false)
.expect_err("trait schema conflicts must fail without concrete values");
assert!(
err.iter()
.any(|e| e.contains("retention") && e.contains("type")),
"type conflict should be reported: {err:?}"
);
}
#[test]
fn test_abstract_valid_narrowing_trait_schema_passes_without_values() {
let schemas = vec![
json!({
"type": "object",
"properties": {"priority": {"type": "string"}}
}),
json!({
"type": "object",
"properties": {
"priority": {"type": "string", "enum": ["low", "high"]},
"note": {"type": "string"}
}
}),
];
let traits = build_effective_traits(&schemas, &json!({}), None);
assert!(
traits.validate(false).is_ok(),
"valid narrowing must pass without values: {:?}",
traits.validate(false)
);
}
#[test]
fn test_descendant_closed_trait_schema_orphans_ancestor_property_fails() {
let schemas = vec![
json!({
"type": "object",
"properties": {"retention": {"type": "string"}}
}),
json!({
"type": "object",
"additionalProperties": false,
"properties": {"topicRef": {"type": "string"}}
}),
];
let traits = build_effective_traits(&schemas, &json!({}), None);
let err = traits
.validate(false)
.expect_err("a closed descendant orphaning an ancestor trait must fail");
assert!(
err.iter()
.any(|e| e.contains("retention") && e.contains("additionalProperties")),
"orphaned ancestor property should be reported: {err:?}"
);
}
#[test]
fn test_descendant_closed_trait_schema_restating_ancestor_property_passes() {
let schemas = vec![
json!({
"type": "object",
"properties": {"retention": {"type": "string"}}
}),
json!({
"type": "object",
"additionalProperties": false,
"properties": {
"retention": {"type": "string"},
"topicRef": {"type": "string"}
}
}),
];
let traits = build_effective_traits(&schemas, &json!({}), None);
assert!(
traits.validate(false).is_ok(),
"restating the ancestor property keeps it usable: {:?}",
traits.validate(false)
);
}
#[test]
fn test_descendant_nested_closed_trait_schema_orphans_ancestor_property_fails() {
let schemas = vec![
json!({
"type": "object",
"properties": {
"routing": {
"type": "object",
"properties": {
"source": {"type": "string"}
}
}
}
}),
json!({
"type": "object",
"properties": {
"routing": {
"type": "object",
"additionalProperties": false,
"properties": {
"target": {"type": "string"}
}
}
}
}),
];
let traits = build_effective_traits(&schemas, &json!({}), None);
let err = traits
.validate(false)
.expect_err("a closed nested descendant orphaning an ancestor trait must fail");
assert!(
err.iter()
.any(|e| { e.contains("routing.source") && e.contains("additionalProperties") }),
"orphaned nested ancestor property should be reported: {err:?}"
);
}
#[test]
fn test_descendant_nested_closed_trait_schema_restating_ancestor_property_passes() {
let schemas = vec![
json!({
"type": "object",
"properties": {
"routing": {
"type": "object",
"properties": {
"source": {"type": "string"}
}
}
}
}),
json!({
"type": "object",
"properties": {
"routing": {
"type": "object",
"additionalProperties": false,
"properties": {
"source": {"type": "string"},
"target": {"type": "string"}
}
}
}
}),
];
let traits = build_effective_traits(&schemas, &json!({}), None);
assert!(
traits.validate(false).is_ok(),
"restating the nested ancestor property keeps it usable: {:?}",
traits.validate(false)
);
}
#[test]
fn test_descendant_nested_allof_closed_branch_orphans_ancestor_property_fails() {
let schemas = vec![
json!({
"type": "object",
"properties": {
"routing": {
"type": "object",
"properties": {
"source": {"type": "string"}
}
}
}
}),
json!({
"type": "object",
"properties": {
"routing": {
"type": "object",
"allOf": [
{
"type": "object",
"additionalProperties": false,
"properties": {
"target": {"type": "string"}
}
},
{
"type": "object",
"properties": {
"source": {"type": "string"}
}
}
]
}
}
}),
];
let traits = build_effective_traits(&schemas, &json!({}), None);
let err = traits
.validate(false)
.expect_err("a closed allOf branch orphaning an ancestor trait must fail");
assert!(
err.iter()
.any(|e| { e.contains("routing.source") && e.contains("additionalProperties") }),
"orphaned nested allOf ancestor property should be reported: {err:?}"
);
}
#[test]
fn test_override_in_chain() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"retention": {"type": "string"}
}
}
}),
),
(
"mid~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {"retention": "P30D"}
}),
),
(
"leaf~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {"retention": "P365D"}
}),
),
];
assert!(validate_traits_chain(&chain).is_ok());
}
#[test]
fn test_both_keywords_in_same_schema() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"topicRef": {"type": "string"},
"retention": {"type": "string", "default": "P30D"}
}
}
}),
),
(
"mid~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"auditRetention": {"type": "string", "default": "P365D"}
}
},
"x-gts-traits": {
"topicRef": "gts.x.core.events.topic.v1~x.test._.audit.v1"
}
}),
),
];
assert!(validate_traits_chain(&chain).is_ok());
}
#[test]
fn test_three_level_chain_missing_in_leaf() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"retention": {"type": "string", "default": "P30D"}
}
}
}),
),
(
"mid~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"priority": {"type": "string"}
},
"required": ["priority"]
}
}),
),
(
"leaf~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {"retention": "P90D"}
}),
),
];
let err = validate_traits_chain(&chain).unwrap_err();
assert!(
err.iter().any(|e| e.contains("priority")),
"should mention missing priority: {err:?}"
);
}
#[test]
fn test_enum_constraint_violation() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "critical"],
"default": "medium"
}
}
}
}),
),
(
"derived~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {"priority": "ultra_high"}
}),
),
];
let err = validate_traits_chain(&chain).unwrap_err();
assert!(!err.is_empty(), "enum violation should fail");
}
#[test]
fn test_minimum_violation() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"maxRetries": {
"type": "integer",
"minimum": 0,
"maximum": 10,
"default": 3
}
}
}
}),
),
(
"derived~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {"maxRetries": -1}
}),
),
];
let err = validate_traits_chain(&chain).unwrap_err();
assert!(!err.is_empty(), "minimum violation should fail");
}
#[test]
fn test_narrowing_valid() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"priority": {"type": "string"},
"retention": {"type": "string", "default": "P30D"}
}
}
}),
),
(
"mid~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "critical"]
}
}
},
"x-gts-traits": {"priority": "high"}
}),
),
];
assert!(validate_traits_chain(&chain).is_ok());
}
#[test]
fn test_narrowing_violation() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"priority": {"type": "string"},
"retention": {"type": "string", "default": "P30D"}
}
}
}),
),
(
"mid~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "critical"]
}
}
},
"x-gts-traits": {"priority": "high"}
}),
),
(
"leaf~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {"priority": "ultra_high"}
}),
),
];
let err = validate_traits_chain(&chain).unwrap_err();
assert!(!err.is_empty(), "narrowing violation should fail");
}
#[test]
fn test_deep_inheritance_chain() {
let mut chain = vec![(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"retention": {"type": "string", "default": "P30D"}
}
}
}),
)];
for i in 1..super::MAX_RECURSION_DEPTH {
chain.push((
format!("level{i}~"),
json!({"$schema": "http://json-schema.org/draft-07/schema#", "type": "object"}),
));
}
assert!(validate_traits_chain(&chain).is_ok());
}
#[test]
fn test_malformed_trait_schema_not_object() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": "not_an_object"
}),
),
(
"derived~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {"foo": "bar"}
}),
),
];
let result = validate_traits_chain(&chain);
assert!(
result.is_err(),
"malformed trait schema should fail: {result:?}"
);
}
#[test]
fn test_trait_values_as_object() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"retry": {
"type": "object",
"properties": {
"maxAttempts": {"type": "integer", "default": 3},
"backoff": {"type": "string", "default": "exponential"}
}
}
}
}
}),
),
(
"derived~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {
"retry": {"maxAttempts": 5}
}
}),
),
];
assert!(
validate_traits_chain(&chain).is_ok(),
"object trait values should be accepted"
);
}
#[test]
fn test_trait_values_as_array() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"tags": {
"type": "array",
"items": {"type": "string"},
"default": []
}
}
}
}),
),
(
"derived~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {
"tags": ["audit", "compliance"]
}
}),
),
];
assert!(
validate_traits_chain(&chain).is_ok(),
"array trait values should be accepted"
);
}
#[test]
fn test_x_gts_keys_inside_trait_schema_are_tolerated() {
let chain = vec![(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"x-gts-traits-schema": {"type": "object"},
"x-gts-traits": {"foo": "bar"},
"properties": {
"retention": {"type": "string"}
}
},
"x-gts-traits": {"retention": "P30D"}
}),
)];
assert!(
validate_traits_chain(&chain).is_ok(),
"x-gts-traits / x-gts-traits-schema nested inside a trait-schema body \
should be tolerated as unknown JSON Schema keywords"
);
}
#[test]
fn test_nested_object_defaults_applied() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"retry": {
"type": "object",
"properties": {
"maxAttempts": {"type": "integer", "default": 3},
"backoff": {"type": "string", "default": "exponential"}
}
}
}
}
}),
),
(
"derived~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {
"retry": {"maxAttempts": 5}
}
}),
),
];
assert!(
validate_traits_chain(&chain).is_ok(),
"nested defaults should fill in missing sub-properties"
);
}
#[test]
fn test_improved_error_message_includes_type() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"topicRef": {"type": "string"},
"retention": {"type": "string", "default": "P30D"}
},
"required": ["topicRef"]
}
}),
),
(
"derived~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {"retention": "P90D"}
}),
),
];
let err = validate_traits_chain(&chain).unwrap_err();
assert!(
err.iter().any(|e| e.contains("type: string")),
"error message should include expected type: {err:?}"
);
}
#[test]
fn test_empty_trait_schema_permits_any_traits() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {}
}),
),
(
"derived~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {"anything": "goes", "count": 42}
}),
),
];
assert!(
validate_traits_chain(&chain).is_ok(),
"empty trait schema should permit any traits"
);
}
#[test]
fn test_duplicate_property_dedup_rightmost_wins() {
let chain = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"priority": {"type": "string"},
"retention": {"type": "string", "default": "P30D"}
}
}
}),
),
(
"mid~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"priority": {
"type": "string",
"enum": ["low", "medium", "high"]
}
}
}
}),
),
(
"leaf~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits": {"priority": "high"}
}),
),
];
assert!(
validate_traits_chain(&chain).is_ok(),
"dedup should keep rightmost definition"
);
let chain_missing = vec![
(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"priority": {"type": "string"},
"retention": {"type": "string", "default": "P30D"}
},
"required": ["priority"]
}
}),
),
(
"mid~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"properties": {
"priority": {
"type": "string",
"enum": ["low", "medium", "high"]
}
}
}
}),
),
(
"leaf~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#", "type": "object"}),
),
];
let err = validate_traits_chain(&chain_missing).unwrap_err();
let unresolved_priority: Vec<_> = err
.iter()
.filter(|e| e.contains("priority") && e.contains("is not resolved"))
.collect();
assert_eq!(
unresolved_priority.len(),
1,
"priority should be reported as unresolved exactly once, got: {unresolved_priority:?}"
);
}
#[test]
fn test_invalid_trait_schema_caught_early() {
let chain = vec![(
"base~".to_owned(),
json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-gts-traits-schema": {
"type": "invalid_type_value"
}
}),
)];
let err = validate_traits_chain(&chain).unwrap_err();
assert!(
err.iter()
.any(|e| e.contains("not a valid JSON Schema") || e.contains("failed to compile")),
"should report invalid JSON Schema early: {err:?}"
);
}
#[test]
fn test_chain_default_leaf_wins_over_ancestor() {
let base_ts = json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"retention": {"type": "string", "default": "P30D"}
}
});
let mid_ts = json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"retention": {"type": "string", "default": "P90D"}
}
});
let leaf_ts = json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"retention": {"type": "string", "default": "P365D"}
}
});
let effective = build_effective_traits_schema(&[base_ts, mid_ts, leaf_ts]);
let materialized = materialize_traits(&effective, &Value::Object(serde_json::Map::new()));
let retention = materialized
.as_object()
.and_then(|m| m.get("retention"))
.and_then(Value::as_str)
.expect("retention should be present after materialization");
assert_eq!(
retention, "P365D",
"leaf-most default must win; got {retention}"
);
}
#[test]
fn test_chain_default_explicit_value_wins_over_defaults() {
let base_ts = json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"retention": {"type": "string", "default": "P30D"}
}
});
let mid_ts = json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"retention": {"type": "string", "default": "P90D"}
}
});
let leaf_ts = json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"retention": {"type": "string", "default": "P365D"}
}
});
let effective = build_effective_traits_schema(&[base_ts, mid_ts, leaf_ts]);
let mut merged = serde_json::Map::new();
merged.insert("retention".to_owned(), Value::String("P42D".to_owned()));
let materialized = materialize_traits(&effective, &Value::Object(merged));
let retention = materialized
.as_object()
.and_then(|m| m.get("retention"))
.and_then(Value::as_str)
.expect("retention should be present after materialization");
assert_eq!(
retention, "P42D",
"explicit chain-merged value must override all defaults; got {retention}"
);
}
#[test]
fn test_chain_default_null_delete_restores_leaf_default() {
let base_ts = json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"retention": {"type": "string", "default": "P30D"}
}
});
let leaf_ts = json!({"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"retention": {"type": "string", "default": "P365D"}
}
});
let mut merged = serde_json::Map::new();
merge_rfc7396_into(
&mut merged,
json!({"retention": "P7D"}).as_object().unwrap(),
);
merge_rfc7396_into(&mut merged, json!({"retention": null}).as_object().unwrap());
assert!(
!merged.contains_key("retention"),
"null patch should remove the key from merged"
);
let effective = build_effective_traits_schema(&[base_ts, leaf_ts]);
let materialized = materialize_traits(&effective, &Value::Object(merged));
let retention = materialized
.as_object()
.and_then(|m| m.get("retention"))
.and_then(Value::as_str)
.expect("retention should be restored from the leaf default");
assert_eq!(
retention, "P365D",
"after null delete, materialization must use the leaf-most default; got {retention}"
);
}
#[test]
fn test_build_effective_traits_materializes_and_pins_dialect() {
let ts = json!({
"type": "object",
"properties": { "retention": {"type": "string", "default": "P30D"} }
});
let merged = json!({});
let traits = super::build_effective_traits(
std::slice::from_ref(&ts),
&merged,
Some("http://json-schema.org/draft-07/schema#"),
);
assert_eq!(
traits.schema["$schema"],
"http://json-schema.org/draft-07/schema#"
);
assert_eq!(traits.values["retention"], "P30D"); }
#[test]
fn test_validate_trait_values_flags_x_gts_ref_violation() {
let schema = json!({
"type": "object",
"properties": {
"topicRef": {"type": "string", "x-gts-ref": "gts.x.core.events.topic.v1~"}
}
});
let values = json!({ "topicRef": "not-a-gts-id" });
let res = super::validate_trait_values(&schema, &values, false);
assert!(
res.is_err(),
"x-gts-ref violation should be reported: {res:?}"
);
}
#[test]
fn test_inline_local_pointers_resolves_against_root() {
let root = json!({
"$defs": { "Retention": {"type": "string", "enum": ["P30D"]} },
"x-gts-traits-schema": {
"type": "object",
"properties": { "retention": {"$ref": "#/$defs/Retention"} }
}
});
let fragment = &root["x-gts-traits-schema"];
let inlined = super::inline_local_pointers(fragment, &root);
let retention = &inlined["properties"]["retention"];
assert!(
retention.get("$ref").is_none(),
"local pointer must be inlined: {retention}"
);
assert_eq!(retention["enum"], json!(["P30D"]));
}
#[test]
fn test_inline_local_pointers_leaves_unresolvable_and_gts_refs_untouched() {
let root = json!({ "type": "object" }); let fragment = json!({
"allOf": [
{"$ref": "gts://gts.x.a.b.v1~"},
{"$ref": "#/$defs/Missing"}
]
});
let inlined = super::inline_local_pointers(&fragment, &root);
assert_eq!(inlined["allOf"][0]["$ref"], "gts://gts.x.a.b.v1~");
assert_eq!(inlined["allOf"][1]["$ref"], "#/$defs/Missing");
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod inline_traits_schema_tests {
use super::*;
use crate::gts::GtsInstanceId;
use schemars::JsonSchema;
fn collect_refs(v: &Value, out: &mut Vec<String>) {
match v {
Value::Object(map) => {
if let Some(Value::String(r)) = map.get("$ref") {
out.push(r.clone());
}
for val in map.values() {
collect_refs(val, out);
}
}
Value::Array(arr) => {
for val in arr {
collect_refs(val, out);
}
}
_ => {}
}
}
#[derive(JsonSchema)]
#[allow(dead_code)]
enum SeverityLevel {
Low,
High,
}
#[derive(JsonSchema)]
#[allow(dead_code)]
struct EnumFieldTraits {
level: SeverityLevel,
}
#[test]
fn enum_field_is_inlined_with_no_dangling_refs() {
let schema = inline_traits_schema_of::<EnumFieldTraits>();
assert!(
schema.get("$defs").is_none(),
"embedded fragment must not carry a $defs block: {schema}"
);
let mut refs = Vec::new();
collect_refs(&schema, &mut refs);
assert!(
refs.is_empty(),
"embedded fragment has dangling refs: {refs:?} in {schema}"
);
let serialized = schema.to_string();
assert!(
serialized.contains("Low") && serialized.contains("High"),
"enum variants should be inlined into the fragment: {schema}"
);
}
#[derive(JsonSchema)]
#[allow(dead_code)]
struct InstanceIdTraits {
topic_ref: GtsInstanceId,
}
#[test]
fn gts_instance_id_field_keeps_canonical_inline_form() {
let schema = inline_traits_schema_of::<InstanceIdTraits>();
let mut refs = Vec::new();
collect_refs(&schema, &mut refs);
assert!(
refs.is_empty(),
"unexpected dangling refs: {refs:?} in {schema}"
);
let prop = &schema["properties"]["topic_ref"];
assert_eq!(prop["type"], "string");
assert_eq!(prop["format"], "gts-instance-id");
assert_eq!(prop["x-gts-ref"], format!("{}*", crate::GTS_ID_PREFIX));
}
}