versponse 0.1.0 → 0.2.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/.rubocop.yml +15 -6
- data/CHANGELOG.md +59 -1
- data/CODE_OF_CONDUCT.md +1 -1
- data/LICENSE.txt +1 -1
- data/README.md +674 -20
- data/Rakefile +26 -3
- data/lib/versponse/base/class_methods.rb +180 -0
- data/lib/versponse/base/dsl.rb +231 -0
- data/lib/versponse/base.rb +35 -0
- data/lib/versponse/builder.rb +423 -0
- data/lib/versponse/cache.rb +47 -0
- data/lib/versponse/callable.rb +74 -0
- data/lib/versponse/class_policies.rb +15 -0
- data/lib/versponse/collapse_if_plan.rb +26 -0
- data/lib/versponse/collections.rb +49 -0
- data/lib/versponse/components.rb +37 -0
- data/lib/versponse/condition.rb +33 -0
- data/lib/versponse/configuration.rb +261 -0
- data/lib/versponse/const_as_enum.rb +39 -0
- data/lib/versponse/context_override/call.rb +27 -0
- data/lib/versponse/context_override/merge.rb +23 -0
- data/lib/versponse/context_override.rb +25 -0
- data/lib/versponse/context_plan.rb +24 -0
- data/lib/versponse/deep_copy.rb +27 -0
- data/lib/versponse/deep_freeze.rb +20 -0
- data/lib/versponse/discriminated_plan.rb +26 -0
- data/lib/versponse/error_policy/recovery.rb +68 -0
- data/lib/versponse/error_policy.rb +97 -0
- data/lib/versponse/expansion.rb +46 -0
- data/lib/versponse/extractors/block.rb +24 -0
- data/lib/versponse/extractors/fallback.rb +27 -0
- data/lib/versponse/extractors/itself.rb +21 -0
- data/lib/versponse/extractors/key.rb +28 -0
- data/lib/versponse/extractors/path.rb +28 -0
- data/lib/versponse/extractors.rb +23 -0
- data/lib/versponse/field_order.rb +23 -0
- data/lib/versponse/field_type.rb +88 -0
- data/lib/versponse/formatters.rb +38 -0
- data/lib/versponse/gem_version.rb +6 -0
- data/lib/versponse/inline_validation/builder.rb +129 -0
- data/lib/versponse/inline_validation/state.rb +16 -0
- data/lib/versponse/inline_validation.rb +86 -0
- data/lib/versponse/internals.rb +553 -0
- data/lib/versponse/json_value.rb +22 -0
- data/lib/versponse/lazy_plan.rb +30 -0
- data/lib/versponse/naming.rb +68 -0
- data/lib/versponse/object_schema.rb +25 -0
- data/lib/versponse/open_api/builder.rb +159 -0
- data/lib/versponse/open_api/component_set.rb +33 -0
- data/lib/versponse/open_api/declarations.rb +90 -0
- data/lib/versponse/open_api/document.rb +187 -0
- data/lib/versponse/open_api/ref_check.rb +89 -0
- data/lib/versponse/open_api/rswag_specs.rb +98 -0
- data/lib/versponse/open_api/sources.rb +72 -0
- data/lib/versponse/open_api/values.rb +29 -0
- data/lib/versponse/open_api.rb +12 -0
- data/lib/versponse/output_validator.rb +82 -0
- data/lib/versponse/pattern.rb +38 -0
- data/lib/versponse/plan.rb +104 -0
- data/lib/versponse/properties/array.rb +56 -0
- data/lib/versponse/properties/base.rb +325 -0
- data/lib/versponse/properties/collection.rb +135 -0
- data/lib/versponse/properties/const.rb +46 -0
- data/lib/versponse/properties/container.rb +51 -0
- data/lib/versponse/properties/enum.rb +93 -0
- data/lib/versponse/properties/field.rb +43 -0
- data/lib/versponse/properties/item_one_of.rb +19 -0
- data/lib/versponse/properties/object.rb +32 -0
- data/lib/versponse/properties/one_of.rb +176 -0
- data/lib/versponse/properties/reference.rb +133 -0
- data/lib/versponse/references.rb +162 -0
- data/lib/versponse/registry.rb +124 -0
- data/lib/versponse/schema_context.rb +69 -0
- data/lib/versponse/schema_merge.rb +24 -0
- data/lib/versponse/selection/scope.rb +36 -0
- data/lib/versponse/selection.rb +92 -0
- data/lib/versponse/selection_schema.rb +101 -0
- data/lib/versponse/steps/checked.rb +43 -0
- data/lib/versponse/steps/checked_read.rb +46 -0
- data/lib/versponse/steps/checked_read_formatted.rb +32 -0
- data/lib/versponse/steps/checked_value.rb +48 -0
- data/lib/versponse/steps/const.rb +44 -0
- data/lib/versponse/steps/nested.rb +98 -0
- data/lib/versponse/steps/one_of.rb +62 -0
- data/lib/versponse/steps/read.rb +48 -0
- data/lib/versponse/steps/read_formatted.rb +28 -0
- data/lib/versponse/steps/value.rb +52 -0
- data/lib/versponse/supported_versions.rb +61 -0
- data/lib/versponse/validator/all.rb +32 -0
- data/lib/versponse/validator/array_check.rb +48 -0
- data/lib/versponse/validator/boolean_check.rb +16 -0
- data/lib/versponse/validator/both.rb +24 -0
- data/lib/versponse/validator/class_check.rb +25 -0
- data/lib/versponse/validator/closed_object_check.rb +78 -0
- data/lib/versponse/validator/combinator.rb +40 -0
- data/lib/versponse/validator/compiler.rb +146 -0
- data/lib/versponse/validator/constant.rb +33 -0
- data/lib/versponse/validator/date_time_check.rb +21 -0
- data/lib/versponse/validator/format_check.rb +49 -0
- data/lib/versponse/validator/integer_check.rb +16 -0
- data/lib/versponse/validator/nullable_check.rb +22 -0
- data/lib/versponse/validator/number_check.rb +58 -0
- data/lib/versponse/validator/object_check.rb +90 -0
- data/lib/versponse/validator/ref.rb +38 -0
- data/lib/versponse/validator/root.rb +31 -0
- data/lib/versponse/validator/string_check.rb +48 -0
- data/lib/versponse/validator/string_keys.rb +21 -0
- data/lib/versponse/validator/type_check.rb +84 -0
- data/lib/versponse/validator/value_check.rb +36 -0
- data/lib/versponse/validator.rb +10 -0
- data/lib/versponse/variant_dispatcher.rb +35 -0
- data/lib/versponse/version.rb +179 -1
- data/lib/versponse/version_resolver.rb +27 -0
- data/lib/versponse/versions.rb +116 -0
- data/lib/versponse/view.rb +126 -0
- data/lib/versponse/wrapper.rb +53 -0
- data/lib/versponse.rb +165 -3
- metadata +151 -12
- data/sig/versponse.rbs +0 -4
data/Rakefile
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
|
-
require "
|
|
4
|
+
require "rake/testtask"
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << "test"
|
|
8
|
+
t.libs << "lib"
|
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
10
|
+
t.warning = true
|
|
11
|
+
end
|
|
7
12
|
|
|
8
13
|
require "rubocop/rake_task"
|
|
9
14
|
|
|
10
15
|
RuboCop::RakeTask.new
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
desc "Check YARD documentation coverage (must be 100%)"
|
|
18
|
+
task :yard do
|
|
19
|
+
require "yard"
|
|
20
|
+
|
|
21
|
+
stats = YARD::CLI::Stats.new
|
|
22
|
+
stats.run("--list-undoc")
|
|
23
|
+
undocumented = stats.instance_variable_get(:@undoc_list) || []
|
|
24
|
+
|
|
25
|
+
abort "\nYARD documentation check failed: #{undocumented.size} undocumented objects found." unless undocumented.empty?
|
|
26
|
+
|
|
27
|
+
puts "\nYARD documentation: 100% documented"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
desc "Benchmark versponse against alba and blueprinter"
|
|
31
|
+
task :bench do
|
|
32
|
+
ruby "-Ilib", "bench/serializers.rb"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
task default: %i[test rubocop yard]
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Versponse
|
|
4
|
+
# Public class methods of {Versponse::Base}
|
|
5
|
+
module Base::ClassMethods
|
|
6
|
+
# Context used when serialize gets none (avoids two allocations per call)
|
|
7
|
+
EMPTY_CONTEXT = {}.freeze
|
|
8
|
+
|
|
9
|
+
# Versions, resolution and caches of the class
|
|
10
|
+
#
|
|
11
|
+
# @return [Versponse::Internals] per-class internals
|
|
12
|
+
# @api private
|
|
13
|
+
def versponse
|
|
14
|
+
@versponse ||= Internals.new(self)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Renders an object, or each item of an Array-like collection, to a Hash with String keys
|
|
18
|
+
#
|
|
19
|
+
# @param object [Object, Hash, #to_ary, Set] source
|
|
20
|
+
# @param version [String, nil] requested version, config.current_version when nil
|
|
21
|
+
# @param view [Symbol] view name
|
|
22
|
+
# @param context [Hash, nil] passed to blocks and conditions; nil for none
|
|
23
|
+
# @param root [String, Symbol, nil] wraps the result under this key
|
|
24
|
+
# @param meta [Hash, nil] added under "meta" with String keys (requires root:)
|
|
25
|
+
# @return [Hash, Array<Hash>] rendered output
|
|
26
|
+
# @param expand [Array<String, Symbol>, nil] dotted paths of expandable associations to expand, or "*"
|
|
27
|
+
# @param fields [Array<String, Symbol>, nil] dotted paths of the fields to render (identifiers and consts always
|
|
28
|
+
# render), "*" or nil for all
|
|
29
|
+
# @raise [ArgumentError] without a version or with meta: but no root:
|
|
30
|
+
# @raise [Versponse::InvalidExpandError] for unknown expand paths (config.unknown_expand = :raise)
|
|
31
|
+
# @raise [Versponse::InvalidFieldsError] for unknown paths or paths below unexpanded associations
|
|
32
|
+
# (config.unknown_fields = :raise)
|
|
33
|
+
# @raise [Versponse::VersionNotFoundError] when no declared version matches, or supported_versions leaves it out
|
|
34
|
+
# @raise [Versponse::ViewNotFoundError] for an unknown or nil view
|
|
35
|
+
# @raise [Versponse::ExtractionError] when reading a value fails
|
|
36
|
+
def serialize(object, version: nil, view: :default, context: EMPTY_CONTEXT, root: nil, meta: nil, expand: nil, fields: nil)
|
|
37
|
+
raise ArgumentError, "meta: requires root:" if meta && root.nil?
|
|
38
|
+
raise ViewNotFoundError, "view: can't be nil; the default view is :default" if view.nil?
|
|
39
|
+
|
|
40
|
+
context ||= EMPTY_CONTEXT
|
|
41
|
+
internals = versponse
|
|
42
|
+
internals.requesting(version) do |canonical|
|
|
43
|
+
expansion = internals.expansion(expand, version: canonical, view:)
|
|
44
|
+
selection = internals.selection(fields, version: canonical, view:, expansion:) unless fields.nil?
|
|
45
|
+
context = context.frozen? ? context : context.dup.freeze
|
|
46
|
+
validate = Versponse.configuration.validate_output && OutputValidator.enabled?(self)
|
|
47
|
+
if selection && !selection.all?
|
|
48
|
+
# Validating plans prove the full shape, so a selection renders with the plain plan
|
|
49
|
+
data = render(object, internals.plan(version: canonical, view:), context, Selection::Scope.new(expansion, selection))
|
|
50
|
+
OutputValidator.call(self, data, version: canonical, view:, selection:) if validate
|
|
51
|
+
# A validating plan checks while rendering; the output goes through OutputValidator only when a check failed
|
|
52
|
+
elsif validate && (inline = internals.validating_plan(version: canonical, view:))
|
|
53
|
+
data = nil
|
|
54
|
+
failed = InlineValidation.run { data = render(object, inline, context, expansion) }
|
|
55
|
+
OutputValidator.call(self, data, version: canonical, view:) if failed
|
|
56
|
+
else
|
|
57
|
+
# The plain plan is looked up only here and for a selection, so a validated render skips it; render is
|
|
58
|
+
# written out: the extra call costs about 2% of a small render in the interpreter
|
|
59
|
+
plan = internals.plan(version: canonical, view:)
|
|
60
|
+
data = Collections.collection?(object) ? Collections.map(Collections.items(object)) { |item| plan.call(item, context, expansion) } : plan.call(object, context, expansion)
|
|
61
|
+
OutputValidator.call(self, data, version: canonical, view:) if validate
|
|
62
|
+
end
|
|
63
|
+
Wrapper.wrap(data, root:, meta:)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @param object [Object] source
|
|
68
|
+
# @param options [Hash] see {#serialize}
|
|
69
|
+
# @return [String] JSON produced by config.json_generator
|
|
70
|
+
def serialize_json(object, **options)
|
|
71
|
+
Versponse.configuration.json_generator.call(serialize(object, **options))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# OpenAPI schema with $refs to other components, or a root/meta wrapper schema
|
|
75
|
+
#
|
|
76
|
+
# @param version [String, nil] requested version, config.current_version when nil
|
|
77
|
+
# @param view [Symbol] view name
|
|
78
|
+
# @param root [String, Symbol, nil] builds a wrapper schema with the response under this key
|
|
79
|
+
# @param meta [Class, nil] class whose $ref describes "meta" (requires root:)
|
|
80
|
+
# @param collection [Boolean] the root key holds an array
|
|
81
|
+
# @return [Hash] frozen schema
|
|
82
|
+
# @raise [ArgumentError] with meta: but no root:
|
|
83
|
+
# @raise [Versponse::DefinitionError] for a meta: that isn't a class with a $ref
|
|
84
|
+
# @raise [Versponse::VersionNotFoundError] when the class, or a Versponse meta: class, doesn't serve the version
|
|
85
|
+
# @raise [Versponse::ViewNotFoundError] for an unknown view (wrappers too)
|
|
86
|
+
def to_schema(version: nil, view: :default, root: nil, meta: nil, collection: false)
|
|
87
|
+
Wrapper.check_meta!(meta)
|
|
88
|
+
raise ArgumentError, "meta: requires root:" if meta && root.nil?
|
|
89
|
+
|
|
90
|
+
versponse.requesting(version) do |canonical|
|
|
91
|
+
next versponse.openapi_schema(version: canonical, view:) unless root
|
|
92
|
+
|
|
93
|
+
# The wrapper only refers to the components, but their versions and the view must exist like for any schema
|
|
94
|
+
versponse.resolve(canonical).properties_for(view)
|
|
95
|
+
meta.versponse.resolve(canonical) if References.versponse?(meta)
|
|
96
|
+
DeepFreeze.call(Wrapper.schema(self, view:, root:, meta:, collection:))
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Schema with every reference inlined, for validation
|
|
101
|
+
#
|
|
102
|
+
# @param version [String, nil] requested version, config.current_version when nil
|
|
103
|
+
# @param view [Symbol] view name
|
|
104
|
+
# @param property [Symbol, String, nil] returns only this property's schema
|
|
105
|
+
# @return [Hash] frozen schema
|
|
106
|
+
# @raise [Versponse::PropertyNotFoundError] for an unknown property
|
|
107
|
+
def to_validation_schema(version: nil, view: :default, property: nil)
|
|
108
|
+
versponse.requesting(version) do |canonical|
|
|
109
|
+
next versponse.property_validation_schema(version: canonical, view:, key: property.to_s) if property
|
|
110
|
+
|
|
111
|
+
versponse.validation_schema(version: canonical, view:)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Component name without the view suffix: the class name without its first namespace, squashed.
|
|
116
|
+
# Override it to rename components (e.g. strip a "Response" suffix).
|
|
117
|
+
#
|
|
118
|
+
# @return [String] base component name
|
|
119
|
+
def base_component_name
|
|
120
|
+
name.to_s.split("::", 2).last.to_s.delete(":")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @param view [Symbol] view name
|
|
124
|
+
# @return [String] component name, with the PascalCase view name for non-default views
|
|
125
|
+
def component_name(view: :default)
|
|
126
|
+
(view.to_sym == :default) ? base_component_name : base_component_name + Naming.pascalize(view)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @param view [Symbol] view name
|
|
130
|
+
# @param property [Symbol, String, nil] points at one property
|
|
131
|
+
# @return [String] "#/components/schemas/<name>" reference
|
|
132
|
+
# @raise [Versponse::DefinitionError] for an anonymous class, which has no component name
|
|
133
|
+
def to_ref(view: :default, property: nil)
|
|
134
|
+
raise DefinitionError, "anonymous response classes have no component; name the class or override base_component_name" if base_component_name.empty?
|
|
135
|
+
|
|
136
|
+
base = "#/components/schemas/#{component_name(view:)}"
|
|
137
|
+
property ? "#{base}/properties/#{property}" : base
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Every expandable dotted path of a response, for scope checks and expand: validation
|
|
141
|
+
#
|
|
142
|
+
# @param version [String, nil] requested version, config.current_version when nil
|
|
143
|
+
# @param view [Symbol] view name
|
|
144
|
+
# @return [Array<String>] sorted, frozen paths such as ["deal.stage", "site"]
|
|
145
|
+
def expandable_paths(version: nil, view: :default)
|
|
146
|
+
versponse.requesting(version) { |canonical| versponse.expandable_paths(version: canonical, view:) }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The response classes each expandable path renders, for metadata such as scopes declared on the classes
|
|
150
|
+
#
|
|
151
|
+
# @param version [String, nil] requested version, config.current_version when nil
|
|
152
|
+
# @param view [Symbol] view name
|
|
153
|
+
# @return [Hash{String => Array<Class>}] frozen, keyed like expandable_paths: a reference's from:, a
|
|
154
|
+
# collection's item: or every one_of variant's class, such as {"site" => [SiteResponse]}
|
|
155
|
+
def expandable_targets(version: nil, view: :default)
|
|
156
|
+
versponse.requesting(version) { |canonical| versponse.expandable_targets(version: canonical, view:) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Every dotted path serialize fields: accepts, for docs and scope checks
|
|
160
|
+
#
|
|
161
|
+
# @param version [String, nil] requested version, config.current_version when nil
|
|
162
|
+
# @param view [Symbol] view name
|
|
163
|
+
# @return [Array<String>] sorted, frozen paths such as ["id", "site", "site.name"]
|
|
164
|
+
def selectable_paths(version: nil, view: :default)
|
|
165
|
+
versponse.requesting(version) { |canonical| versponse.selectable_paths(version: canonical, view:) }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
# @param object [Object] source, or a collection of them
|
|
171
|
+
# @param plan [#call] plan of one object
|
|
172
|
+
# @param context [Hash] frozen serialize context
|
|
173
|
+
# @param expansion [Versponse::Expansion, Versponse::Selection::Scope] expand tree, paired with the selection in a
|
|
174
|
+
# selected render
|
|
175
|
+
# @return [Hash, Array<Hash>] rendered output
|
|
176
|
+
def render(object, plan, context, expansion)
|
|
177
|
+
Collections.collection?(object) ? Collections.map(Collections.items(object)) { |item| plan.call(item, context, expansion) } : plan.call(object, context, expansion)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Versponse
|
|
4
|
+
# Class-level DSL of {Versponse::Base}. All methods are private class methods; they delegate to the
|
|
5
|
+
# class's {Versponse::Builder}, which holds the definition state.
|
|
6
|
+
module Base::DSL
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# Sets the description of the class or the current version
|
|
10
|
+
#
|
|
11
|
+
# @param text [String] description
|
|
12
|
+
# @return [void]
|
|
13
|
+
def description(text)
|
|
14
|
+
versponse.builder.description(text)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Adds schema keywords to the class or the current version
|
|
18
|
+
#
|
|
19
|
+
# @param options [Hash] JSON Schema keywords in snake_case
|
|
20
|
+
# @return [void]
|
|
21
|
+
def schema_options(**options)
|
|
22
|
+
versponse.builder.schema_options(**options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Limits the versions the class serves; resolving a version outside the range raises
|
|
26
|
+
# VersionNotFoundError. Subclasses inherit the range; no arguments removes an inherited one.
|
|
27
|
+
#
|
|
28
|
+
# @param min [String, nil] first supported version (inclusive), defaults to the first declared version
|
|
29
|
+
# @param max [String, nil] last supported version (inclusive), defaults to no upper bound
|
|
30
|
+
# @return [void]
|
|
31
|
+
# @raise [Versponse::DefinitionError] when min sorts after max, inside a version, or after the first version
|
|
32
|
+
def supported_versions(min: nil, max: nil)
|
|
33
|
+
versponse.builder.supported_versions(min:, max:)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Sets the fraction digits of date-time values, replacing the date-time formatter of config.formatters.
|
|
37
|
+
# With from:, it applies from that version on. Subclasses inherit the settings and can add or replace them.
|
|
38
|
+
#
|
|
39
|
+
# @param digits [Integer] fraction digits from 0 (whole seconds) to 9
|
|
40
|
+
# @param from [String, nil] first version it applies to; nil for every version without a later setting
|
|
41
|
+
# @return [void]
|
|
42
|
+
# @raise [Versponse::DefinitionError] inside a version, after the first version, for invalid digits,
|
|
43
|
+
# or when the class already sets the precision from that version
|
|
44
|
+
def time_precision(digits, from: nil)
|
|
45
|
+
versponse.builder.time_precision(digits, from:)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Replaces nil values of the class's field, enum and array properties without default:. Subclasses inherit it.
|
|
49
|
+
#
|
|
50
|
+
# @param callable [#call, nil] called with (object, key, context); nil removes an inherited one
|
|
51
|
+
# @return [void]
|
|
52
|
+
# @raise [Versponse::DefinitionError] inside a version, after the first version, or for a value that can't take
|
|
53
|
+
# (object, key, context)
|
|
54
|
+
def on_nil(callable)
|
|
55
|
+
versponse.builder.on_nil(callable)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Policy for the class's failing properties without their own on_error:. Subclasses inherit it.
|
|
59
|
+
#
|
|
60
|
+
# @param policy [Symbol, #call, nil] :raise, :nullify, :omit, a handler called with (error, object, key, context),
|
|
61
|
+
# or nil to remove an inherited one
|
|
62
|
+
# @return [void]
|
|
63
|
+
# @raise [Versponse::DefinitionError] inside a version, after the first version, or for an unknown policy
|
|
64
|
+
def on_error(policy)
|
|
65
|
+
versponse.builder.on_error(policy)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Declares a version
|
|
69
|
+
#
|
|
70
|
+
# @param name [String] version name, compared as a String
|
|
71
|
+
# @param inherit [Boolean, String, Symbol] true copies the previously declared version, a name copies the
|
|
72
|
+
# closest version at or before it
|
|
73
|
+
# @param exclude_properties [Array<Symbol>] inherited properties to leave out
|
|
74
|
+
# @return [void]
|
|
75
|
+
# @raise [Versponse::DefinitionError] when nested in another version
|
|
76
|
+
# @raise [Versponse::DefinitionError] when inherit: names a version before every declared one
|
|
77
|
+
def version(name, inherit: true, exclude_properties: [], &block)
|
|
78
|
+
versponse.builder.version(name, inherit:, exclude_properties:, &block)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Computes the value of the enclosing inline object, inline collection or one_of
|
|
82
|
+
#
|
|
83
|
+
# @yieldparam object [Object] serialized object
|
|
84
|
+
# @yieldparam context [Hash] serialize context
|
|
85
|
+
# @return [void]
|
|
86
|
+
def source(&block)
|
|
87
|
+
versponse.builder.source(&block)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Declares a scalar field
|
|
91
|
+
#
|
|
92
|
+
# @param name [Symbol] output key
|
|
93
|
+
# @param type [Symbol, Array<Symbol>] string, number, integer, boolean, object, a custom type, or a union like [:integer, :string]
|
|
94
|
+
# @param options [Hash] map:, nullable:, if:, unless:, JSON Schema keywords
|
|
95
|
+
# @yieldparam object [Object] serialized object
|
|
96
|
+
# @yieldparam context [Hash] serialize context
|
|
97
|
+
# @return [void]
|
|
98
|
+
def field(name, type: nil, **options, &block)
|
|
99
|
+
versponse.builder.field(name, type:, **options, &block)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Declares a field of both the full shape and the collapsed shape, rendered for expandable associations that aren't expanded
|
|
103
|
+
#
|
|
104
|
+
# @param name [Symbol] output key
|
|
105
|
+
# @param type [Symbol, nil] field type (with_options type:, else :string)
|
|
106
|
+
# @param collapse_if [Proc, #call, nil] (not taken from with_options) called with (object) or (object, context); when it returns false the
|
|
107
|
+
# association renders in full instead (a collection item: when it is false for any item), and the full shape leaves this field out.
|
|
108
|
+
# One per version.
|
|
109
|
+
# @param options [Hash] map:, nullable:, description: and JSON Schema keywords (no if:/unless:); with_options defaults apply
|
|
110
|
+
# @return [void]
|
|
111
|
+
# @raise [Versponse::DefinitionError] outside a version, with if:/unless:, or with a collapse_if: that doesn't respond to call
|
|
112
|
+
def identifier(name, type: nil, collapse_if: nil, **options, &block)
|
|
113
|
+
versponse.builder.identifier(name, type:, collapse_if:, **options, &block)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Declares a value from a fixed list
|
|
117
|
+
#
|
|
118
|
+
# @param name [Symbol] output key
|
|
119
|
+
# @param type [Symbol, Array<Symbol>, nil] type of the values, as for {#field} (with_options type:); inferred from the values when nil
|
|
120
|
+
# @param values [Array] allowed values, each of the type
|
|
121
|
+
# @param options [Hash] see {#field}
|
|
122
|
+
# @return [void]
|
|
123
|
+
# @raise [Versponse::DefinitionError] when the type can't be inferred, or a value isn't of the type
|
|
124
|
+
def enum(name, values:, type: nil, **options, &block)
|
|
125
|
+
versponse.builder.enum(name, values:, type:, **options, &block)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Declares a constant
|
|
129
|
+
#
|
|
130
|
+
# @param name [Symbol] output key
|
|
131
|
+
# @param value [Object] constant value
|
|
132
|
+
# @param options [Hash] if:, unless:, JSON Schema keywords
|
|
133
|
+
# @return [void]
|
|
134
|
+
# @raise [Versponse::DefinitionError] with a block, map: or nullable: (a constant never reads the object)
|
|
135
|
+
def const(name, value:, **options, &block)
|
|
136
|
+
versponse.builder.const(name, value:, **options, &block)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Declares an array of scalars
|
|
140
|
+
#
|
|
141
|
+
# @param name [Symbol] output key
|
|
142
|
+
# @param type [Symbol, Array<Symbol>] item type, or a union like [:integer, :string]
|
|
143
|
+
# @param item_schema_options [Hash] JSON Schema keywords of each item
|
|
144
|
+
# @param options [Hash] see {#field}
|
|
145
|
+
# @return [void]
|
|
146
|
+
def array(name, type: nil, item_schema_options: {}, **options, &block)
|
|
147
|
+
versponse.builder.array(name, type:, item_schema_options:, **options, &block)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Declares an inline object; the block defines its properties
|
|
151
|
+
#
|
|
152
|
+
# @param name [Symbol] output key
|
|
153
|
+
# @param options [Hash] map: (use "." for the same object), nullable:, if:, unless:, JSON Schema keywords
|
|
154
|
+
# @return [void]
|
|
155
|
+
def object(name, **options, &block)
|
|
156
|
+
versponse.builder.object(name, **options, &block)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Declares an array of objects, either of another response class (item:) or inline (block)
|
|
160
|
+
#
|
|
161
|
+
# With item:, a block computes the value. Without item:, the block defines the item properties.
|
|
162
|
+
#
|
|
163
|
+
# @param name [Symbol] output key
|
|
164
|
+
# @param item [Class, nil] class implementing the reference protocol
|
|
165
|
+
# @param view [Symbol, nil] view of item (with_options view:, else :default)
|
|
166
|
+
# @param options [Hash] see {#field}
|
|
167
|
+
# @return [void]
|
|
168
|
+
def collection(name, item: nil, view: nil, **options, &block)
|
|
169
|
+
versponse.builder.collection(name, item:, view:, **options, &block)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Declares a nested response class, or a variant inside one_of
|
|
173
|
+
#
|
|
174
|
+
# @param name [Symbol, nil] output key, omitted inside one_of
|
|
175
|
+
# @param from [Class] class implementing the reference protocol
|
|
176
|
+
# @param view [Symbol, nil] view of from (with_options view:, else :default)
|
|
177
|
+
# @param property [Symbol, nil] one property of a non-Versponse class's schema
|
|
178
|
+
# @param match [Class, Proc, nil] one_of matcher, compared with ===
|
|
179
|
+
# @param options [Hash] see {#field}
|
|
180
|
+
# @return [void]
|
|
181
|
+
def reference(name = nil, from:, view: nil, property: nil, match: nil, **options, &block)
|
|
182
|
+
versponse.builder.reference(name, from:, view:, property:, match:, **options, &block)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Declares a polymorphic value; the block declares variants with reference from:, match:.
|
|
186
|
+
# Without a name, inside a collection block, it types each item of the collection.
|
|
187
|
+
#
|
|
188
|
+
# @param positional_name [Symbol, nil] output key; nil for the item one_of of a collection
|
|
189
|
+
# @param name [Symbol, nil] the output key as a keyword, as verquest writes it
|
|
190
|
+
# @param discriminator [String, Symbol, nil] OpenAPI discriminator property
|
|
191
|
+
# @param options [Hash] see {#field}; an item one_of takes only nullable: and schema keywords
|
|
192
|
+
# @return [void]
|
|
193
|
+
# @raise [Versponse::DefinitionError] when the name is given both ways
|
|
194
|
+
def one_of(positional_name = nil, name: nil, discriminator: nil, **options, &block)
|
|
195
|
+
versponse.builder.one_of(positional_name, name:, discriminator:, **options, &block)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Declares a view inside a version
|
|
199
|
+
#
|
|
200
|
+
# @param name [Symbol] view name
|
|
201
|
+
# @return [void]
|
|
202
|
+
def view(name, &block)
|
|
203
|
+
versponse.builder.view(name, &block)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Applies another view's operations inside a view
|
|
207
|
+
#
|
|
208
|
+
# @param name [Symbol] included view
|
|
209
|
+
# @return [void]
|
|
210
|
+
def include_view(name)
|
|
211
|
+
versponse.builder.include_view(name)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Removes properties from the current version or view
|
|
215
|
+
#
|
|
216
|
+
# @param names [Array<Symbol>] property names
|
|
217
|
+
# @return [void]
|
|
218
|
+
def exclude_properties(*names)
|
|
219
|
+
versponse.builder.exclude_properties(*names)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Applies options to every property declared in the block. A nested with_options replaces the outer
|
|
223
|
+
# options, and map: is ignored, as in verquest.
|
|
224
|
+
#
|
|
225
|
+
# @param options [Hash] shared options
|
|
226
|
+
# @return [void]
|
|
227
|
+
def with_options(**options, &block)
|
|
228
|
+
versponse.builder.with_options(**options, &block)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Versponse
|
|
4
|
+
# Parent of every response definition
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# class UnitTypeResponse < Versponse::Base
|
|
8
|
+
# version "2025-09" do
|
|
9
|
+
# field :id, type: :string, map: "public_id"
|
|
10
|
+
# reference :site, from: SiteResponse, view: :embedded
|
|
11
|
+
#
|
|
12
|
+
# view :embedded do
|
|
13
|
+
# exclude_properties :site
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
class Base
|
|
18
|
+
extend DSL
|
|
19
|
+
extend ClassMethods
|
|
20
|
+
|
|
21
|
+
# Registers the subclass for {Versponse.components} and {Versponse.reset_caches!}
|
|
22
|
+
#
|
|
23
|
+
# @param subclass [Class] new response class
|
|
24
|
+
# @return [void]
|
|
25
|
+
# @raise [Versponse::DefinitionError] when the parent already declares versions
|
|
26
|
+
def self.inherited(subclass)
|
|
27
|
+
super
|
|
28
|
+
if self != Versponse::Base && !versponse.versions.empty?
|
|
29
|
+
raise DefinitionError, "#{name} has versions and can not be subclassed; subclass a class without versions (e.g. an abstract BaseResponse)"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Registry.add(subclass)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|