Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions gts-id/src/gts_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ mod tests {
assert_eq!(seg.package(), "core");
assert_eq!(seg.namespace(), "events");
assert_eq!(seg.type_name(), "event");
assert_eq!(seg.ver_major(), 1);
assert_eq!(seg.ver_major_opt(), Some(1));
assert_eq!(seg.ver_minor(), Some(2));
}

Expand Down Expand Up @@ -359,14 +359,14 @@ mod tests {
#[test]
fn test_gts_id_version_without_minor() {
let id = GtsId::try_new(&gts_id("x.core.events.event.v1~")).expect("test");
assert_eq!(id.segments[0].ver_major(), 1);
assert_eq!(id.segments[0].ver_major_opt(), Some(1));
assert_eq!(id.segments[0].ver_minor(), None);
}

#[test]
fn test_gts_id_version_with_large_numbers() {
let id = GtsId::try_new(&gts_id("x.core.events.event.v99.999~")).expect("test");
assert_eq!(id.segments[0].ver_major(), 99);
assert_eq!(id.segments[0].ver_major_opt(), Some(99));
assert_eq!(id.segments[0].ver_minor(), Some(999));
}

Expand Down
40 changes: 5 additions & 35 deletions gts-id/src/gts_id_segment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,6 @@ impl GtsIdSegmentParts {
&self.type_name
}

/// The major version, or `0` when unspecified in a wildcard segment.
///
/// Use [`Self::ver_major_opt`] when the distinction between an unspecified
/// version and a real `v0` matters.
#[must_use]
pub fn ver_major(&self) -> u32 {
self.ver_major.unwrap_or(0)
}

/// The major version when one was specified.
#[must_use]
pub fn ver_major_opt(&self) -> Option<u32> {
Expand Down Expand Up @@ -207,15 +198,6 @@ impl GtsIdSegment {
self.parts().map_or("", |p| &p.type_name)
}

/// The major version, or `0` for a UUID tail.
///
/// Use [`Self::ver_major_opt`] when the distinction between an absent
/// version and a real `v0` matters.
#[must_use]
pub fn ver_major(&self) -> u32 {
self.parts().map_or(0, GtsIdSegmentParts::ver_major)
}

/// The major version when this is a named GTS segment.
#[must_use]
pub fn ver_major_opt(&self) -> Option<u32> {
Expand Down Expand Up @@ -347,18 +329,6 @@ impl GtsIdPatternSegment {
}
}

/// The major version, or `0` when unspecified.
///
/// Use [`Self::ver_major_opt`] when the distinction between an unspecified
/// version wildcard and a real `v0` matters.
#[must_use]
pub fn ver_major(&self) -> u32 {
match self {
GtsIdPatternSegment::Segment(s) => s.ver_major(),
GtsIdPatternSegment::Wildcard(p) => p.ver_major(),
}
}

/// The major version when one was specified in this pattern segment.
#[must_use]
pub fn ver_major_opt(&self) -> Option<u32> {
Expand Down Expand Up @@ -661,15 +631,15 @@ mod tests {
assert_eq!(parsed.package(), "core");
assert_eq!(parsed.namespace(), "events");
assert_eq!(parsed.type_name(), "event");
assert_eq!(parsed.ver_major(), 1);
assert_eq!(parsed.ver_major_opt(), Some(1));
assert_eq!(parsed.ver_minor(), None);
assert!(parsed.is_type());
}

#[test]
fn test_valid_segment_with_minor() {
let parsed = GtsIdSegment::parse(1, "x.core.events.event.v1.2~").unwrap();
assert_eq!(parsed.ver_major(), 1);
assert_eq!(parsed.ver_major_opt(), Some(1));
assert_eq!(parsed.ver_minor(), Some(2));
}

Expand Down Expand Up @@ -835,7 +805,7 @@ mod tests {
assert_eq!(parsed.package(), "pkg");
assert_eq!(parsed.namespace(), "ns");
assert_eq!(parsed.type_name(), "type");
assert_eq!(parsed.ver_major(), 0);
assert_eq!(parsed.ver_major_opt(), None);
assert_eq!(parsed.ver_minor(), None);
}

Expand Down Expand Up @@ -882,7 +852,7 @@ mod tests {
assert_eq!(seg.raw(), UUID_TAIL);
assert!(!seg.is_type());
assert_eq!(seg.vendor(), "");
assert_eq!(seg.ver_major(), 0);
assert_eq!(seg.ver_major_opt(), None);
assert_eq!(seg.ver_minor(), None);

#[cfg(feature = "uuid")]
Expand Down Expand Up @@ -920,7 +890,7 @@ mod tests {
assert_eq!(parts.package(), "core");
assert_eq!(parts.namespace(), "events");
assert_eq!(parts.type_name(), "event");
assert_eq!(parts.ver_major(), 1);
assert_eq!(parts.ver_major_opt(), Some(1));
assert_eq!(parts.ver_minor(), Some(2));
assert!(parts.is_type());
}
Expand Down
157 changes: 157 additions & 0 deletions gts-macros/tests/additional_properties_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//! Behavioural assertions for the `additionalProperties` content model the
//! macro emits, one pointer per level under test.
//!
//! These share the `additional_properties_*` fixtures with `golden_tests.rs`,
//! which snapshot-matches whole documents. The snapshot cannot express *which*
//! value is correct — `GTS_GOLDEN=overwrite` blesses whatever the macro emits —
//! so the rule each fixture exists to prove is stated here instead. The
//! fixtures are `include!`d rather than imported because every file under
//! `tests/` is its own crate root, so a module declared in `golden_tests.rs` is
//! not reachable from here.

#![allow(clippy::unwrap_used, clippy::expect_used)]

/// Declares the fixture modules this file asserts over.
macro_rules! fixtures {
($($name:ident),+ $(,)?) => {
$(
mod $name {
include!(concat!("golden/", stringify!($name), ".rs"));
}
)+
};
}

fixtures!(
additional_properties_nested_closed,
additional_properties_explicit_open,
additional_properties_flattened_map,
additional_properties_content_models,
additional_properties_gts_root_open,
additional_properties_gts_derived_open,
);

/// The `additionalProperties` value at `pointer` in the schema generated for
/// `type_id`, or `"<absent>"` when the level states no content model.
///
/// `"<absent>"` is a distinct expectation, not a fallback: on a combinator
/// branch or on a derived type's `allOf` wrapper, both `false` and `true` would
/// be wrong and stating nothing is the correct output.
#[track_caller]
fn at(schemas: &[(String, serde_json::Value)], type_id: &str, pointer: &str) -> String {
let (_, schema) = schemas
.iter()
.find(|(id, _)| id == type_id)
.unwrap_or_else(|| panic!("case does not generate '{type_id}'"));
schema
.pointer(pointer)
.map_or_else(|| "<absent>".to_owned(), ToString::to_string)
}

/// Ordinary nested structs are closed at every level, including the Draft-07
/// `definitions` the nested types land in.
#[test]
fn nested_structs_are_closed_at_every_level() {
let nested = additional_properties_nested_closed::schemas();
let nested_id = "gts.x.test.golden.nestedclosed.v1~";
for pointer in [
"/additionalProperties",
"/definitions/Profile/additionalProperties",
"/definitions/Contact/additionalProperties",
] {
assert_eq!(at(&nested, nested_id, pointer), "false", "{pointer}");
}
}

/// An explicitly open nested struct keeps its own model while the GTS root stays
/// closed - the opt-out applies where it is declared, not upward.
#[test]
fn an_explicit_open_level_does_not_open_the_root() {
let open = additional_properties_explicit_open::schemas();
let open_id = "gts.x.test.golden.explicitopen.v1~";
assert_eq!(at(&open, open_id, "/additionalProperties"), "false");
assert_eq!(
at(
&open,
open_id,
"/definitions/ExtensionPoint/additionalProperties"
),
"true"
);
}

/// A flattened map is the same thing spelled through Serde.
#[test]
fn a_flattened_map_level_stays_open() {
let map = additional_properties_flattened_map::schemas();
let map_id = "gts.x.test.golden.flattenedmap.v1~";
assert_eq!(at(&map, map_id, "/additionalProperties"), "false");
assert_eq!(
at(
&map,
map_id,
"/definitions/ExtensibleMetadata/additionalProperties"
),
"true"
);
}

/// A schema-valued map level keeps its value constraint, and combinator branches
/// are left alone - closing them would reject instances a sibling branch accepts.
#[test]
fn map_value_constraints_and_combinator_branches_are_preserved() {
let models = additional_properties_content_models::schemas();
let models_id = "gts.x.test.golden.contentmodels.v1~";
assert_eq!(at(&models, models_id, "/additionalProperties"), "false");
assert_eq!(
at(
&models,
models_id,
"/properties/labels/additionalProperties"
),
r#"{"type":"string"}"#
);
for branch in ["0", "1"] {
assert_eq!(
at(
&models,
models_id,
&format!("/definitions/Choice/anyOf/{branch}/additionalProperties"),
),
"<absent>",
);
}
}

/// On a GTS base type the explicit open model applies to the root itself.
#[test]
fn a_gts_base_type_can_declare_its_root_open() {
assert_eq!(
at(
&additional_properties_gts_root_open::schemas(),
"gts.x.test.golden.rootopen.v1~",
"/additionalProperties",
),
"true"
);
}

/// On a derived type it applies to the level carrying that type's own fields,
/// not to the `allOf` wrapper - a model on the wrapper would fight the parent's.
#[test]
fn a_derived_type_opens_its_own_field_level_not_the_wrapper() {
let derived = additional_properties_gts_derived_open::schemas();
let derived_id = "gts.x.test.golden.openchain.v1~x.test.audit.payload.v1~";
assert_eq!(
at(&derived, derived_id, "/additionalProperties"),
"<absent>"
);
assert_eq!(
at(
&derived,
derived_id,
"/allOf/1/properties/payload/additionalProperties"
),
"true"
);
}
12 changes: 6 additions & 6 deletions gts-macros/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,7 @@ fn test_gts_id_segments_match_schema() {
assert_eq!(segment.package(), "core");
assert_eq!(segment.namespace(), "events");
assert_eq!(segment.type_name(), "topic");
assert_eq!(segment.ver_major(), 1);
assert_eq!(segment.ver_major_opt(), Some(1));
assert!(
segment.is_type(),
"Schema ID should be a type (ends with ~)"
Expand Down Expand Up @@ -759,7 +759,7 @@ fn test_gts_id_segments_match_instance() {
assert_eq!(type_segment.package(), "core");
assert_eq!(type_segment.namespace(), "events");
assert_eq!(type_segment.type_name(), "topic");
assert_eq!(type_segment.ver_major(), 1);
assert_eq!(type_segment.ver_major_opt(), Some(1));
assert!(type_segment.is_type(), "First segment should be a type");

// Second segment is the instance segment
Expand All @@ -768,7 +768,7 @@ fn test_gts_id_segments_match_instance() {
assert_eq!(instance_segment.package(), "commerce");
assert_eq!(instance_segment.namespace(), "orders");
assert_eq!(instance_segment.type_name(), "orders");
assert_eq!(instance_segment.ver_major(), 1);
assert_eq!(instance_segment.ver_major_opt(), Some(1));
assert_eq!(instance_segment.ver_minor(), Some(0));
}

Expand Down Expand Up @@ -796,8 +796,8 @@ fn test_schema_and_instance_segments_relationship() {
instance_type_segment.type_name()
);
assert_eq!(
schema_segment.ver_major(),
instance_type_segment.ver_major()
schema_segment.ver_major_opt(),
instance_type_segment.ver_major_opt()
);

// get_type_id() should return the schema ID (without the instance segment)
Expand Down Expand Up @@ -849,7 +849,7 @@ fn test_entity_and_gts_id_vendor_package_namespace_match() {
assert_eq!(entity_seg.package(), direct_seg.package());
assert_eq!(entity_seg.namespace(), direct_seg.namespace());
assert_eq!(entity_seg.type_name(), direct_seg.type_name());
assert_eq!(entity_seg.ver_major(), direct_seg.ver_major());
assert_eq!(entity_seg.ver_major_opt(), direct_seg.ver_major_opt());
assert_eq!(entity_seg.ver_minor(), direct_seg.ver_minor());
assert_eq!(entity_seg.is_type(), direct_seg.is_type());
}
Expand Down
29 changes: 23 additions & 6 deletions gts/src/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,16 @@ pub struct GtsIdSegmentInfo {

impl From<&crate::gts::GtsIdSegment> for GtsIdSegmentInfo {
fn from(seg: &crate::gts::GtsIdSegment) -> Self {
// A concrete segment always carries a real major version (including a
// legitimate `v0`), so `ver_major` is never the wildcard "unspecified"
// sentinel here.
// A *named* concrete segment always carries a real major version,
// including a legitimate `v0`. A UUID-tail segment carries none at all,
// and reporting that as `v0` would be the same conflation the pattern
// matcher had to stop making, so the absence is passed through.
Self {
vendor: seg.vendor().to_owned(),
package: seg.package().to_owned(),
namespace: seg.namespace().to_owned(),
type_name: seg.type_name().to_owned(),
ver_major: Some(seg.ver_major()),
ver_major: seg.ver_major_opt(),
ver_minor: seg.ver_minor(),
is_type: seg.is_type(),
}
Expand Down Expand Up @@ -824,6 +825,22 @@ mod tests {
assert!(!result.error.is_empty());
}

/// A UUID-tail segment carries no version at all, so it must report
/// absence rather than a `v0` it never declared - the same conflation
/// `GtsIdPattern::matches_views` had to stop making.
#[test]
fn test_parse_id_uuid_tail_has_no_major_version() {
let result =
GtsOps::parse_id("gts.x.core.events.event.v1~7a1d2f34-5678-49ab-9012-abcdef123456");
assert!(result.ok, "{:?}", result.error);
assert_eq!(result.segments.len(), 2);
assert_eq!(result.segments[0].ver_major, Some(1));
assert_eq!(
result.segments[1].ver_major, None,
"a UUID tail must not claim v0"
);
}

#[test]
fn test_parse_id_version_zero() {
let result = GtsOps::parse_id("gts.x.pkg.ns.type.v0~");
Expand Down Expand Up @@ -1813,8 +1830,8 @@ mod tests {
incompatibility_reasons: vec![],
backward_errors: vec![],
forward_errors: vec![],
specification_version: crate::GTS_SPECIFICATION_VERSION.to_owned(),
implementation_version: crate::GTS_IMPLEMENTATION_VERSION.to_owned(),
specification_version: Some(crate::GTS_SPECIFICATION_VERSION.to_owned()),
implementation_version: Some(crate::GTS_IMPLEMENTATION_VERSION.to_owned()),
casted_entity: Some(json!({"name": "test"})),
error: None,
};
Expand Down
Loading
Loading