openapi_first 1.1.1 → 1.3.0

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/openapi_first/body_parser.rb +3 -1
  3. data/lib/openapi_first/configuration.rb +3 -1
  4. data/lib/openapi_first/definition/operation.rb +65 -5
  5. data/lib/openapi_first/definition/path_item.rb +1 -0
  6. data/lib/openapi_first/definition/request_body.rb +1 -0
  7. data/lib/openapi_first/definition/response.rb +7 -0
  8. data/lib/openapi_first/definition.rb +43 -3
  9. data/lib/openapi_first/error_response.rb +1 -1
  10. data/lib/openapi_first/errors.rb +6 -0
  11. data/lib/openapi_first/failure.rb +28 -4
  12. data/lib/openapi_first/middlewares/request_validation.rb +2 -5
  13. data/lib/openapi_first/middlewares/response_validation.rb +1 -4
  14. data/lib/openapi_first/plugins/default/error_response.rb +4 -4
  15. data/lib/openapi_first/plugins/default.rb +1 -1
  16. data/lib/openapi_first/plugins/jsonapi/error_response.rb +3 -2
  17. data/lib/openapi_first/plugins/jsonapi.rb +1 -1
  18. data/lib/openapi_first/plugins.rb +1 -0
  19. data/lib/openapi_first/request_validation/request_body_validator.rb +1 -1
  20. data/lib/openapi_first/request_validation/validator.rb +1 -0
  21. data/lib/openapi_first/response_validation/validator.rb +1 -0
  22. data/lib/openapi_first/runtime_request.rb +63 -3
  23. data/lib/openapi_first/runtime_response.rb +43 -4
  24. data/lib/openapi_first/schema/validation_error.rb +2 -0
  25. data/lib/openapi_first/schema/validation_result.rb +2 -7
  26. data/lib/openapi_first/schema.rb +1 -0
  27. data/lib/openapi_first/version.rb +1 -1
  28. data/lib/openapi_first.rb +21 -3
  29. metadata +6 -17
  30. data/.github/CODEOWNERS +0 -1
  31. data/.github/workflows/ruby.yml +0 -13
  32. data/.gitignore +0 -11
  33. data/CHANGELOG.md +0 -274
  34. data/Gemfile +0 -18
  35. data/Gemfile.lock +0 -170
  36. data/Gemfile.rack2 +0 -15
  37. data/Gemfile.rack2.lock +0 -99
  38. data/LICENSE.txt +0 -21
  39. data/README.md +0 -225
  40. data/openapi_first.gemspec +0 -47
@@ -16,31 +16,59 @@ module OpenapiFirst
16
16
  @path_item = path_item
17
17
  @operation = operation
18
18
  @original_path_params = path_params
19
+ @error = nil
20
+ @validated = false
19
21
  end
20
22
 
21
23
  def_delegators :@request, :content_type, :media_type, :path
22
24
  def_delegators :@operation, :operation_id, :request_method
23
25
  def_delegator :@path_item, :path, :path_definition
24
26
 
27
+ # Returns the path_item object.
28
+ # @return [PathItem, nil] The path_item object or nil if this request path is not known.
25
29
  attr_reader :path_item
26
30
 
31
+ # Returns the operation object.
32
+ # @return [Operation, nil] The operation object or nil if this request method is not known.
33
+ attr_reader :operation
34
+
35
+ # Returns the error object if validation failed.
36
+ # @return [Failure, nil]
37
+ attr_reader :error
38
+
39
+ # Checks if the request is valid.
40
+ # @return [Boolean] true if the request is valid, false otherwise.
41
+ def valid?
42
+ validate unless @validated
43
+ error.nil?
44
+ end
45
+
46
+ # Checks if the path and request method are known.
47
+ # @return [Boolean] true if the path and request method are known, false otherwise.
27
48
  def known?
28
49
  known_path? && known_request_method?
29
50
  end
30
51
 
52
+ # Checks if the path is known.
53
+ # @return [Boolean] true if the path is known, false otherwise.
31
54
  def known_path?
32
55
  !!path_item
33
56
  end
34
57
 
58
+ # Checks if the request method is known.
59
+ # @return [Boolean] true if the request method is known, false otherwise.
35
60
  def known_request_method?
36
61
  !!operation
37
62
  end
38
63
 
39
- # Merged path and query parameters
64
+ # Returns the merged path and query parameters.
65
+ # @return [Hash] The merged path and query parameters.
40
66
  def params
41
67
  @params ||= query.merge(path_parameters)
42
68
  end
43
69
 
70
+ # Returns the parsed path parameters of the request.
71
+ # @return [Hash]
44
72
  def path_parameters
45
73
  return {} unless operation.path_parameters
46
74
 
@@ -48,6 +76,10 @@ module OpenapiFirst
48
76
  OpenapiParameters::Path.new(operation.path_parameters).unpack(@original_path_params) || {}
49
77
  end
50
78
 
79
+ # Returns the parsed query parameters.
80
+ # This only includes parameters that are defined in the API description.
81
+ # @note This method is aliased as query_parameters.
82
+ # @return [Hash]
51
83
  def query
52
84
  return {} unless operation.query_parameters
53
85
 
@@ -57,12 +89,18 @@ module OpenapiFirst
57
89
 
58
90
  alias query_parameters query
59
91
 
92
+ # Returns the parsed header parameters.
93
+ # This only includes parameters that are defined in the API description.
94
+ # @return [Hash]
60
95
  def headers
61
96
  return {} unless operation.header_parameters
62
97
 
63
98
  @headers ||= OpenapiParameters::Header.new(operation.header_parameters).unpack_env(request.env) || {}
64
99
  end
65
100
 
101
+ # Returns the parsed cookie parameters.
102
+ # This only includes parameters that are defined in the API description.
103
+ # @return [Hash]
66
104
  def cookies
67
105
  return {} unless operation.cookie_parameters
68
106
 
@@ -70,26 +108,48 @@ module OpenapiFirst
70
108
  OpenapiParameters::Cookie.new(operation.cookie_parameters).unpack(request.env[Rack::HTTP_COOKIE]) || {}
71
109
  end
72
110
 
111
+ # Returns the parsed request body.
112
+ # This returns the whole request body with default values applied as defined in the API description.
113
+ # This does not remove any fields that are not defined in the API description.
114
+ # @return [Hash, Array, String, nil] The parsed body of the request.
73
115
  def body
74
116
  @body ||= BodyParser.new.parse(request, request.media_type)
75
117
  end
118
+
76
119
  alias parsed_body body
77
120
 
121
+ # Validates the request.
122
+ # @return [Failure, nil] The Failure object if validation failed.
78
123
  def validate
79
- RequestValidation::Validator.new(operation).validate(self)
124
+ @validated = true
125
+ @error = RequestValidation::Validator.new(operation).validate(self)
80
126
  end
81
127
 
128
+ # Validates the request and raises an error if validation fails.
82
129
  def validate!
83
130
  error = validate
84
131
  error&.raise!
85
132
  end
86
133
 
134
+ # Validates the response.
135
+ # @param rack_response [Rack::Response] The rack response object.
136
+ # @param raise_error [Boolean] Whether to raise an error if validation fails.
137
+ # @return [RuntimeResponse] The validated response object.
138
+ def validate_response(rack_response, raise_error: false)
139
+ validated = response(rack_response).tap(&:validate)
140
+ validated.error&.raise! if raise_error
141
+ validated
142
+ end
143
+
144
+ # Creates a new RuntimeResponse object.
145
+ # @param rack_response [Rack::Response] The rack response object.
146
+ # @return [RuntimeResponse] The RuntimeResponse object.
87
147
  def response(rack_response)
88
148
  RuntimeResponse.new(operation, rack_response)
89
149
  end
90
150
 
91
151
  private
92
152
 
93
- attr_reader :request, :operation
153
+ attr_reader :request
94
154
  end
95
155
  end
@@ -5,56 +5,95 @@ require_relative 'body_parser'
5
5
  require_relative 'response_validation/validator'
6
6
 
7
7
  module OpenapiFirst
8
+ # Represents a response returned by the Rack application and how it relates to the API description.
8
9
  class RuntimeResponse
9
10
  extend Forwardable
10
11
 
11
12
  def initialize(operation, rack_response)
12
13
  @operation = operation
13
14
  @rack_response = rack_response
15
+ @error = nil
14
16
  end
15
17
 
18
+ # @return [Failure, nil] Error object if validation failed.
19
+ attr_reader :error
20
+
21
+ # @attr_reader [Integer] status The HTTP status code of this response.
22
+ # @attr_reader [String] content_type The content_type of the Rack::Response.
16
23
  def_delegators :@rack_response, :status, :content_type
17
- def_delegators :@operation, :name
18
24
 
25
+ # @attr_reader [String] name The name of the operation. Used for generating error messages.
26
+ def_delegators :@operation, :name # @visibility private
27
+
28
+ # Checks if the response is valid. Runs the validation unless it has been run before.
29
+ # @return [Boolean]
30
+ def valid?
31
+ validate unless @validated
32
+ @error.nil?
33
+ end
34
+
35
+ # Checks if the response is defined in the API description.
36
+ # @return [Boolean] Returns true if the response is known, false otherwise.
19
37
  def known?
20
38
  !!response_definition
21
39
  end
22
40
 
41
+ # Checks if the response status is defined in the API description.
42
+ # @return [Boolean] Returns true if the response status is known, false otherwise.
23
43
  def known_status?
24
44
  @operation.response_status_defined?(status)
25
45
  end
26
46
 
47
+ # Returns the description of the response definition if available.
48
+ # @return [String, nil] Returns the description of the response, or nil if not available.
27
49
  def description
28
50
  response_definition&.description
29
51
  end
30
52
 
53
+ # Returns the parsed (JSON) body of the response.
54
+ # @return [Hash, String] Returns the body of the response.
31
55
  def body
32
56
  @body ||= content_type =~ /json/i ? load_json(original_body) : original_body
33
57
  end
34
58
 
59
+ # Returns the headers of the response as defined in the API description.
60
+ # This only returns the headers that are defined in the API description.
61
+ # @return [Hash] Returns the headers of the response.
35
62
  def headers
36
63
  @headers ||= unpack_response_headers
37
64
  end
38
65
 
66
+ # Validates the response.
67
+ # @return [Failure, nil] Returns the validation error, or nil if the response is valid.
39
68
  def validate
40
- ResponseValidation::Validator.new(@operation).validate(self)
69
+ @validated = true
70
+ @error = ResponseValidation::Validator.new(@operation).validate(self)
41
71
  end
42
72
 
73
+ # Validates the response and raises an error if invalid.
74
+ # @raise [ResponseNotFoundError, ResponseInvalidError] Raises an error if the response is invalid.
43
75
  def validate!
44
76
  error = validate
45
77
  error&.raise!
46
78
  end
47
79
 
80
+ # Returns the response definition associated with the response.
81
+ # @return [Definition::Response, nil] Returns the response definition, or nil if not found.
48
82
  def response_definition
49
83
  @response_definition ||= @operation.response_for(status, content_type)
50
84
  end
51
85
 
52
86
  private
53
87
 
88
+ # Usually the body responds to #each, but when using manual response validation without the middleware
89
+ # in Rails request specs the body is a String. So this code handles both cases.
54
90
  def original_body
55
91
  buffered_body = String.new
56
- @rack_response.body.each { |chunk| buffered_body << chunk }
57
- buffered_body
92
+ if @rack_response.body.respond_to?(:each)
93
+ @rack_response.body.each { |chunk| buffered_body.to_s << chunk }
94
+ return buffered_body
95
+ end
96
+ @rack_response.body
58
97
  end
59
98
 
60
99
  def load_json(string)
@@ -2,12 +2,14 @@
2
2
 
3
3
  module OpenapiFirst
4
4
  class Schema
5
+ # One of multiple validation errors. Returned by Schema::ValidationResult#errors.
5
6
  class ValidationError
6
7
  def initialize(json_schemer_error)
7
8
  @error = json_schemer_error
8
9
  end
9
10
 
10
11
  def error = @error['error']
12
+ alias message error
11
13
  def schemer_error = @error
12
14
  def instance_location = @error['data_pointer']
13
15
  def schema_location = @error['schema_pointer']
@@ -4,6 +4,7 @@ require_relative 'validation_error'
4
4
 
5
5
  module OpenapiFirst
6
6
  class Schema
7
+ # Result of validating data against a schema. Return value of Schema#validate.
7
8
  class ValidationResult
8
9
  def initialize(validation, schema:, data:)
9
10
  @validation = validation
@@ -15,18 +16,12 @@ module OpenapiFirst
15
16
 
16
17
  def error? = @validation.any?
17
18
 
19
+ # Returns an array of ValidationError objects.
18
20
  def errors
19
21
  @errors ||= @validation.map do |err|
20
22
  ValidationError.new(err)
21
23
  end
22
24
  end
23
-
24
- # Returns a message that is used in exception messages.
25
- def message
26
- return unless error?
27
-
28
- errors.map(&:error).join('. ')
29
- end
30
25
  end
31
26
  end
32
27
  end
@@ -4,6 +4,7 @@ require 'json_schemer'
4
4
  require_relative 'schema/validation_result'
5
5
 
6
6
  module OpenapiFirst
7
+ # Validate data via JSON Schema. A wrapper around JSONSchemer.
7
8
  class Schema
8
9
  attr_reader :schema
9
10
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '1.1.1'
4
+ VERSION = '1.3.0'
5
5
  end
data/lib/openapi_first.rb CHANGED
@@ -12,14 +12,18 @@ require_relative 'openapi_first/error_response'
12
12
  require_relative 'openapi_first/middlewares/response_validation'
13
13
  require_relative 'openapi_first/middlewares/request_validation'
14
14
 
15
+ # OpenapiFirst is a toolchain to build HTTP APIS based on OpenAPI API descriptions.
15
16
  module OpenapiFirst
16
17
  extend Plugins
17
18
 
18
19
  class << self
20
+ # @return [Configuration]
19
21
  def configuration
20
22
  @configuration ||= Configuration.new
21
23
  end
22
24
 
25
+ # @return [Configuration]
26
+ # @yield [Configuration]
23
27
  def configure
24
28
  yield configuration
25
29
  end
@@ -28,12 +32,26 @@ module OpenapiFirst
28
32
  # Key in rack to find instance of RuntimeRequest
29
33
  REQUEST = 'openapi.request'
30
34
 
31
- def self.load(spec_path, only: nil)
32
- resolved = Bundle.resolve(spec_path)
35
+ # Load and dereference an OpenAPI spec file
36
+ # @return [Definition]
37
+ def self.load(filepath, only: nil)
38
+ resolved = bundle(filepath)
39
+ parse(resolved, only:, filepath:)
40
+ end
41
+
42
+ # Parse a dereferenced Hash
43
+ # @return [Definition]
44
+ def self.parse(resolved, only: nil, filepath: nil)
33
45
  resolved['paths'].filter!(&->(key, _) { only.call(key) }) if only
34
- Definition.new(resolved, spec_path)
46
+ Definition.new(resolved, filepath)
47
+ end
48
+
49
+ # @!visibility private
50
+ def self.bundle(filepath)
51
+ Bundle.resolve(filepath)
35
52
  end
36
53
 
54
+ # @!visibility private
37
55
  module Bundle
38
56
  def self.resolve(spec_path)
39
57
  Dir.chdir(File.dirname(spec_path)) do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi_first
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-12 00:00:00.000000000 Z
11
+ date: 2024-01-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json_refs
@@ -59,7 +59,7 @@ dependencies:
59
59
  - !ruby/object:Gem::Version
60
60
  version: '1.15'
61
61
  - !ruby/object:Gem::Dependency
62
- name: mustermann-contrib
62
+ name: mustermann
63
63
  requirement: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
@@ -119,16 +119,6 @@ executables: []
119
119
  extensions: []
120
120
  extra_rdoc_files: []
121
121
  files:
122
- - ".github/CODEOWNERS"
123
- - ".github/workflows/ruby.yml"
124
- - ".gitignore"
125
- - CHANGELOG.md
126
- - Gemfile
127
- - Gemfile.lock
128
- - Gemfile.rack2
129
- - Gemfile.rack2.lock
130
- - LICENSE.txt
131
- - README.md
132
122
  - lib/openapi_first.rb
133
123
  - lib/openapi_first/body_parser.rb
134
124
  - lib/openapi_first/configuration.rb
@@ -157,12 +147,11 @@ files:
157
147
  - lib/openapi_first/schema/validation_error.rb
158
148
  - lib/openapi_first/schema/validation_result.rb
159
149
  - lib/openapi_first/version.rb
160
- - openapi_first.gemspec
161
150
  homepage: https://github.com/ahx/openapi_first
162
151
  licenses:
163
152
  - MIT
164
153
  metadata:
165
- https://github.com/ahx/openapi_first: https://github.com/ahx/openapi_first
154
+ homepage_uri: https://github.com/ahx/openapi_first
166
155
  source_code_uri: https://github.com/ahx/openapi_first
167
156
  changelog_uri: https://github.com/ahx/openapi_first/blob/main/CHANGELOG.md
168
157
  rubygems_mfa_required: 'true'
@@ -184,5 +173,5 @@ requirements: []
184
173
  rubygems_version: 3.5.3
185
174
  signing_key:
186
175
  specification_version: 4
187
- summary: Implement REST APIs based on OpenApi 3.x
176
+ summary: Implement HTTP APIs based on OpenApi 3.x
188
177
  test_files: []
data/.github/CODEOWNERS DELETED
@@ -1 +0,0 @@
1
- * @ahx
@@ -1,13 +0,0 @@
1
- name: Test
2
- on: [push, pull_request]
3
- jobs:
4
- test:
5
- runs-on: ubuntu-latest
6
- steps:
7
- - uses: actions/checkout@v3
8
- - uses: ruby/setup-ruby@v1
9
- with:
10
- ruby-version: '3.1'
11
- bundler-cache: true # runs 'bundle install' and caches installed gems automatically
12
- - run: BUNDLE_GEMFILE=Gemfile bundle exec rake
13
- - run: BUNDLE_GEMFILE=Gemfile.rack2 bundle lock --add-platform x86_64-linux && bundle exec rake
data/.gitignore DELETED
@@ -1,11 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /tmp/
9
-
10
- # rspec failure tracking
11
- .rspec_status