liminal-openapi 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +83 -0
- data/lib/liminal/openapi/component_reference.rb +21 -0
- data/lib/liminal/openapi/components_builder.rb +29 -0
- data/lib/liminal/openapi/contract_resolver.rb +42 -0
- data/lib/liminal/openapi/document_validator.rb +89 -0
- data/lib/liminal/openapi/error.rb +17 -0
- data/lib/liminal/openapi/example_overrides.rb +61 -0
- data/lib/liminal/openapi/schema_resolver.rb +49 -0
- data/lib/liminal/openapi/verification/document.rb +66 -0
- data/lib/liminal/openapi/verification/error.rb +60 -0
- data/lib/liminal/openapi/verification/normalizer/operation_example_stripper.rb +145 -0
- data/lib/liminal/openapi/verification/normalizer.rb +37 -0
- data/lib/liminal/openapi/verification/runner.rb +52 -0
- data/lib/liminal/openapi/verification/structural_difference.rb +80 -0
- data/lib/liminal/openapi/verification.rb +29 -0
- data/lib/liminal/openapi/version.rb +7 -0
- data/lib/liminal/openapi.rb +64 -0
- data/lib/liminal-openapi.rb +3 -0
- metadata +172 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d0b5af3f5911eaa775a9306325d79b09874d931c8b9c85f2d68df36a44344cd0
|
|
4
|
+
data.tar.gz: 9886a15c8ba31ab5b19cab9f6d389188594cfec9d7ba16a89554cc56ab0e64a2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ce77ab37220564aa92fb2604902095de601e7fd2434ccfd8853cbec5eb181f0d2e9a58d061db3d6b3c8692aa7439e5e2ec141070480285b8d5a568210b771463
|
|
7
|
+
data.tar.gz: 559a90a3f3a6accbb987e7f38631f0e86c6e6688e9c0a12a92dd6cf68c1aa9705dacdca44cedf5f06d8e0216fb680d6a43d79c3993bd1d6e00187b35d4f3183a
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Liminal contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Liminal OpenAPI
|
|
2
|
+
|
|
3
|
+
`liminal-openapi` compiles Liminal schema sources and dry-validation contracts
|
|
4
|
+
into OpenAPI 3.0 Schema Objects. It also registers reusable components and
|
|
5
|
+
validates generated OpenAPI documents without depending on Rswag.
|
|
6
|
+
|
|
7
|
+
## Compile schemas and contracts
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require "liminal/openapi"
|
|
11
|
+
|
|
12
|
+
schema = Liminal::Openapi.contract(
|
|
13
|
+
Contracts::Users::Create,
|
|
14
|
+
exclude: [[:current_user]],
|
|
15
|
+
examples: {[:name] => "Mario"}
|
|
16
|
+
)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Both schema and contract compilation accept Liminal compiler options such as
|
|
20
|
+
`exclude`, `overrides`, and `predicates`. Contract names are used as diagnostic
|
|
21
|
+
context unless an explicit `source_name` is provided.
|
|
22
|
+
|
|
23
|
+
Passing a Hash treats it as a precompiled Schema Object. It is validated through
|
|
24
|
+
`Liminal::Schema.normalize`, copied, and deeply frozen; compiler-only options are
|
|
25
|
+
rejected because there is no dry-schema source to compile.
|
|
26
|
+
|
|
27
|
+
`examples` and `overrides` are path-keyed Hashes. Paths are canonicalized before
|
|
28
|
+
they are merged, duplicate aliases fail fast, and an explicit override takes
|
|
29
|
+
precedence over an example at the same path.
|
|
30
|
+
|
|
31
|
+
## Register components
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
Liminal::Openapi.enrich(document) do |components|
|
|
35
|
+
components.register_contract(:create_user, Contracts::Users::Create)
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Registration validates the OpenAPI version and component container shape needed
|
|
40
|
+
for the mutation. It rejects invalid names, ambiguous string/symbol registries,
|
|
41
|
+
and component collisions, and returns immutable references. Use verification
|
|
42
|
+
for full-document validation.
|
|
43
|
+
|
|
44
|
+
## Verify generated documents
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
require "liminal/openapi/verification"
|
|
48
|
+
|
|
49
|
+
Liminal::Openapi::Verification.call(
|
|
50
|
+
checked: "swagger/v1/actions.yaml",
|
|
51
|
+
generated: "v1/actions.yaml"
|
|
52
|
+
) do |temporary_root|
|
|
53
|
+
generate_document(temporary_root)
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Verification validates both documents and compares their structure: object key
|
|
58
|
+
order is ignored, while array order and scalar types remain significant.
|
|
59
|
+
Documents must be bundled; external `$ref` targets are rejected because changes
|
|
60
|
+
outside either compared file would otherwise escape drift detection.
|
|
61
|
+
|
|
62
|
+
To ignore generated examples without masking unrelated changes:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
Liminal::Openapi::Verification.call(
|
|
66
|
+
checked: "swagger/v1/actions.yaml",
|
|
67
|
+
generated: "v1/actions.yaml",
|
|
68
|
+
ignore: :operation_examples
|
|
69
|
+
) { |temporary_root| generate_document(temporary_root) }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This policy removes only inline OpenAPI-defined example fields beneath operations.
|
|
73
|
+
Property names, component names, defaults, extensions, and arbitrary
|
|
74
|
+
payload fields named `example` or `examples` remain part of the comparison.
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
Run `bundle exec rake` for randomized RSpec and RuboCop checks,
|
|
79
|
+
`bundle exec rake coverage` for enforced line and branch coverage, and
|
|
80
|
+
`bundle exec rake build` to build the gem. The suite includes a real
|
|
81
|
+
`Dry::Validation::Contract` integration.
|
|
82
|
+
|
|
83
|
+
Made with ❤️ by [@olistik](https://olisti.co)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liminal
|
|
4
|
+
module Openapi
|
|
5
|
+
module ComponentReference
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def call(name)
|
|
9
|
+
Liminal::Components.reference(name)
|
|
10
|
+
rescue Liminal::InvalidComponentNameError => e
|
|
11
|
+
raise InvalidComponentName, e.message
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def display_name(name)
|
|
15
|
+
Liminal::Components.normalize_name(name)
|
|
16
|
+
rescue Liminal::InvalidComponentNameError => e
|
|
17
|
+
raise InvalidComponentName, e.message
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liminal
|
|
4
|
+
module Openapi
|
|
5
|
+
class ComponentsBuilder
|
|
6
|
+
def initialize(document)
|
|
7
|
+
@document = document
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def register(name, schema, **compile_options)
|
|
11
|
+
Liminal::Openapi.register(
|
|
12
|
+
document: @document,
|
|
13
|
+
name: name,
|
|
14
|
+
schema: schema,
|
|
15
|
+
**compile_options
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def register_contract(name, contract, **compile_options)
|
|
20
|
+
Liminal::Openapi.register_contract(
|
|
21
|
+
document: @document,
|
|
22
|
+
name: name,
|
|
23
|
+
contract: contract,
|
|
24
|
+
**compile_options
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liminal
|
|
4
|
+
module Openapi
|
|
5
|
+
module ContractResolver
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def call(contract, **compile_options)
|
|
9
|
+
validate_contract!(contract)
|
|
10
|
+
source = contract.schema
|
|
11
|
+
options = options_for(source, contract, compile_options)
|
|
12
|
+
Liminal::Openapi.schema(source, **options)
|
|
13
|
+
rescue InvalidPrecompiledSchema => e
|
|
14
|
+
raise InvalidPrecompiledSchema, contextual_message(contract, e)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate_contract!(contract)
|
|
18
|
+
return if contract.respond_to?(:schema)
|
|
19
|
+
|
|
20
|
+
raise InvalidContractSource,
|
|
21
|
+
"Contract source must respond to schema; received #{contract.class}."
|
|
22
|
+
end
|
|
23
|
+
private_class_method :validate_contract!
|
|
24
|
+
|
|
25
|
+
def options_for(source, contract, options)
|
|
26
|
+
return options unless source.respond_to?(:to_ast)
|
|
27
|
+
return options if options.key?(:source_name)
|
|
28
|
+
return options unless contract.respond_to?(:name)
|
|
29
|
+
|
|
30
|
+
options.merge(source_name: contract.name)
|
|
31
|
+
end
|
|
32
|
+
private_class_method :options_for
|
|
33
|
+
|
|
34
|
+
def contextual_message(contract, error)
|
|
35
|
+
name = contract.name if contract.respond_to?(:name)
|
|
36
|
+
label = name.to_s unless name.nil?
|
|
37
|
+
label && !label.empty? ? "#{label}: #{error.message}" : error.message
|
|
38
|
+
end
|
|
39
|
+
private_class_method :contextual_message
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liminal
|
|
4
|
+
module Openapi
|
|
5
|
+
module DocumentValidator
|
|
6
|
+
OPENAPI_30_PATTERN = /\A3\.0\.(?:0|[1-9]\d*)\z/
|
|
7
|
+
VERSION_PATTERN = /\A(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\z/
|
|
8
|
+
MISSING = Object.new.freeze
|
|
9
|
+
private_constant :OPENAPI_30_PATTERN, :VERSION_PATTERN, :MISSING
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(document, component_name:)
|
|
14
|
+
context = "Cannot register component #{component_name.inspect}"
|
|
15
|
+
unless document.is_a?(Hash)
|
|
16
|
+
raise InvalidDocument, "#{context}: document is #{typed_value(document)}, expected a Hash."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
validate_version!(document, context)
|
|
20
|
+
validate_component_maps!(document, context)
|
|
21
|
+
document
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate_version!(document, context)
|
|
25
|
+
version = single_logical_value(document, "openapi", context)
|
|
26
|
+
reject_swagger!(document, context)
|
|
27
|
+
return if version.equal?(MISSING)
|
|
28
|
+
|
|
29
|
+
validate_openapi_version!(version, context)
|
|
30
|
+
end
|
|
31
|
+
private_class_method :validate_version!
|
|
32
|
+
|
|
33
|
+
def reject_swagger!(document, context)
|
|
34
|
+
return unless document.key?("swagger") || document.key?(:swagger)
|
|
35
|
+
|
|
36
|
+
raise UnsupportedOpenapiVersion,
|
|
37
|
+
"#{context}: Swagger 2.0 documents are unsupported; only OpenAPI 3.0.x is supported."
|
|
38
|
+
end
|
|
39
|
+
private_class_method :reject_swagger!
|
|
40
|
+
|
|
41
|
+
def validate_openapi_version!(version, context)
|
|
42
|
+
return if version.is_a?(String) && OPENAPI_30_PATTERN.match?(version)
|
|
43
|
+
|
|
44
|
+
detail = if version.is_a?(String) && VERSION_PATTERN.match?(version)
|
|
45
|
+
"OpenAPI version #{version.inspect} is unsupported"
|
|
46
|
+
else
|
|
47
|
+
"OpenAPI version #{version.inspect} is malformed"
|
|
48
|
+
end
|
|
49
|
+
raise UnsupportedOpenapiVersion, "#{context}: #{detail}; only OpenAPI 3.0.x is supported."
|
|
50
|
+
end
|
|
51
|
+
private_class_method :validate_openapi_version!
|
|
52
|
+
|
|
53
|
+
def validate_component_maps!(document, context)
|
|
54
|
+
components = single_logical_value(document, "components", context)
|
|
55
|
+
return if components.equal?(MISSING)
|
|
56
|
+
|
|
57
|
+
unless components.is_a?(Hash)
|
|
58
|
+
raise InvalidDocument,
|
|
59
|
+
"#{context}: components is #{typed_value(components)}, expected a Hash."
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
schemas = single_logical_value(components, "schemas", context)
|
|
63
|
+
return if schemas.equal?(MISSING)
|
|
64
|
+
return if schemas.is_a?(Hash)
|
|
65
|
+
|
|
66
|
+
raise InvalidDocument,
|
|
67
|
+
"#{context}: components.schemas is #{typed_value(schemas)}, expected a Hash."
|
|
68
|
+
end
|
|
69
|
+
private_class_method :validate_component_maps!
|
|
70
|
+
|
|
71
|
+
def single_logical_value(container, name, context)
|
|
72
|
+
keys = [name, name.to_sym].select { |key| container.key?(key) }
|
|
73
|
+
if keys.length > 1
|
|
74
|
+
raise InvalidDocument,
|
|
75
|
+
"#{context}: #{name} cannot be present under both String and Symbol keys."
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
keys.empty? ? MISSING : container.fetch(keys.first)
|
|
79
|
+
end
|
|
80
|
+
private_class_method :single_logical_value
|
|
81
|
+
|
|
82
|
+
def typed_value(value)
|
|
83
|
+
article = value.class.to_s.match?(/\A[AEIOU]/i) ? "an" : "a"
|
|
84
|
+
"#{article} #{value.class}"
|
|
85
|
+
end
|
|
86
|
+
private_class_method :typed_value
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liminal
|
|
4
|
+
module Openapi
|
|
5
|
+
class Error < Liminal::Error; end
|
|
6
|
+
|
|
7
|
+
class InvalidDocument < Error; end
|
|
8
|
+
class UnsupportedOpenapiVersion < Error; end
|
|
9
|
+
class InvalidComponentName < Error; end
|
|
10
|
+
class InvalidSchemaSource < Error; end
|
|
11
|
+
class InvalidPrecompiledSchema < InvalidSchemaSource; end
|
|
12
|
+
class InvalidContractSource < Error; end
|
|
13
|
+
class InvalidSchemaOptions < Error; end
|
|
14
|
+
class InvalidOverrideFragment < InvalidSchemaOptions; end
|
|
15
|
+
class CompileOptionsForPrecompiledSchema < Error; end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liminal
|
|
4
|
+
module Openapi
|
|
5
|
+
module ExampleOverrides
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def call(examples:, overrides:)
|
|
9
|
+
validate_hash!(examples, :examples)
|
|
10
|
+
validate_hash!(overrides, :overrides)
|
|
11
|
+
|
|
12
|
+
normalized_examples = normalize_paths(examples, :examples)
|
|
13
|
+
normalized_overrides = normalize_paths(overrides, :overrides) do |path, fragment|
|
|
14
|
+
unless fragment.is_a?(Hash)
|
|
15
|
+
raise InvalidOverrideFragment,
|
|
16
|
+
"Override at #{path.inspect} must be a Hash; received #{fragment.class}."
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
merge(normalized_examples, normalized_overrides)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def merge(examples, overrides)
|
|
24
|
+
result = examples.to_h do |path, value|
|
|
25
|
+
[path, {"example" => value}]
|
|
26
|
+
end
|
|
27
|
+
overrides.each do |path, fragment|
|
|
28
|
+
example = result.fetch(path, {})
|
|
29
|
+
example = {} if fragment.key?("example") || fragment.key?(:example)
|
|
30
|
+
result[path] = example.merge(fragment)
|
|
31
|
+
end
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
private_class_method :merge
|
|
35
|
+
|
|
36
|
+
def validate_hash!(value, name)
|
|
37
|
+
return if value.is_a?(Hash)
|
|
38
|
+
|
|
39
|
+
raise InvalidSchemaOptions,
|
|
40
|
+
"#{name} must be a Hash of paths to values; received #{value.class}."
|
|
41
|
+
end
|
|
42
|
+
private_class_method :validate_hash!
|
|
43
|
+
|
|
44
|
+
def normalize_paths(values, name)
|
|
45
|
+
values.each_with_object({}) do |(path, value), normalized|
|
|
46
|
+
yield(path, value) if block_given?
|
|
47
|
+
canonical = Liminal::Path.normalize(path)
|
|
48
|
+
if normalized.key?(canonical)
|
|
49
|
+
raise InvalidSchemaOptions,
|
|
50
|
+
"#{name} contains more than one entry for path #{Liminal::Path.display(canonical)}."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
normalized[canonical] = value
|
|
54
|
+
rescue ArgumentError => e
|
|
55
|
+
raise InvalidSchemaOptions, "Invalid #{name} path #{path.inspect}: #{e.message}."
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
private_class_method :normalize_paths
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liminal
|
|
4
|
+
module Openapi
|
|
5
|
+
module SchemaResolver
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def call(source, **compile_options)
|
|
9
|
+
schema = if source.is_a?(Hash)
|
|
10
|
+
resolve_precompiled(source, compile_options)
|
|
11
|
+
elsif source.respond_to?(:to_ast)
|
|
12
|
+
Liminal.compile(source, **compile_options)
|
|
13
|
+
else
|
|
14
|
+
raise InvalidSchemaSource,
|
|
15
|
+
"Schema source must be a Hash or a dry-schema source responding to to_ast; " \
|
|
16
|
+
"received #{source.class}."
|
|
17
|
+
end
|
|
18
|
+
deep_freeze(schema)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def resolve_precompiled(source, compile_options)
|
|
22
|
+
unless compile_options.empty?
|
|
23
|
+
options = compile_options.keys.map(&:inspect).sort.join(", ")
|
|
24
|
+
raise CompileOptionsForPrecompiledSchema,
|
|
25
|
+
"Compiler options are not valid for a precompiled OpenAPI schema Hash: #{options}."
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Liminal::Schema.normalize(source)
|
|
29
|
+
rescue Liminal::InvalidSchemaError => e
|
|
30
|
+
raise InvalidPrecompiledSchema, e.message
|
|
31
|
+
end
|
|
32
|
+
private_class_method :resolve_precompiled
|
|
33
|
+
|
|
34
|
+
def deep_freeze(value)
|
|
35
|
+
case value
|
|
36
|
+
when Hash
|
|
37
|
+
value.each do |key, item|
|
|
38
|
+
deep_freeze(key)
|
|
39
|
+
deep_freeze(item)
|
|
40
|
+
end
|
|
41
|
+
when Array
|
|
42
|
+
value.each { |item| deep_freeze(item) }
|
|
43
|
+
end
|
|
44
|
+
value.freeze
|
|
45
|
+
end
|
|
46
|
+
private_class_method :deep_freeze
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liminal
|
|
4
|
+
module Openapi
|
|
5
|
+
module Verification
|
|
6
|
+
module Document
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Naming/PredicateMethod
|
|
10
|
+
def validate!(path)
|
|
11
|
+
normalized_path, document = parse(path)
|
|
12
|
+
validate_document!(normalized_path, document)
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate_bundled!(path)
|
|
17
|
+
normalized_path, document = parse(path)
|
|
18
|
+
sources = document.reference_sources.to_a.map(&:relative_to_root).sort
|
|
19
|
+
raise ExternalReferenceError.new(normalized_path, sources) unless sources.empty?
|
|
20
|
+
|
|
21
|
+
validate_document!(normalized_path, document)
|
|
22
|
+
true
|
|
23
|
+
end
|
|
24
|
+
# rubocop:enable Naming/PredicateMethod
|
|
25
|
+
|
|
26
|
+
def load(path)
|
|
27
|
+
document = Psych.safe_load(
|
|
28
|
+
File.read(path.to_s),
|
|
29
|
+
permitted_classes: [Date, Time],
|
|
30
|
+
aliases: true
|
|
31
|
+
)
|
|
32
|
+
return document if document.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
raise InvalidDocumentError.new(path.to_s, "document root must be an object")
|
|
35
|
+
rescue InvalidDocumentError
|
|
36
|
+
raise
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
raise InvalidDocumentError.new(path.to_s, e.message)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parse(path)
|
|
42
|
+
normalized_path = path.to_s
|
|
43
|
+
raise MissingDocumentError, normalized_path unless File.file?(normalized_path)
|
|
44
|
+
|
|
45
|
+
[normalized_path, Openapi3Parser.load_file(normalized_path)]
|
|
46
|
+
rescue MissingDocumentError, InvalidDocumentError
|
|
47
|
+
raise
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
raise InvalidDocumentError.new(normalized_path, e.message)
|
|
50
|
+
end
|
|
51
|
+
private_class_method :parse
|
|
52
|
+
|
|
53
|
+
def validate_document!(path, document)
|
|
54
|
+
return if document.valid?
|
|
55
|
+
|
|
56
|
+
raise InvalidDocumentError.new(path, document.errors)
|
|
57
|
+
rescue InvalidDocumentError
|
|
58
|
+
raise
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
raise InvalidDocumentError.new(path, e.message)
|
|
61
|
+
end
|
|
62
|
+
private_class_method :validate_document!
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liminal
|
|
4
|
+
module Openapi
|
|
5
|
+
module Verification
|
|
6
|
+
class Error < Liminal::Openapi::Error; end
|
|
7
|
+
class InvalidConfigurationError < Error; end
|
|
8
|
+
|
|
9
|
+
class ExternalReferenceError < InvalidConfigurationError
|
|
10
|
+
attr_reader :path, :sources
|
|
11
|
+
|
|
12
|
+
def initialize(path, sources)
|
|
13
|
+
@path = path
|
|
14
|
+
@sources = sources.freeze
|
|
15
|
+
super("OpenAPI verification requires bundled documents without external $ref targets; " \
|
|
16
|
+
"#{path} references #{sources.join(', ')}")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class MissingDocumentError < Error
|
|
21
|
+
attr_reader :path
|
|
22
|
+
|
|
23
|
+
def initialize(path)
|
|
24
|
+
@path = path
|
|
25
|
+
super("OpenAPI document does not exist: #{path}")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class InvalidDocumentError < Error
|
|
30
|
+
attr_reader :path, :errors
|
|
31
|
+
|
|
32
|
+
def initialize(path, errors)
|
|
33
|
+
@path = path
|
|
34
|
+
@errors = errors
|
|
35
|
+
super("OpenAPI validation failed for #{path}:\n#{format_errors(errors)}")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def format_errors(errors)
|
|
41
|
+
return errors unless errors.respond_to?(:to_h)
|
|
42
|
+
|
|
43
|
+
errors.to_h.map do |location, messages|
|
|
44
|
+
"#{location}: #{messages.join(', ')}"
|
|
45
|
+
end.join("\n")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class DocumentDriftError < Error
|
|
50
|
+
attr_reader :difference
|
|
51
|
+
|
|
52
|
+
def initialize(difference)
|
|
53
|
+
@difference = difference
|
|
54
|
+
super("OpenAPI documents differ at #{difference.pointer}: " \
|
|
55
|
+
"checked=#{difference.checked_label}, generated=#{difference.generated_label}")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liminal
|
|
4
|
+
module Openapi
|
|
5
|
+
module Verification
|
|
6
|
+
module Normalizer
|
|
7
|
+
# Removes only fields that OpenAPI defines as examples beneath
|
|
8
|
+
# operations. Arbitrary payload and map keys named "example" remain
|
|
9
|
+
# significant during verification.
|
|
10
|
+
module OperationExampleStripper
|
|
11
|
+
HTTP_METHODS = %w[delete get head options patch post put trace].freeze
|
|
12
|
+
private_constant :HTTP_METHODS
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def call(paths)
|
|
17
|
+
return unless paths.is_a?(Hash)
|
|
18
|
+
|
|
19
|
+
paths.each_value { |path_item| visit_path_item(path_item) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def visit_path_item(path_item)
|
|
23
|
+
return unless path_item.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
visit_parameters(path_item["parameters"])
|
|
26
|
+
HTTP_METHODS.each { |method| visit_operation(path_item[method]) }
|
|
27
|
+
end
|
|
28
|
+
private_class_method :visit_path_item
|
|
29
|
+
|
|
30
|
+
def visit_operation(operation)
|
|
31
|
+
return unless operation.is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
visit_parameters(operation["parameters"])
|
|
34
|
+
visit_request_body(operation["requestBody"])
|
|
35
|
+
visit_responses(operation["responses"])
|
|
36
|
+
visit_callbacks(operation["callbacks"])
|
|
37
|
+
end
|
|
38
|
+
private_class_method :visit_operation
|
|
39
|
+
|
|
40
|
+
def visit_parameters(parameters)
|
|
41
|
+
return unless parameters.is_a?(Array)
|
|
42
|
+
|
|
43
|
+
parameters.each { |parameter| visit_parameter(parameter) }
|
|
44
|
+
end
|
|
45
|
+
private_class_method :visit_parameters
|
|
46
|
+
|
|
47
|
+
def visit_parameter(parameter)
|
|
48
|
+
return unless parameter.is_a?(Hash)
|
|
49
|
+
|
|
50
|
+
remove_examples(parameter)
|
|
51
|
+
visit_schema(parameter["schema"])
|
|
52
|
+
visit_content(parameter["content"])
|
|
53
|
+
end
|
|
54
|
+
private_class_method :visit_parameter
|
|
55
|
+
|
|
56
|
+
def visit_request_body(request_body)
|
|
57
|
+
visit_content(request_body["content"]) if request_body.is_a?(Hash)
|
|
58
|
+
end
|
|
59
|
+
private_class_method :visit_request_body
|
|
60
|
+
|
|
61
|
+
def visit_responses(responses)
|
|
62
|
+
return unless responses.is_a?(Hash)
|
|
63
|
+
|
|
64
|
+
responses.each_value do |response|
|
|
65
|
+
next unless response.is_a?(Hash)
|
|
66
|
+
|
|
67
|
+
visit_headers(response["headers"])
|
|
68
|
+
visit_content(response["content"])
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
private_class_method :visit_responses
|
|
72
|
+
|
|
73
|
+
def visit_headers(headers)
|
|
74
|
+
return unless headers.is_a?(Hash)
|
|
75
|
+
|
|
76
|
+
headers.each_value { |header| visit_parameter(header) }
|
|
77
|
+
end
|
|
78
|
+
private_class_method :visit_headers
|
|
79
|
+
|
|
80
|
+
def visit_content(content)
|
|
81
|
+
return unless content.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
content.each_value { |media_type| visit_media_type(media_type) }
|
|
84
|
+
end
|
|
85
|
+
private_class_method :visit_content
|
|
86
|
+
|
|
87
|
+
def visit_media_type(media_type)
|
|
88
|
+
return unless media_type.is_a?(Hash)
|
|
89
|
+
|
|
90
|
+
remove_examples(media_type)
|
|
91
|
+
visit_schema(media_type["schema"])
|
|
92
|
+
visit_encodings(media_type["encoding"])
|
|
93
|
+
end
|
|
94
|
+
private_class_method :visit_media_type
|
|
95
|
+
|
|
96
|
+
def visit_encodings(encodings)
|
|
97
|
+
return unless encodings.is_a?(Hash)
|
|
98
|
+
|
|
99
|
+
encodings.each_value do |encoding|
|
|
100
|
+
visit_headers(encoding["headers"]) if encoding.is_a?(Hash)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
private_class_method :visit_encodings
|
|
104
|
+
|
|
105
|
+
def visit_callbacks(callbacks)
|
|
106
|
+
return unless callbacks.is_a?(Hash)
|
|
107
|
+
|
|
108
|
+
callbacks.each_value do |callback|
|
|
109
|
+
next unless callback.is_a?(Hash)
|
|
110
|
+
|
|
111
|
+
callback.each_value { |path_item| visit_path_item(path_item) }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
private_class_method :visit_callbacks
|
|
115
|
+
|
|
116
|
+
def visit_schema(schema)
|
|
117
|
+
return unless schema.is_a?(Hash)
|
|
118
|
+
|
|
119
|
+
schema.delete("example")
|
|
120
|
+
visit_schema_map(schema["properties"])
|
|
121
|
+
visit_schema(schema["items"])
|
|
122
|
+
visit_schema(schema["not"])
|
|
123
|
+
visit_schema(schema["additionalProperties"])
|
|
124
|
+
%w[allOf anyOf oneOf].each do |keyword|
|
|
125
|
+
children = schema[keyword]
|
|
126
|
+
children.each { |child| visit_schema(child) } if children.is_a?(Array)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
private_class_method :visit_schema
|
|
130
|
+
|
|
131
|
+
def visit_schema_map(schemas)
|
|
132
|
+
schemas.each_value { |schema| visit_schema(schema) } if schemas.is_a?(Hash)
|
|
133
|
+
end
|
|
134
|
+
private_class_method :visit_schema_map
|
|
135
|
+
|
|
136
|
+
def remove_examples(value)
|
|
137
|
+
value.delete("example")
|
|
138
|
+
value.delete("examples")
|
|
139
|
+
end
|
|
140
|
+
private_class_method :remove_examples
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "liminal/openapi/verification/normalizer/operation_example_stripper"
|
|
4
|
+
|
|
5
|
+
module Liminal
|
|
6
|
+
module Openapi
|
|
7
|
+
module Verification
|
|
8
|
+
module Normalizer
|
|
9
|
+
OPERATION_EXAMPLES = "operation_examples"
|
|
10
|
+
private_constant :OPERATION_EXAMPLES, :OperationExampleStripper
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(document, ignore:)
|
|
15
|
+
policies = normalize_policies(ignore)
|
|
16
|
+
OperationExampleStripper.call(document.fetch("paths", {})) if policies.include?(OPERATION_EXAMPLES)
|
|
17
|
+
document
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def normalize_policies(ignore)
|
|
21
|
+
Array(ignore).map do |entry|
|
|
22
|
+
unless entry.is_a?(String) || entry.is_a?(Symbol)
|
|
23
|
+
raise InvalidConfigurationError,
|
|
24
|
+
"Verification ignore policies must be a String or Symbol; received #{entry.class}."
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
policy = entry.to_s
|
|
28
|
+
next policy if policy == OPERATION_EXAMPLES
|
|
29
|
+
|
|
30
|
+
raise InvalidConfigurationError, "Unsupported verification ignore policy: #{entry.inspect}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
private_class_method :normalize_policies
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liminal
|
|
4
|
+
module Openapi
|
|
5
|
+
module Verification
|
|
6
|
+
class Runner
|
|
7
|
+
def initialize(checked:, generated:, ignore:)
|
|
8
|
+
@checked = checked.to_s
|
|
9
|
+
@generated = generated.to_s
|
|
10
|
+
@ignore = ignore
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(&generator)
|
|
14
|
+
validate_configuration!(generator)
|
|
15
|
+
Document.validate_bundled!(checked)
|
|
16
|
+
|
|
17
|
+
Dir.mktmpdir("liminal-openapi-verification") do |temporary_root|
|
|
18
|
+
generator.call(temporary_root)
|
|
19
|
+
generated_path = File.join(temporary_root, generated)
|
|
20
|
+
Document.validate_bundled!(generated_path)
|
|
21
|
+
compare!(generated_path)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :checked, :generated, :ignore
|
|
30
|
+
|
|
31
|
+
def validate_configuration!(generator)
|
|
32
|
+
raise InvalidConfigurationError, "A generation block is required" unless generator
|
|
33
|
+
|
|
34
|
+
path = Pathname.new(generated)
|
|
35
|
+
if generated.empty? || path.absolute? || path.each_filename.include?("..")
|
|
36
|
+
raise InvalidConfigurationError,
|
|
37
|
+
"Generated document path must be relative and remain below the temporary root: #{generated.inspect}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Normalizer.call({}, ignore: ignore)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def compare!(generated_path)
|
|
44
|
+
checked_document = Normalizer.call(Document.load(checked), ignore: ignore)
|
|
45
|
+
generated_document = Normalizer.call(Document.load(generated_path), ignore: ignore)
|
|
46
|
+
difference = StructuralDifference.between(checked_document, generated_document)
|
|
47
|
+
raise DocumentDriftError, difference if difference
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liminal
|
|
4
|
+
module Openapi
|
|
5
|
+
module Verification
|
|
6
|
+
class StructuralDifference
|
|
7
|
+
MISSING = Object.new.freeze
|
|
8
|
+
private_constant :MISSING
|
|
9
|
+
|
|
10
|
+
attr_reader :path, :checked, :generated
|
|
11
|
+
|
|
12
|
+
def self.between(checked, generated, path = [])
|
|
13
|
+
return if checked.eql?(generated)
|
|
14
|
+
return between_hashes(checked, generated, path) if checked.is_a?(Hash) && generated.is_a?(Hash)
|
|
15
|
+
return between_arrays(checked, generated, path) if checked.is_a?(Array) && generated.is_a?(Array)
|
|
16
|
+
|
|
17
|
+
new(path, checked, generated)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.between_hashes(checked, generated, path)
|
|
21
|
+
(checked.keys | generated.keys).sort_by(&:to_s).each do |key|
|
|
22
|
+
difference = between(
|
|
23
|
+
checked.fetch(key, MISSING),
|
|
24
|
+
generated.fetch(key, MISSING),
|
|
25
|
+
path + [key]
|
|
26
|
+
)
|
|
27
|
+
return difference if difference
|
|
28
|
+
end
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.between_arrays(checked, generated, path)
|
|
33
|
+
[checked.length, generated.length].max.times do |index|
|
|
34
|
+
difference = between(
|
|
35
|
+
checked.fetch(index, MISSING),
|
|
36
|
+
generated.fetch(index, MISSING),
|
|
37
|
+
path + [index]
|
|
38
|
+
)
|
|
39
|
+
return difference if difference
|
|
40
|
+
end
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
private_class_method :between_hashes, :between_arrays
|
|
44
|
+
|
|
45
|
+
def initialize(path, checked, generated)
|
|
46
|
+
@path = path.freeze
|
|
47
|
+
@checked = checked
|
|
48
|
+
@generated = generated
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def pointer
|
|
52
|
+
return "#" if path.empty?
|
|
53
|
+
|
|
54
|
+
"#/#{path.map { |part| escape_pointer(part) }.join('/')}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def checked_label
|
|
58
|
+
display(checked)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def generated_label
|
|
62
|
+
display(generated)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def escape_pointer(part)
|
|
68
|
+
part.to_s.gsub("~", "~0").gsub("/", "~1")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def display(value)
|
|
72
|
+
return "<missing>" if value.equal?(MISSING)
|
|
73
|
+
|
|
74
|
+
text = value.inspect
|
|
75
|
+
text.length > 200 ? "#{text[0, 197]}..." : text
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "psych"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require "openapi3_parser"
|
|
8
|
+
require "liminal/openapi"
|
|
9
|
+
require "liminal/openapi/verification/error"
|
|
10
|
+
require "liminal/openapi/verification/document"
|
|
11
|
+
require "liminal/openapi/verification/normalizer"
|
|
12
|
+
require "liminal/openapi/verification/structural_difference"
|
|
13
|
+
require "liminal/openapi/verification/runner"
|
|
14
|
+
|
|
15
|
+
module Liminal
|
|
16
|
+
module Openapi
|
|
17
|
+
module Verification
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def call(checked:, generated:, ignore: [], &generator)
|
|
21
|
+
Runner.new(checked: checked, generated: generated, ignore: ignore).call(&generator)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate!(path)
|
|
25
|
+
Document.validate!(path)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "liminal"
|
|
4
|
+
require "liminal/openapi/version"
|
|
5
|
+
require "liminal/openapi/error"
|
|
6
|
+
require "liminal/openapi/example_overrides"
|
|
7
|
+
require "liminal/openapi/schema_resolver"
|
|
8
|
+
require "liminal/openapi/contract_resolver"
|
|
9
|
+
require "liminal/openapi/component_reference"
|
|
10
|
+
require "liminal/openapi/document_validator"
|
|
11
|
+
require "liminal/openapi/components_builder"
|
|
12
|
+
|
|
13
|
+
module Liminal
|
|
14
|
+
module Openapi
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def schema(source, examples: nil, **compile_options)
|
|
18
|
+
if !examples.nil? || compile_options.key?(:overrides)
|
|
19
|
+
overrides = compile_options.key?(:overrides) ? compile_options.delete(:overrides) : {}
|
|
20
|
+
compile_options[:overrides] = ExampleOverrides.call(
|
|
21
|
+
examples: examples.nil? ? {} : examples,
|
|
22
|
+
overrides: overrides
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
SchemaResolver.call(source, **compile_options)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def contract(contract, **compile_options)
|
|
29
|
+
ContractResolver.call(contract, **compile_options)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ref(name)
|
|
33
|
+
ComponentReference.call(name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def register(document:, name:, schema:, **compile_options)
|
|
37
|
+
component_name = ComponentReference.display_name(name)
|
|
38
|
+
DocumentValidator.call(document, component_name: component_name)
|
|
39
|
+
resolved_schema = self.schema(schema, **compile_options)
|
|
40
|
+
register_resolved(document, component_name, resolved_schema)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def register_contract(document:, name:, contract:, **compile_options)
|
|
44
|
+
component_name = ComponentReference.display_name(name)
|
|
45
|
+
DocumentValidator.call(document, component_name: component_name)
|
|
46
|
+
resolved_schema = self.contract(contract, **compile_options)
|
|
47
|
+
register_resolved(document, component_name, resolved_schema)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def enrich(document)
|
|
51
|
+
yield ComponentsBuilder.new(document)
|
|
52
|
+
document
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def register_resolved(document, component_name, schema)
|
|
56
|
+
Liminal::Components.register(
|
|
57
|
+
document: document,
|
|
58
|
+
name: component_name,
|
|
59
|
+
schema: schema
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
private_class_method :register_resolved
|
|
63
|
+
end
|
|
64
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: liminal-openapi
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Maurizio De Magnis
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: liminal
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '0.2'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '0.1'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '0.2'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: openapi3_parser
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: 0.10.1
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: 0.10.1
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: dry-validation
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '1.10'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '1.10'
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: rake
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '13.0'
|
|
67
|
+
type: :development
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '13.0'
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: rspec
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '3.13'
|
|
81
|
+
type: :development
|
|
82
|
+
prerelease: false
|
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '3.13'
|
|
88
|
+
- !ruby/object:Gem::Dependency
|
|
89
|
+
name: rubocop
|
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '1.64'
|
|
95
|
+
- - "<"
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: '2'
|
|
98
|
+
type: :development
|
|
99
|
+
prerelease: false
|
|
100
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - ">="
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: '1.64'
|
|
105
|
+
- - "<"
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: '2'
|
|
108
|
+
- !ruby/object:Gem::Dependency
|
|
109
|
+
name: simplecov
|
|
110
|
+
requirement: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - "~>"
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '0.22'
|
|
115
|
+
type: :development
|
|
116
|
+
prerelease: false
|
|
117
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
118
|
+
requirements:
|
|
119
|
+
- - "~>"
|
|
120
|
+
- !ruby/object:Gem::Version
|
|
121
|
+
version: '0.22'
|
|
122
|
+
description: |
|
|
123
|
+
liminal-openapi compiles dry-schema and dry-validation contracts into
|
|
124
|
+
OpenAPI 3.0 schemas, manages schema components, and verifies generated
|
|
125
|
+
OpenAPI documents without depending on a request-spec framework.
|
|
126
|
+
email: root@olisti.co
|
|
127
|
+
executables: []
|
|
128
|
+
extensions: []
|
|
129
|
+
extra_rdoc_files: []
|
|
130
|
+
files:
|
|
131
|
+
- LICENSE.txt
|
|
132
|
+
- README.md
|
|
133
|
+
- lib/liminal-openapi.rb
|
|
134
|
+
- lib/liminal/openapi.rb
|
|
135
|
+
- lib/liminal/openapi/component_reference.rb
|
|
136
|
+
- lib/liminal/openapi/components_builder.rb
|
|
137
|
+
- lib/liminal/openapi/contract_resolver.rb
|
|
138
|
+
- lib/liminal/openapi/document_validator.rb
|
|
139
|
+
- lib/liminal/openapi/error.rb
|
|
140
|
+
- lib/liminal/openapi/example_overrides.rb
|
|
141
|
+
- lib/liminal/openapi/schema_resolver.rb
|
|
142
|
+
- lib/liminal/openapi/verification.rb
|
|
143
|
+
- lib/liminal/openapi/verification/document.rb
|
|
144
|
+
- lib/liminal/openapi/verification/error.rb
|
|
145
|
+
- lib/liminal/openapi/verification/normalizer.rb
|
|
146
|
+
- lib/liminal/openapi/verification/normalizer/operation_example_stripper.rb
|
|
147
|
+
- lib/liminal/openapi/verification/runner.rb
|
|
148
|
+
- lib/liminal/openapi/verification/structural_difference.rb
|
|
149
|
+
- lib/liminal/openapi/version.rb
|
|
150
|
+
homepage: https://source.olisti.co/olisti.co/liminal-openapi
|
|
151
|
+
licenses:
|
|
152
|
+
- MIT
|
|
153
|
+
metadata:
|
|
154
|
+
rubygems_mfa_required: 'true'
|
|
155
|
+
rdoc_options: []
|
|
156
|
+
require_paths:
|
|
157
|
+
- lib
|
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
159
|
+
requirements:
|
|
160
|
+
- - ">="
|
|
161
|
+
- !ruby/object:Gem::Version
|
|
162
|
+
version: '3.2'
|
|
163
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
164
|
+
requirements:
|
|
165
|
+
- - ">="
|
|
166
|
+
- !ruby/object:Gem::Version
|
|
167
|
+
version: '0'
|
|
168
|
+
requirements: []
|
|
169
|
+
rubygems_version: 4.0.15
|
|
170
|
+
specification_version: 4
|
|
171
|
+
summary: Compile Liminal schemas and verify OpenAPI documents
|
|
172
|
+
test_files: []
|