datacaster 4.2.1 → 5.0.1
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/.github/workflows/rspec.yml +1 -1
- data/config/locales/en.yml +1 -0
- data/lib/datacaster/and_node.rb +27 -0
- data/lib/datacaster/and_with_error_aggregation_node.rb +14 -0
- data/lib/datacaster/array_schema.rb +10 -2
- data/lib/datacaster/context_node.rb +8 -0
- data/lib/datacaster/hash_mapper.rb +22 -0
- data/lib/datacaster/hash_schema.rb +10 -0
- data/lib/datacaster/json_schema_attributes.rb +28 -0
- data/lib/datacaster/json_schema_node.rb +28 -0
- data/lib/datacaster/json_schema_result.rb +239 -0
- data/lib/datacaster/mixin.rb +22 -0
- data/lib/datacaster/or_node.rb +14 -0
- data/lib/datacaster/predefined.rb +83 -24
- data/lib/datacaster/switch_node.rb +34 -3
- data/lib/datacaster/then_node.rb +23 -0
- data/lib/datacaster/transformer.rb +4 -0
- data/lib/datacaster/utils.rb +7 -0
- data/lib/datacaster/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a0e209179b85f6f4ca1b63face9260b3d8c03e7c09796e124c4606e9f8ff0431
|
4
|
+
data.tar.gz: 15b4b5228319b5ff7d670dfd8f23883e714b1017ccc85dc0a3df920a142a0627
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 966d31dc9b9b397ced3bece4e3a74f0c885e5b95dfebe3a35374a85c50afb4bd183222e56095730fbca3c52b079a277f8f43106c14d69192f95032d771a67a18
|
7
|
+
data.tar.gz: 12043768c2e341338af7f1178aca2dead9ffd43c8189f4a2df44ca1114768aaa8fbc465e3d23c4a8e2a41162c8f73c35d789b091e18cbe67e1b5afe90214f2bc
|
data/.github/workflows/rspec.yml
CHANGED
data/config/locales/en.yml
CHANGED
@@ -27,6 +27,7 @@ en:
|
|
27
27
|
relate: "%{left} should be %{op} %{right}"
|
28
28
|
responds_to: "does not respond to %{reference}"
|
29
29
|
string: is not a string
|
30
|
+
boolean: is not a boolean
|
30
31
|
to_boolean: does not look like a boolean
|
31
32
|
to_float: does not look like a float
|
32
33
|
to_integer: does not look like an integer
|
data/lib/datacaster/and_node.rb
CHANGED
@@ -14,6 +14,33 @@ module Datacaster
|
|
14
14
|
)
|
15
15
|
end
|
16
16
|
|
17
|
+
def to_json_schema
|
18
|
+
result =
|
19
|
+
@casters.reduce(JsonSchemaResult.new) do |result, caster|
|
20
|
+
result.apply(caster.to_json_schema, caster.to_json_schema_attributes)
|
21
|
+
end
|
22
|
+
|
23
|
+
mapping =
|
24
|
+
@casters.reduce({}) do |result, caster|
|
25
|
+
result.merge(caster.to_json_schema_attributes[:remaped])
|
26
|
+
end
|
27
|
+
|
28
|
+
result.remap(mapping)
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_json_schema_attributes
|
32
|
+
super.merge(
|
33
|
+
required:
|
34
|
+
@casters.any? { |caster| caster.to_json_schema_attributes[:required] },
|
35
|
+
picked:
|
36
|
+
@casters.flat_map { |caster| caster.to_json_schema_attributes[:picked] },
|
37
|
+
remaped:
|
38
|
+
@casters.reduce({}) do |result, caster|
|
39
|
+
result.merge(caster.to_json_schema_attributes[:remaped])
|
40
|
+
end
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
17
44
|
def inspect
|
18
45
|
"#<Datacaster::AndNode casters: #{@casters.inspect}>"
|
19
46
|
end
|
@@ -22,6 +22,20 @@ module Datacaster
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
+
|
26
|
+
def to_json_schema
|
27
|
+
[@left, @right].reduce(JsonSchemaResult.new) do |result, caster|
|
28
|
+
result.apply(caster.to_json_schema)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_json_schema_attributes
|
33
|
+
super.merge(
|
34
|
+
required:
|
35
|
+
[@left, @right].any? { |caster| caster.to_json_schema_attributes[:required] }
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
25
39
|
def inspect
|
26
40
|
"#<Datacaster::AndWithErrorAggregationNode L: #{@left.inspect} R: #{@right.inspect}>"
|
27
41
|
end
|
@@ -1,7 +1,8 @@
|
|
1
1
|
module Datacaster
|
2
2
|
class ArraySchema < Base
|
3
|
-
def initialize(element_caster, error_keys = {})
|
3
|
+
def initialize(element_caster, error_keys = {}, allow_empty: false)
|
4
4
|
@element_caster = element_caster
|
5
|
+
@allow_empty = allow_empty
|
5
6
|
|
6
7
|
@not_array_error_keys = ['.array', 'datacaster.errors.array']
|
7
8
|
@not_array_error_keys.unshift(error_keys[:array]) if error_keys[:array]
|
@@ -12,7 +13,7 @@ module Datacaster
|
|
12
13
|
|
13
14
|
def cast(array, runtime:)
|
14
15
|
return Datacaster.ErrorResult(I18nValues::Key.new(@not_array_error_keys, value: array)) if !array.respond_to?(:map) || !array.respond_to?(:zip)
|
15
|
-
return Datacaster.ErrorResult(I18nValues::Key.new(@empty_error_keys, value: array)) if array.empty?
|
16
|
+
return Datacaster.ErrorResult(I18nValues::Key.new(@empty_error_keys, value: array)) if array.empty? && !@allow_empty
|
16
17
|
|
17
18
|
runtime.will_check!
|
18
19
|
|
@@ -30,6 +31,13 @@ module Datacaster
|
|
30
31
|
end
|
31
32
|
end
|
32
33
|
|
34
|
+
def to_json_schema
|
35
|
+
JsonSchemaResult.new({
|
36
|
+
'type' => 'array',
|
37
|
+
'items' => @element_caster.to_json_schema
|
38
|
+
})
|
39
|
+
end
|
40
|
+
|
33
41
|
def inspect
|
34
42
|
"#<Datacaster::ArraySchema [#{@element_caster.inspect}]>"
|
35
43
|
end
|
@@ -10,6 +10,14 @@ module Datacaster
|
|
10
10
|
transform_result(result)
|
11
11
|
end
|
12
12
|
|
13
|
+
def to_json_schema
|
14
|
+
@base.to_json_schema
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_json_schema_attributes
|
18
|
+
@base.to_json_schema_attributes
|
19
|
+
end
|
20
|
+
|
13
21
|
def inspect
|
14
22
|
"#<#{self.class.name} base: #{@base.inspect}>"
|
15
23
|
end
|
@@ -72,6 +72,28 @@ module Datacaster
|
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
75
|
+
def to_json_schema
|
76
|
+
result = @fields.values.reduce(JsonSchemaResult.new) do |result, caster|
|
77
|
+
result.apply(caster.to_json_schema)
|
78
|
+
end
|
79
|
+
|
80
|
+
result.without_focus
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_json_schema_attributes
|
84
|
+
super.merge(
|
85
|
+
remaped: @fields.flat_map do |key, caster|
|
86
|
+
picked = caster.to_json_schema_attributes[:picked]
|
87
|
+
|
88
|
+
if picked.any?
|
89
|
+
picked.map { |picked| { picked.to_s => key.to_s } }
|
90
|
+
else
|
91
|
+
{ nil => key.to_s }
|
92
|
+
end
|
93
|
+
end.reduce(&:merge)
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
75
97
|
def inspect
|
76
98
|
field_descriptions =
|
77
99
|
@fields.map do |k, v|
|
@@ -42,6 +42,16 @@ module Datacaster
|
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
|
+
def to_json_schema
|
46
|
+
not_hidden_fields = @fields.reject { |_k ,v| v.to_json_schema_attributes[:hidden] }
|
47
|
+
|
48
|
+
JsonSchemaResult.new({
|
49
|
+
"type" => "object",
|
50
|
+
"properties" => not_hidden_fields.map { |k, v| [k.to_s, v.to_json_schema] }.to_h,
|
51
|
+
"required" => not_hidden_fields.select { |_k ,v| v.to_json_schema_attributes[:required] }.keys.map(&:to_s),
|
52
|
+
})
|
53
|
+
end
|
54
|
+
|
45
55
|
def inspect
|
46
56
|
field_descriptions =
|
47
57
|
@fields.map do |k, v|
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Datacaster
|
2
|
+
class JsonSchemaAttributes < Base
|
3
|
+
def initialize(base, schema_attributes = {}, &block)
|
4
|
+
@base = base
|
5
|
+
@schema_attributes = schema_attributes
|
6
|
+
@block = block
|
7
|
+
end
|
8
|
+
|
9
|
+
def cast(object, runtime:)
|
10
|
+
@base.cast(object, runtime: runtime)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_json_schema_attributes
|
14
|
+
result = @base.to_json_schema_attributes
|
15
|
+
result = result.merge(@schema_attributes)
|
16
|
+
result = @block.(result) if @block
|
17
|
+
result
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_json_schema
|
21
|
+
@base.to_json_schema
|
22
|
+
end
|
23
|
+
|
24
|
+
def inspect
|
25
|
+
"#<#{self.class.name} base: #{@base.inspect}>"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Datacaster
|
2
|
+
class JsonSchemaNode < Base
|
3
|
+
def initialize(base, schema_attributes = {}, &block)
|
4
|
+
@base = base
|
5
|
+
@schema_attributes = schema_attributes.transform_keys(&:to_s)
|
6
|
+
@block = block
|
7
|
+
end
|
8
|
+
|
9
|
+
def cast(object, runtime:)
|
10
|
+
@base.cast(object, runtime: runtime)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_json_schema
|
14
|
+
result = @base.to_json_schema
|
15
|
+
result = result.apply(@schema_attributes)
|
16
|
+
result = @block.(result) if @block
|
17
|
+
result
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_json_schema_attributes
|
21
|
+
@base.to_json_schema_attributes
|
22
|
+
end
|
23
|
+
|
24
|
+
def inspect
|
25
|
+
"#<#{self.class.name} base: #{@base.inspect}>"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
module Datacaster
|
2
|
+
class JsonSchemaResult < Hash
|
3
|
+
def initialize(from = {}, focus = nil)
|
4
|
+
merge!(from)
|
5
|
+
|
6
|
+
if from.is_a?(self.class)
|
7
|
+
@focus = from.focus
|
8
|
+
else
|
9
|
+
@focus = []
|
10
|
+
end
|
11
|
+
|
12
|
+
if focus == false || @focus == false
|
13
|
+
@focus = false
|
14
|
+
return
|
15
|
+
end
|
16
|
+
|
17
|
+
@focus << focus if focus
|
18
|
+
@target = self
|
19
|
+
@focus.each { |k| @target = @target['properties'][k] }
|
20
|
+
end
|
21
|
+
|
22
|
+
def with_focus_key(key)
|
23
|
+
result = apply(
|
24
|
+
"type" => "object",
|
25
|
+
"properties" => key ? { key => {} } : {}
|
26
|
+
)
|
27
|
+
self.class.new(result, key)
|
28
|
+
end
|
29
|
+
|
30
|
+
def without_focus
|
31
|
+
self.class.new(self).reset_focus
|
32
|
+
end
|
33
|
+
|
34
|
+
def remap(mapping)
|
35
|
+
return self if mapping.empty?
|
36
|
+
|
37
|
+
if self['oneOf'] || self['anyOf']
|
38
|
+
type = self.keys.first
|
39
|
+
|
40
|
+
self[type] = self[type].map { |props| object_remap(props, mapping) }
|
41
|
+
else
|
42
|
+
object_remap(self, mapping)
|
43
|
+
end
|
44
|
+
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def object_remap(value, mapping)
|
49
|
+
return value unless value['type'] == 'object'
|
50
|
+
|
51
|
+
mapping.each do |from, to|
|
52
|
+
from_props = value['properties'][from] || {}
|
53
|
+
to_props = value['properties'][to] || {}
|
54
|
+
|
55
|
+
one_to_one_remap = mapping.values.count { _1 == to } == 1
|
56
|
+
|
57
|
+
properties_from = value['properties'].delete(from)
|
58
|
+
properties_to = value['properties'].delete(to)
|
59
|
+
|
60
|
+
if from && (properties_to || properties_from)
|
61
|
+
value['properties'][from] =
|
62
|
+
if one_to_one_remap
|
63
|
+
Datacaster::Utils.deep_merge(to_props, from_props)
|
64
|
+
else
|
65
|
+
self.class.new(properties_from || {})
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
required_from = value['required']&.delete(from)
|
70
|
+
required_to = value['required']&.delete(to)
|
71
|
+
|
72
|
+
if from && one_to_one_remap && (required_from || required_to)
|
73
|
+
value['required'] << from
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
value
|
78
|
+
end
|
79
|
+
|
80
|
+
def apply(other, schema_attributes = {})
|
81
|
+
return self if other.nil? || other.empty?
|
82
|
+
return JsonSchemaResult.new(other) if empty?
|
83
|
+
|
84
|
+
if @focus && !@focus.empty?
|
85
|
+
return with_updated_target(JsonSchemaResult.new(@target).apply(other))
|
86
|
+
end
|
87
|
+
|
88
|
+
# validations after pick(a, b) & transform
|
89
|
+
self_type = self['type']
|
90
|
+
other_type = other['type']
|
91
|
+
|
92
|
+
if (self_type == 'object' || self_type == 'array') && (other_type != 'object' && other_type != 'array')
|
93
|
+
return JsonSchemaResult.new(self)
|
94
|
+
end
|
95
|
+
|
96
|
+
result = self.class.new({})
|
97
|
+
|
98
|
+
if self['required'] || other['required']
|
99
|
+
result['required'] = (
|
100
|
+
(self['required'] || []).to_set | (other['required'] || []).to_set
|
101
|
+
).to_a
|
102
|
+
end
|
103
|
+
|
104
|
+
nested =
|
105
|
+
if self['properties'] && (other['items'] || self['items']) ||
|
106
|
+
self['items'] && (self['properties'] || other['properties']) ||
|
107
|
+
other['items'] && other['properties']
|
108
|
+
raise RuntimeError, "can't merge json schemas due to wrong items/properties combination " \
|
109
|
+
"for #{self.inspect} and #{other.inspect}", caller
|
110
|
+
elsif self['properties'] || other['properties']
|
111
|
+
'properties'
|
112
|
+
elsif self['items'] || other['items']
|
113
|
+
'items'
|
114
|
+
else
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
|
118
|
+
if nested
|
119
|
+
result[nested] = {}
|
120
|
+
|
121
|
+
keys = (self[nested] || {}).keys + (other[nested] || {}).keys
|
122
|
+
keys = keys.to_set
|
123
|
+
|
124
|
+
keys.each do |k|
|
125
|
+
one_k = self[nested] && self[nested][k] || {}
|
126
|
+
two_k = other[nested] && other[nested][k] || {}
|
127
|
+
|
128
|
+
if !one_k.is_a?(Hash) || !two_k.is_a?(Hash)
|
129
|
+
if one_k.empty? && !two_k.is_a?(Hash)
|
130
|
+
result[nested][k] = two_k
|
131
|
+
elsif two_k.empty? && !one_k.is_a?(Hash)
|
132
|
+
result[nested][k] = one_k
|
133
|
+
elsif one_k == two_k
|
134
|
+
result[nested][k] = one_k
|
135
|
+
else
|
136
|
+
raise RuntimeError, "can't merge json schemas due to wrong items/properties combination " \
|
137
|
+
"for #{self.inspect} and #{other.inspect}", caller
|
138
|
+
end
|
139
|
+
elsif one_k.is_a?(Hash) && two_k.is_a?(Hash)
|
140
|
+
result[nested][k] = self.class.new(one_k).apply(two_k)
|
141
|
+
else
|
142
|
+
raise RuntimeError, "can't merge json schemas due to wrong items/properties combination " \
|
143
|
+
"for #{self.inspect} and #{other.inspect}", caller
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
if self['description'] || other['description']
|
150
|
+
result['description'] = other['description'] || self['description']
|
151
|
+
end
|
152
|
+
|
153
|
+
(self.keys + other.keys - %w(required properties items description)).to_set.each do |k|
|
154
|
+
# used to merge switch schemas
|
155
|
+
# TODO: подумать как сделать в обратную сторону
|
156
|
+
# FULL_DETAILS_SCHEMA = Datacaster.partial_schema do
|
157
|
+
# LoanTransferMethods::InitiatorTransferDetailsStruct.schema & switch(
|
158
|
+
# :kind,
|
159
|
+
# product: hash_schema(
|
160
|
+
# currency: string,
|
161
|
+
# us_only: boolean,
|
162
|
+
# name: string,
|
163
|
+
# values: array_of(integer),
|
164
|
+
# official_provider_name: string,
|
165
|
+
# )
|
166
|
+
# ).else(pass)
|
167
|
+
# end
|
168
|
+
|
169
|
+
if schema_attributes[:extendable]
|
170
|
+
case k
|
171
|
+
in 'oneOf'
|
172
|
+
self_one_of = self[k]
|
173
|
+
other_one_of = other[k]
|
174
|
+
|
175
|
+
result_objects = other_one_of.map do |other_obj|
|
176
|
+
other_obj_properties = other_obj['properties'].to_a
|
177
|
+
|
178
|
+
max_same = -1
|
179
|
+
|
180
|
+
# basicly is guessing here, but must be ok in most cases
|
181
|
+
merge_candidate = self_one_of.max_by do |self_obj|
|
182
|
+
next -1 if self_obj.empty?
|
183
|
+
|
184
|
+
self_obj_properties = self_obj['properties'].to_a
|
185
|
+
|
186
|
+
max_same = (self_obj_properties & other_obj_properties).size
|
187
|
+
|
188
|
+
max_same
|
189
|
+
end
|
190
|
+
|
191
|
+
next other_obj if max_same < 1
|
192
|
+
|
193
|
+
Datacaster::Utils.deep_merge(other_obj, merge_candidate)
|
194
|
+
end
|
195
|
+
|
196
|
+
next result[k] = result_objects
|
197
|
+
else
|
198
|
+
raise RuntimeError, "can't merge json schemas due to conflicting field #{k} for " \
|
199
|
+
"#{inspect} and #{other.inspect}", caller
|
200
|
+
end
|
201
|
+
else
|
202
|
+
if self[k] && other[k] && self[k] != other[k]
|
203
|
+
raise RuntimeError, "can't merge json schemas due to conflicting field #{k} for " \
|
204
|
+
"#{inspect} and #{other.inspect}", caller
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
result[k] = other[k] || self[k]
|
209
|
+
end
|
210
|
+
|
211
|
+
result
|
212
|
+
end
|
213
|
+
|
214
|
+
protected
|
215
|
+
|
216
|
+
def focus
|
217
|
+
@focus
|
218
|
+
end
|
219
|
+
|
220
|
+
def reset_focus
|
221
|
+
@focus = []
|
222
|
+
@target = self
|
223
|
+
self
|
224
|
+
end
|
225
|
+
|
226
|
+
private
|
227
|
+
|
228
|
+
def with_updated_target(target)
|
229
|
+
result = self.class.new(self)
|
230
|
+
nested =
|
231
|
+
@focus[0..-2].reduce(result) do |result, k|
|
232
|
+
result['properties'][k] = result['properties'][k].dup
|
233
|
+
result['properties'][k]
|
234
|
+
end
|
235
|
+
nested['properties'][@focus[-1]] = target
|
236
|
+
result
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
data/lib/datacaster/mixin.rb
CHANGED
@@ -81,5 +81,27 @@ module Datacaster
|
|
81
81
|
def inspect
|
82
82
|
"#<Datacaster::Base>"
|
83
83
|
end
|
84
|
+
|
85
|
+
def json_schema(schema_attributes = {}, &block)
|
86
|
+
JsonSchemaNode.new(self, schema_attributes, &block)
|
87
|
+
end
|
88
|
+
|
89
|
+
def json_schema_attributes(schema_attributes = {}, &block)
|
90
|
+
JsonSchemaAttributes.new(self, schema_attributes, &block)
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_json_schema_attributes
|
94
|
+
{
|
95
|
+
required: true,
|
96
|
+
extendable: false,
|
97
|
+
remaped: {},
|
98
|
+
picked: [],
|
99
|
+
hidden: false
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def to_json_schema
|
104
|
+
JsonSchemaResult.new
|
105
|
+
end
|
84
106
|
end
|
85
107
|
end
|
data/lib/datacaster/or_node.rb
CHANGED
@@ -13,6 +13,20 @@ module Datacaster
|
|
13
13
|
@right.with_runtime(runtime).(object)
|
14
14
|
end
|
15
15
|
|
16
|
+
def to_json_schema
|
17
|
+
JsonSchemaResult.new({
|
18
|
+
"anyOf" => [@left, @right].map(&:to_json_schema)
|
19
|
+
})
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_json_schema_attributes
|
23
|
+
super.merge(
|
24
|
+
required:
|
25
|
+
@left.to_json_schema_attributes[:required] &&
|
26
|
+
@right.to_json_schema_attributes[:required]
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
16
30
|
def inspect
|
17
31
|
"#<Datacaster::OrNode L: #{@left.inspect} R: #{@right.inspect}>"
|
18
32
|
end
|
@@ -13,7 +13,13 @@ module Datacaster
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def compare(value, error_key = nil)
|
16
|
-
Comparator.new(value, error_key)
|
16
|
+
comparator = Comparator.new(value, error_key)
|
17
|
+
|
18
|
+
if value.nil?
|
19
|
+
comparator.json_schema(type: 'null')
|
20
|
+
else
|
21
|
+
comparator.json_schema(enum: [value])
|
22
|
+
end
|
17
23
|
end
|
18
24
|
|
19
25
|
def run(&block)
|
@@ -34,8 +40,8 @@ module Datacaster
|
|
34
40
|
Trier.new(catched_exception, error_key, &block)
|
35
41
|
end
|
36
42
|
|
37
|
-
def array_schema(element_caster, error_keys = {})
|
38
|
-
ArraySchema.new(DefinitionDSL.expand(element_caster), error_keys)
|
43
|
+
def array_schema(element_caster, error_keys = {}, allow_empty: false)
|
44
|
+
ArraySchema.new(DefinitionDSL.expand(element_caster), error_keys, allow_empty:)
|
39
45
|
end
|
40
46
|
alias_method :array_of, :array_schema
|
41
47
|
|
@@ -98,7 +104,7 @@ module Datacaster
|
|
98
104
|
I18nValues::Key.new(error_keys, value: x)
|
99
105
|
)
|
100
106
|
end
|
101
|
-
end
|
107
|
+
end.json_schema_attributes(required: false)
|
102
108
|
end
|
103
109
|
|
104
110
|
def any(error_key = nil)
|
@@ -135,7 +141,7 @@ module Datacaster
|
|
135
141
|
else
|
136
142
|
x
|
137
143
|
end
|
138
|
-
end
|
144
|
+
end.json_schema_attributes(required: false)
|
139
145
|
end
|
140
146
|
|
141
147
|
def merge_message_keys(*keys)
|
@@ -149,8 +155,11 @@ module Datacaster
|
|
149
155
|
end
|
150
156
|
|
151
157
|
def optional(base, on: nil)
|
152
|
-
|
153
|
-
|
158
|
+
if on == nil
|
159
|
+
return (absent | base).json_schema { base.to_json_schema }.json_schema_attributes(required: false)
|
160
|
+
end
|
161
|
+
|
162
|
+
caster = cast do |x|
|
154
163
|
if x == Datacaster.absent ||
|
155
164
|
(!on.nil? && x.respond_to?(on) && x.public_send(on))
|
156
165
|
Datacaster.ValidResult(Datacaster.absent)
|
@@ -158,10 +167,15 @@ module Datacaster
|
|
158
167
|
base.(x)
|
159
168
|
end
|
160
169
|
end
|
170
|
+
|
171
|
+
caster
|
172
|
+
.json_schema(base.to_json_schema)
|
173
|
+
.json_schema_attributes(required: false)
|
161
174
|
end
|
162
175
|
|
163
176
|
def pass
|
164
177
|
cast { |v| Datacaster::ValidResult(v) }
|
178
|
+
.json_schema_attributes(required: false)
|
165
179
|
end
|
166
180
|
|
167
181
|
def pass_if(base)
|
@@ -189,6 +203,19 @@ module Datacaster
|
|
189
203
|
end
|
190
204
|
end
|
191
205
|
|
206
|
+
json_schema = -> (previous) do
|
207
|
+
previous = previous.apply({
|
208
|
+
'type' => 'object',
|
209
|
+
'properties' => keys.map { |k, v| [k.to_s, JsonSchemaResult.new] }.to_h
|
210
|
+
})
|
211
|
+
|
212
|
+
if keys.length == 1
|
213
|
+
previous.with_focus_key(keys[0].to_s)
|
214
|
+
else
|
215
|
+
previous.with_focus_key(false)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
192
219
|
must_be(Enumerable) & cast { |input|
|
193
220
|
result =
|
194
221
|
keys.map do |key|
|
@@ -200,7 +227,7 @@ module Datacaster
|
|
200
227
|
end
|
201
228
|
result = keys.length == 1 ? result.first : result
|
202
229
|
Datacaster::ValidResult(result)
|
203
|
-
}
|
230
|
+
}.json_schema(&json_schema).json_schema_attributes(picked: keys)
|
204
231
|
end
|
205
232
|
|
206
233
|
def relate(left, op, right, error_key: nil)
|
@@ -277,7 +304,8 @@ module Datacaster
|
|
277
304
|
end
|
278
305
|
|
279
306
|
def transform_to_value(value)
|
280
|
-
|
307
|
+
value = Datacaster::Utils.deep_freeze(value)
|
308
|
+
transform { value }
|
281
309
|
end
|
282
310
|
|
283
311
|
def with(keys, caster)
|
@@ -299,7 +327,9 @@ module Datacaster
|
|
299
327
|
def numeric(error_key = nil)
|
300
328
|
error_keys = ['.numeric', 'datacaster.errors.numeric']
|
301
329
|
error_keys.unshift(error_key) if error_key
|
302
|
-
check { |x| x.is_a?(Numeric) }.
|
330
|
+
check { |x| x.is_a?(Numeric) }.
|
331
|
+
i18n_key(*error_keys).
|
332
|
+
json_schema(oneOf: [{ 'type' => 'string' }, { 'type' => 'number' }])
|
303
333
|
end
|
304
334
|
|
305
335
|
def decimal(digits = 8, error_key = nil)
|
@@ -311,32 +341,39 @@ module Datacaster
|
|
311
341
|
Float(x)
|
312
342
|
|
313
343
|
BigDecimal(x, digits)
|
314
|
-
end.i18n_key(*error_keys)
|
344
|
+
end.i18n_key(*error_keys).json_schema(type: 'string')
|
315
345
|
end
|
316
346
|
|
317
347
|
def array(error_key = nil)
|
318
348
|
error_keys = ['.array', 'datacaster.errors.array']
|
319
349
|
error_keys.unshift(error_key) if error_key
|
320
|
-
|
350
|
+
|
351
|
+
check { |x| x.is_a?(Array) }.
|
352
|
+
i18n_key(*error_keys).
|
353
|
+
json_schema(type: 'array')
|
321
354
|
end
|
322
355
|
|
323
356
|
def float(error_key = nil)
|
324
357
|
error_keys = ['.float', 'datacaster.errors.float']
|
325
358
|
error_keys.unshift(error_key) if error_key
|
326
|
-
check { |x| x.is_a?(Float) }.i18n_key(*error_keys)
|
359
|
+
check { |x| x.is_a?(Float) }.i18n_key(*error_keys).
|
360
|
+
json_schema(type: 'number', format: 'float')
|
327
361
|
end
|
328
362
|
|
329
363
|
def pattern(regexp, error_key = nil)
|
330
364
|
error_keys = ['.pattern', 'datacaster.errors.pattern']
|
331
365
|
error_keys.unshift(error_key) if error_key
|
332
|
-
string(error_key) & check { |x| x.match?(regexp) }.i18n_key(*error_keys, reference: regexp.inspect)
|
366
|
+
string(error_key) & check { |x| x.match?(regexp) }.i18n_key(*error_keys, reference: regexp.inspect).
|
367
|
+
json_schema(pattern: regexp.inspect)
|
333
368
|
end
|
334
369
|
|
335
370
|
# 'hash' would be a bad method name, because it would override built in Object#hash
|
336
371
|
def hash_value(error_key = nil)
|
337
372
|
error_keys = ['.hash_value', 'datacaster.errors.hash_value']
|
338
373
|
error_keys.unshift(error_key) if error_key
|
339
|
-
check { |x| x.is_a?(Hash) }.
|
374
|
+
check { |x| x.is_a?(Hash) }.
|
375
|
+
i18n_key(*error_keys).
|
376
|
+
json_schema(type: 'object', additionalProperties: true)
|
340
377
|
end
|
341
378
|
|
342
379
|
def hash_with_symbolized_keys(error_key = nil)
|
@@ -346,19 +383,23 @@ module Datacaster
|
|
346
383
|
def included_in(values, error_key: nil)
|
347
384
|
error_keys = ['.included_in', 'datacaster.errors.included_in']
|
348
385
|
error_keys.unshift(error_key) if error_key
|
349
|
-
check { |x| values.include?(x) }.
|
386
|
+
check { |x| values.include?(x) }.
|
387
|
+
i18n_key(*error_keys, reference: values.map(&:to_s).join(', ')).
|
388
|
+
json_schema(enum: values)
|
350
389
|
end
|
351
390
|
|
352
391
|
def integer(error_key = nil)
|
353
392
|
error_keys = ['.integer', 'datacaster.errors.integer']
|
354
393
|
error_keys.unshift(error_key) if error_key
|
355
|
-
check { |x| x.is_a?(Integer) }.i18n_key(*error_keys)
|
394
|
+
check { |x| x.is_a?(Integer) }.i18n_key(*error_keys).
|
395
|
+
json_schema(type: 'integer')
|
356
396
|
end
|
357
397
|
|
358
398
|
def integer32(error_key = nil)
|
359
399
|
error_keys = ['.integer32', 'datacaster.errors.integer32']
|
360
400
|
error_keys.unshift(error_key) if error_key
|
361
|
-
integer(error_key) & check { |x| x.abs <= 2_147_483_647 }.i18n_key(*error_keys)
|
401
|
+
integer(error_key) & check { |x| x.abs <= 2_147_483_647 }.i18n_key(*error_keys).
|
402
|
+
json_schema(format: 'int32')
|
362
403
|
end
|
363
404
|
|
364
405
|
def maximum(max, error_key = nil, inclusive: true)
|
@@ -400,7 +441,8 @@ module Datacaster
|
|
400
441
|
def string(error_key = nil)
|
401
442
|
error_keys = ['.string', 'datacaster.errors.string']
|
402
443
|
error_keys.unshift(error_key) if error_key
|
403
|
-
check { |x| x.is_a?(String) }.i18n_key(*error_keys)
|
444
|
+
check { |x| x.is_a?(String) }.i18n_key(*error_keys).
|
445
|
+
json_schema(type: 'string')
|
404
446
|
end
|
405
447
|
|
406
448
|
def non_empty_string(error_key = nil)
|
@@ -412,7 +454,7 @@ module Datacaster
|
|
412
454
|
def uuid(error_key = nil)
|
413
455
|
error_keys = ['.uuid', 'datacaster.errors.uuid']
|
414
456
|
error_keys.unshift(error_key) if error_key
|
415
|
-
|
457
|
+
pattern(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/, error_key).i18n_key(*error_keys)
|
416
458
|
end
|
417
459
|
|
418
460
|
# Form request types
|
@@ -423,7 +465,8 @@ module Datacaster
|
|
423
465
|
|
424
466
|
string(error_key) &
|
425
467
|
try(catched_exception: [ArgumentError, TypeError]) { |x| DateTime.iso8601(x) }.
|
426
|
-
i18n_key(*error_keys)
|
468
|
+
i18n_key(*error_keys).
|
469
|
+
json_schema(type: 'string', format: 'date-time')
|
427
470
|
end
|
428
471
|
|
429
472
|
def to_boolean(error_key = nil)
|
@@ -438,7 +481,23 @@ module Datacaster
|
|
438
481
|
else
|
439
482
|
Datacaster.ErrorResult(I18nValues::Key.new(error_keys, value: x))
|
440
483
|
end
|
441
|
-
end
|
484
|
+
end.json_schema(oneOf: [
|
485
|
+
{ 'type' => 'string', 'enum' => ['true', 'false', '1', '0'] },
|
486
|
+
{ 'type' => 'boolean' },
|
487
|
+
])
|
488
|
+
end
|
489
|
+
|
490
|
+
def boolean(error_key = nil)
|
491
|
+
error_keys = ['.boolean', 'datacaster.errors.boolean']
|
492
|
+
error_keys.unshift(error_key) if error_key
|
493
|
+
|
494
|
+
cast do |x|
|
495
|
+
if [false, true].include?(x)
|
496
|
+
Datacaster.ValidResult(x)
|
497
|
+
else
|
498
|
+
Datacaster.ErrorResult(I18nValues::Key.new(error_keys, value: x))
|
499
|
+
end
|
500
|
+
end.json_schema(type:'boolean')
|
442
501
|
end
|
443
502
|
|
444
503
|
def to_float(error_key = nil)
|
@@ -447,7 +506,7 @@ module Datacaster
|
|
447
506
|
|
448
507
|
Trier.new([ArgumentError, TypeError]) do |x|
|
449
508
|
Float(x)
|
450
|
-
end.i18n_key(*error_keys)
|
509
|
+
end.i18n_key(*error_keys).json_schema(type: 'number', format: 'float')
|
451
510
|
end
|
452
511
|
|
453
512
|
def to_integer(error_key = nil)
|
@@ -456,7 +515,7 @@ module Datacaster
|
|
456
515
|
|
457
516
|
Trier.new([ArgumentError, TypeError]) do |x|
|
458
517
|
Integer(x)
|
459
|
-
end.i18n_key(*error_keys)
|
518
|
+
end.i18n_key(*error_keys).json_schema(oneOf: [{ 'type' => 'string' }, { 'type' => 'number' }])
|
460
519
|
end
|
461
520
|
|
462
521
|
def optional_param(base)
|
@@ -27,10 +27,12 @@ module Datacaster
|
|
27
27
|
caster_or_value
|
28
28
|
when String, Symbol
|
29
29
|
if strict
|
30
|
-
Datacaster::Predefined.compare(caster_or_value)
|
30
|
+
Datacaster::Predefined.compare(caster_or_value).json_schema { {"type" => "string", "enum" => [caster_or_value.to_s]} }
|
31
31
|
else
|
32
|
-
|
33
|
-
Datacaster::Predefined.compare(caster_or_value.
|
32
|
+
(
|
33
|
+
Datacaster::Predefined.compare(caster_or_value.to_s) |
|
34
|
+
Datacaster::Predefined.compare(caster_or_value.to_sym)
|
35
|
+
).json_schema { {"type" => "string", "enum" => [caster_or_value.to_s]} }
|
34
36
|
end
|
35
37
|
else
|
36
38
|
Datacaster::Predefined.compare(caster_or_value)
|
@@ -43,6 +45,7 @@ module Datacaster
|
|
43
45
|
|
44
46
|
def else(else_caster)
|
45
47
|
raise ArgumentError, "Datacaster: double else clause is not permitted", caller if @else
|
48
|
+
else_caster = DefinitionDSL.expand(else_caster)
|
46
49
|
self.class.new(@base, on_casters: @ons, else_caster: else_caster, pick_key: @pick_key)
|
47
50
|
end
|
48
51
|
|
@@ -75,6 +78,34 @@ module Datacaster
|
|
75
78
|
)
|
76
79
|
end
|
77
80
|
|
81
|
+
def to_json_schema
|
82
|
+
if @ons.empty?
|
83
|
+
raise RuntimeError, "switch caster requires at least one 'on' statement: switch(...).on(condition, cast)", caller
|
84
|
+
end
|
85
|
+
|
86
|
+
base = @base.to_json_schema
|
87
|
+
|
88
|
+
schema_result = @ons.map { |on|
|
89
|
+
base.apply(on[0].to_json_schema).without_focus.apply(on[1].to_json_schema)
|
90
|
+
}
|
91
|
+
|
92
|
+
if @else
|
93
|
+
schema_result << @else.to_json_schema
|
94
|
+
end
|
95
|
+
|
96
|
+
JsonSchemaResult.new( "oneOf" => schema_result )
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_json_schema_attributes
|
100
|
+
super.merge(
|
101
|
+
extendable: true,
|
102
|
+
remaped:
|
103
|
+
[@base, @else, *@ons.map(&:last)].compact.reduce({}) do |result, caster|
|
104
|
+
result.merge(caster.to_json_schema_attributes[:remaped])
|
105
|
+
end
|
106
|
+
)
|
107
|
+
end
|
108
|
+
|
78
109
|
def inspect
|
79
110
|
"#<Datacaster::SwitchNode base: #{@base.inspect} on: #{@ons.inspect} else: #{@else.inspect} pick_key: #{@pick_key.inspect}>"
|
80
111
|
end
|
data/lib/datacaster/then_node.rb
CHANGED
@@ -26,6 +26,29 @@ module Datacaster
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
+
def to_json_schema
|
30
|
+
unless @else
|
31
|
+
raise ArgumentError.new('Datacaster: use "a & b" instead of "a.then(b)" when there is no else-clause')
|
32
|
+
end
|
33
|
+
|
34
|
+
left = @left.to_json_schema
|
35
|
+
|
36
|
+
JsonSchemaResult.new(
|
37
|
+
"oneOf" => [
|
38
|
+
(@left & @then).to_json_schema,
|
39
|
+
JsonSchemaResult.new("not" => left).apply(@else.to_json_schema)
|
40
|
+
]
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_json_schema_attributes
|
45
|
+
super.merge(
|
46
|
+
required:
|
47
|
+
@left.to_json_schema_attributes[:required] &&
|
48
|
+
@else.to_json_schema_attributes[:required]
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
29
52
|
def inspect
|
30
53
|
"#<Datacaster::ThenNode Then: #{@then.inspect} Else: #{@else.inspect}>"
|
31
54
|
end
|
data/lib/datacaster/utils.rb
CHANGED
@@ -1,7 +1,14 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
1
3
|
module Datacaster
|
2
4
|
module Utils
|
3
5
|
extend self
|
4
6
|
|
7
|
+
def deep_merge(first, second)
|
8
|
+
merger = proc { |_, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : Array === v1 && Array === v2 ? v1 | v2 : [:undefined, nil, :nil].include?(v2) ? v1 : v2 }
|
9
|
+
first.merge(second.to_h, &merger)
|
10
|
+
end
|
11
|
+
|
5
12
|
def deep_freeze(value, copy: true)
|
6
13
|
Ractor.make_shareable(value, copy:)
|
7
14
|
end
|
data/lib/datacaster/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: datacaster
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 5.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eugene Zolotarev
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-06-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -152,6 +152,9 @@ files:
|
|
152
152
|
- lib/datacaster/i18n_values/base.rb
|
153
153
|
- lib/datacaster/i18n_values/key.rb
|
154
154
|
- lib/datacaster/i18n_values/scope.rb
|
155
|
+
- lib/datacaster/json_schema_attributes.rb
|
156
|
+
- lib/datacaster/json_schema_node.rb
|
157
|
+
- lib/datacaster/json_schema_result.rb
|
155
158
|
- lib/datacaster/message_keys_merger.rb
|
156
159
|
- lib/datacaster/mixin.rb
|
157
160
|
- lib/datacaster/or_node.rb
|