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 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