openapi_first 1.1.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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