openapi_first 2.1.1 → 2.2.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../schema/validation_result'
4
+
3
5
  module OpenapiFirst
4
6
  module Validators
5
7
  class ResponseBody
6
- def self.for(response_definition, openapi_version:)
8
+ def self.for(response_definition)
7
9
  schema = response_definition&.content_schema
8
10
  return unless schema
9
11
 
10
- new(Schema.new(schema, write: false, openapi_version:))
12
+ new(schema)
11
13
  end
12
14
 
13
15
  def initialize(schema)
@@ -22,7 +24,9 @@ module OpenapiFirst
22
24
  rescue ParseError => e
23
25
  Failure.fail!(:invalid_response_body, message: e.message)
24
26
  end
25
- validation = schema.validate(parsed_body)
27
+ validation = Schema::ValidationResult.new(
28
+ schema.validate(parsed_body, access_mode: 'read')
29
+ )
26
30
  Failure.fail!(:invalid_response_body, errors: validation.errors) if validation.error?
27
31
  end
28
32
  end
@@ -3,11 +3,11 @@
3
3
  module OpenapiFirst
4
4
  module Validators
5
5
  class ResponseHeaders
6
- def self.for(response_definition, openapi_version:)
6
+ def self.for(response_definition)
7
7
  schema = response_definition&.headers_schema
8
- return unless schema
8
+ return unless schema&.value
9
9
 
10
- new(Schema.new(schema, openapi_version:))
10
+ new(schema)
11
11
  end
12
12
 
13
13
  def initialize(schema)
@@ -17,7 +17,9 @@ module OpenapiFirst
17
17
  attr_reader :schema
18
18
 
19
19
  def call(parsed_request)
20
- validation = schema.validate(parsed_request.headers)
20
+ validation = Schema::ValidationResult.new(
21
+ schema.validate(parsed_request.headers)
22
+ )
21
23
  Failure.fail!(:invalid_response_header, errors: validation.errors) if validation.error?
22
24
  end
23
25
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '2.1.1'
4
+ VERSION = '2.2.1'
5
5
  end
data/lib/openapi_first.rb CHANGED
@@ -1,18 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'yaml'
4
- require 'multi_json'
5
- require_relative 'openapi_first/json_refs'
3
+ require_relative 'openapi_first/json'
4
+ require_relative 'openapi_first/file_loader'
6
5
  require_relative 'openapi_first/errors'
7
6
  require_relative 'openapi_first/configuration'
8
7
  require_relative 'openapi_first/definition'
9
8
  require_relative 'openapi_first/version'
10
- require_relative 'openapi_first/schema'
11
9
  require_relative 'openapi_first/middlewares/response_validation'
12
10
  require_relative 'openapi_first/middlewares/request_validation'
13
11
 
14
12
  # OpenapiFirst is a toolchain to build HTTP APIS based on OpenAPI API descriptions.
15
13
  module OpenapiFirst
14
+ autoload :Test, 'openapi_first/test'
15
+
16
16
  # Key in rack to find instance of Request
17
17
  REQUEST = 'openapi.request'
18
18
  FAILURE = :openapi_first_validation_failure
@@ -53,23 +53,16 @@ module OpenapiFirst
53
53
  def self.load(filepath, only: nil, &)
54
54
  raise FileNotFoundError, "File not found: #{filepath}" unless File.exist?(filepath)
55
55
 
56
- resolved = Bundle.resolve(filepath)
57
- parse(resolved, only:, filepath:, &)
56
+ contents = FileLoader.load(filepath)
57
+ parse(contents, only:, filepath:, &)
58
58
  end
59
59
 
60
60
  # Parse a dereferenced Hash
61
61
  # @return [Definition]
62
- def self.parse(resolved, only: nil, filepath: nil, &)
63
- resolved['paths'].filter!(&->(key, _) { only.call(key) }) if only
64
- Definition.new(resolved, filepath, &)
65
- end
66
-
67
- # @!visibility private
68
- module Bundle
69
- def self.resolve(spec_path)
70
- @file_cache ||= {}
71
- @file_cache[File.expand_path(spec_path).to_sym] ||= JsonRefs.load(spec_path)
72
- end
62
+ def self.parse(contents, only: nil, filepath: nil, &)
63
+ # TODO: This needs to work with unresolved contents as well
64
+ contents['paths'].filter!(&->(key, _) { only.call(key) }) if only
65
+ Definition.new(contents, filepath, &)
73
66
  end
74
67
  end
75
68
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi_first
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-09-19 00:00:00.000000000 Z
10
+ date: 2025-01-16 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: hana
@@ -44,20 +43,6 @@ dependencies:
44
43
  - - "<"
45
44
  - !ruby/object:Gem::Version
46
45
  version: '3.0'
47
- - !ruby/object:Gem::Dependency
48
- name: multi_json
49
- requirement: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - "~>"
52
- - !ruby/object:Gem::Version
53
- version: '1.15'
54
- type: :runtime
55
- prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: '1.15'
61
46
  - !ruby/object:Gem::Dependency
62
47
  name: openapi_parameters
63
48
  requirement: !ruby/object:Gem::Requirement
@@ -98,7 +83,6 @@ dependencies:
98
83
  - - "<"
99
84
  - !ruby/object:Gem::Version
100
85
  version: '4.0'
101
- description:
102
86
  email:
103
87
  - andreas.haller@posteo.de
104
88
  executables: []
@@ -118,24 +102,30 @@ files:
118
102
  - lib/openapi_first/error_responses/jsonapi.rb
119
103
  - lib/openapi_first/errors.rb
120
104
  - lib/openapi_first/failure.rb
121
- - lib/openapi_first/json_refs.rb
105
+ - lib/openapi_first/file_loader.rb
106
+ - lib/openapi_first/json.rb
107
+ - lib/openapi_first/json_pointer.rb
122
108
  - lib/openapi_first/middlewares/request_validation.rb
123
109
  - lib/openapi_first/middlewares/response_validation.rb
110
+ - lib/openapi_first/ref_resolver.rb
124
111
  - lib/openapi_first/request.rb
112
+ - lib/openapi_first/request_body_parsers.rb
125
113
  - lib/openapi_first/request_parser.rb
126
114
  - lib/openapi_first/request_validator.rb
127
115
  - lib/openapi_first/response.rb
116
+ - lib/openapi_first/response_body_parsers.rb
128
117
  - lib/openapi_first/response_parser.rb
129
118
  - lib/openapi_first/response_validator.rb
130
119
  - lib/openapi_first/router.rb
131
120
  - lib/openapi_first/router/find_content.rb
132
121
  - lib/openapi_first/router/find_response.rb
133
122
  - lib/openapi_first/router/path_template.rb
134
- - lib/openapi_first/schema.rb
135
123
  - lib/openapi_first/schema/validation_error.rb
136
124
  - lib/openapi_first/schema/validation_result.rb
137
125
  - lib/openapi_first/test.rb
138
126
  - lib/openapi_first/test/methods.rb
127
+ - lib/openapi_first/test/minitest_helpers.rb
128
+ - lib/openapi_first/test/plain_helpers.rb
139
129
  - lib/openapi_first/validated_request.rb
140
130
  - lib/openapi_first/validated_response.rb
141
131
  - lib/openapi_first/validators/request_body.rb
@@ -152,7 +142,6 @@ metadata:
152
142
  source_code_uri: https://github.com/ahx/openapi_first
153
143
  changelog_uri: https://github.com/ahx/openapi_first/blob/main/CHANGELOG.md
154
144
  rubygems_mfa_required: 'true'
155
- post_install_message:
156
145
  rdoc_options: []
157
146
  require_paths:
158
147
  - lib
@@ -167,8 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
167
156
  - !ruby/object:Gem::Version
168
157
  version: '0'
169
158
  requirements: []
170
- rubygems_version: 3.5.19
171
- signing_key:
159
+ rubygems_version: 3.6.2
172
160
  specification_version: 4
173
161
  summary: Implement HTTP APIs based on OpenApi 3.x
174
162
  test_files: []
@@ -1,151 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This is a fork of the json_refs gem, which does not use
4
- # open-uri, does not call chdir and adds caching of files during dereferencing.
5
- # The original code is available at https://github.com/tzmfreedom/json_refs
6
- # See also https://github.com/tzmfreedom/json_refs/pull/11
7
- # The code was originally written by Makoto Tajitsu with the MIT License.
8
- #
9
- # The MIT License (MIT)
10
-
11
- # Copyright (c) 2017 Makoto Tajitsu
12
-
13
- # Permission is hereby granted, free of charge, to any person obtaining a copy
14
- # of this software and associated documentation files (the "Software"), to deal
15
- # in the Software without restriction, including without limitation the rights
16
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
- # copies of the Software, and to permit persons to whom the Software is
18
- # furnished to do so, subject to the following conditions:
19
-
20
- # The above copyright notice and this permission notice shall be included in
21
- # all copies or substantial portions of the Software.
22
-
23
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29
- # THE SOFTWARE.
30
-
31
- require 'hana'
32
- require 'json'
33
- require 'yaml'
34
-
35
- module OpenapiFirst
36
- module JsonRefs # :nodoc:
37
- class << self
38
- def dereference(doc)
39
- file_cache = {}
40
- Dereferencer.new(Dir.pwd, doc, file_cache).call
41
- end
42
-
43
- def load(filename)
44
- doc_dir = File.dirname(filename)
45
- doc = Loader.handle(filename)
46
- file_cache = {}
47
- Dereferencer.new(filename, doc_dir, doc, file_cache).call
48
- end
49
- end
50
-
51
- module LocalRef
52
- module_function
53
-
54
- def call(path:, doc:)
55
- Hana::Pointer.new(path[1..]).eval(doc)
56
- end
57
- end
58
-
59
- module Loader
60
- module_function
61
-
62
- def handle(filename)
63
- body = File.read(filename)
64
- return JSON.parse(body) if File.extname(filename) == '.json'
65
-
66
- YAML.unsafe_load(body)
67
- end
68
- end
69
-
70
- class Dereferencer
71
- def initialize(filename, doc_dir, doc, file_cache)
72
- @filename = filename
73
- @doc = doc
74
- @doc_dir = doc_dir
75
- @file_cache = file_cache
76
- end
77
-
78
- def call(doc = @doc, keys = [])
79
- if doc.is_a?(Array)
80
- doc.each_with_index do |value, idx|
81
- call(value, keys + [idx])
82
- end
83
- elsif doc.is_a?(Hash)
84
- if doc.key?('$ref')
85
- dereference(keys, doc['$ref'])
86
- else
87
- doc.each do |key, value|
88
- call(value, keys + [key])
89
- end
90
- end
91
- end
92
- doc
93
- end
94
-
95
- private
96
-
97
- attr_reader :doc_dir
98
-
99
- def dereference(paths, referenced_path)
100
- key = paths.pop
101
- target = paths.inject(@doc) do |obj, k|
102
- obj[k]
103
- end
104
- value = follow_referenced_value(referenced_path)
105
- target[key] = value
106
- end
107
-
108
- def follow_referenced_value(referenced_path)
109
- value = referenced_value(referenced_path)
110
- return referenced_value(value['$ref']) if value.is_a?(Hash) && value.key?('$ref')
111
-
112
- value
113
- end
114
-
115
- def referenced_value(referenced_path)
116
- filepath, pointer = referenced_path.split('#')
117
- pointer&.prepend('#')
118
- return dereference_local(pointer) if filepath.empty?
119
-
120
- dereferenced_file = dereference_file(filepath)
121
- return dereferenced_file if pointer.nil?
122
-
123
- LocalRef.call(
124
- path: pointer,
125
- doc: dereferenced_file
126
- )
127
- end
128
-
129
- def dereference_local(referenced_path)
130
- LocalRef.call(path: referenced_path, doc: @doc)
131
- end
132
-
133
- def dereference_file(referenced_path)
134
- referenced_path = File.expand_path(referenced_path, doc_dir) unless File.absolute_path?(referenced_path)
135
- @file_cache[referenced_path] ||= load_referenced_file(referenced_path)
136
- end
137
-
138
- def load_referenced_file(absolute_path)
139
- directory = File.dirname(absolute_path)
140
-
141
- unless File.exist?(absolute_path)
142
- raise FileNotFoundError,
143
- "Problem while loading file referenced in #{@filename}: File not found #{absolute_path}"
144
- end
145
-
146
- referenced_doc = Loader.handle(absolute_path)
147
- Dereferencer.new(@filename, directory, referenced_doc, @file_cache).call
148
- end
149
- end
150
- end
151
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json_schemer'
4
- require_relative 'schema/validation_result'
5
-
6
- module OpenapiFirst
7
- # Validate data via JSON Schema. A wrapper around JSONSchemer.
8
- class Schema
9
- attr_reader :schema
10
-
11
- SCHEMAS = {
12
- '3.1' => 'https://spec.openapis.org/oas/3.1/dialect/base',
13
- '3.0' => 'json-schemer://openapi30/schema'
14
- }.freeze
15
-
16
- def initialize(schema, openapi_version: '3.1', write: true, after_property_validation: nil)
17
- @schemer = JSONSchemer.schema(
18
- schema,
19
- access_mode: write ? 'write' : 'read',
20
- meta_schema: SCHEMAS.fetch(openapi_version),
21
- insert_property_defaults: true,
22
- output_format: 'classic',
23
- before_property_validation: method(:before_property_validation),
24
- after_property_validation:
25
- )
26
- end
27
-
28
- def validate(data)
29
- ValidationResult.new(@schemer.validate(data))
30
- end
31
-
32
- private
33
-
34
- def before_property_validation(data, property, property_schema, parent)
35
- binary_format(data, property, property_schema, parent)
36
- end
37
-
38
- def binary_format(data, property, property_schema, _parent)
39
- return unless property_schema.is_a?(Hash) && property_schema['format'] == 'binary'
40
-
41
- data[property] = data.dig(property, :tempfile)&.read if data[property]
42
- end
43
- end
44
- end