openapi_parser 0.9.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e01493b04243073a1fec0acaa56db780076e3c30f3a64534da419bd745408b87
4
- data.tar.gz: 1e57cd341560e800018fa82bbc4801910bbdfdf4a580ccbdaaa63fa0fe442d4a
3
+ metadata.gz: 77010f8eb5e5b8dd1dadd5e365fb13cf11619215f19c38bcd8d6b46bb3927c80
4
+ data.tar.gz: f2b4a9f19eddc1fba4eefb6d74e6c5f0cd285712aa70b7b3970c5b6121701beb
5
5
  SHA512:
6
- metadata.gz: 28182a2367a7ff7314f0c8b35c51d321ce7ce6e9238cd72c83bd57e3cb1970bc9000e19b6a725a47b14ed6262befff61dd325106f9a1389bd72ffa7de7230fe2
7
- data.tar.gz: 0c5560de5b4579933d135d0046a3ae4aaa3e701d83779ff8efd0713023ad5f6d6f37b485f41fa9eb3c3ac5b26c1118afe724d2b9787440e1bfd229c6f0d823a9
6
+ metadata.gz: 90d397850737878d78fd697ab6a60ec004b47496bad48a246226cce54c1835c7c8ee1725c02f09fe581827c76a16bd8af62d1b0310d8cefaca67b3ff480ab863
7
+ data.tar.gz: 4d227f8b4497cb297fcf73515914c15ecc05060f3e30d275bf5e493124e4b6bd23b47e5f6f95a80a519ba74d7da5f51612820fe52867d1da58c4a92cfc7283ff
data/.gitignore CHANGED
@@ -7,6 +7,9 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
 
10
+ # RubyMine
11
+ .idea
12
+
10
13
  # rspec failure tracking
11
14
  .rspec_status
12
- Gemfile.lock
15
+ Gemfile.lock
@@ -6,9 +6,10 @@ language: ruby
6
6
 
7
7
  rvm:
8
8
  - 2.3.8
9
- - 2.4.6
10
- - 2.5.5
11
- - 2.6.3
9
+ - 2.4.10
10
+ - 2.5.8
11
+ - 2.6.6
12
+ - 2.7.1
12
13
  - ruby-head
13
14
 
14
15
  cache: bundler
@@ -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
 
@@ -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
- c = Config.new(config)
19
- root = Schemas::OpenAPI.new(schema, c)
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
- OpenAPIParser::ReferenceExpander.expand(root) if c.expand_reference
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
- root
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
- return nil unless reference.start_with?(object_reference)
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
- if path_template?(schema_path_item)
74
- result[param_name(schema_path_item)] = request_path_item
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 patchs with parameters extracted
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[0].size }
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: false, allow_data_type: false
5
+ openapi_attr_hash_body_objects 'path', PathItem, reference: true, allow_data_type: false
6
6
  end
7
7
  end
@@ -1,3 +1,3 @@
1
1
  module OpenAPIParser
2
- VERSION = '0.9.0'.freeze
2
+ VERSION = '0.12.0'.freeze
3
3
  end
@@ -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', '~> 10.0'
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.9.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-03-22 00:00:00.000000000 Z
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: '0'
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: '0'
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: '10.0'
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: '10.0'
82
+ version: 12.3.3
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: rspec
85
85
  requirement: !ruby/object:Gem::Requirement