Skip to content
Open
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
Expand Down Expand Up @@ -175,6 +176,125 @@ public void resolveInlineModel2EqualInnerModels() {
assertNull(duplicateAddress);
}

@Test
public void resolveInlineModelDeduplicatesAgainstExistingComponentSchema() {
// When a pre-existing components/schemas entry has the same title and content as an
// inline schema encountered during flattening, the inline schema should reuse the
// pre-existing name rather than being registered as a numbered variant (e.g. Foo_1).
// Regression test for: external $ref chains producing ContainerMapping + ContainerMapping_1.
OpenAPI openapi = new OpenAPI();
openapi.setComponents(new Components());

// Pre-existing named schema registered directly in components
openapi.getComponents().addSchemas("ContainerMapping", new ObjectSchema()
.title("ContainerMapping")
.description("Describes the mapping of essence from a container")
.addProperty("track_index", new IntegerSchema()));

// Schema whose inline property content is identical to the pre-existing ContainerMapping
openapi.getComponents().addSchemas("FlowCore", new ObjectSchema()
.name("FlowCore")
.addProperty("container_mapping", new ObjectSchema()
.title("ContainerMapping")
.description("Describes the mapping of essence from a container")
.addProperty("track_index", new IntegerSchema())));

new InlineModelResolver().flatten(openapi);

// The pre-existing entry must still exist
assertNotNull(openapi.getComponents().getSchemas().get("ContainerMapping"));
// No numbered duplicate should have been created
assertNull(openapi.getComponents().getSchemas().get("ContainerMapping_1"));
}

@Test
public void resolveInlineModelDeduplicatesAcrossExternalRefChains() {
// Regression test: same external schema referenced from two schemas (one via plain $ref,
// one via $ref + sibling description as allowed in OpenAPI 3.1) must not produce a
// duplicate numbered model (Container_Mapping_1).
ParseOptions parseOptions = new ParseOptions();
parseOptions.setResolve(true);
parseOptions.setResolveResponses(true);
OpenAPI openAPI = new OpenAPIParser().readLocation(
"src/test/resources/3_0/inline-model-resolver-dedup/root.yaml",
null, parseOptions).getOpenAPI();
new InlineModelResolver().flatten(openAPI);

Map<String, Schema> schemas = openAPI.getComponents().getSchemas();
assertNotNull(schemas.get("Container_Mapping"));
assertNull("Duplicate Container_Mapping_1 must not exist", schemas.get("Container_Mapping_1"));
}

@Test
public void resolveInlineModelDeduplicatesWhenParserMutatesPropertyDescriptions() {
// Regression test: when the Swagger Parser shares and mutates resolved Schema objects
// (e.g. a shared uuid.json resolved Schema has its description overwritten by whichever
// property referencing it was last processed), two Schema objects from the same external
// file may end up with different serialised JSON despite being structurally identical.
// The structural-hash fallback in matchGenerated() (serialised without any 'description'
// fields at any level) must still deduplicate them rather than creating a numbered variant.
OpenAPI openapi = new OpenAPI();
openapi.setComponents(new Components());
openapi.setPaths(new Paths());

// Schema A: properties have "correct" descriptions (as the file author wrote them)
Schema widgetA = new ObjectSchema()
.title("Widget")
.description("A reusable widget")
.addProperty("id", new StringSchema().description("Widget identifier"))
.addProperty("name", new StringSchema().description("Widget name"));

// Schema B: same title/description/property-names, but 'id' has a DIFFERENT description
// (simulating what the Swagger Parser does when it shares and mutates a resolved sub-schema)
Schema widgetB = new ObjectSchema()
.title("Widget")
.description("A reusable widget")
.addProperty("id", new StringSchema().description("MUTATED description from another schema"))
.addProperty("name", new StringSchema().description("Widget name"));

ApiResponse responseA = new ApiResponse()
.description("OK")
.content(new Content().addMediaType("application/json",
new MediaType().schema(widgetA)));
ApiResponse responseB = new ApiResponse()
.description("OK")
.content(new Content().addMediaType("application/json",
new MediaType().schema(widgetB)));

openapi.getPaths()
.addPathItem("/a", new PathItem().get(
new Operation().operationId("getA").responses(new ApiResponses().addApiResponse("200", responseA))))
.addPathItem("/b", new PathItem().get(
new Operation().operationId("getB").responses(new ApiResponses().addApiResponse("200", responseB))));

new InlineModelResolver().flatten(openapi);

Map<String, Schema> schemas = openapi.getComponents().getSchemas();
assertNotNull("Widget schema must exist", schemas.get("Widget"));
assertNull("Duplicate Widget_1 must not exist — shape-fingerprint dedup must fire", schemas.get("Widget_1"));
}

@Test
public void resolveInlineModelDeduplicatesMultipleRefsToSameExternalFile() {
// Regression test: when the same external schema file is referenced from three separate
// paths (simulating DeletionRequest appearing multiple times in the TAMS spec), the parser
// may share the same Java Schema object across all three references. After the first
// processing mutates the object (inline sub-schemas replaced with $refs), subsequent
// encounters hash differently. The identity-based fast path in matchGenerated() must
// recognise the same object reference and avoid registering a numbered duplicate.
ParseOptions parseOptions = new ParseOptions();
parseOptions.setResolve(true);
parseOptions.setResolveResponses(true);
OpenAPI openAPI = new OpenAPIParser().readLocation(
"src/test/resources/3_0/inline-model-resolver-dedup/root.yaml",
null, parseOptions).getOpenAPI();
new InlineModelResolver().flatten(openAPI);

Map<String, Schema> schemas = openAPI.getComponents().getSchemas();
assertNotNull(schemas.get("Deletion_Request"));
assertNull("Duplicate Deletion_Request_1 must not exist", schemas.get("Deletion_Request_1"));
}

@Test
public void resolveInlineModel2DifferentInnerModelsWithSameTitle() {
OpenAPI openapi = new OpenAPI();
Expand Down Expand Up @@ -1205,4 +1325,111 @@ public void doNotWrapSingleAllOfRefs() {
assertNotNull(allOfRefWithDescriptionAndReadonly.getAllOf());
assertEquals(numberRangeRef, ((Schema) allOfRefWithDescriptionAndReadonly.getAllOf().get(0)).get$ref());
}

@Test
public void resolveInlineModelDeduplicatesWhenParserMutatesPropertyTypes() {
// Regression test: the Swagger Parser shares a single resolved Schema object across all
// usages of an external type (e.g. storage-backend.json) and strips the 'type' field
// from its properties between processing passes. The first usage sees properties with
// type:"string"; the second usage sees the same object but with type stripped to null.
// IgnoreVolatileFieldsMixIn now strips 'type' in addition to 'description', so the
// structural-hash fallback in matchGenerated() must still unify them rather than creating
// a numbered variant.
OpenAPI openapi = new OpenAPI();
openapi.setComponents(new Components());
openapi.setPaths(new Paths());

// First inline schema: properties carry explicit type annotations (as delivered by the
// parser on first encounter of the shared external schema object)
StringSchema prop1First = new StringSchema();
prop1First.setDescription("The store type");
StringSchema prop2First = new StringSchema();
prop2First.setDescription("The provider");
Schema schemaFirstPass = new ObjectSchema()
.title("StorageBackend")
.description("A storage backend")
.addProperty("store_type", prop1First)
.addProperty("provider", prop2First);

// Second inline schema: same structure but 'type' has been stripped from properties,
// simulating the Swagger Parser mutating the shared resolved Schema object between passes.
StringSchema prop1Second = new StringSchema();
prop1Second.setType(null); // simulate parser stripping the type field
prop1Second.setDescription("The store type");
StringSchema prop2Second = new StringSchema();
prop2Second.setType(null);
prop2Second.setDescription("The provider");
Schema schemaSecondPass = new ObjectSchema()
.title("StorageBackend")
.description("A storage backend")
.addProperty("store_type", prop1Second)
.addProperty("provider", prop2Second);

openapi.getPaths()
.addPathItem("/a", new PathItem().get(new Operation().operationId("getA")
.responses(new ApiResponses().addApiResponse("200", new ApiResponse()
.description("OK")
.content(new Content().addMediaType("application/json",
new MediaType().schema(schemaFirstPass)))))))
.addPathItem("/b", new PathItem().get(new Operation().operationId("getB")
.responses(new ApiResponses().addApiResponse("200", new ApiResponse()
.description("OK")
.content(new Content().addMediaType("application/json",
new MediaType().schema(schemaSecondPass)))))));

new InlineModelResolver().flatten(openapi);

Map<String, Schema> schemas = openapi.getComponents().getSchemas();
assertNotNull("StorageBackend schema must exist", schemas.get("StorageBackend"));
assertNull("Duplicate StorageBackend_1 must not exist — type-stripped structural match must fire",
schemas.get("StorageBackend_1"));
}

@Test
public void deduplicateComponentsRemovesNumberedDuplicateOfTitledSchemaAndRewritesRefs() {
// Regression test: when flattening creates a numbered duplicate of a titled component
// (e.g. FlowSegment_1 alongside FlowSegment) because matchGenerated() missed the match
// due to T0-vs-T1 pre-populate timing, deduplicateComponents() must remove the duplicate
// and rewrite all $refs to it throughout the spec so the generated code only contains one
// class.
OpenAPI openapi = new OpenAPI();
openapi.setComponents(new Components());
openapi.setPaths(new Paths());

Schema canonical = new ObjectSchema()
.title("Widget")
.description("A widget")
.addProperty("name", new StringSchema());

// Duplicate: same title and structure — simulates what flatten() can produce when the
// pre-populate T0 signature no longer matches the T1 form of the same inline schema.
Schema duplicate = new ObjectSchema()
.title("Widget")
.description("A widget")
.addProperty("name", new StringSchema());

openapi.getComponents().addSchemas("Widget", canonical);
openapi.getComponents().addSchemas("Widget_1", duplicate);

// Path whose response references the numbered duplicate
openapi.getPaths().addPathItem("/widgets", new PathItem().get(
new Operation().operationId("getWidget").responses(
new ApiResponses().addApiResponse("200", new ApiResponse()
.description("OK")
.content(new Content().addMediaType("application/json",
new MediaType().schema(new Schema<>()
.$ref("#/components/schemas/Widget_1"))))))));

new InlineModelResolver().flatten(openapi);

Map<String, Schema> schemas = openapi.getComponents().getSchemas();
assertNotNull("Canonical Widget must survive deduplication", schemas.get("Widget"));
assertNull("Duplicate Widget_1 must be removed by deduplicateComponents()", schemas.get("Widget_1"));

// The $ref in the path response must have been rewritten to the canonical name
Schema responseSchema = openapi.getPaths().get("/widgets").getGet()
.getResponses().get("200").getContent().get("application/json").getSchema();
assertEquals("$ref must be rewritten from Widget_1 to Widget",
"#/components/schemas/Widget", responseSchema.get$ref());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
title: Collection Item
description: An item in a collection
type: object
properties:
id:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
title: Container Mapping
description: Defines the location of Flow essence data in a container track
type: object
properties:
track_index:
description: A zero-based track index
type: integer
minimum: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
title: Deletion Request
description: Describes an ongoing deletion request
type: object
required:
- id
- status
properties:
id:
type: string
format: uuid
status:
type: string
enum: [created, done]
error:
description: Provides more information for the error status
$ref: error.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
title: Error
description: An API error response
type: object
properties:
code:
type: integer
message:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
title: Flow Collection
description: A collection of flows
type: array
items:
type: object
title: Flow Collection Item
allOf:
- $ref: collection-item.yaml
- type: object
properties:
container_mapping:
description: Describes the mapping of the Flow essence from this collection's container
$ref: container-mapping.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
title: Flow Core
description: Core flow properties
type: object
properties:
id:
type: string
container_mapping:
$ref: container-mapping.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
title: Video Flow
description: A video flow
type: object
allOf:
- $ref: flow-core.yaml
- type: object
properties:
format:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
openapi: "3.1.0"
info:
title: Dedup Test
version: "1.0"
paths:
/flows:
get:
operationId: getFlows
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: flow-video.yaml
/flow-collections:
get:
operationId: getFlowCollections
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: flow-collection.yaml
/deletions/{id}:
get:
operationId: getDeletion
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: deletion-request.yaml
/deletions/{id}/status:
get:
operationId: getDeletionStatus
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: deletion-request.yaml
/deletions:
get:
operationId: listDeletions
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: deletion-request.yaml
components: {}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ docs/models/TestCollectionEndingWithWordList.md
docs/models/TestCollectionEndingWithWordListObject.md
docs/models/TestDescendants.md
docs/models/TestDescendantsObjectType.md
docs/models/TestEnumParametersEnumHeaderStringParameter.md
docs/models/TestEnumParametersEnumQueryDoubleParameter.md
docs/models/TestEnumParametersEnumQueryIntegerParameter.md
docs/models/TestEnumParametersRequestEnumFormString.md
Expand Down Expand Up @@ -290,7 +289,6 @@ src/Org.OpenAPITools/Model/TestCollectionEndingWithWordList.cs
src/Org.OpenAPITools/Model/TestCollectionEndingWithWordListObject.cs
src/Org.OpenAPITools/Model/TestDescendants.cs
src/Org.OpenAPITools/Model/TestDescendantsObjectType.cs
src/Org.OpenAPITools/Model/TestEnumParametersEnumHeaderStringParameter.cs
src/Org.OpenAPITools/Model/TestEnumParametersEnumQueryDoubleParameter.cs
src/Org.OpenAPITools/Model/TestEnumParametersEnumQueryIntegerParameter.cs
src/Org.OpenAPITools/Model/TestEnumParametersRequestEnumFormString.cs
Expand Down
Loading