skooma 0.1.0 → 0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -1
- data/README.md +62 -6
- data/lib/skooma/dialects/oas_3_1.rb +2 -0
- data/lib/skooma/env_mapper.rb +42 -0
- data/lib/skooma/inflector.rb +1 -1
- data/lib/skooma/keywords/oas_3_1/dialect/any_of.rb +38 -0
- data/lib/skooma/keywords/oas_3_1/dialect/discriminator.rb +32 -1
- data/lib/skooma/keywords/oas_3_1/dialect/one_of.rb +38 -0
- data/lib/skooma/matchers/be_valid_document.rb +38 -0
- data/lib/skooma/matchers/conform_request_schema.rb +37 -0
- data/lib/skooma/matchers/conform_response_schema.rb +34 -0
- data/lib/skooma/matchers/conform_schema.rb +11 -0
- data/lib/skooma/matchers/wrapper.rb +71 -0
- data/lib/skooma/minitest.rb +41 -0
- data/lib/skooma/objects/parameter/keywords/value_parser.rb +6 -4
- data/lib/skooma/output_format.rb +5 -2
- data/lib/skooma/rspec.rb +12 -145
- data/lib/skooma/validators/double.rb +2 -0
- data/lib/skooma/validators/float.rb +2 -0
- data/lib/skooma/validators/int_32.rb +2 -0
- data/lib/skooma/validators/int_64.rb +2 -0
- data/lib/skooma/version.rb +1 -1
- data/lib/skooma.rb +3 -3
- metadata +14 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eef2c83a80d1526de1a465acbb29b70fc053b6b5e54ed1e6b6ed131f56a8fe0a
|
4
|
+
data.tar.gz: 0c4cdec06703bdeb6fd3cf6922401552afaa13aa20b9189e472e703288ba60aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b5089b0a710836b072b723014cfaaf88b4504853f09a42ddf6a6f9e36ed50271b082dfcc184e047f2bf4481e6a343b4bebbd3bced7dfcec2c5a4f85697fbf113
|
7
|
+
data.tar.gz: aacb7329ef950a1fc9ff87513a85803e5530cacaec97f0dad1330bf20fb45e9bc339fe9004af11e1b90cfa6cbb725a2cd339a69c6a23721b667e8d10959b37fb
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning].
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [0.2.1] - 2023-10-23
|
11
|
+
|
12
|
+
### Fixed
|
13
|
+
|
14
|
+
- Raise error when parameter attributes misses required keys. ([@skryukov])
|
15
|
+
- Fix output format. ([@skryukov])
|
16
|
+
|
17
|
+
## [0.2.0] - 2023-10-23
|
18
|
+
|
19
|
+
### Added
|
20
|
+
|
21
|
+
- Add `minitest` and `rake-test` support. ([@skryukov])
|
22
|
+
- Add `discriminator` keyword support. ([@skryukov])
|
23
|
+
|
24
|
+
### Fixed
|
25
|
+
|
26
|
+
- Fix Zeitwerk eager loading. ([@skryukov])
|
27
|
+
|
10
28
|
## [0.1.0] - 2023-09-27
|
11
29
|
|
12
30
|
### Added
|
@@ -15,7 +33,9 @@ and this project adheres to [Semantic Versioning].
|
|
15
33
|
|
16
34
|
[@skryukov]: https://github.com/skryukov
|
17
35
|
|
18
|
-
[Unreleased]: https://github.com/skryukov/skooma/compare/v0.1
|
36
|
+
[Unreleased]: https://github.com/skryukov/skooma/compare/v0.2.1...HEAD
|
37
|
+
[0.2.1]: https://github.com/skryukov/skooma/compare/v0.2.0...v0.2.1
|
38
|
+
[0.2.0]: https://github.com/skryukov/skooma/compare/v0.1.0...v0.2.0
|
19
39
|
[0.1.0]: https://github.com/skryukov/skooma/commits/v0.1.0
|
20
40
|
|
21
41
|
[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
|
data/README.md
CHANGED
@@ -3,12 +3,16 @@
|
|
3
3
|
[](https://rubygems.org/gems/skooma)
|
4
4
|
[](https://github.com/skryukov/skooma/actions/workflows/main.yml)
|
5
5
|
|
6
|
+
<img align="right" height="150" width="150" title="Skooma logo" src="./assets/logo.svg">
|
7
|
+
|
6
8
|
Skooma is a Ruby library for validating API implementations against OpenAPI documents.
|
7
9
|
|
8
|
-
Features
|
10
|
+
### Features
|
11
|
+
|
9
12
|
- Supports OpenAPI 3.1.0
|
10
13
|
- Supports OpenAPI document validation
|
11
14
|
- Supports request/response validations against OpenAPI document
|
15
|
+
- Includes RSpec and Minitest helpers
|
12
16
|
|
13
17
|
<a href="https://evilmartians.com/?utm_source=skooma&utm_campaign=project_page">
|
14
18
|
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54">
|
@@ -26,20 +30,24 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
26
30
|
|
27
31
|
## Usage
|
28
32
|
|
29
|
-
|
33
|
+
Skooma provides `rspec` and `minitest` helpers for validating OpenAPI documents and requests/responses against them.
|
34
|
+
Skooma helpers are designed to be used with `rails` request specs or `rack-test`.
|
35
|
+
|
36
|
+
### RSpec
|
37
|
+
|
38
|
+
#### Configuration
|
30
39
|
|
31
40
|
```ruby
|
32
41
|
# spec/rails_helper.rb
|
33
42
|
|
34
43
|
RSpec.configure do |config|
|
35
44
|
# ...
|
36
|
-
Skooma.create_registry
|
37
45
|
path_to_openapi = Rails.root.join("docs", "openapi.yml")
|
38
46
|
config.include Skooma::RSpec[path_to_openapi], type: :request
|
39
47
|
end
|
40
48
|
```
|
41
49
|
|
42
|
-
|
50
|
+
#### Validate OpenAPI document
|
43
51
|
|
44
52
|
```ruby
|
45
53
|
# spec/openapi_spec.rb
|
@@ -53,7 +61,7 @@ describe "OpenAPI document", type: :request do
|
|
53
61
|
end
|
54
62
|
```
|
55
63
|
|
56
|
-
|
64
|
+
#### Validate request
|
57
65
|
|
58
66
|
```ruby
|
59
67
|
# spec/requests/feed_spec.rb
|
@@ -94,6 +102,55 @@ end
|
|
94
102
|
# " [\"animalId\", \"food\", \"amount\"]"}]}
|
95
103
|
```
|
96
104
|
|
105
|
+
### Minitest
|
106
|
+
|
107
|
+
#### Configuration
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
# test/test_helper.rb
|
111
|
+
|
112
|
+
ActionDispatch::IntegrationTest.include Skooma::Minitest[Rails.root.join("docs", "openapi.yml")]
|
113
|
+
```
|
114
|
+
|
115
|
+
#### Validate OpenAPI document
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
# test/openapi_test.rb
|
119
|
+
|
120
|
+
require "test_helper"
|
121
|
+
|
122
|
+
class OpenapiTest < ActionDispatch::IntegrationTest
|
123
|
+
test "is valid OpenAPI document" do
|
124
|
+
assert_is_valid_document(skooma_openapi_schema)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
#### Validate request
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
# test/integration/items_test.rb
|
133
|
+
|
134
|
+
require "test_helper"
|
135
|
+
|
136
|
+
class ItemsTest < ActionDispatch::IntegrationTest
|
137
|
+
test "GET /" do
|
138
|
+
get "/"
|
139
|
+
assert_conform_schema(200)
|
140
|
+
end
|
141
|
+
|
142
|
+
test "POST / conforms to schema with 201 response code" do
|
143
|
+
post "/", params: {foo: "bar"}, as: :json
|
144
|
+
assert_conform_schema(201)
|
145
|
+
end
|
146
|
+
|
147
|
+
test "POST / conforms to schema with 400 response code" do
|
148
|
+
post "/", params: {foo: "baz"}, as: :json
|
149
|
+
assert_conform_response_schema(400)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
97
154
|
## Alternatives
|
98
155
|
|
99
156
|
- [openapi_first](https://github.com/ahx/openapi_first)
|
@@ -103,7 +160,6 @@ end
|
|
103
160
|
|
104
161
|
- Full support for external `$ref`s
|
105
162
|
- Full OpenAPI 3.1.0 support:
|
106
|
-
- `discriminator` keyword
|
107
163
|
- respect `style` and `explode` keywords
|
108
164
|
- xml
|
109
165
|
- Callbacks and webhooks validations
|
@@ -12,6 +12,8 @@ module Skooma
|
|
12
12
|
|
13
13
|
registry.add_vocabulary(
|
14
14
|
"https://spec.openapis.org/oas/3.1/vocab/base",
|
15
|
+
Skooma::Keywords::OAS31::Dialect::AnyOf,
|
16
|
+
Skooma::Keywords::OAS31::Dialect::OneOf,
|
15
17
|
Skooma::Keywords::OAS31::Dialect::Discriminator,
|
16
18
|
Skooma::Keywords::OAS31::Dialect::Xml,
|
17
19
|
Skooma::Keywords::OAS31::Dialect::ExternalDocs,
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Skooma
|
4
|
+
module EnvMapper
|
5
|
+
class << self
|
6
|
+
PLAIN_HEADERS = %w[CONTENT_LENGTH CONTENT_TYPE].freeze
|
7
|
+
REGEXP_HTTP = /^HTTP_/.freeze
|
8
|
+
|
9
|
+
def call(env, response = nil, with_response: true, with_request: true)
|
10
|
+
result = {
|
11
|
+
"method" => env["REQUEST_METHOD"].downcase,
|
12
|
+
"path" => env["PATH_INFO"]
|
13
|
+
}
|
14
|
+
result["request"] = map_request(env) if with_request
|
15
|
+
result["response"] = map_response(response) if response && with_response
|
16
|
+
|
17
|
+
result
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def map_request(env)
|
23
|
+
{
|
24
|
+
"query" => env["rack.request.query_string"] || env["QUERY_STRING"],
|
25
|
+
"headers" => env.select { |k, _| k.start_with?("HTTP_") || PLAIN_HEADERS.include?(k) }.transform_keys { |k| k.sub(REGEXP_HTTP, "").split("_").map(&:capitalize).join("-") },
|
26
|
+
"body" => env["RAW_POST_DATA"]
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def map_response(response)
|
31
|
+
status, headers, body = response.to_a
|
32
|
+
full_body = +""
|
33
|
+
body.each { |chunk| full_body << chunk }
|
34
|
+
{
|
35
|
+
"status" => status,
|
36
|
+
"headers" => headers.to_h,
|
37
|
+
"body" => full_body
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/skooma/inflector.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Skooma
|
4
|
+
module Keywords
|
5
|
+
module OAS31
|
6
|
+
module Dialect
|
7
|
+
class AnyOf < JSONSkooma::Keywords::Applicator::AnyOf
|
8
|
+
self.key = "anyOf"
|
9
|
+
self.value_schema = :array_of_schemas
|
10
|
+
self.depends_on = %w[discriminator]
|
11
|
+
|
12
|
+
def evaluate(instance, result)
|
13
|
+
discriminator_schema = result.sibling(instance, "discriminator")&.annotation
|
14
|
+
reorder_json(discriminator_schema)
|
15
|
+
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def reorder_json(discriminator_schema)
|
22
|
+
return unless discriminator_schema
|
23
|
+
|
24
|
+
first = @json.delete_at(@json.index { |schema| resolve_uri(schema["$ref"]) == discriminator_schema })
|
25
|
+
@json.unshift first if first
|
26
|
+
end
|
27
|
+
|
28
|
+
def resolve_uri(uri)
|
29
|
+
uri = URI.parse(uri)
|
30
|
+
return uri if uri.absolute?
|
31
|
+
|
32
|
+
parent_schema.base_uri + uri if parent_schema.base_uri
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -4,8 +4,39 @@ module Skooma
|
|
4
4
|
module Keywords
|
5
5
|
module OAS31
|
6
6
|
module Dialect
|
7
|
-
|
7
|
+
# Discriminator keyword is an annotation keyword,
|
8
|
+
# it does not affect validation of allOf/anyOf/oneOf schemas.
|
9
|
+
# See https://github.com/OAI/OpenAPI-Specification/pull/2618
|
10
|
+
class Discriminator < JSONSkooma::Keywords::Base
|
8
11
|
self.key = "discriminator"
|
12
|
+
|
13
|
+
def evaluate(instance, result)
|
14
|
+
value = instance[json["propertyName"]]
|
15
|
+
uri = mapped_uri(value)
|
16
|
+
return result.failure("Could not resolve discriminator for value `#{value.inspect}`") if uri.nil?
|
17
|
+
|
18
|
+
parent_schema.registry.schema(
|
19
|
+
uri,
|
20
|
+
metaschema_uri: parent_schema.metaschema_uri,
|
21
|
+
cache_id: parent_schema.cache_id
|
22
|
+
)
|
23
|
+
result.annotate(uri)
|
24
|
+
rescue JSONSkooma::RegistryError => e
|
25
|
+
result.failure("Could not resolve discriminator mapping: #{e.message}")
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def mapped_uri(value)
|
31
|
+
uri = json["mapping"]&.fetch(value, value)
|
32
|
+
return if uri.nil?
|
33
|
+
|
34
|
+
uri = "#/components/schemas/#{uri}" unless uri.start_with?("#") || uri.include?("/")
|
35
|
+
uri = URI.parse(uri)
|
36
|
+
return uri if uri.absolute?
|
37
|
+
|
38
|
+
parent_schema.base_uri + uri if parent_schema.base_uri
|
39
|
+
end
|
9
40
|
end
|
10
41
|
end
|
11
42
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Skooma
|
4
|
+
module Keywords
|
5
|
+
module OAS31
|
6
|
+
module Dialect
|
7
|
+
class OneOf < JSONSkooma::Keywords::Applicator::OneOf
|
8
|
+
self.key = "oneOf"
|
9
|
+
self.value_schema = :array_of_schemas
|
10
|
+
self.depends_on = %w[discriminator]
|
11
|
+
|
12
|
+
def evaluate(instance, result)
|
13
|
+
discriminator_schema = result.sibling(instance, "discriminator")&.annotation
|
14
|
+
reorder_json(discriminator_schema)
|
15
|
+
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def reorder_json(discriminator_schema)
|
22
|
+
return unless discriminator_schema
|
23
|
+
|
24
|
+
first = @json.delete_at(@json.index { |schema| resolve_uri(schema["$ref"]) == discriminator_schema })
|
25
|
+
@json.unshift first if first
|
26
|
+
end
|
27
|
+
|
28
|
+
def resolve_uri(uri)
|
29
|
+
uri = URI.parse(uri)
|
30
|
+
return uri if uri.absolute?
|
31
|
+
|
32
|
+
parent_schema.base_uri + uri if parent_schema.base_uri
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Skooma
|
4
|
+
module Matchers
|
5
|
+
class BeValidDocument
|
6
|
+
def matches?(actual)
|
7
|
+
@actual = actual
|
8
|
+
return false unless comparable?
|
9
|
+
|
10
|
+
@result = @actual.validate
|
11
|
+
@result.valid?
|
12
|
+
end
|
13
|
+
|
14
|
+
def description
|
15
|
+
"be a valid OpenAPI document"
|
16
|
+
end
|
17
|
+
|
18
|
+
def failure_message
|
19
|
+
return "expected value to be an OpenAPI object" unless comparable?
|
20
|
+
|
21
|
+
<<~MSG
|
22
|
+
must valid against OpenAPI specification:
|
23
|
+
#{pretty(@result.output(:detailed))}
|
24
|
+
MSG
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def pretty(result)
|
30
|
+
PP.pp(result, +"")
|
31
|
+
end
|
32
|
+
|
33
|
+
def comparable?
|
34
|
+
@actual.is_a?(Skooma::Objects::OpenAPI)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Skooma
|
4
|
+
module Matchers
|
5
|
+
class ConformRequestSchema
|
6
|
+
def initialize(schema, mapped_response)
|
7
|
+
@schema = schema
|
8
|
+
@mapped_response = mapped_response
|
9
|
+
end
|
10
|
+
|
11
|
+
def matches?(*)
|
12
|
+
@result = @schema.evaluate(@mapped_response)
|
13
|
+
@result.valid?
|
14
|
+
end
|
15
|
+
|
16
|
+
def description
|
17
|
+
"conform request schema"
|
18
|
+
end
|
19
|
+
|
20
|
+
def failure_message
|
21
|
+
<<~MSG
|
22
|
+
ENV:
|
23
|
+
#{pretty(@mapped_response)}
|
24
|
+
|
25
|
+
Validation Result:
|
26
|
+
#{pretty(@result.output(:skooma))}
|
27
|
+
MSG
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def pretty(result)
|
33
|
+
PP.pp(result, +"")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Skooma
|
4
|
+
module Matchers
|
5
|
+
class ConformResponseSchema < ConformRequestSchema
|
6
|
+
def initialize(schema, mapped_response, expected)
|
7
|
+
super(schema, mapped_response)
|
8
|
+
@expected = expected
|
9
|
+
end
|
10
|
+
|
11
|
+
def description
|
12
|
+
"conform response schema with #{@expected} response code"
|
13
|
+
end
|
14
|
+
|
15
|
+
def matches?(*)
|
16
|
+
return false unless status_matches?
|
17
|
+
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def failure_message
|
22
|
+
return "Expected #{@expected} status code" unless status_matches?
|
23
|
+
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def status_matches?
|
30
|
+
@mapped_response["response"]["status"] == @expected
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
|
5
|
+
module Skooma
|
6
|
+
module Matchers
|
7
|
+
class Wrapper < Module
|
8
|
+
TEST_REGISTRY_NAME = "skooma_test_registry"
|
9
|
+
|
10
|
+
class << self
|
11
|
+
alias_method :[], :new
|
12
|
+
end
|
13
|
+
|
14
|
+
module DefaultHelperMethods
|
15
|
+
def mapped_response(with_response: true, with_request: true)
|
16
|
+
Skooma::EnvMapper.call(
|
17
|
+
request_object.env,
|
18
|
+
response_object,
|
19
|
+
with_response: with_response,
|
20
|
+
with_request: with_request
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def request_object
|
25
|
+
# `rails` integration
|
26
|
+
return request if defined?(::ActionDispatch)
|
27
|
+
# `rack-test` integration
|
28
|
+
return last_request if defined?(::Rack::Test)
|
29
|
+
|
30
|
+
raise "Request object not found"
|
31
|
+
end
|
32
|
+
|
33
|
+
def response_object
|
34
|
+
# `rails` integration
|
35
|
+
return response if defined?(::ActionDispatch)
|
36
|
+
# `rack-test` integration
|
37
|
+
return last_response if defined?(::Rack::Test)
|
38
|
+
|
39
|
+
raise "Response object not found"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(helper_methods_module, openapi_path, base_uri: "https://skoomarb.dev/")
|
44
|
+
super()
|
45
|
+
|
46
|
+
registry = create_test_registry
|
47
|
+
pathname = Pathname.new(openapi_path)
|
48
|
+
registry.add_source(
|
49
|
+
base_uri,
|
50
|
+
JSONSkooma::Sources::Local.new(pathname.dirname.to_s)
|
51
|
+
)
|
52
|
+
schema = registry.schema(URI.parse("#{base_uri}#{pathname.basename}"), schema_class: Skooma::Objects::OpenAPI)
|
53
|
+
|
54
|
+
include DefaultHelperMethods
|
55
|
+
include helper_methods_module
|
56
|
+
|
57
|
+
define_method :skooma_openapi_schema do
|
58
|
+
schema
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def create_test_registry
|
65
|
+
JSONSkooma::Registry[TEST_REGISTRY_NAME]
|
66
|
+
rescue JSONSkooma::RegistryError
|
67
|
+
Skooma.create_registry(name: TEST_REGISTRY_NAME)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Skooma
|
4
|
+
# Minitest helpers for OpenAPI schema validation
|
5
|
+
# @example
|
6
|
+
# describe TestApp do
|
7
|
+
# include Skooma::RSpec[Rails.root.join("docs", "openapi.yml")]
|
8
|
+
# # ...
|
9
|
+
# end
|
10
|
+
class Minitest < Matchers::Wrapper
|
11
|
+
module HelperMethods
|
12
|
+
def assert_conform_schema(expected_status)
|
13
|
+
matcher = Matchers::ConformSchema.new(skooma_openapi_schema, mapped_response, expected_status)
|
14
|
+
|
15
|
+
assert matcher.matches?, -> { matcher.failure_message }
|
16
|
+
end
|
17
|
+
|
18
|
+
def assert_conform_request_schema
|
19
|
+
matcher = Matchers::ConformRequestSchema.new(skooma_openapi_schema, mapped_response(with_response: false))
|
20
|
+
|
21
|
+
assert matcher.matches?, -> { matcher.failure_message }
|
22
|
+
end
|
23
|
+
|
24
|
+
def assert_conform_response_schema(expected_status)
|
25
|
+
matcher = Matchers::ConformResponseSchema.new(skooma_openapi_schema, mapped_response(with_request: false), expected_status)
|
26
|
+
|
27
|
+
assert matcher.matches?, -> { matcher.failure_message }
|
28
|
+
end
|
29
|
+
|
30
|
+
def assert_is_valid_document(document)
|
31
|
+
matcher = Matchers::BeValidDocument.new
|
32
|
+
|
33
|
+
assert matcher.matches?(document), -> { matcher.failure_message }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(openapi_path, **params)
|
38
|
+
super(HelperMethods, openapi_path, **params)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -9,9 +9,11 @@ module Skooma
|
|
9
9
|
module ValueParser
|
10
10
|
class << self
|
11
11
|
def call(instance, result)
|
12
|
-
type = result.sibling(instance, "in")
|
13
|
-
key
|
14
|
-
|
12
|
+
type = result.sibling(instance, "in")&.annotation
|
13
|
+
raise Error, "Missing `in` key #{result.path}" unless type
|
14
|
+
|
15
|
+
key = result.sibling(instance, "name")&.annotation
|
16
|
+
raise Error, "Missing `name` key #{instance.path}: #{key}" unless key
|
15
17
|
|
16
18
|
case type
|
17
19
|
when "query"
|
@@ -30,7 +32,7 @@ module Skooma
|
|
30
32
|
when "cookie"
|
31
33
|
# instance["headers"]["Cookie"]
|
32
34
|
else
|
33
|
-
raise Error, "Unknown location: #{
|
35
|
+
raise Error, "Unknown location: #{type}"
|
34
36
|
end
|
35
37
|
end
|
36
38
|
|
data/lib/skooma/output_format.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Skooma
|
2
4
|
module OutputFormat
|
3
5
|
class << self
|
@@ -16,8 +18,9 @@ module Skooma
|
|
16
18
|
"keywordLocation" => node.path.to_s
|
17
19
|
}
|
18
20
|
|
19
|
-
child_data =
|
20
|
-
|
21
|
+
child_data = []
|
22
|
+
node.each_children do |child|
|
23
|
+
child_data << node_data(child) unless child.valid?
|
21
24
|
end
|
22
25
|
|
23
26
|
if first || child_data.length > 1
|
data/lib/skooma/rspec.rb
CHANGED
@@ -1,166 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "yaml"
|
4
|
-
|
5
3
|
module Skooma
|
6
4
|
# RSpec matchers for OpenAPI schema validation
|
7
5
|
# @example
|
8
|
-
# require "skooma/rspec"
|
9
|
-
# # ...
|
10
6
|
# RSpec.configure do |config|
|
11
7
|
# # ...
|
12
8
|
# config.include Skooma::RSpec[Rails.root.join("docs", "openapi.yml")], type: :request
|
13
9
|
# end
|
14
|
-
class RSpec <
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
module Mapper
|
20
|
-
PLAIN_HEADERS = %w[CONTENT_LENGTH CONTENT_TYPE].freeze
|
21
|
-
REGEXP_HTTP = /^HTTP_/.freeze
|
22
|
-
|
23
|
-
def mapped_response(response: true, request: true)
|
24
|
-
result = {
|
25
|
-
"method" => env["REQUEST_METHOD"].downcase,
|
26
|
-
"path" => env["PATH_INFO"]
|
27
|
-
}
|
28
|
-
|
29
|
-
if response
|
30
|
-
result["response"] = {
|
31
|
-
"status" => response_data[0],
|
32
|
-
"headers" => response_data[1],
|
33
|
-
"body" => response_data[2]
|
34
|
-
}
|
35
|
-
end
|
36
|
-
|
37
|
-
if request
|
38
|
-
result["request"] = {
|
39
|
-
"query" => env["rack.request.query_string"] || env["QUERY_STRING"],
|
40
|
-
"headers" => env.select { |k, _| k.start_with?("HTTP_") || PLAIN_HEADERS.include?(k) }.transform_keys { |k| k.sub(REGEXP_HTTP, "").split("_").map(&:capitalize).join("-") },
|
41
|
-
"body" => env["RAW_POST_DATA"]
|
42
|
-
}
|
43
|
-
end
|
44
|
-
|
45
|
-
result
|
10
|
+
class RSpec < Matchers::Wrapper
|
11
|
+
module HelperMethods
|
12
|
+
def conform_schema(expected_status)
|
13
|
+
Matchers::ConformSchema.new(skooma_openapi_schema, mapped_response, expected_status)
|
46
14
|
end
|
47
15
|
|
48
|
-
def
|
49
|
-
|
16
|
+
def conform_response_schema(expected_status)
|
17
|
+
Matchers::ConformResponseSchema.new(skooma_openapi_schema, mapped_response(with_request: false), expected_status)
|
50
18
|
end
|
51
19
|
|
52
|
-
def
|
53
|
-
|
20
|
+
def conform_request_schema
|
21
|
+
Matchers::ConformRequestSchema.new(skooma_openapi_schema, mapped_response(with_response: false))
|
54
22
|
end
|
55
|
-
end
|
56
|
-
|
57
|
-
def initialize(openapi_path, base_uri: "https://skoomarb.dev/")
|
58
|
-
super()
|
59
|
-
|
60
|
-
pathname = Pathname.new(openapi_path)
|
61
|
-
|
62
|
-
registry = Skooma.create_registry
|
63
|
-
registry.add_source(
|
64
|
-
base_uri,
|
65
|
-
JSONSkooma::Sources::Local.new(pathname.dirname.to_s)
|
66
|
-
)
|
67
|
-
schema = registry.schema(URI.parse("#{base_uri}#{pathname.basename}"), schema_class: Skooma::Objects::OpenAPI)
|
68
23
|
|
69
|
-
|
70
|
-
|
71
|
-
define_method :skooma_openapi_schema do
|
72
|
-
schema
|
24
|
+
def be_valid_document
|
25
|
+
Matchers::BeValidDocument.new
|
73
26
|
end
|
74
27
|
end
|
75
|
-
end
|
76
|
-
|
77
|
-
::RSpec::Matchers.define(:conform_schema) do |expected_status|
|
78
|
-
match do
|
79
|
-
next false unless response.status == expected_status
|
80
|
-
|
81
|
-
@result = skooma_openapi_schema.evaluate(mapped_response)
|
82
|
-
@result.valid?
|
83
|
-
end
|
84
|
-
|
85
|
-
description do
|
86
|
-
"conform schema with #{expected_status} response code"
|
87
|
-
end
|
88
|
-
|
89
|
-
failure_message do
|
90
|
-
<<~MSG
|
91
|
-
ENV:
|
92
|
-
#{PP.pp(mapped_response, +"")}
|
93
|
-
|
94
|
-
Validation Result:
|
95
|
-
#{@result ? PP.pp(@result.output(:skooma), +"") : "Expected #{expected_status} status code"}
|
96
|
-
MSG
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
::RSpec::Matchers.define(:conform_request_schema) do
|
101
|
-
match do
|
102
|
-
@result = skooma_openapi_schema.evaluate(mapped_response(response: false))
|
103
|
-
@result.valid?
|
104
|
-
end
|
105
|
-
|
106
|
-
description do
|
107
|
-
"conform request schema"
|
108
|
-
end
|
109
|
-
|
110
|
-
failure_message do
|
111
|
-
<<~MSG
|
112
|
-
ENV:
|
113
|
-
#{PP.pp(mapped_response, +"")}
|
114
|
-
|
115
|
-
Validation Result:
|
116
|
-
#{@result ? PP.pp(@result.output(:skooma), +"") : "Expected #{expected_status} status code"}
|
117
|
-
MSG
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
::RSpec::Matchers.define(:conform_response_schema) do |expected_status|
|
122
|
-
match do
|
123
|
-
next false unless response.status == expected_status
|
124
|
-
|
125
|
-
@result = skooma_openapi_schema.evaluate(mapped_response(request: false))
|
126
|
-
@result.valid?
|
127
|
-
end
|
128
|
-
|
129
|
-
description do
|
130
|
-
"conform response schema with #{expected_status} response code"
|
131
|
-
end
|
132
|
-
|
133
|
-
failure_message do
|
134
|
-
<<~MSG
|
135
|
-
ENV:
|
136
|
-
#{PP.pp(mapped_response, +"")}
|
137
|
-
|
138
|
-
Validation Result:
|
139
|
-
#{@result ? PP.pp(@result.output(:skooma), +"") : "Expected #{expected_status} status code"}
|
140
|
-
MSG
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
::RSpec::Matchers.define(:be_valid_document) do
|
145
|
-
match do |actual|
|
146
|
-
@actual = actual
|
147
|
-
next false unless comparable?
|
148
|
-
|
149
|
-
@result = actual.validate
|
150
|
-
@result.valid?
|
151
|
-
end
|
152
|
-
|
153
|
-
description { "be a valid OpenAPI document" }
|
154
|
-
|
155
|
-
failure_message do
|
156
|
-
next "expected value to be an OpenAPI object" unless comparable?
|
157
|
-
|
158
|
-
pretty_output = PP.pp(@result.output(:detailed), +"")
|
159
|
-
"must valid against OpenAPI specification:\n#{pretty_output}"
|
160
|
-
end
|
161
28
|
|
162
|
-
def
|
163
|
-
|
29
|
+
def initialize(openapi_path, **params)
|
30
|
+
super(HelperMethods, openapi_path, **params)
|
164
31
|
end
|
165
32
|
end
|
166
33
|
end
|
data/lib/skooma/version.rb
CHANGED
data/lib/skooma.rb
CHANGED
@@ -6,7 +6,7 @@ require "zeitwerk"
|
|
6
6
|
require_relative "skooma/inflector"
|
7
7
|
|
8
8
|
loader = Zeitwerk::Loader.for_gem
|
9
|
-
loader.inflector = Skooma::Inflector.new
|
9
|
+
loader.inflector = Skooma::Inflector.new(__FILE__)
|
10
10
|
loader.setup
|
11
11
|
|
12
12
|
module Skooma
|
@@ -19,8 +19,8 @@ module Skooma
|
|
19
19
|
JSONSkooma::Formatters.register :skooma, OutputFormat
|
20
20
|
|
21
21
|
class << self
|
22
|
-
def create_registry
|
23
|
-
JSONSkooma.create_registry("2020-12", "oas-3.1", name:
|
22
|
+
def create_registry(name: REGISTRY_NAME)
|
23
|
+
JSONSkooma.create_registry("2020-12", "oas-3.1", name: name, assert_formats: true)
|
24
24
|
end
|
25
25
|
end
|
26
26
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: skooma
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Svyatoslav Kryukov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-10-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|
@@ -30,17 +30,17 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '0.
|
33
|
+
version: '0.2'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '0.
|
40
|
+
version: '0.2'
|
41
41
|
description: I bring some sugar for your APIs.
|
42
42
|
email:
|
43
|
-
-
|
43
|
+
- me@skryukov.dev
|
44
44
|
executables: []
|
45
45
|
extensions: []
|
46
46
|
extra_rdoc_files: []
|
@@ -55,14 +55,23 @@ files:
|
|
55
55
|
- lib/skooma.rb
|
56
56
|
- lib/skooma/body_parsers.rb
|
57
57
|
- lib/skooma/dialects/oas_3_1.rb
|
58
|
+
- lib/skooma/env_mapper.rb
|
58
59
|
- lib/skooma/inflector.rb
|
59
60
|
- lib/skooma/instance.rb
|
60
61
|
- lib/skooma/keywords/oas_3_1.rb
|
62
|
+
- lib/skooma/keywords/oas_3_1/dialect/any_of.rb
|
61
63
|
- lib/skooma/keywords/oas_3_1/dialect/discriminator.rb
|
62
64
|
- lib/skooma/keywords/oas_3_1/dialect/example.rb
|
63
65
|
- lib/skooma/keywords/oas_3_1/dialect/external_docs.rb
|
66
|
+
- lib/skooma/keywords/oas_3_1/dialect/one_of.rb
|
64
67
|
- lib/skooma/keywords/oas_3_1/dialect/xml.rb
|
65
68
|
- lib/skooma/keywords/oas_3_1/schema.rb
|
69
|
+
- lib/skooma/matchers/be_valid_document.rb
|
70
|
+
- lib/skooma/matchers/conform_request_schema.rb
|
71
|
+
- lib/skooma/matchers/conform_response_schema.rb
|
72
|
+
- lib/skooma/matchers/conform_schema.rb
|
73
|
+
- lib/skooma/matchers/wrapper.rb
|
74
|
+
- lib/skooma/minitest.rb
|
66
75
|
- lib/skooma/objects/base.rb
|
67
76
|
- lib/skooma/objects/base/keywords/deprecated.rb
|
68
77
|
- lib/skooma/objects/base/keywords/description.rb
|