openapi_parser 0.9.0 → 0.12.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 +4 -4
- data/.gitignore +4 -1
- data/.travis.yml +4 -3
- data/CHANGELOG.md +19 -0
- data/lib/openapi_parser.rb +77 -4
- data/lib/openapi_parser/concerns/findable.rb +20 -1
- data/lib/openapi_parser/errors.rb +11 -0
- data/lib/openapi_parser/path_item_finder.rb +41 -6
- data/lib/openapi_parser/schema_validators/base.rb +1 -1
- data/lib/openapi_parser/schema_validators/string_validator.rb +11 -0
- data/lib/openapi_parser/schemas/openapi.rb +28 -1
- data/lib/openapi_parser/schemas/paths.rb +1 -1
- data/lib/openapi_parser/version.rb +1 -1
- data/openapi_parser.gemspec +2 -2
- metadata +10 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77010f8eb5e5b8dd1dadd5e365fb13cf11619215f19c38bcd8d6b46bb3927c80
|
4
|
+
data.tar.gz: f2b4a9f19eddc1fba4eefb6d74e6c5f0cd285712aa70b7b3970c5b6121701beb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 90d397850737878d78fd697ab6a60ec004b47496bad48a246226cce54c1835c7c8ee1725c02f09fe581827c76a16bd8af62d1b0310d8cefaca67b3ff480ab863
|
7
|
+
data.tar.gz: 4d227f8b4497cb297fcf73515914c15ecc05060f3e30d275bf5e493124e4b6bd23b47e5f6f95a80a519ba74d7da5f51612820fe52867d1da58c4a92cfc7283ff
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,24 @@
|
|
1
1
|
## Unreleased
|
2
2
|
|
3
|
+
* Find path by extracted params than path length #84
|
4
|
+
* Unescape ref URI before lookup in OpenAPIParser::Findable #85
|
5
|
+
* Improved path parameter matching code to allow file extensions, multiple parameters inside one path element, etc #90
|
6
|
+
|
7
|
+
## 0.11.2 (2020-05-23)
|
8
|
+
* Allow date and time content in YAML #81
|
9
|
+
|
10
|
+
## 0.11.1 (2020-05-09)
|
11
|
+
* fix too many warning
|
12
|
+
|
13
|
+
## 0.11.0 (2020-05-09)
|
14
|
+
* Add committee friendly interface to use remote references. #74
|
15
|
+
* Prevent SystemStackError on recursive schema reference #76
|
16
|
+
* support newest ruby versions #78
|
17
|
+
|
18
|
+
## 0.10.0 (2020-04-01)
|
19
|
+
* Support $ref to objects in other OpenAPI yaml files #66
|
20
|
+
* Allow $ref for path item objects #71
|
21
|
+
|
3
22
|
## 0.9.0 (2020-03-22)
|
4
23
|
* Added support for validating UUID formatted strings #67
|
5
24
|
|
data/lib/openapi_parser.rb
CHANGED
@@ -1,4 +1,9 @@
|
|
1
|
+
require 'uri'
|
1
2
|
require 'time'
|
3
|
+
require 'json'
|
4
|
+
require 'psych'
|
5
|
+
require 'pathname'
|
6
|
+
require 'open-uri'
|
2
7
|
|
3
8
|
require 'openapi_parser/version'
|
4
9
|
require 'openapi_parser/config'
|
@@ -13,14 +18,82 @@ require 'openapi_parser/reference_expander'
|
|
13
18
|
|
14
19
|
module OpenAPIParser
|
15
20
|
class << self
|
21
|
+
# Load schema hash object. Uri is not set for returned schema.
|
16
22
|
# @return [OpenAPIParser::Schemas::OpenAPI]
|
17
23
|
def parse(schema, config = {})
|
18
|
-
|
19
|
-
|
24
|
+
load_hash(schema, config: Config.new(config), uri: nil, schema_registry: {})
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param filepath [String] Path of the file containing the passed schema.
|
28
|
+
# Used for resolving remote $ref if provided.
|
29
|
+
# If file path is relative, it is resolved using working directory.
|
30
|
+
# @return [OpenAPIParser::Schemas::OpenAPI]
|
31
|
+
def parse_with_filepath(schema, filepath, config = {})
|
32
|
+
load_hash(schema, config: Config.new(config), uri: filepath && file_uri(filepath), schema_registry: {})
|
33
|
+
end
|
20
34
|
|
21
|
-
|
35
|
+
# Load schema in specified filepath. If file path is relative, it is resolved using working directory.
|
36
|
+
# @return [OpenAPIParser::Schemas::OpenAPI]
|
37
|
+
def load(filepath, config = {})
|
38
|
+
load_uri(file_uri(filepath), config: Config.new(config), schema_registry: {})
|
39
|
+
end
|
40
|
+
|
41
|
+
# Load schema located by the passed uri. Uri must be absolute.
|
42
|
+
# @return [OpenAPIParser::Schemas::OpenAPI]
|
43
|
+
def load_uri(uri, config:, schema_registry:)
|
44
|
+
# Open-uri doesn't open file scheme uri, so we try to open file path directly
|
45
|
+
# File scheme uri which points to a remote file is not supported.
|
46
|
+
content = if uri.scheme == 'file'
|
47
|
+
open(uri.path, &:read)
|
48
|
+
else
|
49
|
+
uri.open(&:read)
|
50
|
+
end
|
22
51
|
|
23
|
-
|
52
|
+
extension = Pathname.new(uri.path).extname
|
53
|
+
load_hash(parse_file(content, extension), config: config, uri: uri, schema_registry: schema_registry)
|
24
54
|
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def file_uri(filepath)
|
59
|
+
path = Pathname.new(filepath)
|
60
|
+
path = Pathname.getwd + path if path.relative?
|
61
|
+
URI.join("file:///", path.to_s)
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_file(content, extension)
|
65
|
+
case extension.downcase
|
66
|
+
when '.yaml', '.yml'
|
67
|
+
parse_yaml(content)
|
68
|
+
when '.json'
|
69
|
+
parse_json(content)
|
70
|
+
else
|
71
|
+
# When extension is something we don't know, try to parse as json first. If it fails, parse as yaml
|
72
|
+
begin
|
73
|
+
parse_json(content)
|
74
|
+
rescue JSON::ParserError
|
75
|
+
parse_yaml(content)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def parse_yaml(content)
|
81
|
+
# FIXME: when drop ruby 2.5, we should use permitted_classes
|
82
|
+
(Gem::Version.create(RUBY_VERSION) < Gem::Version.create("2.6.0")) ?
|
83
|
+
Psych.safe_load(content, [Date, Time]) :
|
84
|
+
Psych.safe_load(content, permitted_classes: [Date, Time])
|
85
|
+
end
|
86
|
+
|
87
|
+
def parse_json(content)
|
88
|
+
JSON.parse(content)
|
89
|
+
end
|
90
|
+
|
91
|
+
def load_hash(hash, config:, uri:, schema_registry:)
|
92
|
+
root = Schemas::OpenAPI.new(hash, config, uri: uri, schema_registry: schema_registry)
|
93
|
+
|
94
|
+
OpenAPIParser::ReferenceExpander.expand(root) if config.expand_reference
|
95
|
+
|
96
|
+
root
|
97
|
+
end
|
25
98
|
end
|
26
99
|
end
|
@@ -1,9 +1,14 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
1
3
|
module OpenAPIParser::Findable
|
2
4
|
# @param [String] reference
|
3
5
|
# @return [OpenAPIParser::Findable]
|
4
6
|
def find_object(reference)
|
5
|
-
|
7
|
+
reference = URI.unescape(reference)
|
6
8
|
return self if object_reference == reference
|
9
|
+
remote_reference = !reference.start_with?('#')
|
10
|
+
return find_remote_object(reference) if remote_reference
|
11
|
+
return nil unless reference.start_with?(object_reference)
|
7
12
|
|
8
13
|
@find_object_cache = {} unless defined? @find_object_cache
|
9
14
|
if (obj = @find_object_cache[reference])
|
@@ -26,8 +31,22 @@ module OpenAPIParser::Findable
|
|
26
31
|
end
|
27
32
|
|
28
33
|
def purge_object_cache
|
34
|
+
@purged = false unless defined? @purged
|
35
|
+
|
36
|
+
return if @purged
|
37
|
+
|
29
38
|
@find_object_cache = {}
|
39
|
+
@purged = true
|
30
40
|
|
31
41
|
_openapi_all_child_objects.values.each(&:purge_object_cache)
|
32
42
|
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def find_remote_object(reference)
|
47
|
+
reference_uri = URI(reference)
|
48
|
+
fragment = reference_uri.fragment
|
49
|
+
reference_uri.fragment = nil
|
50
|
+
root.load_another_schema(reference_uri)&.find_object("##{fragment}")
|
51
|
+
end
|
33
52
|
end
|
@@ -178,6 +178,17 @@ module OpenAPIParser
|
|
178
178
|
end
|
179
179
|
end
|
180
180
|
|
181
|
+
class InvalidUUIDFormat < OpenAPIError
|
182
|
+
def initialize(value, reference)
|
183
|
+
super(reference)
|
184
|
+
@value = value
|
185
|
+
end
|
186
|
+
|
187
|
+
def message
|
188
|
+
"#{@reference} Value: #{@value} is not conformant with UUID format"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
181
192
|
class NotExistStatusCodeDefinition < OpenAPIError
|
182
193
|
def message
|
183
194
|
"#{@reference} status code definition does not exist"
|
@@ -38,7 +38,41 @@ class OpenAPIParser::PathItemFinder
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
+
def parse_path_parameters(schema_path, request_path)
|
42
|
+
parameters = path_parameters(schema_path)
|
43
|
+
return nil if parameters.empty?
|
44
|
+
|
45
|
+
# If there are regex special characters in the path, the regex will
|
46
|
+
# be too permissive, so escape the non-parameter parts.
|
47
|
+
components = []
|
48
|
+
unprocessed = schema_path.dup
|
49
|
+
parameters.each do |parameter|
|
50
|
+
parts = unprocessed.partition(parameter)
|
51
|
+
components << Regexp.escape(parts[0]) unless parts[0] == ''
|
52
|
+
components << "(?<#{param_name(parameter)}>.+)"
|
53
|
+
unprocessed = parts[2]
|
54
|
+
end
|
55
|
+
components << Regexp.escape(unprocessed) unless unprocessed == ''
|
56
|
+
|
57
|
+
regex = components.join('')
|
58
|
+
matches = request_path.match(regex)
|
59
|
+
return nil unless matches
|
60
|
+
|
61
|
+
# Match up the captured names with the captured values as a hash
|
62
|
+
matches.names.zip(matches.captures).to_h
|
63
|
+
end
|
64
|
+
|
41
65
|
private
|
66
|
+
def path_parameters(schema_path)
|
67
|
+
# OAS3 follows a RFC6570 subset for URL templates
|
68
|
+
# https://swagger.io/docs/specification/serialization/#uri-templates
|
69
|
+
# A URL template param can be preceded optionally by a "." or ";", and can be succeeded optionally by a "*";
|
70
|
+
# this regex returns a match of the full parameter name with all of these modifiers. Ex: {;id*}
|
71
|
+
parameters = schema_path.scan(/(\{[\.;]*[^\{\*\}]+\**\})/)
|
72
|
+
# The `String#scan` method returns an array of arrays; we want an array of strings
|
73
|
+
parameters.collect { |param| param[0] }
|
74
|
+
end
|
75
|
+
|
42
76
|
# check if there is a identical path in the schema (without any param)
|
43
77
|
def matches_directly?(request_path, http_method)
|
44
78
|
@paths.path[request_path]&.operation(http_method)
|
@@ -70,8 +104,9 @@ class OpenAPIParser::PathItemFinder
|
|
70
104
|
splitted_request_path.zip(splitted_schema_path).reduce({}) do |result, zip_item|
|
71
105
|
request_path_item, schema_path_item = zip_item
|
72
106
|
|
73
|
-
|
74
|
-
|
107
|
+
params = parse_path_parameters(schema_path_item, request_path_item)
|
108
|
+
if params
|
109
|
+
result.merge!(params)
|
75
110
|
else
|
76
111
|
return if schema_path_item != request_path_item
|
77
112
|
end
|
@@ -80,7 +115,7 @@ class OpenAPIParser::PathItemFinder
|
|
80
115
|
end
|
81
116
|
end
|
82
117
|
|
83
|
-
# find all matching
|
118
|
+
# find all matching paths with parameters extracted
|
84
119
|
# EXAMPLE:
|
85
120
|
# [
|
86
121
|
# ['/user/{id}/edit', { 'id' => 1 }],
|
@@ -94,7 +129,7 @@ class OpenAPIParser::PathItemFinder
|
|
94
129
|
splitted_schema_path = path.split('/')
|
95
130
|
|
96
131
|
next result if different_depth_or_method?(splitted_schema_path, splitted_request_path, path_item, http_method)
|
97
|
-
|
132
|
+
|
98
133
|
extracted_params = extract_params(splitted_request_path, splitted_schema_path)
|
99
134
|
result << [path, extracted_params] if extracted_params
|
100
135
|
result
|
@@ -105,12 +140,12 @@ class OpenAPIParser::PathItemFinder
|
|
105
140
|
# EXAMPLE: find_path_and_params('get', '/user/1') => ['/user/{id}', { 'id' => 1 }]
|
106
141
|
def find_path_and_params(http_method, request_path)
|
107
142
|
return [request_path, {}] if matches_directly?(request_path, http_method)
|
108
|
-
|
143
|
+
|
109
144
|
matching = matching_paths_with_params(request_path, http_method)
|
110
145
|
|
111
146
|
# if there are many matching paths, return the one with the smallest number of params
|
112
147
|
# (prefer /user/{id}/action over /user/{param_1}/{param_2} )
|
113
|
-
matching.min_by { |match| match[
|
148
|
+
matching.min_by { |match| match[1].size }
|
114
149
|
end
|
115
150
|
|
116
151
|
def parse_request_path(http_method, request_path)
|
@@ -34,7 +34,7 @@ class OpenAPIParser::SchemaValidator
|
|
34
34
|
unless resolved_schema
|
35
35
|
return [nil, OpenAPIParser::NotExistDiscriminatorMappedSchema.new(mapping_target, discriminator.object_reference)]
|
36
36
|
end
|
37
|
-
validatable.validate_schema(value, resolved_schema, {discriminator_property_name: discriminator.property_name})
|
37
|
+
validatable.validate_schema(value, resolved_schema, **{discriminator_property_name: discriminator.property_name})
|
38
38
|
end
|
39
39
|
end
|
40
40
|
end
|
@@ -27,6 +27,9 @@ class OpenAPIParser::SchemaValidator
|
|
27
27
|
value, err = validate_email_format(value, schema)
|
28
28
|
return [nil, err] if err
|
29
29
|
|
30
|
+
value, err = validate_uuid_format(value, schema)
|
31
|
+
return [nil, err] if err
|
32
|
+
|
30
33
|
[value, nil]
|
31
34
|
end
|
32
35
|
|
@@ -75,5 +78,13 @@ class OpenAPIParser::SchemaValidator
|
|
75
78
|
|
76
79
|
return [nil, OpenAPIParser::InvalidEmailFormat.new(value, schema.object_reference)]
|
77
80
|
end
|
81
|
+
|
82
|
+
def validate_uuid_format(value, schema)
|
83
|
+
return [value, nil] unless schema.format == 'uuid'
|
84
|
+
|
85
|
+
return [value, nil] if value.match(/[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}/)
|
86
|
+
|
87
|
+
return [nil, OpenAPIParser::InvalidUUIDFormat.new(value, schema.object_reference)]
|
88
|
+
end
|
78
89
|
end
|
79
90
|
end
|
@@ -5,11 +5,16 @@
|
|
5
5
|
|
6
6
|
module OpenAPIParser::Schemas
|
7
7
|
class OpenAPI < Base
|
8
|
-
def initialize(raw_schema, config)
|
8
|
+
def initialize(raw_schema, config, uri: nil, schema_registry: {})
|
9
9
|
super('#', nil, self, raw_schema)
|
10
10
|
@find_object_cache = {}
|
11
11
|
@path_item_finder = OpenAPIParser::PathItemFinder.new(paths) if paths # invalid definition
|
12
12
|
@config = config
|
13
|
+
@uri = uri
|
14
|
+
@schema_registry = schema_registry
|
15
|
+
|
16
|
+
# schema_registery is shared among schemas, and prevents a schema from being loaded multiple times
|
17
|
+
schema_registry[uri] = self if uri
|
13
18
|
end
|
14
19
|
|
15
20
|
# @!attribute [r] openapi
|
@@ -28,5 +33,27 @@ module OpenAPIParser::Schemas
|
|
28
33
|
def request_operation(http_method, request_path)
|
29
34
|
OpenAPIParser::RequestOperation.create(http_method, request_path, @path_item_finder, @config)
|
30
35
|
end
|
36
|
+
|
37
|
+
# load another schema with shared config and schema_registry
|
38
|
+
# @return [OpenAPIParser::Schemas::OpenAPI]
|
39
|
+
def load_another_schema(uri)
|
40
|
+
resolved_uri = resolve_uri(uri)
|
41
|
+
return if resolved_uri.nil?
|
42
|
+
|
43
|
+
loaded = @schema_registry[resolved_uri]
|
44
|
+
return loaded if loaded
|
45
|
+
|
46
|
+
OpenAPIParser.load_uri(resolved_uri, config: @config, schema_registry: @schema_registry)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def resolve_uri(uri)
|
52
|
+
if uri.absolute?
|
53
|
+
uri
|
54
|
+
else
|
55
|
+
@uri&.merge(uri)
|
56
|
+
end
|
57
|
+
end
|
31
58
|
end
|
32
59
|
end
|
@@ -2,6 +2,6 @@ module OpenAPIParser::Schemas
|
|
2
2
|
class Paths < Base
|
3
3
|
# @!attribute [r] path
|
4
4
|
# @return [Hash{String => PathItem, Reference}, nil]
|
5
|
-
openapi_attr_hash_body_objects 'path', PathItem, reference:
|
5
|
+
openapi_attr_hash_body_objects 'path', PathItem, reference: true, allow_data_type: false
|
6
6
|
end
|
7
7
|
end
|
data/openapi_parser.gemspec
CHANGED
@@ -24,9 +24,9 @@ Gem::Specification.new do |spec|
|
|
24
24
|
|
25
25
|
spec.add_development_dependency 'bundler', '>= 1.16'
|
26
26
|
spec.add_development_dependency 'fincop'
|
27
|
-
spec.add_development_dependency 'pry'
|
27
|
+
spec.add_development_dependency 'pry', '~> 0.12.0'
|
28
28
|
spec.add_development_dependency 'pry-byebug'
|
29
|
-
spec.add_development_dependency 'rake', '
|
29
|
+
spec.add_development_dependency 'rake', '>= 12.3.3'
|
30
30
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
31
31
|
spec.add_development_dependency 'rspec-parameterized'
|
32
32
|
spec.add_development_dependency 'simplecov'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: openapi_parser
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.12.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ota42y
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -42,16 +42,16 @@ dependencies:
|
|
42
42
|
name: pry
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 0.12.0
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 0.12.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: pry-byebug
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -70,16 +70,16 @@ dependencies:
|
|
70
70
|
name: rake
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- - "
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
75
|
+
version: 12.3.3
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- - "
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version:
|
82
|
+
version: 12.3.3
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: rspec
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|