verquest 0.6.3 → 0.7.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 +47 -0
- data/README.md +324 -51
- data/Rakefile +5 -0
- data/docs/rails.md +169 -0
- data/lib/verquest/base/condition_class_methods.rb +211 -0
- data/lib/verquest/base/private_class_methods.rb +34 -15
- data/lib/verquest/base/public_class_methods.rb +51 -1
- data/lib/verquest/base.rb +1 -0
- data/lib/verquest/conditions/branch.rb +111 -0
- data/lib/verquest/conditions/condition.rb +194 -0
- data/lib/verquest/conditions/conjunction.rb +39 -0
- data/lib/verquest/conditions/fragment_merge.rb +52 -0
- data/lib/verquest/conditions/property_merge.rb +47 -0
- data/lib/verquest/conditions/rule_set.rb +58 -0
- data/lib/verquest/conditions/rules/absent.rb +25 -0
- data/lib/verquest/conditions/rules/alternatives.rb +28 -0
- data/lib/verquest/conditions/rules/any.rb +27 -0
- data/lib/verquest/conditions/rules/equals.rb +22 -0
- data/lib/verquest/conditions/rules/names_rule.rb +23 -0
- data/lib/verquest/conditions/rules/none.rb +24 -0
- data/lib/verquest/conditions/rules/present.rb +22 -0
- data/lib/verquest/conditions/rules/property_rule.rb +48 -0
- data/lib/verquest/conditions/rules/satisfies.rb +22 -0
- data/lib/verquest/conditions/rules/value_in.rb +22 -0
- data/lib/verquest/conditions/rules.rb +17 -0
- data/lib/verquest/conditions.rb +36 -0
- data/lib/verquest/configuration.rb +19 -1
- data/lib/verquest/gem_version.rb +1 -1
- data/lib/verquest/helper_methods/conditional_properties.rb +126 -0
- data/lib/verquest/helper_methods/unnamed_one_of.rb +35 -0
- data/lib/verquest/properties/array.rb +15 -1
- data/lib/verquest/properties/base.rb +116 -10
- data/lib/verquest/properties/collection.rb +51 -18
- data/lib/verquest/properties/const.rb +5 -4
- data/lib/verquest/properties/enum.rb +1 -5
- data/lib/verquest/properties/field.rb +1 -0
- data/lib/verquest/properties/object.rb +38 -4
- data/lib/verquest/properties/one_of/discriminated_schema.rb +84 -0
- data/lib/verquest/properties/one_of.rb +86 -54
- data/lib/verquest/properties/reference.rb +49 -37
- data/lib/verquest/query/coercer.rb +165 -0
- data/lib/verquest/query/definition_check.rb +91 -0
- data/lib/verquest/query/parameters.rb +79 -0
- data/lib/verquest/query.rb +25 -0
- data/lib/verquest/transformer/collection.rb +32 -0
- data/lib/verquest/transformer/const_as_enum.rb +47 -0
- data/lib/verquest/transformer/layout.rb +83 -0
- data/lib/verquest/transformer/mapping_layout.rb +166 -0
- data/lib/verquest/transformer/one_of.rb +111 -0
- data/lib/verquest/transformer/path.rb +188 -0
- data/lib/verquest/transformer/plan.rb +183 -0
- data/lib/verquest/transformer/scope.rb +64 -0
- data/lib/verquest/transformer.rb +43 -880
- data/lib/verquest/version.rb +152 -79
- data/lib/verquest.rb +6 -0
- metadata +43 -8
data/docs/rails.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Rails and rswag
|
|
2
|
+
|
|
3
|
+
Verquest doesn't depend on Rails, so it ships no Rails integration. Its three locations map onto Rails request methods, though: `location :body` (the default) reads `request_parameters`, `location :query` reads `query_parameters`, and `location :path` reads `path_parameters`. This guide gives you two small pieces to copy into your app:
|
|
4
|
+
|
|
5
|
+
- a controller concern that processes all three from one declaration per class;
|
|
6
|
+
- an rswag shared context that documents query and path classes in your request specs.
|
|
7
|
+
|
|
8
|
+
## API version
|
|
9
|
+
|
|
10
|
+
Verquest picks the request version from `config.current_version`. With the version in the URL (`/2025-06/deals`), keep it in `Current` and read it from there:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
# config/routes.rb
|
|
14
|
+
scope "/:api_version", constraints: {api_version: /\d{4}-\d{2}/} do
|
|
15
|
+
resources :deals, only: %i[index show update] do
|
|
16
|
+
resource :win, only: :create, module: :deals
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# app/models/current.rb
|
|
21
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
22
|
+
attribute :api_version
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# config/initializers/verquest.rb
|
|
26
|
+
Verquest.configure do |config|
|
|
27
|
+
config.current_version = -> { Current.api_version }
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Controller concern
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# app/controllers/concerns/verquest_params.rb
|
|
35
|
+
module VerquestParams
|
|
36
|
+
extend ActiveSupport::Concern
|
|
37
|
+
|
|
38
|
+
included do
|
|
39
|
+
before_action :process_verquest_params
|
|
40
|
+
attr_reader :body_params, :query_params, :path_params
|
|
41
|
+
|
|
42
|
+
# Keep JSON bodies as sent, without a copy under the controller's name
|
|
43
|
+
wrap_parameters false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class_methods do
|
|
47
|
+
# `param:` is the route key a path class's `id` comes from
|
|
48
|
+
def verquest_params(request_class, param: :id, only: nil, except: nil)
|
|
49
|
+
verquest_declarations << {class: request_class, param:, only: Array(only), except: Array(except)}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def verquest_declarations
|
|
53
|
+
@verquest_declarations ||= []
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def process_verquest_params
|
|
60
|
+
declarations = self.class.verquest_declarations.select { |declaration| verquest_declaration_applies?(declaration) }
|
|
61
|
+
by_location = declarations.group_by { |declaration| declaration[:class].location }
|
|
62
|
+
|
|
63
|
+
# Every path class applies, so /deals/:deal_id/notes/:id can check both IDs
|
|
64
|
+
@path_params = by_location.fetch(:path, []).to_h { |declaration| process_path_param(declaration) }
|
|
65
|
+
# Unknown query keys (cache busters, tracking) are dropped instead of rejected
|
|
66
|
+
@query_params = by_location[:query]&.first&.dig(:class)&.process(request.query_parameters, remove_extra_root_keys: true)
|
|
67
|
+
@body_params = by_location[:body]&.first&.dig(:class)&.process(request.request_parameters)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def process_path_param(declaration)
|
|
71
|
+
param = declaration[:param]
|
|
72
|
+
[param.to_s, declaration[:class].process({"id" => request.path_parameters[param]})["id"]]
|
|
73
|
+
rescue Verquest::InvalidParamsError => e
|
|
74
|
+
# Point errors at the route key (/deal_id), not the class's /id
|
|
75
|
+
errors = e.errors.map { |error| error.merge(pointer: "/#{param}", message: error[:message].sub("`/id`", "`/#{param}`")) }
|
|
76
|
+
raise Verquest::InvalidParamsError.new(e.message, errors:)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def verquest_declaration_applies?(declaration)
|
|
80
|
+
action = action_name.to_sym
|
|
81
|
+
(declaration[:only].empty? || declaration[:only].include?(action)) && declaration[:except].exclude?(action)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The concern expects `config.validation_error_handling = :raise` (the default). Set the version and render errors once, in the base controller:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
class ApplicationController < ActionController::API
|
|
90
|
+
# Declared before the include, so the version is set when the concern's before_action runs
|
|
91
|
+
before_action { Current.api_version = params[:api_version] }
|
|
92
|
+
include VerquestParams
|
|
93
|
+
|
|
94
|
+
rescue_from Verquest::InvalidParamsError do |error|
|
|
95
|
+
render json: {errors: error.errors}, status: :unprocessable_content
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Each path class validates one ID as `id`, so one class serves both the member route (`/deals/:id`) and nested routes (`/deals/:deal_id/win`):
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
class DealPath < Verquest::Base
|
|
104
|
+
location :path
|
|
105
|
+
|
|
106
|
+
version "2025-06" do
|
|
107
|
+
field :id, type: :string, required: true, pattern: "^deal_", description: "The ID of the deal"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
class DealsController < ApplicationController
|
|
114
|
+
verquest_params DealPath, only: %i[show update]
|
|
115
|
+
verquest_params DealsIndexQuery, only: :index
|
|
116
|
+
verquest_params DealUpdateRequest, only: :update
|
|
117
|
+
|
|
118
|
+
def show
|
|
119
|
+
render json: Deal.find_by!(public_id: path_params["id"])
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
class Deals::WinsController < ApplicationController
|
|
124
|
+
verquest_params DealPath, param: :deal_id # POST /deals/:deal_id/win
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`GET /deals/con_123` then returns 422 with `{"pointer" => "/id", ...}`, and `POST /deals/con_123/win` with `{"pointer" => "/deal_id", ...}`.
|
|
129
|
+
|
|
130
|
+
## rswag
|
|
131
|
+
|
|
132
|
+
A shared context documents a query or path class with `to_parameters`. `name:` renames a path class's parameter to the route key, and `description:` replaces its description:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
# spec/support/verquest.rb
|
|
136
|
+
RSpec.shared_context "verquest parameters" do |request_class, version:, name: nil, description: nil|
|
|
137
|
+
request_class.to_parameters(version:).each do |attributes|
|
|
138
|
+
parameter(attributes.merge({name:, description:}.compact))
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
path "/2025-06/deals/{deal_id}" do
|
|
145
|
+
include_context "verquest parameters", DealPath, version: "2025-06", name: "deal_id"
|
|
146
|
+
|
|
147
|
+
patch "Update a deal" do
|
|
148
|
+
include_context "verquest parameters", ExpandQuery, version: "2025-06"
|
|
149
|
+
parameter name: :payload, in: :body, schema: {"$ref" => DealUpdateRequest.to_ref}
|
|
150
|
+
|
|
151
|
+
response "200", "deal updated" do
|
|
152
|
+
let(:deal_id) { create(:deal).public_id }
|
|
153
|
+
let(:payload) { {name: "New name"} }
|
|
154
|
+
|
|
155
|
+
run_test!
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
rswag skips optional query parameters without a matching `let`, so specs only define the ones they send.
|
|
162
|
+
|
|
163
|
+
Body classes are registered as components in `swagger_helper.rb` (see [Component Generation for OpenAPI](../README.md#component-generation-for-openapi)):
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
components: {
|
|
167
|
+
schemas: [DealUpdateRequest].to_h { |request| [request.component_name, request.to_schema(version: "2025-06")] }
|
|
168
|
+
}
|
|
169
|
+
```
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Verquest
|
|
4
|
+
# DSL class methods for conditional schemas (`if` / `then` / `else`)
|
|
5
|
+
#
|
|
6
|
+
# `condition` opens a {Verquest::Conditions::Condition} on the current scope.
|
|
7
|
+
# `given`, `then_apply` and `else_apply` make its parts the current scope, so
|
|
8
|
+
# `field`, `object` and the rest add branch properties exactly as they add
|
|
9
|
+
# regular ones, and the rule methods add rules.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# version "2025-09" do
|
|
13
|
+
# field :kind, type: :string
|
|
14
|
+
#
|
|
15
|
+
# condition :company do
|
|
16
|
+
# given { equals :kind, "company" }
|
|
17
|
+
# then_apply { field :vat_id, type: :string, required: true }
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @api private
|
|
22
|
+
module Base::ConditionClassMethods
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Declares a condition on the current scope
|
|
26
|
+
#
|
|
27
|
+
# @param name [Symbol, nil] Optional name, used to exclude the condition in a later version
|
|
28
|
+
# @param document_as [Symbol, nil] `:one_of` to document the condition as a oneOf of its branches
|
|
29
|
+
# in `to_schema`, while validation keeps `if` / `then` / `else`. Only for a condition in a version.
|
|
30
|
+
# @yield Block declaring `given`, `then_apply` and `else_apply`
|
|
31
|
+
# @return [void]
|
|
32
|
+
# @raise [ArgumentError] If the current scope can't hold conditions, or document_as is used outside a version
|
|
33
|
+
def condition(name = nil, document_as: nil, &block)
|
|
34
|
+
raise ArgumentError, "condition can only be declared in a version, object or inline collection" unless current_scope.respond_to?(:add_condition)
|
|
35
|
+
raise ArgumentError, "document_as: :one_of can only be used on a condition in a version" if document_as && !current_scope.is_a?(Version)
|
|
36
|
+
|
|
37
|
+
condition = Conditions::Condition.new(name:, document_as:)
|
|
38
|
+
current_scope.add_condition(condition)
|
|
39
|
+
within_scope(condition, &block)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Declares the `if` of the current condition
|
|
43
|
+
#
|
|
44
|
+
# @yield Block declaring rules
|
|
45
|
+
# @return [void]
|
|
46
|
+
def given(&block)
|
|
47
|
+
open_condition_part(:given, &block)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Declares the `then` of the current condition
|
|
51
|
+
#
|
|
52
|
+
# The title, description and deprecated flag document the branch's
|
|
53
|
+
# variant when the condition is declared with `document_as: :one_of`.
|
|
54
|
+
#
|
|
55
|
+
# @param title [String, nil] Title of the branch's oneOf variant
|
|
56
|
+
# @param description [String, nil] Description of the branch's oneOf variant
|
|
57
|
+
# @param deprecated [Boolean, nil] Whether the branch's oneOf variant is deprecated
|
|
58
|
+
# @yield Block declaring rules and properties
|
|
59
|
+
# @return [void]
|
|
60
|
+
def then_apply(title: nil, description: nil, deprecated: nil, &block)
|
|
61
|
+
open_condition_part(:then, title:, description:, deprecated:, &block)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Declares the `else` of the current condition
|
|
65
|
+
#
|
|
66
|
+
# The title, description and deprecated flag document the branch's
|
|
67
|
+
# variant when the condition is declared with `document_as: :one_of`.
|
|
68
|
+
#
|
|
69
|
+
# @param title [String, nil] Title of the branch's oneOf variant
|
|
70
|
+
# @param description [String, nil] Description of the branch's oneOf variant
|
|
71
|
+
# @param deprecated [Boolean, nil] Whether the branch's oneOf variant is deprecated
|
|
72
|
+
# @yield Block declaring rules and properties
|
|
73
|
+
# @return [void]
|
|
74
|
+
def else_apply(title: nil, description: nil, deprecated: nil, &block)
|
|
75
|
+
open_condition_part(:else, title:, description:, deprecated:, &block)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Declares a condition that applies the block when a property is present
|
|
79
|
+
#
|
|
80
|
+
# Shorthand for `condition(name) { given { present key }; then_apply { ... } }`.
|
|
81
|
+
#
|
|
82
|
+
# @param key [Symbol] The property whose presence triggers the block
|
|
83
|
+
# @param name [Symbol, nil] Optional condition name
|
|
84
|
+
# @yield Block declaring rules and properties, like `then_apply`
|
|
85
|
+
# @return [void]
|
|
86
|
+
def depends_on(key, name = nil, &block)
|
|
87
|
+
condition(name) do
|
|
88
|
+
given { present key }
|
|
89
|
+
then_apply(&block)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Removes inherited named conditions from the current version
|
|
94
|
+
#
|
|
95
|
+
# A named condition can be declared only once per version, so exclude an
|
|
96
|
+
# inherited one before declaring a condition with the same name.
|
|
97
|
+
#
|
|
98
|
+
# @param names [Array<Symbol>] The condition names
|
|
99
|
+
# @return [void]
|
|
100
|
+
# @raise [ArgumentError] If a condition is not defined
|
|
101
|
+
def exclude_conditions(*names)
|
|
102
|
+
names.each { |name| current_scope.remove_condition(name) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Requires the named properties to be present
|
|
106
|
+
#
|
|
107
|
+
# @param names [Array<Symbol>] The property names
|
|
108
|
+
# @return [void]
|
|
109
|
+
def present(*names)
|
|
110
|
+
add_rule(Conditions::Rules::Present.new(names))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Forbids the named properties
|
|
114
|
+
#
|
|
115
|
+
# @param names [Array<Symbol>] The property names
|
|
116
|
+
# @return [void]
|
|
117
|
+
def absent(*names)
|
|
118
|
+
add_rule(Conditions::Rules::Absent.new(names))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Requires a property to hold one value
|
|
122
|
+
#
|
|
123
|
+
# @param name [Symbol] The property name
|
|
124
|
+
# @param value [Object] The value
|
|
125
|
+
# @return [void]
|
|
126
|
+
def equals(name, value)
|
|
127
|
+
add_rule(Conditions::Rules::Equals.new(name, value))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Requires a property to hold one of several values
|
|
131
|
+
#
|
|
132
|
+
# @param name [Symbol] The property name
|
|
133
|
+
# @param values [Array] The values
|
|
134
|
+
# @return [void]
|
|
135
|
+
def value_in(name, values)
|
|
136
|
+
add_rule(Conditions::Rules::ValueIn.new(name, values))
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Requires a property to match schema options (camelized like field options)
|
|
140
|
+
#
|
|
141
|
+
# @param name [Symbol] The property name
|
|
142
|
+
# @param schema_options [Hash] The schema options
|
|
143
|
+
# @return [void]
|
|
144
|
+
def satisfies(name, **schema_options)
|
|
145
|
+
add_rule(Conditions::Rules::Satisfies.new(name, camelize(schema_options)))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Requires at least one of the rules in the block to hold
|
|
149
|
+
#
|
|
150
|
+
# @yield Block declaring the alternative rules
|
|
151
|
+
# @return [void]
|
|
152
|
+
def any(&block)
|
|
153
|
+
add_rule_set(Conditions::Rules::Any.new, &block)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Requires none of the rules in the block to hold
|
|
157
|
+
#
|
|
158
|
+
# @yield Block declaring the rules
|
|
159
|
+
# @return [void]
|
|
160
|
+
def none(&block)
|
|
161
|
+
add_rule_set(Conditions::Rules::None.new, &block)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Makes a part of the current condition the scope for the block
|
|
165
|
+
#
|
|
166
|
+
# @param part [Symbol] :given, :then or :else
|
|
167
|
+
# @yield Block declaring the part
|
|
168
|
+
# @return [void]
|
|
169
|
+
# @raise [ArgumentError] If the current scope is not a condition
|
|
170
|
+
def open_condition_part(part, **options, &block)
|
|
171
|
+
raise ArgumentError, "given, then_apply and else_apply can only be used inside a condition" unless current_scope.is_a?(Conditions::Condition)
|
|
172
|
+
|
|
173
|
+
within_scope(current_scope.open(part, **options), &block)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Adds a rule to the current scope
|
|
177
|
+
#
|
|
178
|
+
# @param rule [#to_schema, #property_names] The rule
|
|
179
|
+
# @return [void]
|
|
180
|
+
# @raise [ArgumentError] If the current scope can't hold rules
|
|
181
|
+
def add_rule(rule)
|
|
182
|
+
raise ArgumentError, "Rules can only be used inside given, then_apply, else_apply, depends_on, any or none" unless current_scope.respond_to?(:add_rule)
|
|
183
|
+
|
|
184
|
+
current_scope.add_rule(rule)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Adds a rule that holds other rules and makes it the scope for the block
|
|
188
|
+
#
|
|
189
|
+
# @param rule_set [Verquest::Conditions::Rules::Any] The rule
|
|
190
|
+
# @yield Block declaring the nested rules
|
|
191
|
+
# @return [void]
|
|
192
|
+
def add_rule_set(rule_set, &block)
|
|
193
|
+
add_rule(rule_set)
|
|
194
|
+
within_scope(rule_set, &block)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Runs the block with another current scope
|
|
198
|
+
#
|
|
199
|
+
# @param scope [Object] The scope for the block
|
|
200
|
+
# @yield Block evaluated in the class context
|
|
201
|
+
# @return [void]
|
|
202
|
+
def within_scope(scope, &block)
|
|
203
|
+
previous_scope = current_scope
|
|
204
|
+
@current_scope = scope
|
|
205
|
+
|
|
206
|
+
instance_exec(&block)
|
|
207
|
+
ensure
|
|
208
|
+
@current_scope = previous_scope
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -48,7 +48,7 @@ module Verquest
|
|
|
48
48
|
# @yield Block defining the version's structure and properties
|
|
49
49
|
# @return [void]
|
|
50
50
|
def version(name, inherit: true, exclude_properties: [], &block)
|
|
51
|
-
version = Version.new(name:)
|
|
51
|
+
version = Version.new(name:, location:)
|
|
52
52
|
versions.add(version)
|
|
53
53
|
|
|
54
54
|
if inherit && @current_scope
|
|
@@ -125,8 +125,9 @@ module Verquest
|
|
|
125
125
|
# @param required [Boolean, Array<Symbol>] Whether the field is required
|
|
126
126
|
# @param nullable [Boolean] Whether the field can be null
|
|
127
127
|
# @param schema_options [Hash] Additional schema options for the field
|
|
128
|
+
# @param parameter_examples [Hash, nil] Named OpenAPI Example Objects for the query parameter (query requests only)
|
|
128
129
|
# @return [void]
|
|
129
|
-
def field(name, type: nil, map: nil, required: nil, nullable: nil, **schema_options)
|
|
130
|
+
def field(name, type: nil, map: nil, required: nil, nullable: nil, parameter_examples: nil, **schema_options)
|
|
130
131
|
camelize(schema_options)
|
|
131
132
|
|
|
132
133
|
type = default_options.fetch(:type, type)
|
|
@@ -135,7 +136,7 @@ module Verquest
|
|
|
135
136
|
schema_options = default_options.except(:type, :required, :nullable).merge(schema_options)
|
|
136
137
|
|
|
137
138
|
field = Properties::Field.new(name:, type:, map:, required:, nullable:, **schema_options)
|
|
138
|
-
|
|
139
|
+
add_property(field, parameter_examples)
|
|
139
140
|
end
|
|
140
141
|
|
|
141
142
|
# Defines a new enum property for the current version scope
|
|
@@ -146,8 +147,9 @@ module Verquest
|
|
|
146
147
|
# @param required [Boolean, Array<Symbol>] Whether the enum is required
|
|
147
148
|
# @param nullable [Boolean] Whether the enum can be null
|
|
148
149
|
# @param schema_options [Hash] Additional schema options for the enum
|
|
150
|
+
# @param parameter_examples [Hash, nil] Named OpenAPI Example Objects for the query parameter (query requests only)
|
|
149
151
|
# @return [void]
|
|
150
|
-
def enum(name, values:, map: nil, required: nil, nullable: nil, **schema_options)
|
|
152
|
+
def enum(name, values:, map: nil, required: nil, nullable: nil, parameter_examples: nil, **schema_options)
|
|
151
153
|
camelize(schema_options)
|
|
152
154
|
|
|
153
155
|
required = default_options.fetch(:required, false) if required.nil?
|
|
@@ -155,7 +157,7 @@ module Verquest
|
|
|
155
157
|
schema_options = default_options.except(:required, :nullable).merge(schema_options)
|
|
156
158
|
|
|
157
159
|
enum_property = Properties::Enum.new(name:, values:, map:, required:, nullable:, **schema_options)
|
|
158
|
-
|
|
160
|
+
add_property(enum_property, parameter_examples)
|
|
159
161
|
end
|
|
160
162
|
|
|
161
163
|
# Defines a new constant property for the current version scope
|
|
@@ -164,14 +166,17 @@ module Verquest
|
|
|
164
166
|
# @param value [Object] The value of the constant
|
|
165
167
|
# @param map [String, nil] An optional mapping to another constant
|
|
166
168
|
# @param required [Boolean, Array<Symbol>] Whether the constant is required
|
|
169
|
+
# @param nullable [Boolean] Whether the constant can be null
|
|
167
170
|
# @param schema_options [Hash] Additional schema options for the constant
|
|
171
|
+
# @param parameter_examples [Hash, nil] Named OpenAPI Example Objects for the query parameter (query requests only)
|
|
168
172
|
# @return [void]
|
|
169
|
-
def const(name, value:, map: nil, required: nil, **schema_options)
|
|
173
|
+
def const(name, value:, map: nil, required: nil, nullable: nil, parameter_examples: nil, **schema_options)
|
|
170
174
|
camelize(schema_options)
|
|
171
175
|
required = default_options.fetch(:required, false) if required.nil?
|
|
176
|
+
nullable = default_options.fetch(:nullable, false) if nullable.nil?
|
|
172
177
|
|
|
173
|
-
const = Properties::Const.new(name:, value:, map:, required:, **schema_options)
|
|
174
|
-
|
|
178
|
+
const = Properties::Const.new(name:, value:, map:, required:, nullable:, **schema_options)
|
|
179
|
+
add_property(const, parameter_examples)
|
|
175
180
|
end
|
|
176
181
|
|
|
177
182
|
# Defines a new object for the current version scope
|
|
@@ -181,9 +186,10 @@ module Verquest
|
|
|
181
186
|
# @param required [Boolean, Array<Symbol>] Whether the object is required
|
|
182
187
|
# @param nullable [Boolean] Whether the object can be null
|
|
183
188
|
# @param schema_options [Hash] Additional schema options for the object
|
|
189
|
+
# @param parameter_examples [Hash, nil] Named OpenAPI Example Objects for the query parameter (query requests only)
|
|
184
190
|
# @yield Block executed in the context of the new object definition
|
|
185
191
|
# @return [void]
|
|
186
|
-
def object(name, map: nil, required: nil, nullable: nil, **schema_options, &block)
|
|
192
|
+
def object(name, map: nil, required: nil, nullable: nil, parameter_examples: nil, **schema_options, &block)
|
|
187
193
|
unless block_given?
|
|
188
194
|
raise ArgumentError, "a block must be given to define the object"
|
|
189
195
|
end
|
|
@@ -194,7 +200,7 @@ module Verquest
|
|
|
194
200
|
schema_options = default_options.except(:type, :required, :nullable).merge(schema_options)
|
|
195
201
|
|
|
196
202
|
object = Properties::Object.new(name:, map:, required:, nullable:, **schema_options)
|
|
197
|
-
|
|
203
|
+
add_property(object, parameter_examples)
|
|
198
204
|
|
|
199
205
|
if block_given?
|
|
200
206
|
previous_scope = current_scope
|
|
@@ -249,13 +255,14 @@ module Verquest
|
|
|
249
255
|
# @param map [String, nil] An optional mapping to another reference
|
|
250
256
|
# @param required [Boolean, Array<Symbol>] Whether the reference is required
|
|
251
257
|
# @param nullable [Boolean] Whether this reference can be null
|
|
258
|
+
# @param parameter_examples [Hash, nil] Named OpenAPI Example Objects for the query parameter (query requests only)
|
|
252
259
|
# @return [void]
|
|
253
|
-
def reference(name, from:, property: nil, map: nil, required: nil, nullable: nil)
|
|
260
|
+
def reference(name, from:, property: nil, map: nil, required: nil, nullable: nil, parameter_examples: nil)
|
|
254
261
|
required = default_options.fetch(:required, false) if required.nil?
|
|
255
262
|
nullable = default_options.fetch(:nullable, false) if nullable.nil?
|
|
256
263
|
|
|
257
264
|
reference = Properties::Reference.new(name:, from:, property:, map:, required:, nullable:)
|
|
258
|
-
|
|
265
|
+
add_property(reference, parameter_examples)
|
|
259
266
|
end
|
|
260
267
|
|
|
261
268
|
# Defines a new array property for the current version scope
|
|
@@ -265,10 +272,12 @@ module Verquest
|
|
|
265
272
|
# @param required [Boolean, Array<Symbol>] Whether the array property is required
|
|
266
273
|
# @param nullable [Boolean] Whether this array can be null
|
|
267
274
|
# @param map [String, nil] An optional mapping to another array property
|
|
275
|
+
# @param explode [Boolean, nil] In query requests, whether items are sent as repeated keys (true) or comma-separated (false); nil uses the configuration
|
|
268
276
|
# @param item_schema_options [Hash] Additional JSON schema options for the array items (merged with custom type options)
|
|
269
277
|
# @param schema_options [Hash] Additional schema options for the array property
|
|
278
|
+
# @param parameter_examples [Hash, nil] Named OpenAPI Example Objects for the query parameter (query requests only)
|
|
270
279
|
# @return [void]
|
|
271
|
-
def array(name, type:, required: nil, nullable: nil, map: nil, item_schema_options: {}, **schema_options)
|
|
280
|
+
def array(name, type:, required: nil, nullable: nil, map: nil, explode: nil, parameter_examples: nil, item_schema_options: {}, **schema_options)
|
|
272
281
|
camelize(schema_options)
|
|
273
282
|
|
|
274
283
|
type = default_options.fetch(:type, type)
|
|
@@ -276,8 +285,8 @@ module Verquest
|
|
|
276
285
|
nullable = default_options.fetch(:nullable, false) if nullable.nil?
|
|
277
286
|
schema_options = default_options.except(:type, :required, :nullable).merge(schema_options)
|
|
278
287
|
|
|
279
|
-
array = Properties::Array.new(name:, type:, required:, nullable:, map:, item_schema_options:, **schema_options)
|
|
280
|
-
|
|
288
|
+
array = Properties::Array.new(name:, type:, required:, nullable:, map:, explode:, item_schema_options:, **schema_options)
|
|
289
|
+
add_property(array, parameter_examples)
|
|
281
290
|
end
|
|
282
291
|
|
|
283
292
|
# Defines a oneOf property for polymorphic schemas
|
|
@@ -310,6 +319,16 @@ module Verquest
|
|
|
310
319
|
@current_scope = previous_scope if block_given?
|
|
311
320
|
end
|
|
312
321
|
|
|
322
|
+
# Adds a property to the current scope
|
|
323
|
+
#
|
|
324
|
+
# @param property [Verquest::Properties::Base] The property to add
|
|
325
|
+
# @param parameter_examples [Hash, nil] Named OpenAPI Example Objects for the query parameter
|
|
326
|
+
# @return [void]
|
|
327
|
+
def add_property(property, parameter_examples)
|
|
328
|
+
property.parameter_examples = parameter_examples
|
|
329
|
+
current_scope.add(property)
|
|
330
|
+
end
|
|
331
|
+
|
|
313
332
|
# Excludes specified properties from the current scope by removing them
|
|
314
333
|
# from the version's property set
|
|
315
334
|
#
|
|
@@ -6,6 +6,9 @@ module Verquest
|
|
|
6
6
|
# This module contains class methods that handle parameter mapping, schema generation,
|
|
7
7
|
# and validation functionality for Verquest API request objects.
|
|
8
8
|
module Base::PublicClassMethods
|
|
9
|
+
# Where request params can come from
|
|
10
|
+
LOCATIONS = %i[body query path].freeze
|
|
11
|
+
|
|
9
12
|
# Maps incoming parameters to the appropriate structure based on version mapping
|
|
10
13
|
#
|
|
11
14
|
# @param params [Hash] The parameters to be mapped
|
|
@@ -22,7 +25,8 @@ module Verquest
|
|
|
22
25
|
|
|
23
26
|
params = params.dup
|
|
24
27
|
params = params.to_unsafe_h if params.respond_to?(:to_unsafe_h)
|
|
25
|
-
params = params.slice(*version_class.
|
|
28
|
+
params = params.slice(*version_class.all_properties.keys) if remove_extra_root_keys && !version_class.combination?
|
|
29
|
+
params = version_class.coerce_params(params)
|
|
26
30
|
|
|
27
31
|
if validate && (validation_result = version_class.validate_params(params: params)) && validation_result.any?
|
|
28
32
|
case Verquest.configuration.validation_error_handling
|
|
@@ -52,6 +56,52 @@ module Verquest
|
|
|
52
56
|
end
|
|
53
57
|
end
|
|
54
58
|
|
|
59
|
+
# Sets or returns where the request's params come from
|
|
60
|
+
#
|
|
61
|
+
# A query request (`location :query`) or path request (`location :path`)
|
|
62
|
+
# converts strings to the types its schema expects before validating them,
|
|
63
|
+
# and documents its properties with {#to_parameters}. Subclasses inherit
|
|
64
|
+
# the location.
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# class ListQuery < Verquest::Base
|
|
68
|
+
# location :query
|
|
69
|
+
#
|
|
70
|
+
# version "2025-06" do
|
|
71
|
+
# field :page, type: :integer
|
|
72
|
+
# end
|
|
73
|
+
# end
|
|
74
|
+
#
|
|
75
|
+
# @param value [Symbol, nil] `:body`, `:query` or `:path` to set the location, nil to return it
|
|
76
|
+
# @return [Symbol] The location, `:body` unless set on this class or a superclass
|
|
77
|
+
# @raise [ArgumentError] If the value is not a known location, or a version is already defined
|
|
78
|
+
def location(value = nil)
|
|
79
|
+
return @location || (superclass.respond_to?(:location) ? superclass.location : :body) if value.nil?
|
|
80
|
+
|
|
81
|
+
raise ArgumentError, "Location must be one of #{LOCATIONS.join(", ")}" unless LOCATIONS.include?(value)
|
|
82
|
+
raise ArgumentError, "Location must be set before the first version" if current_scope
|
|
83
|
+
|
|
84
|
+
@location = value
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns OpenAPI parameter objects for a query or path request
|
|
88
|
+
#
|
|
89
|
+
# One parameter per property, with symbol keys, ready for rswag's `parameter`.
|
|
90
|
+
# Schemas are inlined, arrays get `style: :form` and their explode value, and
|
|
91
|
+
# objects get `style: :deepObject`.
|
|
92
|
+
#
|
|
93
|
+
# @example
|
|
94
|
+
# ListQuery.to_parameters(version: "2025-06").each { |p| parameter(p) }
|
|
95
|
+
#
|
|
96
|
+
# @param version [String, nil] Specific version to use, defaults to configuration setting
|
|
97
|
+
# @return [Array<Hash>] The OpenAPI parameter objects
|
|
98
|
+
# @raise [ArgumentError] If the request is a body request
|
|
99
|
+
def to_parameters(version: nil)
|
|
100
|
+
raise ArgumentError, "#{name} has no parameters (location #{location.inspect})" if location == :body
|
|
101
|
+
|
|
102
|
+
resolve(version).parameters
|
|
103
|
+
end
|
|
104
|
+
|
|
55
105
|
# Returns the JSON schema for the request
|
|
56
106
|
#
|
|
57
107
|
# @param version [String, nil] Specific version to use, defaults to configuration setting
|