openapi_first 4.2.0 → 4.3.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/CHANGELOG.md +10 -0
- data/lib/openapi_first/configuration.rb +2 -2
- data/lib/openapi_first/definition.rb +2 -2
- data/lib/openapi_first/json.rb +12 -9
- data/lib/openapi_first/path_template.rb +30 -0
- data/lib/openapi_first/registry.rb +0 -1
- data/lib/openapi_first/router/path_trie.rb +85 -0
- data/lib/openapi_first/router/segment_pattern.rb +29 -0
- data/lib/openapi_first/router.rb +7 -15
- data/lib/openapi_first/sinatra.rb +2 -5
- data/lib/openapi_first/test/configuration.rb +2 -2
- data/lib/openapi_first/version.rb +1 -1
- metadata +4 -2
- data/lib/openapi_first/router/path_template.rb +0 -47
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db682408d3927edb6c83c2778f16edecfa428c3e85bdc33ec96664259ddd3f0b
|
|
4
|
+
data.tar.gz: 4519ccef3e6c27da6372f52fea09a1e36aa09b5648534edce6caf4d7bdbca8b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d37cc2c5d0d99331907a5d565eba3a404e492c7cef71c154e5badd52fe05fb2c7761d17cbe2f94dfec14cda1df59e354f5848bcdc44b515069d325a1316d14aa
|
|
7
|
+
data.tar.gz: 20d09dc7debef84d08504202bd6b2acabc33d172486d79d82685bce22362dbf15f920bda84287619bc7517f42da5701c454bf1e23c74a7eeb0c870ab8ae7025c
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 4.3.0
|
|
6
|
+
|
|
7
|
+
- Fix: Passing a block to `config.register` not works when called inside `OpenapiFirst.configure` block. Before the block was ignored.
|
|
8
|
+
- Faster routing for OADs with many dynamic paths. The router now looks up path templates in a prefix tree of path segments instead of trying each template one by one.
|
|
9
|
+
- Changed: If more than one path template matches a request path, the router now compares the templates segment by segment from left to right. For example `/users/{id}/{action}` wins over `/{tenant}/settings/profile` for `/users/settings/profile`. Before, the template with more literal characters overall won.
|
|
10
|
+
|
|
11
|
+
## 4.2.1
|
|
12
|
+
|
|
13
|
+
- Fix deprecation warnings when using multi_json >= 1.21. openapi_first now uses `MultiJSON.parse`/`MultiJSON.generate` if available ([#508](https://github.com/ahx/openapi_first/issues/508)).
|
|
14
|
+
|
|
5
15
|
## 4.2.0
|
|
6
16
|
|
|
7
17
|
- Added `config.regexp_resolver` configuration option, `'ruby'` (default) or `'ecma'` (also as Symbol). The value is passed to json_schemer's `regexp_resolver` configuration. In OpenAPI, the pattern keyword specifies a regular expression that must conform to the ECMA-262 standard. So using `'ecma'` is recommended.
|
|
@@ -19,8 +19,8 @@ module OpenapiFirst
|
|
|
19
19
|
@regexp_resolver = nil
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def register(path_or_definition, as: :default)
|
|
23
|
-
OpenapiFirst.register(path_or_definition, as
|
|
22
|
+
def register(path_or_definition, as: :default, &)
|
|
23
|
+
OpenapiFirst.register(path_or_definition, as:, &)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
attr_reader :hooks, :request_validation_error_response, :regexp_resolver
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'path_template'
|
|
3
4
|
require_relative 'builder'
|
|
4
5
|
require 'forwardable'
|
|
5
6
|
|
|
@@ -80,8 +81,7 @@ module OpenapiFirst
|
|
|
80
81
|
request_def = routes.lazy.flat_map(&:requests).find { |r| r.operation_id == operation_id.to_s }
|
|
81
82
|
raise ArgumentError, "Operation #{operation_id.inspect} is not defined in #{key}." unless request_def
|
|
82
83
|
|
|
83
|
-
request_def.path
|
|
84
|
-
name = Regexp.last_match(1)
|
|
84
|
+
PathTemplate.expand(request_def.path) do |name|
|
|
85
85
|
params.fetch(name.to_sym) do
|
|
86
86
|
params.fetch(name) do
|
|
87
87
|
raise ArgumentError, "Missing path parameter #{name.inspect} for operation #{operation_id.inspect}."
|
data/lib/openapi_first/json.rb
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# This tries to load MultiJson if available to keep compatibility
|
|
4
|
-
# with MultiJson
|
|
4
|
+
# with MultiJson
|
|
5
5
|
|
|
6
6
|
begin
|
|
7
7
|
require 'multi_json'
|
|
8
|
+
|
|
8
9
|
module OpenapiFirst
|
|
9
10
|
# Compatibility with MultiJson
|
|
10
11
|
# @visibility private
|
|
11
12
|
module JSON
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
MultiJson
|
|
13
|
+
if defined?(::MultiJSON)
|
|
14
|
+
# multi_json >= 1.21.0
|
|
15
|
+
ParserError = MultiJSON::ParseError
|
|
16
|
+
def self.parse(string) = MultiJSON.parse(string)
|
|
17
|
+
def self.generate(object) = MultiJSON.generate(object)
|
|
18
|
+
else
|
|
19
|
+
# multi_json < 1.21.0
|
|
20
|
+
ParserError = MultiJson::ParseError
|
|
21
|
+
def self.parse(string) = MultiJson.load(string)
|
|
22
|
+
def self.generate(object) = MultiJson.dump(object)
|
|
20
23
|
end
|
|
21
24
|
end
|
|
22
25
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiFirst
|
|
4
|
+
# Path templating as described in https://spec.openapis.org/oas/v3.1.0#path-templating
|
|
5
|
+
# @visibility private
|
|
6
|
+
module PathTemplate
|
|
7
|
+
VARIABLE = /\{(?<name>[^{}]+)\}/
|
|
8
|
+
SEGMENT_VARIABLE = /\A#{VARIABLE}\z/
|
|
9
|
+
|
|
10
|
+
# @return [Boolean] whether the path contains at least one variable
|
|
11
|
+
def self.template?(path)
|
|
12
|
+
path.match?(VARIABLE)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [Boolean] whether the path segment consists of exactly one variable
|
|
16
|
+
def self.variable?(segment)
|
|
17
|
+
segment.match?(SEGMENT_VARIABLE)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Array<String>] the variable names in order of appearance
|
|
21
|
+
def self.variable_names(template)
|
|
22
|
+
template.scan(VARIABLE).flatten
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Replaces each variable with the value the block returns for its name
|
|
26
|
+
def self.expand(template)
|
|
27
|
+
template.gsub(VARIABLE) { yield Regexp.last_match(:name) }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../path_template'
|
|
4
|
+
require_relative 'segment_pattern'
|
|
5
|
+
|
|
6
|
+
module OpenapiFirst
|
|
7
|
+
class Router
|
|
8
|
+
# @visibility private
|
|
9
|
+
class PathTrie
|
|
10
|
+
def initialize
|
|
11
|
+
@static = {}
|
|
12
|
+
@partial = {}
|
|
13
|
+
@variable = nil
|
|
14
|
+
@leaf = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add(template, value)
|
|
18
|
+
names = PathTemplate.variable_names(template)
|
|
19
|
+
node = template.split('/', -1).reduce(self) { |parent, segment| parent.child(segment) }
|
|
20
|
+
node.leaf ||= [value, names]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def match(path)
|
|
24
|
+
values = []
|
|
25
|
+
value, names = find(path.split('/', -1), 0, values)
|
|
26
|
+
[value, names.zip(values).to_h] if value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def find(segments, index, values)
|
|
30
|
+
return @leaf if index == segments.size
|
|
31
|
+
|
|
32
|
+
@static[segments[index]]&.find(segments, index + 1, values) ||
|
|
33
|
+
find_partial(segments, index, values) ||
|
|
34
|
+
find_variable(segments, index, values)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
protected
|
|
38
|
+
|
|
39
|
+
attr_accessor :leaf
|
|
40
|
+
|
|
41
|
+
def child(segment)
|
|
42
|
+
return @variable ||= PathTrie.new if PathTemplate.variable?(segment)
|
|
43
|
+
return partial_child(segment) if PathTemplate.template?(segment)
|
|
44
|
+
|
|
45
|
+
@static[segment] ||= PathTrie.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def find_partial(segments, index, values)
|
|
51
|
+
@partial.each_value do |pattern, node|
|
|
52
|
+
captures = pattern.match(segments[index])
|
|
53
|
+
next unless captures
|
|
54
|
+
|
|
55
|
+
values.concat(captures)
|
|
56
|
+
found = node.find(segments, index + 1, values)
|
|
57
|
+
return found if found
|
|
58
|
+
|
|
59
|
+
values.pop(captures.size)
|
|
60
|
+
end
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def find_variable(segments, index, values)
|
|
65
|
+
segment = segments[index]
|
|
66
|
+
return if @variable.nil? || segment.empty?
|
|
67
|
+
|
|
68
|
+
values << segment
|
|
69
|
+
found = @variable.find(segments, index + 1, values)
|
|
70
|
+
values.pop unless found
|
|
71
|
+
found
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def partial_child(segment)
|
|
75
|
+
@partial[segment] ||= [SegmentPattern.new(segment), PathTrie.new]
|
|
76
|
+
@partial = @partial.sort_by.with_index { |(key, _), i| [-literal_length(key), i] }.to_h
|
|
77
|
+
@partial[segment].last
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def literal_length(segment)
|
|
81
|
+
segment.gsub(PathTemplate::VARIABLE, '').length
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../path_template'
|
|
4
|
+
|
|
5
|
+
module OpenapiFirst
|
|
6
|
+
class Router
|
|
7
|
+
# Matches one path segment against a segment of a path template that mixes literals and variables
|
|
8
|
+
# @visibility private
|
|
9
|
+
class SegmentPattern
|
|
10
|
+
def initialize(segment)
|
|
11
|
+
@pattern = build_pattern(segment)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @return [Array<String>, nil] the variable values in order of appearance, or nil
|
|
15
|
+
def match(segment)
|
|
16
|
+
@pattern.match(segment)&.captures
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def build_pattern(segment)
|
|
22
|
+
parts = segment.split(PathTemplate::VARIABLE).each_slice(2).map do |literal, name|
|
|
23
|
+
"#{Regexp.escape(literal)}#{'([^/?#]+)' if name}"
|
|
24
|
+
end
|
|
25
|
+
/\A#{parts.join}\z/
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/openapi_first/router.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
3
|
+
require_relative 'path_template'
|
|
4
|
+
require_relative 'router/path_trie'
|
|
4
5
|
require_relative 'router/find_content'
|
|
5
6
|
require_relative 'router/find_response'
|
|
6
7
|
|
|
@@ -27,15 +28,14 @@ module OpenapiFirst
|
|
|
27
28
|
|
|
28
29
|
def initialize
|
|
29
30
|
@static = {}
|
|
30
|
-
@dynamic = {}
|
|
31
|
+
@dynamic = {}
|
|
32
|
+
@trie = PathTrie.new
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
# Returns an enumerator of all routes
|
|
34
36
|
def routes
|
|
35
37
|
@static.chain(@dynamic).lazy.flat_map do |path, request_methods|
|
|
36
|
-
request_methods.
|
|
37
|
-
next if request_method == :template
|
|
38
|
-
|
|
38
|
+
request_methods.map do |request_method, content|
|
|
39
39
|
Route.new(path:, request_method:, requests: content.requests.each_value.lazy.uniq,
|
|
40
40
|
responses: content.responses.each_value.lazy.flat_map(&:values))
|
|
41
41
|
end
|
|
@@ -95,7 +95,7 @@ module OpenapiFirst
|
|
|
95
95
|
def route_at(path, request_method)
|
|
96
96
|
request_method = request_method.upcase
|
|
97
97
|
path_item = if PathTemplate.template?(path)
|
|
98
|
-
@dynamic[path] ||= {
|
|
98
|
+
@dynamic[path] ||= {}.tap { @trie.add(path, _1) }
|
|
99
99
|
else
|
|
100
100
|
@static[path] ||= {}
|
|
101
101
|
end
|
|
@@ -112,15 +112,7 @@ module OpenapiFirst
|
|
|
112
112
|
found = @static[request_path]
|
|
113
113
|
return [found, {}] if found
|
|
114
114
|
|
|
115
|
-
@
|
|
116
|
-
params = path_item[:template].match(request_path)
|
|
117
|
-
next best unless params
|
|
118
|
-
|
|
119
|
-
candidate = [path_item, params]
|
|
120
|
-
next candidate unless best
|
|
121
|
-
|
|
122
|
-
params.values.sum(&:length) < best[1].values.sum(&:length) ? candidate : best
|
|
123
|
-
end
|
|
115
|
+
@trie.match(request_path)
|
|
124
116
|
end
|
|
125
117
|
end
|
|
126
118
|
end
|
|
@@ -62,9 +62,6 @@ module OpenapiFirst
|
|
|
62
62
|
# encoded characters, ...) - a request Sinatra matches is not re-checked against openapi_first's own
|
|
63
63
|
# path matching. Avoid path shapes where the two routers disagree.
|
|
64
64
|
module Sinatra
|
|
65
|
-
PATH_PARAMETER = /\{[^}]+\}/
|
|
66
|
-
private_constant :PATH_PARAMETER
|
|
67
|
-
|
|
68
65
|
# The configuration lives in Sinatra settings (rather than plain instance variables) so a
|
|
69
66
|
# subclass of a configured app inherits the loaded description and its operation index.
|
|
70
67
|
def self.registered(app)
|
|
@@ -133,7 +130,7 @@ module OpenapiFirst
|
|
|
133
130
|
end
|
|
134
131
|
|
|
135
132
|
def operation_handler(path_template, block)
|
|
136
|
-
param_names =
|
|
133
|
+
param_names = PathTemplate.variable_names(path_template)
|
|
137
134
|
proc do |*captures|
|
|
138
135
|
path_params = param_names.zip(captures).to_h
|
|
139
136
|
validated = settings.openapi_definition.validate_request(request, path_template:, path_params:)
|
|
@@ -147,7 +144,7 @@ module OpenapiFirst
|
|
|
147
144
|
end
|
|
148
145
|
|
|
149
146
|
def sinatra_pattern(path)
|
|
150
|
-
|
|
147
|
+
PathTemplate.expand(path) { |name| ":#{name.gsub(/[^A-Za-z0-9_]/, '_')}" }
|
|
151
148
|
end
|
|
152
149
|
|
|
153
150
|
def build_operation_index(definition)
|
|
@@ -25,8 +25,8 @@ module OpenapiFirst
|
|
|
25
25
|
# Register OADs, but don't load them just yet
|
|
26
26
|
# @param [OpenapiFirst::OAD] oad The OAD to register
|
|
27
27
|
# @param [Symbol] as The name to register the OAD under
|
|
28
|
-
def register(oad, as: :default)
|
|
29
|
-
Test.register(oad, as
|
|
28
|
+
def register(oad, as: :default, &)
|
|
29
|
+
Test.register(oad, as:, &)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# Observe a rack app
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openapi_first
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andreas Haller
|
|
@@ -108,6 +108,7 @@ files:
|
|
|
108
108
|
- lib/openapi_first/parameter_content_parsers.rb
|
|
109
109
|
- lib/openapi_first/parameters_parser.rb
|
|
110
110
|
- lib/openapi_first/parsed_request.rb
|
|
111
|
+
- lib/openapi_first/path_template.rb
|
|
111
112
|
- lib/openapi_first/plugins.rb
|
|
112
113
|
- lib/openapi_first/plugins/x_public.rb
|
|
113
114
|
- lib/openapi_first/query_string_parser.rb
|
|
@@ -125,7 +126,8 @@ files:
|
|
|
125
126
|
- lib/openapi_first/router.rb
|
|
126
127
|
- lib/openapi_first/router/find_content.rb
|
|
127
128
|
- lib/openapi_first/router/find_response.rb
|
|
128
|
-
- lib/openapi_first/router/
|
|
129
|
+
- lib/openapi_first/router/path_trie.rb
|
|
130
|
+
- lib/openapi_first/router/segment_pattern.rb
|
|
129
131
|
- lib/openapi_first/schema/hash.rb
|
|
130
132
|
- lib/openapi_first/schema/validation_error.rb
|
|
131
133
|
- lib/openapi_first/schema/validation_result.rb
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module OpenapiFirst
|
|
4
|
-
class Router
|
|
5
|
-
# @visibility private
|
|
6
|
-
class PathTemplate
|
|
7
|
-
# See also https://spec.openapis.org/oas/v3.1.0#path-templating
|
|
8
|
-
TEMPLATE_EXPRESSION = /(\{[^{}]+\})/
|
|
9
|
-
|
|
10
|
-
def self.template?(string)
|
|
11
|
-
string.include?('{')
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def initialize(template)
|
|
15
|
-
@template = template
|
|
16
|
-
@pattern = build_pattern(template)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def to_s
|
|
20
|
-
@template
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def match(path)
|
|
24
|
-
return {} if path == @template
|
|
25
|
-
|
|
26
|
-
matches = path.match(@pattern)
|
|
27
|
-
return unless matches
|
|
28
|
-
|
|
29
|
-
matches.named_captures
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def build_pattern(template)
|
|
35
|
-
parts = template.split(TEMPLATE_EXPRESSION).map! do |part|
|
|
36
|
-
if part.start_with?('{')
|
|
37
|
-
"(?<#{part[1..-2]}>[^/?#]+)"
|
|
38
|
-
else
|
|
39
|
-
Regexp.escape(part)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
/^#{parts.join}$/
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|