openapi_first 2.1.1 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/README.md +0 -3
  4. data/lib/openapi_first/body_parser.rb +29 -16
  5. data/lib/openapi_first/builder.rb +143 -30
  6. data/lib/openapi_first/definition.rb +5 -5
  7. data/lib/openapi_first/error_responses/default.rb +1 -1
  8. data/lib/openapi_first/error_responses/jsonapi.rb +1 -1
  9. data/lib/openapi_first/file_loader.rb +21 -0
  10. data/lib/openapi_first/json.rb +25 -0
  11. data/lib/openapi_first/json_pointer.rb +22 -0
  12. data/lib/openapi_first/ref_resolver.rb +142 -0
  13. data/lib/openapi_first/request.rb +17 -56
  14. data/lib/openapi_first/request_body_parsers.rb +47 -0
  15. data/lib/openapi_first/request_parser.rb +11 -9
  16. data/lib/openapi_first/request_validator.rb +16 -9
  17. data/lib/openapi_first/response.rb +3 -21
  18. data/lib/openapi_first/response_body_parsers.rb +29 -0
  19. data/lib/openapi_first/response_parser.rb +9 -26
  20. data/lib/openapi_first/response_validator.rb +2 -2
  21. data/lib/openapi_first/test/methods.rb +9 -10
  22. data/lib/openapi_first/test/minitest_helpers.rb +28 -0
  23. data/lib/openapi_first/test/plain_helpers.rb +26 -0
  24. data/lib/openapi_first/test.rb +6 -0
  25. data/lib/openapi_first/validated_request.rb +13 -29
  26. data/lib/openapi_first/validators/request_body.rb +9 -23
  27. data/lib/openapi_first/validators/request_parameters.rb +17 -25
  28. data/lib/openapi_first/validators/response_body.rb +7 -3
  29. data/lib/openapi_first/validators/response_headers.rb +6 -4
  30. data/lib/openapi_first/version.rb +1 -1
  31. data/lib/openapi_first.rb +10 -17
  32. metadata +11 -23
  33. data/lib/openapi_first/json_refs.rb +0 -151
  34. data/lib/openapi_first/schema.rb +0 -44
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ceb641d45921bafb376d7fe7ea3a2f42b76c8900a5d8598e6ef018afca0304e
4
- data.tar.gz: 123c15a5a4a149e69a40773b0a62543257f3145fbc06c788207e65757ba3c404
3
+ metadata.gz: 9458fb5b815dcc64722a1829dc2082f60a92b36ce7af9fa8529c041544b190d4
4
+ data.tar.gz: 5f5982e1e71f5f1a150860be23291863ff3c431b3bce332899b01150e2c9fc11
5
5
  SHA512:
6
- metadata.gz: 3e279fa5f4239b1f41020b788370377535ef5cf959d2f2a23df61240310d4b729484e0a1154ff11250e21164c15b1ab285b3173fc2f2e3ac84603a5722e60ec7
7
- data.tar.gz: f96790f87324ea1ce7f78b6d3b41f86de709f990edf26a97ce1ad01b7929229198f9114ceb33d1aad94912211253ddc26dce6acae5566b386c805bb1219d886d
6
+ metadata.gz: c92c5cbda9bcd884299a265fcc713c0370e0766670e2b3199af9169dfe29855ed78ae5ad1a70fd98326a4f764403e0b9429132b97ee9f3b887d5d9af772acd69
7
+ data.tar.gz: '05083e92a954621b54667d8f99c133b2489e70eaa2b7332885c867053c81667a0eba0779ad210c150c8402814dff6d144044349271e8fe06808bafe0e5ad94ca'
data/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 2.2.1
6
+
7
+ - Fix issue with $ref resolving paths poiting outside directories `$ref: '../a/b.yaml'` (https://github.com/ahx/openapi_first/issues/313)
8
+ - Remove warning about missing assertions when using assert_api_conform (https://github.com/ahx/openapi_first/issues/313)
9
+
10
+ ## 2.2.0
11
+
12
+ - Fix support for discriminator in response bodies if no mapping is defined (https://github.com/ahx/openapi_first/issues/285)
13
+ - Fix support for discriminator in request bodies if no mapping is defined
14
+ - Replace bundled json_refs fork with own code
15
+ - Better error messages when OpenAPI file has invalid references ("$ref")
16
+ - Autoload OpenapiFirst::Test module. There is no need to `require 'openapi_first/test'` anymore.
17
+ - Remove multi_json dependency. openapi_first uses multi_json if available or the default json gem otherwise.
18
+ If you want to use multi_json, make sure to add it to your Gemfile.
19
+
5
20
  ## 2.1.1
6
21
 
7
22
  - Fix issue with non file downloads / JSON responses https://github.com/ahx/openapi_first/issues/281
data/README.md CHANGED
@@ -79,8 +79,6 @@ validated_response.parsed_headers
79
79
  definition.validate_response(rack_request,rack_response, raise_error: true) # Raises OpenapiFirst::ResponseInvalidError or OpenapiFirst::ResponseNotFoundError
80
80
  ```
81
81
 
82
- OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
83
-
84
82
  ## Rack Middlewares
85
83
 
86
84
  ### Request validation
@@ -210,7 +208,6 @@ Here is how to set it up for Rails integration tests:
210
208
 
211
209
  ```ruby
212
210
  # test_helper.rb
213
- require 'openapi_first/test'
214
211
  OpenapiFirst::Test.register('openapi/v1.openapi.yaml')
215
212
  ```
216
213
 
@@ -1,32 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'multi_json'
4
-
5
3
  module OpenapiFirst
6
4
  # @!visibility private
7
- class BodyParser
8
- def initialize(content_type)
9
- @is_json = :json if /json/i.match?(content_type)
5
+ module BodyParser
6
+ def self.[](content_type)
7
+ case content_type
8
+ when /json/i
9
+ JsonBodyParser
10
+ when %r{multipart/form-data}i
11
+ MultipartBodyParser
12
+ else
13
+ DefaultBodyParser
14
+ end
15
+ end
16
+
17
+ def self.read_body(request)
18
+ body = request.body&.read
19
+ request.body.rewind if request.body.respond_to?(:rewind)
20
+ body
10
21
  end
11
22
 
12
- def parse(request)
23
+ JsonBodyParser = lambda do |request|
13
24
  body = read_body(request)
14
25
  return if body.nil? || body.empty?
15
26
 
16
- return MultiJson.load(body) if @is_json
17
- return request.POST if request.form_data?
18
-
19
- body
20
- rescue MultiJson::ParseError
27
+ JSON.parse(body)
28
+ rescue JSON::ParserError
21
29
  Failure.fail!(:invalid_body, message: 'Failed to parse request body as JSON')
22
30
  end
23
31
 
24
- private
32
+ MultipartBodyParser = lambda do |request|
33
+ request.POST.transform_values do |value|
34
+ value.is_a?(Hash) && value[:tempfile] ? value[:tempfile].read : value
35
+ end
36
+ end
25
37
 
26
- def read_body(request)
27
- body = request.body&.read
28
- request.body.rewind if request.body.respond_to?(:rewind)
29
- body
38
+ # This returns the post data parsed by rack or the raw body
39
+ DefaultBodyParser = lambda do |request|
40
+ return request.POST if request.form_data?
41
+
42
+ read_body(request)
30
43
  end
31
44
  end
32
45
  end
@@ -1,33 +1,59 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json_schemer'
4
+ require_relative 'json_pointer'
5
+ require_relative 'ref_resolver'
6
+
3
7
  module OpenapiFirst
4
8
  # Builds parts of a Definition
5
9
  # This knows how to read a resolved OpenAPI document and build {Request} and {Response} objects.
6
- class Builder
10
+ class Builder # rubocop:disable Metrics/ClassLength
7
11
  REQUEST_METHODS = %w[get head post put patch delete trace options].freeze
8
12
 
9
13
  # Builds a router from a resolved OpenAPI document.
10
- # @param resolved [Hash] The resolved OpenAPI document.
14
+ # @param contents [Hash] The OpenAPI document Hash.
11
15
  # @param config [OpenapiFirst::Configuration] The configuration object.
12
- def self.build_router(resolved, config)
13
- openapi_version = (resolved['openapi'] || resolved['swagger'])[0..2]
14
- new(resolved, config, openapi_version).router
16
+ def self.build_router(contents, filepath:, config:)
17
+ new(contents, filepath:, config:).router
15
18
  end
16
19
 
17
- def initialize(resolved, config, openapi_version)
18
- @resolved = resolved
20
+ def initialize(contents, filepath:, config:)
21
+ @schemer_configuration = JSONSchemer::Configuration.new(
22
+ meta_schema: detect_meta_schema(contents, filepath),
23
+ insert_property_defaults: true
24
+ )
19
25
  @config = config
20
- @openapi_version = openapi_version
26
+ @contents = RefResolver.for(contents, dir: filepath && File.dirname(filepath))
21
27
  end
22
28
 
23
- attr_reader :resolved, :openapi_version, :config
29
+ attr_reader :config
30
+ private attr_reader :schemer_configuration
31
+
32
+ def detect_meta_schema(document, filepath)
33
+ # Copied from JSONSchemer 🙇🏻‍♂️
34
+ version = document['openapi']
35
+ case version
36
+ when /\A3\.1\.\d+\z/
37
+ @document_schema = JSONSchemer.openapi31_document
38
+ document.fetch('jsonSchemaDialect') { JSONSchemer::OpenAPI31::BASE_URI.to_s }
39
+ when /\A3\.0\.\d+\z/
40
+ @document_schema = JSONSchemer.openapi30_document
41
+ JSONSchemer::OpenAPI30::BASE_URI.to_s
42
+ else
43
+ raise Error, "Unsupported OpenAPI version #{version.inspect} #{filepath}"
44
+ end
45
+ end
24
46
 
25
47
  def router # rubocop:disable Metrics/MethodLength
26
48
  router = OpenapiFirst::Router.new
27
- resolved['paths'].each do |path, path_item_object|
28
- path_item_object.slice(*REQUEST_METHODS).keys.map do |request_method|
49
+ @contents.fetch('paths').each do |path, path_item_object|
50
+ path_item_object.resolved.keys.intersection(REQUEST_METHODS).map do |request_method|
29
51
  operation_object = path_item_object[request_method]
30
- build_requests(path, request_method, operation_object, path_item_object).each do |request|
52
+ parameters = operation_object['parameters']&.resolved.to_a.chain(path_item_object['parameters']&.resolved.to_a)
53
+ parameters = parse_parameters(parameters)
54
+
55
+ build_requests(path:, request_method:, operation_object:,
56
+ parameters:).each do |request|
31
57
  router.add_request(
32
58
  request,
33
59
  request_method:,
@@ -35,7 +61,7 @@ module OpenapiFirst
35
61
  content_type: request.content_type
36
62
  )
37
63
  end
38
- build_responses(operation_object).each do |response|
64
+ build_responses(responses: operation_object['responses']).each do |response|
39
65
  router.add_response(
40
66
  response,
41
67
  request_method:,
@@ -49,33 +75,120 @@ module OpenapiFirst
49
75
  router
50
76
  end
51
77
 
52
- def build_requests(path, request_method, operation_object, path_item_object)
53
- hooks = config.hooks
54
- path_item_parameters = path_item_object['parameters']
55
- parameters = operation_object['parameters'].to_a.chain(path_item_parameters.to_a)
56
- required_body = operation_object.dig('requestBody', 'required') == true
57
- result = operation_object.dig('requestBody', 'content')&.map do |content_type, content|
58
- Request.new(path:, request_method:, operation_object:, parameters:, content_type:,
59
- content_schema: content['schema'], required_body:, hooks:, openapi_version:)
78
+ def parse_parameters(parameters)
79
+ grouped_parameters = group_parameters(parameters)
80
+ ParsedParameters.new(
81
+ query: grouped_parameters[:query],
82
+ path: grouped_parameters[:path],
83
+ cookie: grouped_parameters[:cookie],
84
+ header: grouped_parameters[:header],
85
+ query_schema: build_parameter_schema(grouped_parameters[:query]),
86
+ path_schema: build_parameter_schema(grouped_parameters[:path]),
87
+ cookie_schema: build_parameter_schema(grouped_parameters[:cookie]),
88
+ header_schema: build_parameter_schema(grouped_parameters[:header])
89
+ )
90
+ end
91
+
92
+ def build_parameter_schema(parameters)
93
+ schema = build_parameters_schema(parameters)
94
+
95
+ JSONSchemer.schema(schema,
96
+ configuration: schemer_configuration,
97
+ after_property_validation: config.hooks[:after_request_parameter_property_validation])
98
+ end
99
+
100
+ def build_requests(path:, request_method:, operation_object:, parameters:)
101
+ required_body = operation_object['requestBody']&.resolved&.fetch('required', false) == true
102
+ result = operation_object.dig('requestBody', 'content')&.map do |content_type, content_object|
103
+ content_schema = content_object['schema'].schema(
104
+ configuration: schemer_configuration,
105
+ after_property_validation: config.hooks[:after_request_body_property_validation]
106
+ )
107
+ Request.new(path:, request_method:,
108
+ operation_object: operation_object.resolved,
109
+ parameters:, content_type:,
110
+ content_schema:,
111
+ required_body:)
60
112
  end || []
61
113
  return result if required_body
62
114
 
63
115
  result << Request.new(
64
- path:, request_method:, operation_object:,
116
+ path:, request_method:, operation_object: operation_object.resolved,
65
117
  parameters:, content_type: nil, content_schema: nil,
66
- required_body:, hooks:, openapi_version:
118
+ required_body:
67
119
  )
68
120
  end
69
121
 
70
- def build_responses(operation_object)
71
- Array(operation_object['responses']).flat_map do |status, response_object|
72
- headers = response_object['headers']
122
+ def build_responses(responses:)
123
+ return [] unless responses
124
+
125
+ responses.flat_map do |status, response_object|
126
+ headers = response_object['headers']&.resolved
127
+ headers_schema = JSONSchemer::Schema.new(
128
+ build_headers_schema(headers),
129
+ configuration: schemer_configuration
130
+ )
73
131
  response_object['content']&.map do |content_type, content_object|
74
- content_schema = content_object['schema']
75
- Response.new(status:, headers:, content_type:, content_schema:, openapi_version:)
76
- end || Response.new(status:, headers:, content_type: nil,
77
- content_schema: nil, openapi_version:)
132
+ content_schema = content_object['schema'].schema(configuration: schemer_configuration)
133
+ Response.new(status:,
134
+ headers:,
135
+ headers_schema:,
136
+ content_type:,
137
+ content_schema:)
138
+ end || Response.new(status:, headers:, headers_schema:, content_type: nil,
139
+ content_schema: nil)
140
+ end
141
+ end
142
+
143
+ IGNORED_HEADER_PARAMETERS = Set['Content-Type', 'Accept', 'Authorization'].freeze
144
+ private_constant :IGNORED_HEADER_PARAMETERS
145
+
146
+ def group_parameters(parameter_definitions)
147
+ result = {}
148
+ parameter_definitions&.each do |parameter|
149
+ (result[parameter['in'].to_sym] ||= []) << parameter
150
+ end
151
+ result[:header]&.reject! { IGNORED_HEADER_PARAMETERS.include?(_1['name']) }
152
+ result
153
+ end
154
+
155
+ def build_headers_schema(headers_object)
156
+ return unless headers_object&.any?
157
+
158
+ properties = {}
159
+ required = []
160
+ headers_object.each do |name, header|
161
+ schema = header['schema']
162
+ next if name.casecmp('content-type').zero?
163
+
164
+ properties[name] = schema if schema
165
+ required << name if header['required']
78
166
  end
167
+ {
168
+ 'properties' => properties,
169
+ 'required' => required
170
+ }
79
171
  end
172
+
173
+ def build_parameters_schema(parameters)
174
+ return unless parameters
175
+
176
+ properties = {}
177
+ required = []
178
+ parameters.each do |parameter|
179
+ schema = parameter['schema']
180
+ name = parameter['name']
181
+ properties[name] = schema if schema
182
+ required << name if parameter['required']
183
+ end
184
+
185
+ {
186
+ 'properties' => properties,
187
+ 'required' => required
188
+ }
189
+ end
190
+
191
+ ParsedParameters = Data.define(:path, :query, :header, :cookie, :path_schema, :query_schema, :header_schema,
192
+ :cookie_schema)
80
193
  end
81
194
  end
@@ -22,16 +22,16 @@ module OpenapiFirst
22
22
  # @return [Router]
23
23
  attr_reader :router
24
24
 
25
- # @param resolved [Hash] The resolved OpenAPI document.
25
+ # @param contents [Hash] The OpenAPI document.
26
26
  # @param filepath [String] The file path of the OpenAPI document.
27
- def initialize(resolved, filepath = nil)
27
+ def initialize(contents, filepath = nil)
28
28
  @filepath = filepath
29
29
  @config = OpenapiFirst.configuration.clone
30
30
  yield @config if block_given?
31
31
  @config.freeze
32
- @router = Builder.build_router(resolved, @config)
33
- @resolved = resolved
34
- @paths = resolved['paths'].keys # TODO: Move into builder as well
32
+ @router = Builder.build_router(contents, filepath:, config:)
33
+ @resolved = contents
34
+ @paths = @router.routes.map(&:path).to_a.uniq # TODO: Refactor
35
35
  end
36
36
 
37
37
  # Gives access to the raw resolved Hash. Like `mydefinition['components'].dig('schemas', 'Stations')`
@@ -26,7 +26,7 @@ module OpenapiFirst
26
26
  status:
27
27
  }
28
28
  result[:errors] = errors if failure.errors
29
- MultiJson.dump(result)
29
+ JSON.generate(result)
30
30
  end
31
31
 
32
32
  def type = failure.type
@@ -8,7 +8,7 @@ module OpenapiFirst
8
8
  OpenapiFirst.register_error_response(:jsonapi, self)
9
9
 
10
10
  def body
11
- MultiJson.dump({ errors: serialized_errors })
11
+ JSON.generate({ errors: serialized_errors })
12
12
  end
13
13
 
14
14
  def content_type
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module OpenapiFirst
6
+ # @!visibility private
7
+ module FileLoader
8
+ module_function
9
+
10
+ def load(file_path)
11
+ raise FileNotFoundError, "File not found #{file_path}" unless File.exist?(file_path)
12
+
13
+ body = File.read(file_path)
14
+ extname = File.extname(file_path)
15
+ return JSON.parse(body) if extname == '.json'
16
+ return YAML.unsafe_load(body) if ['.yaml', '.yml'].include?(extname)
17
+
18
+ body
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This tries to load MultiJson if available to keep compatibility
4
+ # with MultiJson until next major version
5
+
6
+ begin
7
+ require 'multi_json'
8
+ module OpenapiFirst
9
+ # Compatibility with MultiJson
10
+ # @visibility private
11
+ module JSON
12
+ ParserError = MultiJson::ParseError
13
+
14
+ def self.parse(string)
15
+ MultiJson.load(string)
16
+ end
17
+
18
+ def self.generate(object)
19
+ MultiJson.dump(object)
20
+ end
21
+ end
22
+ end
23
+ rescue LoadError
24
+ require 'json'
25
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ # Functions to handle JSON Pointers
5
+ # @!visibility private
6
+ module JsonPointer
7
+ ESCAPE_CHARS = { '~' => '~0', '/' => '~1', '+' => '%2B' }.freeze
8
+ ESCAPE_REGEX = Regexp.union(ESCAPE_CHARS.keys)
9
+
10
+ module_function
11
+
12
+ def append(root, *tokens)
13
+ "#{root}/" + tokens.map do |token|
14
+ escape_json_pointer_token(token)
15
+ end.join('/')
16
+ end
17
+
18
+ def escape_json_pointer_token(token)
19
+ token.to_s.gsub(ESCAPE_REGEX, ESCAPE_CHARS)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json_schemer'
4
+
5
+ module OpenapiFirst
6
+ # This is here to give traverse an OAD while keeping $refs intact
7
+ # @visibility private
8
+ module RefResolver
9
+ def self.load(file_path)
10
+ contents = OpenapiFirst::FileLoader.load(file_path)
11
+ self.for(contents, dir: File.dirname(File.expand_path(file_path)))
12
+ end
13
+
14
+ def self.for(value, context: value, dir: Dir.pwd)
15
+ case value
16
+ when ::Hash
17
+ Hash.new(value, context:, dir:)
18
+ when ::Array
19
+ Array.new(value, context:, dir:)
20
+ when ::NilClass
21
+ nil
22
+ else
23
+ Simple.new(value)
24
+ end
25
+ end
26
+
27
+ # @visibility private
28
+ module Diggable
29
+ def dig(*keys)
30
+ keys.inject(self) do |result, key|
31
+ break unless result.respond_to?(:[])
32
+
33
+ result[key]
34
+ end
35
+ end
36
+ end
37
+
38
+ # @visibility private
39
+ module Resolvable
40
+ def initialize(value, context: value, dir: nil)
41
+ @value = value
42
+ @context = context
43
+ @dir = (dir && File.absolute_path(dir)) || Dir.pwd
44
+ end
45
+
46
+ # The value of this node
47
+ attr_reader :value
48
+ # The path of the file sytem directory where this was loaded from
49
+ attr_reader :dir
50
+ # The object where this node was found in
51
+ attr_reader :context
52
+
53
+ def resolve_ref(pointer)
54
+ if pointer.start_with?('#')
55
+ value = Hana::Pointer.new(pointer[1..]).eval(context)
56
+ raise "Unknown reference #{pointer} in #{context}" unless value
57
+
58
+ return RefResolver.for(value, dir:)
59
+ end
60
+
61
+ relative_path, file_pointer = pointer.split('#')
62
+ full_path = File.expand_path(relative_path, dir)
63
+ return RefResolver.load(full_path) unless file_pointer
64
+
65
+ file_contents = FileLoader.load(full_path)
66
+ new_dir = File.dirname(full_path)
67
+ value = Hana::Pointer.new(file_pointer).eval(file_contents)
68
+ RefResolver.for(value, dir: new_dir)
69
+ end
70
+ end
71
+
72
+ # @visibility private
73
+ class Simple
74
+ include Resolvable
75
+
76
+ def resolved = value
77
+ end
78
+
79
+ # @visibility private
80
+ class Hash
81
+ include Resolvable
82
+ include Diggable
83
+ include Enumerable
84
+
85
+ def resolved
86
+ return resolve_ref(value['$ref']).value if value.key?('$ref')
87
+
88
+ value
89
+ end
90
+
91
+ def [](key)
92
+ return resolve_ref(@value['$ref'])[key] if !@value.key?(key) && @value.key?('$ref')
93
+
94
+ RefResolver.for(@value[key], dir:, context:)
95
+ end
96
+
97
+ def fetch(key)
98
+ return resolve_ref(@value['$ref']).fetch(key) if !@value.key?(key) && @value.key?('$ref')
99
+
100
+ RefResolver.for(@value.fetch(key), dir:, context:)
101
+ end
102
+
103
+ def each
104
+ resolved.each do |key, value|
105
+ yield key, RefResolver.for(value, dir:, context:)
106
+ end
107
+ end
108
+
109
+ def schema(options = {})
110
+ ref_resolver = JSONSchemer::CachedResolver.new do |uri|
111
+ FileLoader.load(uri.path)
112
+ end
113
+ base_uri = URI::File.build({ path: "#{dir}/" })
114
+ root = JSONSchemer::Schema.new(context, base_uri:, ref_resolver:, **options)
115
+ JSONSchemer::Schema.new(value, nil, root, base_uri:, **options)
116
+ end
117
+ end
118
+
119
+ # @visibility private
120
+ class Array
121
+ include Resolvable
122
+ include Diggable
123
+
124
+ def [](index)
125
+ item = @value[index]
126
+ return resolve_ref(item['$ref']) if item.is_a?(::Hash) && item.key?('$ref')
127
+
128
+ RefResolver.for(item, dir:, context:)
129
+ end
130
+
131
+ def resolved
132
+ value.map do |item|
133
+ if item.respond_to?(:key?) && item.key?('$ref')
134
+ resolve_ref(item['$ref']).resolved
135
+ else
136
+ item
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end