@@ -97,6 +97,95 @@ pub struct SchemaWithNestedContactV1 {
9797 pub contact : NestedContact ,
9898}
9999
100+ fn composed_contact_schema ( generator : & mut schemars:: SchemaGenerator ) -> schemars:: Schema {
101+ let contact = generator. subschema_for :: < NestedContact > ( ) ;
102+ serde_json:: from_value ( serde_json:: json!( {
103+ "allOf" : [
104+ contact,
105+ {
106+ "type" : "object" ,
107+ "properties" : {
108+ "label" : { "type" : "string" }
109+ } ,
110+ "required" : [ "label" ]
111+ }
112+ ]
113+ } ) )
114+ . expect ( "test schema" )
115+ }
116+
117+ #[ struct_to_gts_schema(
118+ dir_path = "schemas" ,
119+ base = true ,
120+ type_id = gts_id!( "x.test.nested.composed_definition.v1~" ) ,
121+ description = "Schema composing a definition with sibling properties" ,
122+ properties = "schema_type,composed"
123+ ) ]
124+ #[ derive( Debug ) ]
125+ pub struct SchemaWithComposedDefinitionV1 {
126+ #[ serde( rename = "type" ) ]
127+ pub schema_type : GtsTypeId ,
128+ #[ schemars( schema_with = "composed_contact_schema" ) ]
129+ pub composed : serde_json:: Value ,
130+ }
131+
132+ // Newtype structs without a doc comment: Schemars emits each definition as a
133+ // bare `{"$ref": ...}` alias, so the combinator branch only reaches
134+ // `NestedContact` through two hops of aliasing.
135+ #[ derive( Debug , serde:: Serialize , serde:: Deserialize , schemars:: JsonSchema ) ]
136+ pub struct ContactAlias ( pub NestedContact ) ;
137+
138+ #[ derive( Debug , serde:: Serialize , serde:: Deserialize , schemars:: JsonSchema ) ]
139+ pub struct ContactAliasAlias ( pub ContactAlias ) ;
140+
141+ fn composed_alias_schema ( generator : & mut schemars:: SchemaGenerator ) -> schemars:: Schema {
142+ let alias = generator. subschema_for :: < ContactAliasAlias > ( ) ;
143+ serde_json:: from_value ( serde_json:: json!( {
144+ "allOf" : [
145+ alias,
146+ {
147+ "type" : "object" ,
148+ "properties" : {
149+ "label" : { "type" : "string" }
150+ } ,
151+ "required" : [ "label" ]
152+ }
153+ ]
154+ } ) )
155+ . expect ( "test schema" )
156+ }
157+
158+ #[ struct_to_gts_schema(
159+ dir_path = "schemas" ,
160+ base = true ,
161+ type_id = gts_id!( "x.test.nested.aliased_definition.v1~" ) ,
162+ description = "Schema composing an aliased definition with sibling properties" ,
163+ properties = "schema_type,composed"
164+ ) ]
165+ #[ derive( Debug ) ]
166+ pub struct SchemaWithAliasedDefinitionV1 {
167+ #[ serde( rename = "type" ) ]
168+ pub schema_type : GtsTypeId ,
169+ #[ schemars( schema_with = "composed_alias_schema" ) ]
170+ pub composed : serde_json:: Value ,
171+ }
172+
173+ #[ struct_to_gts_schema(
174+ dir_path = "schemas" ,
175+ base = true ,
176+ type_id = gts_id!( "x.test.nested.shared_definition.v1~" ) ,
177+ description = "Schema using one definition as a combinator branch and as a property" ,
178+ properties = "schema_type,composed,plain"
179+ ) ]
180+ #[ derive( Debug ) ]
181+ pub struct SchemaWithSharedDefinitionV1 {
182+ #[ serde( rename = "type" ) ]
183+ pub schema_type : GtsTypeId ,
184+ #[ schemars( schema_with = "composed_contact_schema" ) ]
185+ pub composed : serde_json:: Value ,
186+ pub plain : NestedContact ,
187+ }
188+
100189#[ derive( Debug , serde:: Serialize , serde:: Deserialize , schemars:: JsonSchema ) ]
101190#[ schemars( extend( "additionalProperties" = true ) ) ]
102191pub struct OpenExtensionPoint {
@@ -466,6 +555,107 @@ mod tests {
466555 jsonschema:: validator_for ( & schema) . expect ( "emitted Draft-07 schema must compile" ) ;
467556 }
468557
558+ #[ test]
559+ fn test_definition_referenced_by_combinator_branch_stays_open ( ) {
560+ let schema = SchemaWithComposedDefinitionV1 :: gts_schema_with_refs ( ) ;
561+ assert_eq ! (
562+ schema. pointer( "/properties/composed/allOf/0/$ref" ) ,
563+ Some ( & serde_json:: json!( "#/definitions/NestedContact" ) )
564+ ) ;
565+ assert ! (
566+ schema
567+ . pointer( "/definitions/NestedContact/additionalProperties" )
568+ . is_none( ) ,
569+ "a definition composed with sibling properties must stay open:\n {}" ,
570+ serde_json:: to_string_pretty( & schema) . unwrap( )
571+ ) ;
572+
573+ let validator =
574+ jsonschema:: validator_for ( & schema) . expect ( "emitted Draft-07 schema must compile" ) ;
575+ let instance = serde_json:: json!( {
576+ "type" : "gts.x.test.nested.composed_definition.v1~" ,
577+ "composed" : {
578+ "email" : "dev@example.com" ,
579+ "label" : "primary"
580+ }
581+ } ) ;
582+ assert ! (
583+ validator. is_valid( & instance) ,
584+ "combinator siblings should not be rejected by a closed definition"
585+ ) ;
586+ }
587+
588+ /// The branch may reach its definition through a chain of aliasing
589+ /// definitions, which Schemars emits for newtype structs.
590+ #[ test]
591+ fn test_definition_aliased_by_combinator_branch_stays_open ( ) {
592+ let schema = SchemaWithAliasedDefinitionV1 :: gts_schema_with_refs ( ) ;
593+ assert_eq ! (
594+ schema. pointer( "/definitions/ContactAlias/$ref" ) ,
595+ Some ( & serde_json:: json!( "#/definitions/NestedContact" ) ) ,
596+ "test relies on Schemars emitting a bare $ref alias:\n {}" ,
597+ serde_json:: to_string_pretty( & schema) . unwrap( )
598+ ) ;
599+ assert ! (
600+ schema
601+ . pointer( "/definitions/NestedContact/additionalProperties" )
602+ . is_none( ) ,
603+ "a definition an alias chain composes with sibling properties must stay open:\n {}" ,
604+ serde_json:: to_string_pretty( & schema) . unwrap( )
605+ ) ;
606+
607+ let validator =
608+ jsonschema:: validator_for ( & schema) . expect ( "emitted Draft-07 schema must compile" ) ;
609+ let instance = serde_json:: json!( {
610+ "type" : "gts.x.test.nested.aliased_definition.v1~" ,
611+ "composed" : {
612+ "email" : "dev@example.com" ,
613+ "label" : "primary"
614+ }
615+ } ) ;
616+ assert ! (
617+ validator. is_valid( & instance) ,
618+ "combinator siblings should not be rejected through an alias chain"
619+ ) ;
620+ }
621+
622+ /// Reachability is tracked per `definitions` entry, not per use site, so one
623+ /// composed use keeps the entry open for its ordinary uses too. That trades
624+ /// the in-place evolvability of the ordinary level for a satisfiable
625+ /// composition - see the pass documentation in `gts-macros/src/lib.rs`.
626+ #[ test]
627+ fn test_shared_definition_stays_open_for_its_ordinary_use ( ) {
628+ let schema = SchemaWithSharedDefinitionV1 :: gts_schema_with_refs ( ) ;
629+ assert_eq ! (
630+ schema. pointer( "/properties/plain/$ref" ) ,
631+ Some ( & serde_json:: json!( "#/definitions/NestedContact" ) )
632+ ) ;
633+ assert ! (
634+ schema
635+ . pointer( "/definitions/NestedContact/additionalProperties" )
636+ . is_none( ) ,
637+ "a definition shared with a combinator branch must stay open:\n {}" ,
638+ serde_json:: to_string_pretty( & schema) . unwrap( )
639+ ) ;
640+
641+ let validator =
642+ jsonschema:: validator_for ( & schema) . expect ( "emitted Draft-07 schema must compile" ) ;
643+ let instance = serde_json:: json!( {
644+ "type" : "gts.x.test.nested.shared_definition.v1~" ,
645+ "composed" : {
646+ "email" : "dev@example.com" ,
647+ "label" : "primary"
648+ } ,
649+ "plain" : {
650+ "email" : "ops@example.com"
651+ }
652+ } ) ;
653+ assert ! (
654+ validator. is_valid( & instance) ,
655+ "both uses of the shared definition must still accept valid instances"
656+ ) ;
657+ }
658+
469659 /// Nested object levels are closed so that a later definition of the type
470660 /// can add an optional property backward compatibly (gts-spec sec 4.4-4.5),
471661 /// while the levels where closing would be wrong are left alone.
0 commit comments