Module: OpenapiRuby::Adapters::ContextResolution
- Defined in:
- lib/openapi_ruby/adapters/context_resolution.rb
Overview
Shared by the Minitest and RSpec Style 2 adapters.
Style 2 separates the api_path declaration from the request that
exercises it, so the request has to be matched back to a declaration.
Everything used to do that here is a hard fact about the request, never a
guess: the verb, which path params the template needs against which the
caller supplied, whether the candidate declares the status the assertion
demands, and whether the remaining params are declared on the operation.
That leaves one case it cannot decide. /timers/{id} and
/timers/{id}/start under the same verb, the same status and the same
{id} are indistinguishable from the call site — the information simply
isn't there. Picking one silently sends the request to the wrong endpoint
and validates it against the wrong response schema, so a test passes while
exercising something else. Raise instead, and name the candidates; the
author resolves it by passing api_path: or by scoping the declarations.
Class Method Summary collapse
- .accounted_keys(context, method) ⇒ Object
- .ambiguity_message(matches, method, owner) ⇒ Object
- .declares_status?(context, method, expected_status) ⇒ Boolean
-
.fewest_unaccounted(candidates, method, supplied) ⇒ Object
Prefer the candidate that can explain the most supplied keys as either a path param of its own template or a parameter declared on it.
- .find_declared(contexts, api_path) ⇒ Object
- .keys_of(params) ⇒ Object
- .narrow(candidates) ⇒ Object
-
.path_params_fit?(context, required, supplied) ⇒ Boolean
A template only fits if it needs no path param the caller did not supply, and uses every param the caller explicitly declared as one.
- .resolve(contexts, method, path_params, owner:, params: {}, expected_status: nil, api_path: nil) ⇒ Object
- .template_params(context) ⇒ Object
- .unknown_path_message(contexts, api_path, owner) ⇒ Object
Class Method Details
.accounted_keys(context, method) ⇒ Object
69 70 71 72 73 |
# File 'lib/openapi_ruby/adapters/context_resolution.rb', line 69 def accounted_keys(context, method) declared = context.path_parameters + (context.operations[method.to_s]&.parameters || []) template_params(context) | declared.filter_map { |param| param["name"]&.to_s } end |
.ambiguity_message(matches, method, owner) ⇒ Object
95 96 97 98 99 100 101 102 |
# File 'lib/openapi_ruby/adapters/context_resolution.rb', line 95 def (matches, method, owner) paths = matches.map { |ctx| ctx.path_template.inspect }.join(", ") "#{method.to_s.upcase} matches more than one api_path in #{owner}: #{paths}. " \ "Requests are matched on the verb, the path params supplied and the declared " \ "response status, none of which tell these apart. Pass api_path: to pick one, " \ "or declare each api_path in its own class or nested describe block." end |
.declares_status?(context, method, expected_status) ⇒ Boolean
56 57 58 |
# File 'lib/openapi_ruby/adapters/context_resolution.rb', line 56 def declares_status?(context, method, expected_status) context.operations[method.to_s].responses.key?(expected_status.to_s) end |
.fewest_unaccounted(candidates, method, supplied) ⇒ Object
Prefer the candidate that can explain the most supplied keys as either a path param of its own template or a parameter declared on it. A key that fits nowhere means the request was probably meant for a sibling path.
63 64 65 66 67 |
# File 'lib/openapi_ruby/adapters/context_resolution.rb', line 63 def fewest_unaccounted(candidates, method, supplied) ranked = candidates.group_by { |ctx| (supplied - accounted_keys(ctx, method)).size } ranked[ranked.keys.min] end |
.find_declared(contexts, api_path) ⇒ Object
79 80 81 82 83 |
# File 'lib/openapi_ruby/adapters/context_resolution.rb', line 79 def find_declared(contexts, api_path) template = api_path.respond_to?(:path_template) ? api_path.path_template : api_path.to_s contexts.find { |ctx| ctx.path_template == template } end |
.keys_of(params) ⇒ Object
91 92 93 |
# File 'lib/openapi_ruby/adapters/context_resolution.rb', line 91 def keys_of(params) params.keys.map(&:to_s) end |
.narrow(candidates) ⇒ Object
85 86 87 88 89 |
# File 'lib/openapi_ruby/adapters/context_resolution.rb', line 85 def narrow(candidates) narrowed = candidates.select { |ctx| yield(ctx) } narrowed.empty? ? candidates : narrowed end |
.path_params_fit?(context, required, supplied) ⇒ Boolean
A template only fits if it needs no path param the caller did not supply, and uses every param the caller explicitly declared as one.
50 51 52 53 54 |
# File 'lib/openapi_ruby/adapters/context_resolution.rb', line 50 def path_params_fit?(context, required, supplied) template = template_params(context) (required - template).empty? && (template - supplied).empty? end |
.resolve(contexts, method, path_params, owner:, params: {}, expected_status: nil, api_path: nil) ⇒ Object
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/openapi_ruby/adapters/context_resolution.rb', line 24 def resolve(contexts, method, path_params, owner:, params: {}, expected_status: nil, api_path: nil) if api_path selected = find_declared(contexts, api_path) raise OpenapiRuby::Error, (contexts, api_path, owner) unless selected return selected end candidates = contexts.select { |ctx| ctx.operations.key?(method.to_s) } return candidates.first if candidates.size <= 1 supplied = keys_of(params) | keys_of(path_params) required = keys_of(path_params) candidates = narrow(candidates) { |ctx| path_params_fit?(ctx, required, supplied) } if expected_status candidates = narrow(candidates) { |ctx| declares_status?(ctx, method, expected_status) } end candidates = fewest_unaccounted(candidates, method, supplied) return candidates.first if candidates.size == 1 raise OpenapiRuby::AmbiguousApiPath, (candidates, method, owner) end |
.template_params(context) ⇒ Object
75 76 77 |
# File 'lib/openapi_ruby/adapters/context_resolution.rb', line 75 def template_params(context) context.path_template.scan(/\{(\w+)\}/).flatten end |
.unknown_path_message(contexts, api_path, owner) ⇒ Object
104 105 106 107 108 109 110 |
# File 'lib/openapi_ruby/adapters/context_resolution.rb', line 104 def (contexts, api_path, owner) template = api_path.respond_to?(:path_template) ? api_path.path_template : api_path.to_s declared = contexts.map { |ctx| ctx.path_template.inspect }.join(", ") "No api_path #{template.inspect} declared in #{owner}. " \ "Declared: #{declared.empty? ? "none" : declared}." end |