openapi_first 4.2.1 → 4.4.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 +12 -0
- data/README.md +13 -0
- data/lib/openapi_first/child_configuration.rb +6 -2
- data/lib/openapi_first/configuration.rb +2 -2
- data/lib/openapi_first/definition.rb +13 -4
- data/lib/openapi_first/middlewares/request_validation.rb +7 -8
- 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 +94 -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
- data/lib/openapi_first.rb +9 -4
- 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: ef15e925e193150f3425c32bd94986f3c069f12af86a88ad043886de13c1ca8a
|
|
4
|
+
data.tar.gz: d4f11e849a469be720cbd5973ffb04e039664101ed44484d4b4f4b2c5ef542f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8aa700d2587944d67f5e2372fe7793c1cd71a76e6c772bf56938564e4f4c4221cb29e54e0ec37bec66c1da370415aabbebb7f722f4cd4fbeba478919308df6cb
|
|
7
|
+
data.tar.gz: 6ef3870f3b011ac35065775da7265d6bc6dff0b121ec0ee42a7e5b47a9fd69d17dbc25ea85723615206d9704afcaee4f65ff3f04e6c85f430ea4781316260ff8
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 4.4.0
|
|
6
|
+
|
|
7
|
+
- Changed: Percent-encoded path parameters are now decoded. For example `/files/my%20file` yields `my file` for `/files/{name}`. A `+` stays a `+`. Values with a malformed encoding or that do not decode to valid UTF-8 are kept as they are. If your application decodes path parameters itself, remove that, or values get decoded twice. Known limitation: An encoded delimiter inside an array or object path parameter is decoded before the value is split, so `/files/a%2Cb,c` yields `["a", "b", "c"]`.
|
|
8
|
+
- Added: You can pass a block to `OpenapiFirst::Middlewares::RequestValidation` to configure hooks and other options specifically for that middleware. With a registered name or a Definition it creates a copy of that OAD, so the registered OAD stays unchanged.
|
|
9
|
+
- Fix: `OpenapiFirst.load` now accepts and applies a configuration block.
|
|
10
|
+
|
|
11
|
+
## 4.3.0
|
|
12
|
+
|
|
13
|
+
- Fix: Passing a block to `config.register` not works when called inside `OpenapiFirst.configure` block. Before the block was ignored.
|
|
14
|
+
- 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.
|
|
15
|
+
- 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.
|
|
16
|
+
|
|
5
17
|
## 4.2.1
|
|
6
18
|
|
|
7
19
|
- 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)).
|
data/README.md
CHANGED
|
@@ -84,6 +84,19 @@ The request validation middleware returns a 4xx if the request is invalid or not
|
|
|
84
84
|
use OpenapiFirst::Middlewares::RequestValidation
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
You can pass a block to configure [hooks](#hooks) and other options for this middleware only:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
use OpenapiFirst::Middlewares::RequestValidation, :v1 do |config|
|
|
91
|
+
config.request_validation_error_response = :jsonapi
|
|
92
|
+
config.after_request_validation do |validated_request|
|
|
93
|
+
warn validated_request.error.message if validated_request.invalid?
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
If you pass a registered name or a Definition together with a block, the middleware uses a copy of that OAD that keeps its configuration and adds yours. The registered OAD stays unchanged.
|
|
99
|
+
|
|
87
100
|
#### Error responses
|
|
88
101
|
|
|
89
102
|
openapi_first produces a useful machine readable error response that can be customized.
|
|
@@ -6,16 +6,20 @@ module OpenapiFirst
|
|
|
6
6
|
def initialize(parent:)
|
|
7
7
|
super()
|
|
8
8
|
@parent = parent
|
|
9
|
-
@request_validation_error_response =
|
|
9
|
+
@request_validation_error_response = nil
|
|
10
10
|
@path = parent.path
|
|
11
11
|
@regexp_resolver = parent.regexp_resolver
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
private attr_reader :parent
|
|
15
15
|
|
|
16
|
+
def request_validation_error_response
|
|
17
|
+
@request_validation_error_response || parent.request_validation_error_response
|
|
18
|
+
end
|
|
19
|
+
|
|
16
20
|
HOOKS.each do |hook|
|
|
17
21
|
define_method(hook) do |&block|
|
|
18
|
-
return hooks[hook].chain(parent.
|
|
22
|
+
return hooks[hook].chain(parent.public_send(hook)) if block.nil?
|
|
19
23
|
|
|
20
24
|
hooks[hook] << block
|
|
21
25
|
block
|
|
@@ -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
|
|
|
@@ -23,10 +24,11 @@ module OpenapiFirst
|
|
|
23
24
|
# @param contents [Hash] The OpenAPI document.
|
|
24
25
|
# @param filepath [String] The file path of the OpenAPI document.
|
|
25
26
|
# @param path_prefix [String,nil] An optional path prefix, that is not documented, that all requests begin with.
|
|
26
|
-
|
|
27
|
+
# @param config [Configuration] The configuration to use. Gets yielded and frozen.
|
|
28
|
+
def initialize(contents, filepath = nil, path_prefix = nil, config: OpenapiFirst.configuration.child)
|
|
27
29
|
@filepath = filepath
|
|
28
30
|
@path_prefix = path_prefix
|
|
29
|
-
@config =
|
|
31
|
+
@config = config
|
|
30
32
|
yield @config if block_given?
|
|
31
33
|
@config.freeze
|
|
32
34
|
@router = Builder.build_router(contents, filepath:, config:)
|
|
@@ -34,6 +36,14 @@ module OpenapiFirst
|
|
|
34
36
|
@paths = @router.routes.map(&:path).to_a.uniq # TODO: Refactor
|
|
35
37
|
end
|
|
36
38
|
|
|
39
|
+
# Returns a new Definition of the same document whose configuration is a child of this one's.
|
|
40
|
+
# @visibility private
|
|
41
|
+
# @yield [Configuration]
|
|
42
|
+
# @return [Definition]
|
|
43
|
+
def configure(&)
|
|
44
|
+
Definition.new(@resolved, filepath, path_prefix, config: @config.child, &)
|
|
45
|
+
end
|
|
46
|
+
|
|
37
47
|
# Gives access to the raw resolved Hash. Like `mydefinition['components'].dig('schemas', 'Stations')`
|
|
38
48
|
# @!method [](key)
|
|
39
49
|
# @return [Hash]
|
|
@@ -80,8 +90,7 @@ module OpenapiFirst
|
|
|
80
90
|
request_def = routes.lazy.flat_map(&:requests).find { |r| r.operation_id == operation_id.to_s }
|
|
81
91
|
raise ArgumentError, "Operation #{operation_id.inspect} is not defined in #{key}." unless request_def
|
|
82
92
|
|
|
83
|
-
request_def.path
|
|
84
|
-
name = Regexp.last_match(1)
|
|
93
|
+
PathTemplate.expand(request_def.path) do |name|
|
|
85
94
|
params.fetch(name.to_sym) do
|
|
86
95
|
params.fetch(name) do
|
|
87
96
|
raise ArgumentError, "Missing path parameter #{name.inspect} for operation #{operation_id.inspect}."
|
|
@@ -29,20 +29,19 @@ module OpenapiFirst
|
|
|
29
29
|
# :error_response The Class to use for error responses.
|
|
30
30
|
# This can be a Symbol-name of an registered error response (:default, :jsonapi)
|
|
31
31
|
# or it can be set to false to disable returning a response.
|
|
32
|
-
# default:
|
|
33
|
-
|
|
32
|
+
# default: the configured request_validation_error_response
|
|
33
|
+
# @yield [Configuration] Optional block to configure hooks and other options for this middleware.
|
|
34
|
+
# If spec is a registered name or a Definition, this uses a new Definition that inherits its configuration,
|
|
35
|
+
# so the registered Definition is not changed.
|
|
36
|
+
def initialize(app, spec = nil, options = {}, &)
|
|
34
37
|
@app = app
|
|
35
38
|
if spec.is_a?(Hash)
|
|
36
39
|
options = spec
|
|
37
40
|
spec = options[:spec]
|
|
38
41
|
end
|
|
39
42
|
@raise = options.fetch(:raise_error, false)
|
|
43
|
+
@definition = OpenapiFirst.load(spec || :default, &)
|
|
40
44
|
@error_response_class = error_response_option(options[:error_response])
|
|
41
|
-
|
|
42
|
-
spec ||= :default
|
|
43
|
-
spec = OpenapiFirst[spec] if spec.is_a?(Symbol)
|
|
44
|
-
|
|
45
|
-
@definition = spec.is_a?(Definition) ? spec : OpenapiFirst.load(spec)
|
|
46
45
|
end
|
|
47
46
|
|
|
48
47
|
# @attr_reader [Proc] app The upstream Rack application
|
|
@@ -70,7 +69,7 @@ module OpenapiFirst
|
|
|
70
69
|
return if value == false
|
|
71
70
|
return OpenapiFirst.find_error_response(value) if value.is_a?(Symbol)
|
|
72
71
|
|
|
73
|
-
value ||
|
|
72
|
+
value || @definition.config.request_validation_error_response
|
|
74
73
|
end
|
|
75
74
|
end
|
|
76
75
|
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,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack/utils'
|
|
4
|
+
require_relative '../path_template'
|
|
5
|
+
require_relative 'segment_pattern'
|
|
6
|
+
|
|
7
|
+
module OpenapiFirst
|
|
8
|
+
class Router
|
|
9
|
+
# @visibility private
|
|
10
|
+
class PathTrie
|
|
11
|
+
def initialize
|
|
12
|
+
@static = {}
|
|
13
|
+
@partial = {}
|
|
14
|
+
@variable = nil
|
|
15
|
+
@leaf = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def add(template, value)
|
|
19
|
+
names = PathTemplate.variable_names(template)
|
|
20
|
+
node = template.split('/', -1).reduce(self) { |parent, segment| parent.child(segment) }
|
|
21
|
+
node.leaf ||= [value, names]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def match(path)
|
|
25
|
+
values = []
|
|
26
|
+
value, names = find(path.split('/', -1), 0, values)
|
|
27
|
+
return unless value
|
|
28
|
+
|
|
29
|
+
values.map! { decode(_1) } if path.include?('%')
|
|
30
|
+
[value, names.zip(values).to_h]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def find(segments, index, values)
|
|
34
|
+
return @leaf if index == segments.size
|
|
35
|
+
|
|
36
|
+
@static[segments[index]]&.find(segments, index + 1, values) ||
|
|
37
|
+
find_partial(segments, index, values) ||
|
|
38
|
+
find_variable(segments, index, values)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
protected
|
|
42
|
+
|
|
43
|
+
attr_accessor :leaf
|
|
44
|
+
|
|
45
|
+
def child(segment)
|
|
46
|
+
return @variable ||= PathTrie.new if PathTemplate.variable?(segment)
|
|
47
|
+
return partial_child(segment) if PathTemplate.template?(segment)
|
|
48
|
+
|
|
49
|
+
@static[segment] ||= PathTrie.new
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def decode(value)
|
|
55
|
+
decoded = Rack::Utils.unescape_path(value).force_encoding(Encoding::UTF_8)
|
|
56
|
+
decoded.valid_encoding? ? decoded : value
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def find_partial(segments, index, values)
|
|
60
|
+
@partial.each_value do |pattern, node|
|
|
61
|
+
captures = pattern.match(segments[index])
|
|
62
|
+
next unless captures
|
|
63
|
+
|
|
64
|
+
values.concat(captures)
|
|
65
|
+
found = node.find(segments, index + 1, values)
|
|
66
|
+
return found if found
|
|
67
|
+
|
|
68
|
+
values.pop(captures.size)
|
|
69
|
+
end
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def find_variable(segments, index, values)
|
|
74
|
+
segment = segments[index]
|
|
75
|
+
return if @variable.nil? || segment.empty?
|
|
76
|
+
|
|
77
|
+
values << segment
|
|
78
|
+
found = @variable.find(segments, index + 1, values)
|
|
79
|
+
values.pop unless found
|
|
80
|
+
found
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def partial_child(segment)
|
|
84
|
+
@partial[segment] ||= [SegmentPattern.new(segment), PathTrie.new]
|
|
85
|
+
@partial = @partial.sort_by.with_index { |(key, _), i| [-literal_length(key), i] }.to_h
|
|
86
|
+
@partial[segment].last
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def literal_length(segment)
|
|
90
|
+
segment.gsub(PathTemplate::VARIABLE, '').length
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
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
|
data/lib/openapi_first.rb
CHANGED
|
@@ -62,15 +62,20 @@ module OpenapiFirst
|
|
|
62
62
|
configuration.plugin(name, **)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
# Load and dereference an OpenAPI spec file or return the Definition if it's already loaded
|
|
66
|
-
#
|
|
65
|
+
# Load and dereference an OpenAPI spec file or return the Definition if it's already loaded.
|
|
66
|
+
# If a block is given together with a Definition or Symbol,
|
|
67
|
+
# this returns a new Definition with the configuration block applied.
|
|
68
|
+
# @param filepath_or_definition [String, Symbol, Definition] The path to the file, a registered name or a Definition
|
|
67
69
|
# @param only [Proc, nil] An optional proc to filter paths. It is called with the path string and should return
|
|
68
70
|
# true/false
|
|
69
71
|
# @param path_prefix [String, nil] An optional path prefix, that is not documented, that all requests begin with.
|
|
72
|
+
# @yieldparam config [Configuration] Configuration of the returned Definition.
|
|
70
73
|
# @return [Definition]
|
|
71
74
|
def self.load(filepath_or_definition, only: nil, path_prefix: nil, &)
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
if filepath_or_definition.is_a?(Definition) || filepath_or_definition.is_a?(Symbol)
|
|
76
|
+
definition = self[filepath_or_definition]
|
|
77
|
+
return block_given? ? definition.configure(&) : definition
|
|
78
|
+
end
|
|
74
79
|
|
|
75
80
|
filepath = filepath_or_definition
|
|
76
81
|
raise FileNotFoundError, "File not found: #{filepath}" unless File.exist?(filepath)
|
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.4.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
|