openapi_first 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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.0
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-11 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,20 +102,24 @@ 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
@@ -152,7 +140,6 @@ metadata:
152
140
  source_code_uri: https://github.com/ahx/openapi_first
153
141
  changelog_uri: https://github.com/ahx/openapi_first/blob/main/CHANGELOG.md
154
142
  rubygems_mfa_required: 'true'
155
- post_install_message:
156
143
  rdoc_options: []
157
144
  require_paths:
158
145
  - lib
@@ -167,8 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
167
154
  - !ruby/object:Gem::Version
168
155
  version: '0'
169
156
  requirements: []
170
- rubygems_version: 3.5.19
171
- signing_key:
157
+ rubygems_version: 3.6.2
172
158
  specification_version: 4
173
159
  summary: Implement HTTP APIs based on OpenApi 3.x
174
160
  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