Skip to content

Commit 846bc8d

Browse files
committed
Refactor reference resolution for OpenAPI 3.1
Sorry - this is way too big for a single commit. This changes the approach for handling references to be one where data can be merged between a chain of references. Previously the approach was to only allow a single $ref field (as per OpenAPI 3.0) The reason for making this change is based on aspects in the specification notably the reference object [1] that allows title and summary fields to be overridden through the referencing process. As far as I could tell schemas also share this property as per the newer JSON Schema specifications (most applicable one for OpenAPI 3.1 is 2020-12) though I found it quite hard to find a clear source on behaviour [2]. The approach taken to implementing this [1]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#referenceObject [2]: https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-00#section-8.2.3.1
1 parent 18af061 commit 846bc8d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1241
-542
lines changed

lib/openapi3_parser/node/array.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def each(&block)
4040
# @return [Boolean]
4141
def ==(other)
4242
other.instance_of?(self.class) &&
43-
node_context.same_data_and_source?(other.node_context)
43+
node_context.same_data_inputs?(other.node_context)
4444
end
4545

4646
# Used to access a node relative to this node

lib/openapi3_parser/node/context.rb

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,15 @@ class Context
2020
# @param [NodeFactory::Context] factory_context
2121
# @return [Node::Context]
2222
def self.root(factory_context)
23-
location = Source::Location.new(factory_context.source, [])
23+
document_location = Source::Location.new(factory_context.source, [])
24+
25+
source_location = factory_context.source_location
26+
input_locations = input_location?(factory_context.input) ? [source_location] : []
27+
2428
new(factory_context.input,
25-
document_location: location,
26-
source_location: factory_context.source_location)
29+
document_location: document_location,
30+
source_locations: [source_location],
31+
input_locations: input_locations)
2732
end
2833

2934
# Create a context for the child of a previous context
@@ -38,9 +43,16 @@ def self.next_field(parent_context, field, factory_context)
3843
field
3944
)
4045

46+
input_locations = if input_location?(factory_context.input)
47+
[factory_context.source_location]
48+
else
49+
[]
50+
end
51+
4152
new(factory_context.input,
4253
document_location: document_location,
43-
source_location: factory_context.source_location)
54+
source_locations: [factory_context.source_location],
55+
input_locations: input_locations)
4456
end
4557

4658
# Create a context for a the a field that is the result of a reference
@@ -49,36 +61,63 @@ def self.next_field(parent_context, field, factory_context)
4961
# @param [NodeFactory::Context] reference_factory_context
5062
# @return [Node::Context]
5163
def self.resolved_reference(current_context, reference_factory_context)
52-
new(reference_factory_context.input,
64+
input_locations = if input_location?(reference_factory_context.input)
65+
current_context.input_locations + [reference_factory_context.source_location]
66+
else
67+
current_context.input_locations
68+
end
69+
70+
input = merge_reference_input(current_context.input, reference_factory_context.input)
71+
new(input,
5372
document_location: current_context.document_location,
54-
source_location: reference_factory_context.source_location)
73+
source_locations: current_context.source_locations + [reference_factory_context.source_location],
74+
input_locations: input_locations)
75+
end
76+
77+
def self.merge_reference_input(current_input, reference_input)
78+
can_merge = reference_input.respond_to?(:merge) && current_input.respond_to?(:merge)
79+
80+
return reference_input unless can_merge
81+
82+
input = reference_input.merge(current_input)
83+
input.delete("$ref")
84+
input
85+
end
86+
87+
def self.input_location?(input)
88+
return true unless input.respond_to?(:keys)
89+
90+
input.keys != ["$ref"]
5591
end
5692

57-
attr_reader :input, :document_location, :source_location
93+
attr_reader :input, :document_location, :source_locations, :input_locations
5894

59-
# @param input
60-
# @param [Source::Location] document_location
61-
# @param [Source::Location] source_location
62-
def initialize(input, document_location:, source_location:)
95+
# @param input
96+
# @param [Source::Location] document_location
97+
# @param [Array<Source::Location>] source_locations
98+
# @param [Array<Source::Location>] input_locations
99+
def initialize(input, document_location:, source_locations:, input_locations:)
63100
@input = input
64101
@document_location = document_location
65-
@source_location = source_location
102+
@source_locations = source_locations
103+
@input_locations = input_locations
66104
end
67105

68106
# @param [Context] other
69107
# @return [Boolean]
70108
def ==(other)
71109
document_location == other.document_location &&
72-
same_data_and_source?(other)
110+
source_locations == other.source_locations &&
111+
same_data_inputs?(other)
73112
end
74113

75114
# Check that contexts are the same without concern for document location
76115
#
77116
# @param [Context] other
78117
# @return [Boolean]
79-
def same_data_and_source?(other)
118+
def same_data_inputs?(other)
80119
input == other.input &&
81-
source_location == other.source_location
120+
input_locations == other.input_locations
82121
end
83122

84123
# The OpenAPI document associated with this context
@@ -88,17 +127,24 @@ def document
88127
document_location.source.document
89128
end
90129

91-
# The source file used to provide the data for this node
130+
# The source files used to provide the data for this node
131+
#
132+
# @return [Array<Source>]
133+
def sources
134+
[source_locations].map(&:source)
135+
end
136+
137+
# The source files used to provide the input for this node
92138
#
93-
# @return [Source]
94-
def source
95-
source_location.source
139+
# @return [Array<Source>]
140+
def input_sources
141+
[input_locations].map(&:source)
96142
end
97143

98144
# @return [String]
99145
def inspect
100146
%{#{self.class.name}(document_location: #{document_location}, } +
101-
%{source_location: #{source_location})}
147+
%{input_locations: #{input_locations.join(', ')})}
102148
end
103149

104150
# A string representing the location of the node
@@ -107,7 +153,9 @@ def inspect
107153
def location_summary
108154
summary = document_location.to_s
109155

110-
summary += " (#{source_location})" if document_location != source_location
156+
if input_locations.length > 1 || document_location != input_locations.first
157+
summary += " (#{input_locations.join(', ')})"
158+
end
111159

112160
summary
113161
end

lib/openapi3_parser/node/map.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def extension(value)
5353
# @return [Boolean]
5454
def ==(other)
5555
other.instance_of?(self.class) &&
56-
node_context.same_data_and_source?(other.node_context)
56+
node_context.same_data_inputs?(other.node_context)
5757
end
5858

5959
# Iterates through the data of this node, used by Enumerable

lib/openapi3_parser/node/object.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def extension(value)
5353
# @return [Boolean]
5454
def ==(other)
5555
other.instance_of?(self.class) &&
56-
node_context.same_data_and_source?(other.node_context)
56+
node_context.same_data_inputs?(other.node_context)
5757
end
5858

5959
# Iterates through the data of this node, used by Enumerable
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
require "openapi3_parser/node/object"
4+
5+
module Openapi3Parser
6+
module Node
7+
module Schema
8+
class OasDialect3_1 < Node::Object # rubocop:disable Naming/ClassAndModuleCamelCase
9+
end
10+
end
11+
end
12+
end

lib/openapi3_parser/node/schema/v3_0.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class V3_0 < Node::Object # rubocop:disable Naming/ClassAndModuleCamelCase
3535
#
3636
# @return [String, nil]
3737
def name
38-
segments = node_context.source_location.pointer.segments
38+
segments = node_context.source_locations.first.pointer.segments
3939
segments[-1] if segments[-2] == "schemas"
4040
end
4141

lib/openapi3_parser/node_factory/array.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ def use_default?
6161
raw_input.empty?
6262
end
6363

64+
def build_node(data, node_context)
65+
Node::Array.new(data, node_context) if data
66+
end
67+
6468
private
6569

6670
def build_data(raw_input)
@@ -87,15 +91,17 @@ def initialize_value_factory(field_context)
8791
end
8892
end
8993

90-
def build_node(data, node_context)
91-
Node::Array.new(data, node_context) if data
92-
end
93-
9494
def build_resolved_input
9595
return unless data
9696

9797
data.map do |value|
98-
value.respond_to?(:resolved_input) ? value.resolved_input : value
98+
if value.respond_to?(:in_recursive_loop?) && value.in_recursive_loop?
99+
RecursiveResolvedInput.new(value)
100+
elsif value.respond_to?(:resolved_input)
101+
value.resolved_input
102+
else
103+
value
104+
end
99105
end
100106
end
101107

lib/openapi3_parser/node_factory/callback.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ def initialize(context)
1111
value_factory: NodeFactory::PathItem)
1212
end
1313

14-
private
15-
1614
def build_node(data, node_context)
1715
Node::Callback.new(data, node_context)
1816
end

lib/openapi3_parser/node_factory/components.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ class Components < NodeFactory::Object
1616
field "links", factory: :links_factory
1717
field "callbacks", factory: :callbacks_factory
1818

19-
private
20-
21-
def build_object(data, context)
22-
Node::Components.new(data, context)
19+
def build_node(data, node_context)
20+
Node::Components.new(data, node_context)
2321
end
2422

23+
private
24+
2525
def schemas_factory(context)
2626
NodeFactory::Map.new(
2727
context,

lib/openapi3_parser/node_factory/contact.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,8 @@ class Contact < NodeFactory::Object
1818
input_type: String,
1919
validate: Validation::InputValidator.new(Validators::Email)
2020

21-
private
22-
23-
def build_object(data, context)
24-
Node::Contact.new(data, context)
21+
def build_node(data, node_context)
22+
Node::Contact.new(data, node_context)
2523
end
2624
end
2725
end

0 commit comments

Comments
 (0)