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
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Versponse
|
|
4
|
+
module Steps
|
|
5
|
+
# Renders a nested object, or each item of a collection, with a child plan
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
class Nested
|
|
9
|
+
# @return [String] output key
|
|
10
|
+
attr_reader :key
|
|
11
|
+
|
|
12
|
+
# @param key [String] output key
|
|
13
|
+
# @param extractor [#call] value reader
|
|
14
|
+
# @param condition [Versponse::Condition, nil] if:/unless:
|
|
15
|
+
# @param plan [#call] child plan
|
|
16
|
+
# @param collection [Boolean] render each item
|
|
17
|
+
# @param collapsed_plan [#call, nil] plan used when the association is expandable and not expanded
|
|
18
|
+
# @param collapse_if [#call, nil] the referenced identifier's condition; when it is false for the value
|
|
19
|
+
# (for a collection: for any item) the full plan is used instead of collapsed_plan
|
|
20
|
+
# @param nil_fails [Boolean] in a validating plan, a nil value fails its schema (see InlineValidation)
|
|
21
|
+
# @param context_override [#call, nil] computes the subtree's context from (object, context)
|
|
22
|
+
def initialize(key:, extractor:, condition:, plan:, collection:, collapsed_plan: nil, collapse_if: nil, nil_fails: false, context_override: nil)
|
|
23
|
+
@key = key
|
|
24
|
+
@extractor = extractor
|
|
25
|
+
@condition = condition
|
|
26
|
+
@plan = plan
|
|
27
|
+
@collection = collection
|
|
28
|
+
@collapsed_plan = collapsed_plan
|
|
29
|
+
@collapse_if = collapse_if
|
|
30
|
+
@nil_fails = nil_fails
|
|
31
|
+
@context_override = context_override
|
|
32
|
+
freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Boolean] true with if:/unless:
|
|
36
|
+
def conditional?
|
|
37
|
+
!@condition.nil?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [Boolean] true when each item of a collection is rendered
|
|
41
|
+
def collection?
|
|
42
|
+
@collection
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [Array<#call>] the child plan, and the collapsed plan when there is one
|
|
46
|
+
def plans
|
|
47
|
+
[@plan, @collapsed_plan].compact
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param plan [#call] child plan
|
|
51
|
+
# @param collapsed_plan [#call, nil] plan used when collapsed
|
|
52
|
+
# @param nil_fails [Boolean] a nil value fails validation (validating plans only)
|
|
53
|
+
# @return [Nested] the same step rendering with these plans
|
|
54
|
+
def with_plans(plan:, collapsed_plan:, nil_fails: false)
|
|
55
|
+
self.class.new(key: @key, extractor: @extractor, condition: @condition, plan:, collection: @collection,
|
|
56
|
+
collapsed_plan:, collapse_if: @collapse_if, nil_fails:, context_override: @context_override)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @param object [Object] source
|
|
60
|
+
# @param hash [Boolean] whether the source is a Hash
|
|
61
|
+
# @param context [Hash] serialize context
|
|
62
|
+
# @param output [Hash] Hash being built
|
|
63
|
+
# @param expansion [Versponse::Expansion] expand tree node of the parent
|
|
64
|
+
# @return [void]
|
|
65
|
+
# @raise [Versponse::ExtractionError] with the item index in its keys when a collection item fails
|
|
66
|
+
def call(object, hash, context, output, expansion)
|
|
67
|
+
return if @condition && !@condition.call(object, context)
|
|
68
|
+
|
|
69
|
+
value = @extractor.call(object, hash, context)
|
|
70
|
+
output[@key] = if value.nil?
|
|
71
|
+
InlineValidation.failed! if @nil_fails
|
|
72
|
+
nil
|
|
73
|
+
else
|
|
74
|
+
context = @context_override.call(object, context) if @context_override
|
|
75
|
+
# Errors from the child plans are marked, so only an explicit association policy recovers them
|
|
76
|
+
begin
|
|
77
|
+
collapse = @collapsed_plan && !expansion.expand?(@key) && (@collapse_if.nil? || collapse?(value, context))
|
|
78
|
+
plan = collapse ? @collapsed_plan : @plan
|
|
79
|
+
node = expansion.child(@key)
|
|
80
|
+
@collection ? Collections.map(Collections.items(value)) { |item| plan.call(item, context, node) } : plan.call(value, context, node)
|
|
81
|
+
rescue => e
|
|
82
|
+
ErrorPolicy.mark_nested!(e)
|
|
83
|
+
raise
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# A collection collapses only when every item passes, so its items share one shape
|
|
91
|
+
def collapse?(value, context)
|
|
92
|
+
return @collapse_if.call(value, context) unless @collection
|
|
93
|
+
|
|
94
|
+
Collections.items(value).all? { |item| @collapse_if.call(item, context) }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Versponse
|
|
4
|
+
module Steps
|
|
5
|
+
# Renders a value with the plan of the variant its dispatcher picks
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
class OneOf
|
|
9
|
+
# @return [String] output key
|
|
10
|
+
attr_reader :key
|
|
11
|
+
|
|
12
|
+
# @param key [String] output key
|
|
13
|
+
# @param extractor [#call] value reader
|
|
14
|
+
# @param condition [Versponse::Condition, nil] if:/unless:
|
|
15
|
+
# @param dispatcher [Versponse::VariantDispatcher] variant matching
|
|
16
|
+
# @param collapsed_dispatcher [Versponse::VariantDispatcher, nil] matching to collapsed variants (expandable one_of)
|
|
17
|
+
# @param context_override [#call, nil] computes the variants' context from (object, context)
|
|
18
|
+
def initialize(key:, extractor:, condition:, dispatcher:, collapsed_dispatcher: nil, context_override: nil)
|
|
19
|
+
@key = key
|
|
20
|
+
@extractor = extractor
|
|
21
|
+
@condition = condition
|
|
22
|
+
@dispatcher = dispatcher
|
|
23
|
+
@collapsed_dispatcher = collapsed_dispatcher
|
|
24
|
+
@context_override = context_override
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Boolean] true with if:/unless:
|
|
29
|
+
def conditional?
|
|
30
|
+
!@condition.nil?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param object [Object] source
|
|
34
|
+
# @param hash [Boolean] whether the source is a Hash
|
|
35
|
+
# @param context [Hash] serialize context
|
|
36
|
+
# @param output [Hash] Hash being built
|
|
37
|
+
# @param expansion [Versponse::Expansion] expand tree node of the parent
|
|
38
|
+
# @return [void]
|
|
39
|
+
# @raise [KeyError] when no variant matches (the plan wraps it in an ExtractionError)
|
|
40
|
+
def call(object, hash, context, output, expansion)
|
|
41
|
+
return if @condition && !@condition.call(object, context)
|
|
42
|
+
|
|
43
|
+
value = @extractor.call(object, hash, context)
|
|
44
|
+
return output[@key] = nil if value.nil?
|
|
45
|
+
|
|
46
|
+
context = @context_override.call(object, context) if @context_override
|
|
47
|
+
dispatcher = (@collapsed_dispatcher && !expansion.expand?(@key)) ? @collapsed_dispatcher : @dispatcher
|
|
48
|
+
output[@key] = dispatch(dispatcher, value, context, expansion.child(@key))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Errors from the variant plans (and a value matching no variant) are marked like a nested step's
|
|
54
|
+
def dispatch(dispatcher, value, context, node)
|
|
55
|
+
dispatcher.call(value, context, node)
|
|
56
|
+
rescue => e
|
|
57
|
+
ErrorPolicy.mark_nested!(e)
|
|
58
|
+
raise
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Versponse
|
|
4
|
+
module Steps
|
|
5
|
+
# A plain field: reads a method or Hash key and formats it, with no condition, block or path.
|
|
6
|
+
# The common case, without Value's extractor and condition layers.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
class Read
|
|
10
|
+
# @return [String] output key
|
|
11
|
+
attr_reader :key
|
|
12
|
+
|
|
13
|
+
# @param key [String] output key
|
|
14
|
+
# @param name [String, Symbol] method or Hash key to read
|
|
15
|
+
# @param formatter [#call, nil] applied to non-nil values
|
|
16
|
+
def initialize(key:, name:, formatter: nil)
|
|
17
|
+
@key = key
|
|
18
|
+
@symbol = name.to_sym
|
|
19
|
+
@string = name.to_s.freeze
|
|
20
|
+
@formatter = formatter
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param check [#valid?] checker of the property's schema
|
|
25
|
+
# @param recheck [Boolean] check an unfrozen value again after the render
|
|
26
|
+
# @return [CheckedRead] the same read, checking what it writes
|
|
27
|
+
def checked(check, recheck:)
|
|
28
|
+
CheckedRead.new(key: @key, name: @string, formatter: @formatter, check:, recheck:)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Boolean] false: a read has no if:/unless:
|
|
32
|
+
def conditional?
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param object [Object] source
|
|
37
|
+
# @param hash [Boolean] whether the source is a Hash
|
|
38
|
+
# @param _context [Hash] unused
|
|
39
|
+
# @param output [Hash] Hash being built
|
|
40
|
+
# @param _expansion [Versponse::Expansion] unused
|
|
41
|
+
# @return [void]
|
|
42
|
+
# @raise [KeyError] when a Hash has neither key; NoMethodError when an object has no such method
|
|
43
|
+
def call(object, hash, _context, output, _expansion)
|
|
44
|
+
output[@key] = hash ? object.fetch(@symbol) { object.fetch(@string) } : object.public_send(@symbol)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Versponse
|
|
4
|
+
module Steps
|
|
5
|
+
# A plain field with a formatter, applied to non-nil values
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
class ReadFormatted < Read
|
|
9
|
+
# @param check [#valid?] checker of the property's schema
|
|
10
|
+
# @param recheck [Boolean] check an unfrozen value again after the render
|
|
11
|
+
# @return [CheckedReadFormatted] the same read, checking the formatted value it writes
|
|
12
|
+
def checked(check, recheck:)
|
|
13
|
+
CheckedReadFormatted.new(key: @key, name: @string, formatter: @formatter, check:, recheck:)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param object [Object] source
|
|
17
|
+
# @param hash [Boolean] whether the source is a Hash
|
|
18
|
+
# @param _context [Hash] unused
|
|
19
|
+
# @param output [Hash] Hash being built
|
|
20
|
+
# @param _expansion [Versponse::Expansion] unused
|
|
21
|
+
# @return [void]
|
|
22
|
+
def call(object, hash, _context, output, _expansion)
|
|
23
|
+
value = hash ? object.fetch(@symbol) { object.fetch(@string) } : object.public_send(@symbol)
|
|
24
|
+
output[@key] = value.nil? ? nil : @formatter.call(value)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Versponse
|
|
4
|
+
# Compiled operations a plan runs for each property
|
|
5
|
+
module Steps
|
|
6
|
+
# Writes one extracted value, formatted when a formatter is bound
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
class Value
|
|
10
|
+
# @return [String] output key
|
|
11
|
+
attr_reader :key
|
|
12
|
+
|
|
13
|
+
# @param key [String] output key
|
|
14
|
+
# @param extractor [#call] value reader
|
|
15
|
+
# @param condition [Versponse::Condition, nil] if:/unless:
|
|
16
|
+
# @param formatter [#call, nil] applied to non-nil values
|
|
17
|
+
def initialize(key:, extractor:, condition:, formatter:)
|
|
18
|
+
@key = key
|
|
19
|
+
@extractor = extractor
|
|
20
|
+
@condition = condition
|
|
21
|
+
@formatter = formatter
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param check [#valid?] checker of the property's schema
|
|
26
|
+
# @param recheck [Boolean] check an unfrozen value again after the render
|
|
27
|
+
# @return [CheckedValue] the same step, checking what it writes
|
|
28
|
+
def checked(check, recheck:)
|
|
29
|
+
CheckedValue.new(key: @key, extractor: @extractor, condition: @condition, formatter: @formatter, check:, recheck:)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Boolean] true with if:/unless:
|
|
33
|
+
def conditional?
|
|
34
|
+
!@condition.nil?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param object [Object] source
|
|
38
|
+
# @param hash [Boolean] whether the source is a Hash
|
|
39
|
+
# @param context [Hash] serialize context
|
|
40
|
+
# @param output [Hash] Hash being built
|
|
41
|
+
# @param _expansion [Versponse::Expansion] unused
|
|
42
|
+
# @return [void]
|
|
43
|
+
def call(object, hash, context, output, _expansion)
|
|
44
|
+
return if @condition && !@condition.call(object, context)
|
|
45
|
+
|
|
46
|
+
value = @extractor.call(object, hash, context)
|
|
47
|
+
value = @formatter.call(value) if @formatter && !value.nil?
|
|
48
|
+
output[@key] = value
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Versponse
|
|
4
|
+
# Inclusive range of versions a response class serves, declared with supported_versions.
|
|
5
|
+
# Versions are compared with config.version_resolver, so the range uses the same order as
|
|
6
|
+
# resolution (string order by default, where "preview" sorts after dated versions).
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
class SupportedVersions
|
|
10
|
+
# @return [String, nil] first supported version, nil to start at the first declared version
|
|
11
|
+
attr_reader :min
|
|
12
|
+
# @return [String, nil] last supported version, nil for no upper bound
|
|
13
|
+
attr_reader :max
|
|
14
|
+
|
|
15
|
+
# The resolver picks earlier for a request of later only when earlier sorts at or before it
|
|
16
|
+
#
|
|
17
|
+
# @param earlier [String] version expected first
|
|
18
|
+
# @param later [String] version expected second
|
|
19
|
+
# @return [Boolean] true when earlier sorts at or before later
|
|
20
|
+
def self.at_or_before?(earlier, later)
|
|
21
|
+
Versponse.configuration.version_resolver.call(later, [earlier]) == earlier
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param min [String, nil] first supported version (inclusive)
|
|
25
|
+
# @param max [String, nil] last supported version (inclusive)
|
|
26
|
+
# @raise [Versponse::DefinitionError] when min sorts after max
|
|
27
|
+
def initialize(min:, max:)
|
|
28
|
+
@min = min&.to_s
|
|
29
|
+
@max = max&.to_s
|
|
30
|
+
raise DefinitionError, "supported_versions min: #{@min} is after max: #{@max}" if @min && @max && !self.class.at_or_before?(@min, @max)
|
|
31
|
+
|
|
32
|
+
freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Without min, earlier versions are left to the resolver, which finds nothing before the first declared one
|
|
36
|
+
#
|
|
37
|
+
# @param version [String] version name
|
|
38
|
+
# @return [Boolean] true when the range includes the version
|
|
39
|
+
def covers?(version)
|
|
40
|
+
(min.nil? || self.class.at_or_before?(min, version)) && (max.nil? || self.class.at_or_before?(version, max))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Only max is checked: declared blocks don't have to be inside the range, so min: "preview"
|
|
44
|
+
# with a single version "2025-09" block serves "preview" from 2025-09
|
|
45
|
+
#
|
|
46
|
+
# @param declared [Array<String>] declared version names, oldest first
|
|
47
|
+
# @param owner [String] class name, for the message
|
|
48
|
+
# @return [void]
|
|
49
|
+
# @raise [Versponse::DefinitionError] when max sorts before every declared version, so no request could resolve
|
|
50
|
+
def check!(declared, owner)
|
|
51
|
+
return if max.nil? || declared.empty? || declared.any? { |version| self.class.at_or_before?(version, max) }
|
|
52
|
+
|
|
53
|
+
raise DefinitionError, "#{owner}: supported_versions max: #{max} is before every declared version (#{declared.join(", ")})"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [String] the range for messages, like "min: 2025-09, max: none"
|
|
57
|
+
def to_s
|
|
58
|
+
"min: #{min || "first declared"}, max: #{max || "none"}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Versponse
|
|
4
|
+
module Validator
|
|
5
|
+
# Combines the checkers of one schema; all must pass
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
class All
|
|
9
|
+
# @param checkers [Array<#valid?>] checkers of one schema
|
|
10
|
+
# @return [#valid?] VALID for none, the checker itself for one, else an All
|
|
11
|
+
def self.for(checkers)
|
|
12
|
+
return Constant::VALID if checkers.empty?
|
|
13
|
+
return checkers.first if checkers.one?
|
|
14
|
+
return Both.new(*checkers) if checkers.size == 2
|
|
15
|
+
|
|
16
|
+
new(checkers)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param checkers [Array<#valid?>] two or more checkers
|
|
20
|
+
def initialize(checkers)
|
|
21
|
+
@checkers = checkers.freeze
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param data [Object] value to check
|
|
26
|
+
# @return [Boolean] true when every checker passes
|
|
27
|
+
def valid?(data)
|
|
28
|
+
@checkers.all? { |checker| checker.valid?(data) }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Versponse
|
|
4
|
+
module Validator
|
|
5
|
+
# items, minItems and maxItems
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
class ArrayCheck
|
|
9
|
+
# @return [Array<String>] keywords this check owns
|
|
10
|
+
def self.keywords = %w[items minItems maxItems]
|
|
11
|
+
|
|
12
|
+
# @return [Class] the class a type folded into this check requires
|
|
13
|
+
def self.shape = Array
|
|
14
|
+
|
|
15
|
+
# @param schema [Hash] schema
|
|
16
|
+
# @param compiler [Versponse::Validator::Compiler] compiles the items schema
|
|
17
|
+
# @return [ArrayCheck, nil] the check, nil without array keywords
|
|
18
|
+
def self.build(schema, compiler)
|
|
19
|
+
return unless keywords.any? { |keyword| schema.key?(keyword) }
|
|
20
|
+
|
|
21
|
+
new(compiler.compile(schema.fetch("items", true)), Range.new(schema["minItems"], schema["maxItems"]))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param items [#valid?] checker of every item
|
|
25
|
+
# @param sizes [Range] allowed number of items
|
|
26
|
+
# @param strict [Boolean] true when anything but an Array fails (type: "array" folded in)
|
|
27
|
+
def initialize(items, sizes, strict: false)
|
|
28
|
+
@items = items
|
|
29
|
+
@sizes = sizes
|
|
30
|
+
@strict = strict
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [ArrayCheck] the same check, failing anything but an Array
|
|
35
|
+
def strict
|
|
36
|
+
self.class.new(@items, @sizes, strict: true)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param data [Object] value to check; anything but an Array passes unless strict
|
|
40
|
+
# @return [Boolean] true when the Array matches
|
|
41
|
+
def valid?(data)
|
|
42
|
+
return !@strict unless data.is_a?(Array)
|
|
43
|
+
|
|
44
|
+
@sizes.cover?(data.size) && data.all? { |item| @items.valid?(item) }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Versponse
|
|
4
|
+
module Validator
|
|
5
|
+
# type: boolean, compared with == as json_schemer does
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
class BooleanCheck
|
|
9
|
+
# @param data [Object] value to check
|
|
10
|
+
# @return [Boolean] true when data is true or false
|
|
11
|
+
def valid?(data)
|
|
12
|
+
data == true || data == false
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Versponse
|
|
4
|
+
module Validator
|
|
5
|
+
# Two checkers, called without an Array loop (the common case: a type and one other check)
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
class Both
|
|
9
|
+
# @param first [#valid?] first checker
|
|
10
|
+
# @param second [#valid?] second checker
|
|
11
|
+
def initialize(first, second)
|
|
12
|
+
@first = first
|
|
13
|
+
@second = second
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param data [Object] value to check
|
|
18
|
+
# @return [Boolean] true when both checkers accept data
|
|
19
|
+
def valid?(data)
|
|
20
|
+
@first.valid?(data) && @second.valid?(data)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Versponse
|
|
4
|
+
module Validator
|
|
5
|
+
# A type check that is one class test (only number, string, array, object, or integer where only Integers count)
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
class ClassCheck
|
|
9
|
+
# @return [Class] the class data must be
|
|
10
|
+
attr_reader :klass
|
|
11
|
+
|
|
12
|
+
# @param klass [Class] the class data must be
|
|
13
|
+
def initialize(klass)
|
|
14
|
+
@klass = klass
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param data [Object] value to check
|
|
19
|
+
# @return [Boolean] true when data is of the class
|
|
20
|
+
def valid?(data)
|
|
21
|
+
data.is_a?(@klass)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Versponse
|
|
4
|
+
module Validator
|
|
5
|
+
# An object with additionalProperties: false whose required keys are all declared properties: each declared key
|
|
6
|
+
# is fetched and checked, and a count of the present ones equal to the Hash size proves there are no others
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
class ClosedObjectCheck
|
|
10
|
+
# Marks a declared key the Hash doesn't have
|
|
11
|
+
MISSING = Object.new.freeze
|
|
12
|
+
private_constant :MISSING
|
|
13
|
+
|
|
14
|
+
# @return [Class] the class a type folded into this check requires
|
|
15
|
+
def self.shape = Hash
|
|
16
|
+
|
|
17
|
+
# @param properties [Hash{String => #valid?}] checkers of the declared properties
|
|
18
|
+
# @param required [Array<String>] required keys, all of them declared properties
|
|
19
|
+
# @param sizes [Range] allowed number of properties
|
|
20
|
+
# @param strict [Boolean] true when anything but a Hash fails (type: "object" folded in)
|
|
21
|
+
def initialize(properties:, required:, sizes:, strict: false)
|
|
22
|
+
@properties = properties
|
|
23
|
+
@required_keys = required
|
|
24
|
+
@keys = properties.keys.freeze
|
|
25
|
+
@checks = properties.values.freeze
|
|
26
|
+
@required = @keys.map { |key| required.include?(key) }.freeze
|
|
27
|
+
@sizes = sizes
|
|
28
|
+
@strict = strict
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [ClosedObjectCheck] the same check, failing anything but a Hash
|
|
33
|
+
def strict
|
|
34
|
+
self.class.new(properties: @properties, required: @required_keys, sizes: @sizes, strict: true)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param data [Object] value to check; anything but a Hash passes unless strict
|
|
38
|
+
# @return [Boolean] true when the Hash matches
|
|
39
|
+
def valid?(data)
|
|
40
|
+
return !@strict unless data.is_a?(Hash)
|
|
41
|
+
|
|
42
|
+
found = 0
|
|
43
|
+
index = 0
|
|
44
|
+
while index < @keys.size
|
|
45
|
+
value = data.fetch(@keys[index], MISSING)
|
|
46
|
+
if MISSING.equal?(value)
|
|
47
|
+
return reject(data) if @required[index]
|
|
48
|
+
else
|
|
49
|
+
return reject(data) unless @checks[index].valid?(value)
|
|
50
|
+
|
|
51
|
+
found += 1
|
|
52
|
+
end
|
|
53
|
+
index += 1
|
|
54
|
+
end
|
|
55
|
+
(found == data.size && @sizes.cover?(found)) || reject(data)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# json_schemer reads Symbol keys as Strings, so a false is certain only for a Hash with String keys; one with
|
|
61
|
+
# Symbol keys is checked again with their Strings, and any other Hash throws {Root::DEFER}
|
|
62
|
+
#
|
|
63
|
+
# @param data [Hash] value being checked
|
|
64
|
+
# @return [Boolean] false, or the answer for the Hash with String keys
|
|
65
|
+
def reject(data)
|
|
66
|
+
data.each_key do |key|
|
|
67
|
+
next if key.is_a?(String)
|
|
68
|
+
|
|
69
|
+
strings = StringKeys.call(data)
|
|
70
|
+
throw Root::DEFER unless strings
|
|
71
|
+
|
|
72
|
+
return valid?(strings)
|
|
73
|
+
end
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Versponse
|
|
4
|
+
module Validator
|
|
5
|
+
# anyOf, oneOf and allOf
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
class Combinator
|
|
9
|
+
# Keywords and the Enumerable predicate each applies to its branches
|
|
10
|
+
MODES = {"anyOf" => :any?, "oneOf" => :one?, "allOf" => :all?}.freeze
|
|
11
|
+
|
|
12
|
+
# @return [Array<String>] keywords this check owns
|
|
13
|
+
def self.keywords = MODES.keys
|
|
14
|
+
|
|
15
|
+
# @param schema [Hash] schema
|
|
16
|
+
# @param compiler [Versponse::Validator::Compiler] compiles the branches
|
|
17
|
+
# @return [#valid?, nil] the check, nil without combinators
|
|
18
|
+
def self.build(schema, compiler)
|
|
19
|
+
combinators = MODES.filter_map do |keyword, mode|
|
|
20
|
+
new(schema[keyword].map { |branch| compiler.compile(branch) }, mode) if schema.key?(keyword)
|
|
21
|
+
end
|
|
22
|
+
All.for(combinators) unless combinators.empty?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param branches [Array<#valid?>] checkers of the branches
|
|
26
|
+
# @param mode [Symbol] :any?, :one? or :all?
|
|
27
|
+
def initialize(branches, mode)
|
|
28
|
+
@branches = branches.freeze
|
|
29
|
+
@mode = mode
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param data [Object] value to check
|
|
34
|
+
# @return [Boolean] true when the branches match as the mode requires
|
|
35
|
+
def valid?(data)
|
|
36
|
+
@branches.public_send(@mode) { |branch| branch.valid?(data) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|