Skip to content

Commit 2e5ceba

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]. To support this there are now some new concepts: - A node context now has multiple source_locations - as data can be combined from multiple places - A node context has a concept, input_locations, which record all of the data involved in a node that aren't purely references (i.e. all the source locations that contributed to the node data) - There is now a Referenceable module that can be mixed into NodeFactory classes, this allows NodeFactories to act more as the mediator in charge of a reference, whereas previously more went through a Reference field (this is to reflect the nature of merging) - Aspects of the ObjectFactory::NodeBuilder class have been separated into ObjectFactory::NodeErrors, ObjectFactory::ResolvedInputBuilder and a new iteration of it's namesake. This is to reflect an increase in complexity in node building due to the node data merging - As part of the new NodeBuilder class the building of node objects is now done by the NodeBuilder class calling public methods on node factories. This has led to them having their #build_object methods replaced with #build_node methods that need a public interface [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 659737d commit 2e5ceba

60 files changed

Lines changed: 1241 additions & 542 deletions

Some content is hidden

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

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)