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
Original file line number Diff line number Diff line change
Expand Up @@ -862,9 +862,20 @@ private String matchGenerated(Schema model) {
}
// Structural match: compare with volatile fields stripped at every level.
// See generatedStructuralSignature field for a full explanation of why this is needed.
String structural = computeStructuralSignature(model);
if (generatedStructuralSignature.containsKey(structural)) {
return generatedStructuralSignature.get(structural);
//
// Only applied to *titled* schemas. A title denotes a named type that should be reused
// wherever it appears, so parser-induced volatile differences (description, type,
// example) must not split it into numbered duplicates. Anonymous/untitled inline
// schemas, however, may be intentionally distinct even when structurally identical once
// those volatile fields are stripped (e.g. two response properties that differ only by
// description) — unifying them silently changes the generated type of one property and
// breaks user code. This mirrors the titled-only guards in flatten() pre-population and
// deduplicateComponents().
if (model.getTitle() != null) {
String structural = computeStructuralSignature(model);
if (generatedStructuralSignature.containsKey(structural)) {
return generatedStructuralSignature.get(structural);
}
}
} catch (JsonProcessingException e) {
e.printStackTrace();
Expand All @@ -876,7 +887,11 @@ private String matchGenerated(Schema model) {
private void addGenerated(String name, Schema model) {
try {
generatedSignature.put(structureMapper.writeValueAsString(model), name);
generatedStructuralSignature.putIfAbsent(computeStructuralSignature(model), name);
// Only register the volatile-stripped structural signature for titled schemas; untitled
// inline schemas must not participate in the structural-match fallback (see matchGenerated).
if (model.getTitle() != null) {
generatedStructuralSignature.putIfAbsent(computeStructuralSignature(model), name);
}
} catch (JsonProcessingException e) {
e.printStackTrace();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,61 @@ public void resolveInlineModelDeduplicatesWhenParserMutatesPropertyDescriptions(
assertNull("Duplicate Widget_1 must not exist — shape-fingerprint dedup must fire", schemas.get("Widget_1"));
}

@Test
public void resolveInlineModelKeepsUntitledSchemasDifferingOnlyByDescriptionDistinct() {
// Regression test for #24004: two distinct *untitled* inline object schemas that differ
// only in their descriptions (here the nested 'result' property: "ABC Result" vs
// "DEF Result") must NOT be merged. The volatile-stripped structural-signature fallback in
// matchGenerated() collapses them once description/type are removed; that fallback is only
// intended to unify titled named types across parser volatility, so it must not fire for
// untitled inline schemas — otherwise 'def' silently gets the type generated for 'abc' and
// breaks user code that expects two separate types (regression introduced in 7.23).
OpenAPI openapi = new OpenAPI();
openapi.setComponents(new Components());
openapi.setPaths(new Paths());

Schema abc = new ObjectSchema()
.description("first container")
.addProperty("result", new StringSchema().description("ABC Result"));
Schema def = new ObjectSchema()
.description("second container")
.addProperty("result", new StringSchema().description("DEF Result"));

Schema response = new ObjectSchema()
.addProperty("abc", abc)
.addProperty("def", def);

ApiResponse apiResponse = new ApiResponse()
.description("OK")
.content(new Content().addMediaType("application/json",
new MediaType().schema(response)));

openapi.getPaths().addPathItem("/default", new PathItem().get(
new Operation().operationId("apiGetDefault")
.responses(new ApiResponses().addApiResponse("200", apiResponse))));

new InlineModelResolver().flatten(openapi);

// Locate the flattened response model (the only component schema carrying both properties).
Schema responseModel = null;
for (Schema candidate : openapi.getComponents().getSchemas().values()) {
if (candidate.getProperties() != null
&& candidate.getProperties().containsKey("abc")
&& candidate.getProperties().containsKey("def")) {
responseModel = candidate;
break;
}
}
assertNotNull("Flattened response model with abc/def properties must exist", responseModel);

String abcRef = ((Schema) responseModel.getProperties().get("abc")).get$ref();
String defRef = ((Schema) responseModel.getProperties().get("def")).get$ref();
assertNotNull("abc property must be a $ref to a generated schema", abcRef);
assertNotNull("def property must be a $ref to a generated schema", defRef);
assertFalse("abc and def must resolve to DISTINCT schemas, not be merged: " + abcRef,
abcRef.equals(defRef));
}

@Test
public void resolveInlineModelDeduplicatesMultipleRefsToSameExternalFile() {
// Regression test: when the same external schema file is referenced from three separate
Expand Down
4 changes: 4 additions & 0 deletions samples/client/others/crystal-qdrant/.openapi-generator/FILES
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ src/qdrant-api/models/facet_value.cr
src/qdrant-api/models/facet_value_hit.cr
src/qdrant-api/models/field_condition.cr
src/qdrant-api/models/filter.cr
src/qdrant-api/models/filter_must.cr
src/qdrant-api/models/filter_must_not.cr
src/qdrant-api/models/filter_selector.cr
src/qdrant-api/models/filter_should.cr
src/qdrant-api/models/float_index_params.cr
Expand Down Expand Up @@ -215,6 +217,7 @@ src/qdrant-api/models/points_batch.cr
src/qdrant-api/models/points_list.cr
src/qdrant-api/models/points_selector.cr
src/qdrant-api/models/prefetch.cr
src/qdrant-api/models/prefetch_prefetch.cr
src/qdrant-api/models/product_quantization.cr
src/qdrant-api/models/product_quantization_config.cr
src/qdrant-api/models/quantization_config.cr
Expand Down Expand Up @@ -312,6 +315,7 @@ src/qdrant-api/models/text_index_type.cr
src/qdrant-api/models/tokenizer_type.cr
src/qdrant-api/models/tracker_status.cr
src/qdrant-api/models/tracker_status_one_of.cr
src/qdrant-api/models/tracker_status_one_of1.cr
src/qdrant-api/models/tracker_telemetry.cr
src/qdrant-api/models/update_collection.cr
src/qdrant-api/models/update_operation.cr
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# #Qdrant API
#
#The version of the OpenAPI document: master
#Contact: andrey@vasnetsov.com
#Generated by: https://openapi-generator.tech
#Generator version: 7.24.0-SNAPSHOT
#

require "../spec_helper"

# Unit tests for Qdrant::Api::FilterMustNot
# Automatically generated by openapi-generator (https://openapi-generator.tech)
# Please update as you see appropriate
Spectator.describe Qdrant::Api::FilterMustNot do
describe "union (anyOf)" do
it "is (de)serialisable as a union alias" do
expect(Qdrant::Api::FilterMustNot.responds_to?(:from_json)).to be_true
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# #Qdrant API
#
#The version of the OpenAPI document: master
#Contact: andrey@vasnetsov.com
#Generated by: https://openapi-generator.tech
#Generator version: 7.24.0-SNAPSHOT
#

require "../spec_helper"

# Unit tests for Qdrant::Api::FilterMust
# Automatically generated by openapi-generator (https://openapi-generator.tech)
# Please update as you see appropriate
Spectator.describe Qdrant::Api::FilterMust do
describe "union (anyOf)" do
it "is (de)serialisable as a union alias" do
expect(Qdrant::Api::FilterMust.responds_to?(:from_json)).to be_true
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# #Qdrant API
#
#The version of the OpenAPI document: master
#Contact: andrey@vasnetsov.com
#Generated by: https://openapi-generator.tech
#Generator version: 7.24.0-SNAPSHOT
#

require "../spec_helper"

# Unit tests for Qdrant::Api::PrefetchPrefetch
# Automatically generated by openapi-generator (https://openapi-generator.tech)
# Please update as you see appropriate
Spectator.describe Qdrant::Api::PrefetchPrefetch do
describe "union (anyOf)" do
it "is (de)serialisable as a union alias" do
expect(Qdrant::Api::PrefetchPrefetch.responds_to?(:from_json)).to be_true
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# #Qdrant API
#
#The version of the OpenAPI document: master
#Contact: andrey@vasnetsov.com
#Generated by: https://openapi-generator.tech
#Generator version: 7.24.0-SNAPSHOT
#

require "../spec_helper"

# Unit tests for Qdrant::Api::TrackerStatusOneOf1
# Automatically generated by openapi-generator (https://openapi-generator.tech)
# Please update as you see appropriate
Spectator.describe Qdrant::Api::TrackerStatusOneOf1 do
describe "required-field enforcement" do
# A required, non-nilable property without a default makes JSON::Serializable
# reject a document that omits it. (Assumes at least one required field has no
# default; models where every required field has a default are not present in
# the generated samples.)
it "raises when required properties are missing" do
expect { Qdrant::Api::TrackerStatusOneOf1.from_json("{}") }.to raise_error(JSON::SerializableError)
end
end
end
4 changes: 4 additions & 0 deletions samples/client/others/crystal-qdrant/src/qdrant-api.cr
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ require "./qdrant-api/models/facet_value"
require "./qdrant-api/models/facet_value_hit"
require "./qdrant-api/models/field_condition"
require "./qdrant-api/models/filter"
require "./qdrant-api/models/filter_must"
require "./qdrant-api/models/filter_must_not"
require "./qdrant-api/models/filter_selector"
require "./qdrant-api/models/filter_should"
require "./qdrant-api/models/float_index_params"
Expand Down Expand Up @@ -212,6 +214,7 @@ require "./qdrant-api/models/points_batch"
require "./qdrant-api/models/points_list"
require "./qdrant-api/models/points_selector"
require "./qdrant-api/models/prefetch"
require "./qdrant-api/models/prefetch_prefetch"
require "./qdrant-api/models/product_quantization"
require "./qdrant-api/models/product_quantization_config"
require "./qdrant-api/models/quantization_config"
Expand Down Expand Up @@ -309,6 +312,7 @@ require "./qdrant-api/models/text_index_type"
require "./qdrant-api/models/tokenizer_type"
require "./qdrant-api/models/tracker_status"
require "./qdrant-api/models/tracker_status_one_of"
require "./qdrant-api/models/tracker_status_one_of1"
require "./qdrant-api/models/tracker_telemetry"
require "./qdrant-api/models/update_collection"
require "./qdrant-api/models/update_operation"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ module Qdrant::Api
property min_should : MinShould?

@[JSON::Field(key: "must", emit_null: false)]
property must : FilterShould?
property must : FilterMust?

@[JSON::Field(key: "must_not", emit_null: false)]
property must_not : FilterShould?
property must_not : FilterMustNot?


# Initializes the object
# @param [Hash] attributes Model attributes in the form of hash
def initialize(@should : FilterShould? = nil, @min_should : MinShould? = nil, @must : FilterShould? = nil, @must_not : FilterShould? = nil)
def initialize(@should : FilterShould? = nil, @min_should : MinShould? = nil, @must : FilterMust? = nil, @must_not : FilterMustNot? = nil)
end

# Show invalid properties with the reasons. Usually used together with valid?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# #Qdrant API
#
#The version of the OpenAPI document: master
#Contact: andrey@vasnetsov.com
#Generated by: https://openapi-generator.tech
#Generator version: 7.24.0-SNAPSHOT
#

module Qdrant::Api
# All conditions must match
# FilterMust (OpenAPI anyOf): a value matching at least one of the listed schemas.
# Implemented as a thin wrapper that (de)serialises by trying each member in order
# (the first that parses wins), so it transparently handles scalar, array and object members.
class FilterMust
getter value

def initialize(@value : Array(Condition))
end
def initialize(@value : Condition)
end

# List of classes defined in anyOf (OpenAPI v3)
def self.openapi_any_of
[
Array(Condition), Condition
]
end

def self.from_json(string_or_io) : self
from_json_any(JSON.parse(string_or_io))
end

def self.new(pull : JSON::PullParser)
from_json_any(JSON::Any.new(pull))
end

def self.from_json_any(data : JSON::Any) : self
json = data.to_json
begin
return new(Array(Condition).from_json(json))
rescue JSON::ParseException | ArgumentError | TypeCastError
end
begin
return new(Condition.from_json(json))
rescue JSON::ParseException | ArgumentError | TypeCastError
end
raise JSON::ParseException.new("`#{json}` doesn't match any schema listed in FilterMust (anyOf)", 0, 0)
end

def self.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node)
begin
return new(Array(Condition).new(ctx, node))
rescue YAML::ParseException | ArgumentError | TypeCastError
end
begin
return new(Condition.new(ctx, node))
rescue YAML::ParseException | ArgumentError | TypeCastError
end
raise YAML::ParseException.new("doesn't match any schema listed in FilterMust (anyOf)", 0, 0)
end

def to_json(builder : JSON::Builder)
@value.to_json(builder)
end

def to_yaml(builder : YAML::Nodes::Builder)
@value.to_yaml(builder)
end
end

end
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# #Qdrant API
#
#The version of the OpenAPI document: master
#Contact: andrey@vasnetsov.com
#Generated by: https://openapi-generator.tech
#Generator version: 7.24.0-SNAPSHOT
#

module Qdrant::Api
# All conditions must NOT match
# FilterMustNot (OpenAPI anyOf): a value matching at least one of the listed schemas.
# Implemented as a thin wrapper that (de)serialises by trying each member in order
# (the first that parses wins), so it transparently handles scalar, array and object members.
class FilterMustNot
getter value

def initialize(@value : Array(Condition))
end
def initialize(@value : Condition)
end

# List of classes defined in anyOf (OpenAPI v3)
def self.openapi_any_of
[
Array(Condition), Condition
]
end

def self.from_json(string_or_io) : self
from_json_any(JSON.parse(string_or_io))
end

def self.new(pull : JSON::PullParser)
from_json_any(JSON::Any.new(pull))
end

def self.from_json_any(data : JSON::Any) : self
json = data.to_json
begin
return new(Array(Condition).from_json(json))
rescue JSON::ParseException | ArgumentError | TypeCastError
end
begin
return new(Condition.from_json(json))
rescue JSON::ParseException | ArgumentError | TypeCastError
end
raise JSON::ParseException.new("`#{json}` doesn't match any schema listed in FilterMustNot (anyOf)", 0, 0)
end

def self.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node)
begin
return new(Array(Condition).new(ctx, node))
rescue YAML::ParseException | ArgumentError | TypeCastError
end
begin
return new(Condition.new(ctx, node))
rescue YAML::ParseException | ArgumentError | TypeCastError
end
raise YAML::ParseException.new("doesn't match any schema listed in FilterMustNot (anyOf)", 0, 0)
end

def to_json(builder : JSON::Builder)
@value.to_json(builder)
end

def to_yaml(builder : YAML::Nodes::Builder)
@value.to_yaml(builder)
end
end

end
Loading
Loading