skooma 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -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/output_format.rb +2 -0
- 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: 784c4f022f60aced719808f543bcdc9a153df4a58dd44179888f73f9c7c8fa3f
|
4
|
+
data.tar.gz: c60b8a11cd1ccbefcc83207a0ad2734281d8e194b58fd2637dd25bd48e7577a3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c17febaba56cd351329f42acd06a74fdbd02782ccaef3d855098cc41159ee31ef8862cba849a4591f15a6580ffe69705fc783a5ab026f52fa5eefbf55963cd8f
|
7
|
+
data.tar.gz: 35f93cdc5ace695d8e3bf7f315a573e9498e1799fb4bbd9f012d097b7b04ea0a3626b57239e67fbf49a5193d07947aa7c671543630353b10401a16bc66fb0625
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning].
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [0.2.0] - 2023-10-23
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- Add `minitest` and `rake-test` support. ([@skryukov])
|
15
|
+
- Add `discriminator` keyword support. ([@skryukov])
|
16
|
+
|
17
|
+
### Fixed
|
18
|
+
|
19
|
+
- Fix Zeitwerk eager loading. ([@skryukov])
|
20
|
+
|
10
21
|
## [0.1.0] - 2023-09-27
|
11
22
|
|
12
23
|
### Added
|
@@ -15,7 +26,8 @@ and this project adheres to [Semantic Versioning].
|
|
15
26
|
|
16
27
|
[@skryukov]: https://github.com/skryukov
|
17
28
|
|
18
|
-
[Unreleased]: https://github.com/skryukov/skooma/compare/v0.
|
29
|
+
[Unreleased]: https://github.com/skryukov/skooma/compare/v0.2.0...HEAD
|
30
|
+
[0.2.0]: https://github.com/skryukov/skooma/compare/v0.1.0...v0.2.0
|
19
31
|
[0.1.0]: https://github.com/skryukov/skooma/commits/v0.1.0
|
20
32
|
|
21
33
|
[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
|
data/README.md
CHANGED
@@ -3,12 +3,16 @@
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/skooma.svg)](https://rubygems.org/gems/skooma)
|
4
4
|
[![Ruby](https://github.com/skryukov/skooma/actions/workflows/main.yml/badge.svg)](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
|
data/lib/skooma/output_format.rb
CHANGED
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.
|
4
|
+
version: 0.2.0
|
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-22 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
|