openapi_first 1.3.2 → 1.3.5

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: d004ac9e317cce0a7d9df95e49b3abbe435e9dab2f23d91880b29cb670ece06f
4
- data.tar.gz: c8a1b0ee8238adbf6869aa4a81f642fa0bae0091eabab63417ee142df50ded47
3
+ metadata.gz: 631b8fea22128020990af7edb3605d0e842d04b0f876affb4277b3f81b571921
4
+ data.tar.gz: 3fc61f1ca41bb0d380c188681bad5477c36c6cbeeeec5fabf1700d468987aae5
5
5
  SHA512:
6
- metadata.gz: 2e937d066ee559653a60c28b910da5f02def4b73cbb653673c89b2402f96f2dbe79aad482e4c92c23270d0cf9b9e8b35b2423770abc89590056bcd9b5d16332d
7
- data.tar.gz: edab0f813f6e98f2db2e245d6d306b0218e678a5ff0e8b22330d6bb69354011d4c254a9e38b2bf15869294b7f9e5d2b19eb7511079665f414b15f2f3da8b2357
6
+ metadata.gz: e2b1210a029a62742928ebf2e285a58d9d48d2a809373cce1652a13f8b538675b37be361c99625d6e0bb6cf70ea58ebbaba7486f8ac73896cf5989b74257cd06
7
+ data.tar.gz: 0e5d4798b159fd71af8da50703644f71a16cc276bfdafcaff3cf64777042d6ed9b78ac62ba1dfe07cbd1026cf8d359b77124a5abe39d239030c5d5f5b967b12c
data/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.3.5
6
+
7
+ - Added support for `/some/{kebab-cased}` path parameters ([#245](https://github.com/ahx/openapi_first/issues/245))
8
+
9
+ ## 1.3.4
10
+
11
+ - Fixed handling "binary" format in optional multipart file uploads
12
+ - Cache the resolved OAD. This especially makes things run faster in tests.
13
+
14
+ ## 1.3.3 (yanked)
15
+
5
16
  ## 1.3.2
6
17
 
7
18
  ### Changed
@@ -95,7 +95,7 @@ module OpenapiFirst
95
95
  # Returns a unique name for this operation. Used for generating error messages.
96
96
  # @visibility private
97
97
  def name
98
- @name ||= "#{method.upcase} #{path} (#{operation_id})"
98
+ @name ||= "#{method.upcase} #{path}"
99
99
  end
100
100
 
101
101
  # Returns the path parameters of the operation.
@@ -1,19 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'forwardable'
3
4
  require_relative 'operation'
5
+ require_relative 'path_template'
4
6
 
5
7
  module OpenapiFirst
6
8
  class Definition
7
9
  # A pathItem as defined in the OpenAPI document.
8
10
  class PathItem
11
+ extend Forwardable
12
+
9
13
  def initialize(path, path_item_object, openapi_version:)
10
14
  @path = path
11
15
  @path_item_object = path_item_object
12
16
  @openapi_version = openapi_version
17
+ @path_template = PathTemplate.new(path)
13
18
  end
14
19
 
15
20
  attr_reader :path
16
21
 
22
+ def_delegator :@path_template, :match
23
+
17
24
  def operation(request_method)
18
25
  return unless @path_item_object[request_method]
19
26
 
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiFirst
4
+ class Definition
5
+ # @visibility private
6
+ class PathTemplate
7
+ # See also https://spec.openapis.org/oas/v3.1.0#path-templating
8
+ TEMPLATE_EXPRESSION = /(\{[^}]+\})/
9
+ TEMPLATE_EXPRESSION_NAME = /\{([^}]+)\}/
10
+ ALLOWED_PARAMETER_CHARACTERS = %r{([^/?#]+)}
11
+
12
+ def initialize(template)
13
+ @template = template
14
+ @names = template.scan(TEMPLATE_EXPRESSION_NAME).flatten
15
+ @pattern = build_pattern(template)
16
+ end
17
+
18
+ def match(path)
19
+ return {} if path == @template
20
+ return if @names.empty?
21
+
22
+ matches = path.match(@pattern)
23
+ return unless matches
24
+
25
+ values = matches.captures
26
+ @names.zip(values).to_h
27
+ end
28
+
29
+ private
30
+
31
+ def build_pattern(template)
32
+ parts = template.split(TEMPLATE_EXPRESSION).map! do |part|
33
+ part.start_with?('{') ? ALLOWED_PARAMETER_CHARACTERS : Regexp.escape(part)
34
+ end
35
+
36
+ %r{^#{parts.join}/?$}
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'mustermann'
4
3
  require_relative 'definition/path_item'
5
4
  require_relative 'runtime_request'
6
5
  require_relative 'request_validation/validator'
@@ -99,14 +98,12 @@ module OpenapiFirst
99
98
  end
100
99
 
101
100
  def search_for_path_item(request_path)
102
- paths.find do |path, path_item_object|
103
- template = Mustermann.new(path)
104
- path_params = template.params(request_path)
101
+ path_items.find do |path_item|
102
+ path_params = path_item.match(request_path)
105
103
  next unless path_params
106
- next unless path_params.size == template.names.size
107
104
 
108
105
  return [
109
- PathItem.new(path, path_item_object, openapi_version:),
106
+ path_item,
110
107
  path_params
111
108
  ]
112
109
  end
@@ -71,8 +71,8 @@ module OpenapiFirst
71
71
  private
72
72
 
73
73
  def generate_message
74
- messages = errors&.take(4)&.map(&:error)
75
- messages << "... (#{errors.size} errors total)" if errors && errors.size > 4
74
+ messages = errors&.take(3)&.map(&:error)
75
+ messages << "... (#{errors.size} errors total)" if errors && errors.size > 3
76
76
  messages&.join('. ')
77
77
  end
78
78
  end
@@ -32,112 +32,122 @@ require 'hana'
32
32
  require 'json'
33
33
  require 'yaml'
34
34
 
35
- module JsonRefs
36
- class << self
37
- def dereference(doc)
38
- file_cache = {}
39
- Dereferencer.new(Dir.pwd, doc, file_cache).call
40
- end
35
+ module OpenapiFirst
36
+ class FileNotFoundError < StandardError; end
37
+
38
+ module JsonRefs
39
+ class << self
40
+ def dereference(doc)
41
+ file_cache = {}
42
+ Dereferencer.new(Dir.pwd, doc, file_cache).call
43
+ end
41
44
 
42
- def load(filename)
43
- doc_dir = File.dirname(filename)
44
- doc = Loader.handle(filename)
45
- file_cache = {}
46
- Dereferencer.new(doc_dir, doc, file_cache).call
45
+ def load(filename)
46
+ doc_dir = File.dirname(filename)
47
+ doc = Loader.handle(filename)
48
+ file_cache = {}
49
+ Dereferencer.new(filename, doc_dir, doc, file_cache).call
50
+ end
47
51
  end
48
- end
49
52
 
50
- module LocalRef
51
- module_function
53
+ module LocalRef
54
+ module_function
52
55
 
53
- def call(path:, doc:)
54
- Hana::Pointer.new(path[1..]).eval(doc)
56
+ def call(path:, doc:)
57
+ Hana::Pointer.new(path[1..]).eval(doc)
58
+ end
55
59
  end
56
- end
57
60
 
58
- module Loader
59
- module_function
61
+ module Loader
62
+ module_function
60
63
 
61
- def handle(filename)
62
- body = File.read(filename)
63
- return JSON.parse(body) if File.extname(filename) == '.json'
64
+ def handle(filename)
65
+ body = File.read(filename)
66
+ return JSON.parse(body) if File.extname(filename) == '.json'
64
67
 
65
- YAML.unsafe_load(body)
68
+ YAML.unsafe_load(body)
69
+ end
66
70
  end
67
- end
68
71
 
69
- class Dereferencer
70
- def initialize(doc_dir, doc, file_cache)
71
- @doc = doc
72
- @doc_dir = doc_dir
73
- @file_cache = file_cache
74
- end
72
+ class Dereferencer
73
+ def initialize(filename, doc_dir, doc, file_cache)
74
+ @filename = filename
75
+ @doc = doc
76
+ @doc_dir = doc_dir
77
+ @file_cache = file_cache
78
+ end
75
79
 
76
- def call(doc = @doc, keys = [])
77
- if doc.is_a?(Array)
78
- doc.each_with_index do |value, idx|
79
- call(value, keys + [idx])
80
- end
81
- elsif doc.is_a?(Hash)
82
- if doc.key?('$ref')
83
- dereference(keys, doc['$ref'])
84
- else
85
- doc.each do |key, value|
86
- call(value, keys + [key])
80
+ def call(doc = @doc, keys = [])
81
+ if doc.is_a?(Array)
82
+ doc.each_with_index do |value, idx|
83
+ call(value, keys + [idx])
84
+ end
85
+ elsif doc.is_a?(Hash)
86
+ if doc.key?('$ref')
87
+ dereference(keys, doc['$ref'])
88
+ else
89
+ doc.each do |key, value|
90
+ call(value, keys + [key])
91
+ end
87
92
  end
88
93
  end
94
+ doc
89
95
  end
90
- doc
91
- end
92
96
 
93
- private
97
+ private
94
98
 
95
- attr_reader :doc_dir
99
+ attr_reader :doc_dir
96
100
 
97
- def dereference(paths, referenced_path)
98
- key = paths.pop
99
- target = paths.inject(@doc) do |obj, k|
100
- obj[k]
101
+ def dereference(paths, referenced_path)
102
+ key = paths.pop
103
+ target = paths.inject(@doc) do |obj, k|
104
+ obj[k]
105
+ end
106
+ value = follow_referenced_value(referenced_path)
107
+ target[key] = value
101
108
  end
102
- value = follow_referenced_value(referenced_path)
103
- target[key] = value
104
- end
105
109
 
106
- def follow_referenced_value(referenced_path)
107
- value = referenced_value(referenced_path)
108
- return referenced_value(value['$ref']) if value.is_a?(Hash) && value.key?('$ref')
110
+ def follow_referenced_value(referenced_path)
111
+ value = referenced_value(referenced_path)
112
+ return referenced_value(value['$ref']) if value.is_a?(Hash) && value.key?('$ref')
109
113
 
110
- value
111
- end
114
+ value
115
+ end
112
116
 
113
- def referenced_value(referenced_path)
114
- filepath, pointer = referenced_path.split('#')
115
- pointer&.prepend('#')
116
- return dereference_local(pointer) if filepath.empty?
117
+ def referenced_value(referenced_path)
118
+ filepath, pointer = referenced_path.split('#')
119
+ pointer&.prepend('#')
120
+ return dereference_local(pointer) if filepath.empty?
117
121
 
118
- dereferenced_file = dereference_file(filepath)
119
- return dereferenced_file if pointer.nil?
122
+ dereferenced_file = dereference_file(filepath)
123
+ return dereferenced_file if pointer.nil?
120
124
 
121
- LocalRef.call(
122
- path: pointer,
123
- doc: dereferenced_file
124
- )
125
- end
125
+ LocalRef.call(
126
+ path: pointer,
127
+ doc: dereferenced_file
128
+ )
129
+ end
126
130
 
127
- def dereference_local(referenced_path)
128
- LocalRef.call(path: referenced_path, doc: @doc)
129
- end
131
+ def dereference_local(referenced_path)
132
+ LocalRef.call(path: referenced_path, doc: @doc)
133
+ end
130
134
 
131
- def dereference_file(referenced_path)
132
- referenced_path = File.expand_path(referenced_path, doc_dir) unless File.absolute_path?(referenced_path)
133
- @file_cache[referenced_path] ||= load_referenced_file(referenced_path)
134
- end
135
+ def dereference_file(referenced_path)
136
+ referenced_path = File.expand_path(referenced_path, doc_dir) unless File.absolute_path?(referenced_path)
137
+ @file_cache[referenced_path] ||= load_referenced_file(referenced_path)
138
+ end
135
139
 
136
- def load_referenced_file(absolute_path)
137
- directory = File.dirname(absolute_path)
140
+ def load_referenced_file(absolute_path)
141
+ directory = File.dirname(absolute_path)
138
142
 
139
- referenced_doc = Loader.handle(absolute_path)
140
- Dereferencer.new(directory, referenced_doc, @file_cache).call
143
+ unless File.exist?(absolute_path)
144
+ raise FileNotFoundError,
145
+ "Problem while loading file referenced in #{@filename}: File not found #{absolute_path}"
146
+ end
147
+
148
+ referenced_doc = Loader.handle(absolute_path)
149
+ Dereferencer.new(@filename, directory, referenced_doc, @file_cache).call
150
+ end
141
151
  end
142
152
  end
143
153
  end
@@ -22,8 +22,10 @@ module OpenapiFirst
22
22
  # @attr_reader [String] content_type The content_type of the Rack::Response.
23
23
  def_delegators :@rack_response, :status, :content_type
24
24
 
25
- # @attr_reader [String] name The name of the operation. Used for generating error messages.
26
- def_delegators :@operation, :name # @visibility private
25
+ # @return [String] name The name of the operation. Used for generating error messages.
26
+ def name
27
+ "#{@operation.name} response status: #{status}"
28
+ end
27
29
 
28
30
  # Checks if the response is valid. Runs the validation unless it has been run before.
29
31
  # @return [Boolean]
@@ -42,7 +42,7 @@ module OpenapiFirst
42
42
  def binary_format(data, property, property_schema, _parent)
43
43
  return unless property_schema.is_a?(Hash) && property_schema['format'] == 'binary'
44
44
 
45
- data[property] = data[property][:tempfile].read
45
+ data[property] = data.dig(property, :tempfile)&.read if data[property]
46
46
  end
47
47
  end
48
48
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiFirst
4
- VERSION = '1.3.2'
4
+ VERSION = '1.3.5'
5
5
  end
data/lib/openapi_first.rb CHANGED
@@ -49,7 +49,8 @@ module OpenapiFirst
49
49
  # @!visibility private
50
50
  module Bundle
51
51
  def self.resolve(spec_path)
52
- JsonRefs.load(spec_path)
52
+ @file_cache ||= {}
53
+ @file_cache[File.expand_path(spec_path).to_sym] ||= JsonRefs.load(spec_path)
53
54
  end
54
55
  end
55
56
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi_first
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Haller
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-04 00:00:00.000000000 Z
11
+ date: 2024-03-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hana
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.15'
55
- - !ruby/object:Gem::Dependency
56
- name: mustermann
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: 3.0.0
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: 3.0.0
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: openapi_parameters
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +108,7 @@ files:
122
108
  - lib/openapi_first/definition.rb
123
109
  - lib/openapi_first/definition/operation.rb
124
110
  - lib/openapi_first/definition/path_item.rb
111
+ - lib/openapi_first/definition/path_template.rb
125
112
  - lib/openapi_first/definition/request_body.rb
126
113
  - lib/openapi_first/definition/response.rb
127
114
  - lib/openapi_first/definition/responses.rb