schemurai 3.1.0-aarch64-linux-gnu
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +395 -0
- data/lib/schemurai/backend.rb +34 -0
- data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
- data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
- data/lib/schemurai/container_checks.rb +534 -0
- data/lib/schemurai/decimal.rb +11 -0
- data/lib/schemurai/dialect.rb +106 -0
- data/lib/schemurai/dialect_keywords.rb +91 -0
- data/lib/schemurai/dialects/draft2019_09.rb +27 -0
- data/lib/schemurai/dialects/draft2020_12.rb +27 -0
- data/lib/schemurai/dialects/draft7.rb +27 -0
- data/lib/schemurai/ecma_regexp.rb +46 -0
- data/lib/schemurai/error_message.rb +102 -0
- data/lib/schemurai/evaluation.rb +63 -0
- data/lib/schemurai/evaluation_limits.rb +65 -0
- data/lib/schemurai/evaluation_pool.rb +66 -0
- data/lib/schemurai/evaluator.rb +861 -0
- data/lib/schemurai/formats.rb +403 -0
- data/lib/schemurai/json_pointer.rb +11 -0
- data/lib/schemurai/limit_budget.rb +78 -0
- data/lib/schemurai/limit_exceeded.rb +18 -0
- data/lib/schemurai/limited_evaluator.rb +43 -0
- data/lib/schemurai/meta_schemas/draft2019_09.rb +149 -0
- data/lib/schemurai/meta_schemas/draft2020_12.rb +149 -0
- data/lib/schemurai/meta_schemas/draft7.rb +121 -0
- data/lib/schemurai/meta_schemas.rb +28 -0
- data/lib/schemurai/native/4.0/schemurai_native.so +0 -0
- data/lib/schemurai/native/diagnostics.rb +53 -0
- data/lib/schemurai/native/evaluator.rb +87 -0
- data/lib/schemurai/native/null_numeric.rb +23 -0
- data/lib/schemurai/number_checks.rb +128 -0
- data/lib/schemurai/schema_domain.rb +95 -0
- data/lib/schemurai/schema_graph.rb +538 -0
- data/lib/schemurai/schema_node.rb +55 -0
- data/lib/schemurai/string_checks.rb +166 -0
- data/lib/schemurai/type_checks.rb +59 -0
- data/lib/schemurai/unique_items.rb +68 -0
- data/lib/schemurai/value_equality.rb +72 -0
- data/lib/schemurai/version.rb +6 -0
- data/lib/schemurai/vm/compiler.rb +410 -0
- data/lib/schemurai/vm/evaluator.rb +663 -0
- data/lib/schemurai/vm/limited_evaluator.rb +27 -0
- data/lib/schemurai.rb +307 -0
- data/lib/schemurai_native.rb +4 -0
- data/sig/schemurai.rbs +108 -0
- metadata +121 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../decimal"
|
|
4
|
+
require_relative "../number_checks"
|
|
5
|
+
require_relative "../type_checks"
|
|
6
|
+
|
|
7
|
+
module Schemurai
|
|
8
|
+
module VM
|
|
9
|
+
NumberRules = Data.define(
|
|
10
|
+
:mask,
|
|
11
|
+
:maximum,
|
|
12
|
+
:minimum,
|
|
13
|
+
:exclusive_maximum,
|
|
14
|
+
:exclusive_minimum,
|
|
15
|
+
:multiple_of
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
MAXIMUM = Internal::NumberChecks::MAXIMUM
|
|
19
|
+
MINIMUM = Internal::NumberChecks::MINIMUM
|
|
20
|
+
EXCLUSIVE_MAXIMUM = Internal::NumberChecks::EXCLUSIVE_MAXIMUM
|
|
21
|
+
EXCLUSIVE_MINIMUM = Internal::NumberChecks::EXCLUSIVE_MINIMUM
|
|
22
|
+
MULTIPLE_OF = Internal::NumberChecks::MULTIPLE_OF
|
|
23
|
+
|
|
24
|
+
TypeRules = Data.define(:mask, :names)
|
|
25
|
+
|
|
26
|
+
TYPE_NULL = Internal::TypeChecks::NULL
|
|
27
|
+
TYPE_BOOLEAN = Internal::TypeChecks::BOOLEAN
|
|
28
|
+
TYPE_OBJECT = Internal::TypeChecks::OBJECT
|
|
29
|
+
TYPE_ARRAY = Internal::TypeChecks::ARRAY
|
|
30
|
+
TYPE_NUMBER = Internal::TypeChecks::NUMBER
|
|
31
|
+
TYPE_INTEGER = Internal::TypeChecks::INTEGER
|
|
32
|
+
TYPE_STRING = Internal::TypeChecks::STRING
|
|
33
|
+
|
|
34
|
+
TRACKS_EVALUATION = 1 << 0
|
|
35
|
+
TRACKS_DYNAMIC_SCOPE = 1 << 1
|
|
36
|
+
|
|
37
|
+
DIALECT = Schemurai.const_get(:Internal)::Dialect
|
|
38
|
+
KEYWORD_TYPE = DIALECT::TYPE
|
|
39
|
+
KEYWORD_ENUM = DIALECT::ENUM
|
|
40
|
+
KEYWORD_COMBINER = DIALECT::COMBINER
|
|
41
|
+
KEYWORD_NUMBER = DIALECT::NUMBER
|
|
42
|
+
KEYWORD_STRING = DIALECT::STRING
|
|
43
|
+
KEYWORD_ARRAY = DIALECT::ARRAY
|
|
44
|
+
KEYWORD_OBJECT = DIALECT::OBJECT
|
|
45
|
+
private_constant :DIALECT
|
|
46
|
+
|
|
47
|
+
TYPE_BITS = Internal::TypeChecks::BITS
|
|
48
|
+
|
|
49
|
+
TYPE_OPCODES = {
|
|
50
|
+
"null" => :type_null,
|
|
51
|
+
"boolean" => :type_boolean,
|
|
52
|
+
"object" => :type_object,
|
|
53
|
+
"array" => :type_array,
|
|
54
|
+
"number" => :type_number,
|
|
55
|
+
"integer" => :type_integer,
|
|
56
|
+
"string" => :type_string
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
59
|
+
ReferenceRules = Data.define(:value, :fragment)
|
|
60
|
+
|
|
61
|
+
StringRules = Data.define(
|
|
62
|
+
:max_length,
|
|
63
|
+
:min_length,
|
|
64
|
+
:pattern,
|
|
65
|
+
:format,
|
|
66
|
+
:format_assertion,
|
|
67
|
+
:decode_base64,
|
|
68
|
+
:parse_json
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
ArrayRules = Data.define(
|
|
72
|
+
:max_items,
|
|
73
|
+
:min_items,
|
|
74
|
+
:unique,
|
|
75
|
+
:prefix_items,
|
|
76
|
+
:items,
|
|
77
|
+
:items_list,
|
|
78
|
+
:additional,
|
|
79
|
+
:contains,
|
|
80
|
+
:min_contains,
|
|
81
|
+
:max_contains,
|
|
82
|
+
:count_contains,
|
|
83
|
+
:unevaluated
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
ObjectRules = Data.define(
|
|
87
|
+
:max_properties,
|
|
88
|
+
:min_properties,
|
|
89
|
+
:required,
|
|
90
|
+
:properties,
|
|
91
|
+
:patterns,
|
|
92
|
+
:additional,
|
|
93
|
+
:property_names,
|
|
94
|
+
:dependencies,
|
|
95
|
+
:dependent_required,
|
|
96
|
+
:dependent_schemas,
|
|
97
|
+
:unevaluated
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
ConditionalRules = Data.define(
|
|
101
|
+
:condition,
|
|
102
|
+
:then_branch,
|
|
103
|
+
:else_branch
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
class Program
|
|
107
|
+
attr_reader :node, :code, :dynamic_anchor
|
|
108
|
+
|
|
109
|
+
def initialize(node)
|
|
110
|
+
@node = node
|
|
111
|
+
@tracks_evaluation = false
|
|
112
|
+
@tracks_dynamic_scope = false
|
|
113
|
+
schema = node.schema
|
|
114
|
+
if schema.instance_of?(Hash)
|
|
115
|
+
@recursive_anchor = schema["$recursiveAnchor"] == true
|
|
116
|
+
dynamic_anchor = schema["$dynamicAnchor"]
|
|
117
|
+
@dynamic_anchor = -dynamic_anchor if dynamic_anchor.instance_of?(String)
|
|
118
|
+
else
|
|
119
|
+
@recursive_anchor = false
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def emit(code, opcode, operand)
|
|
124
|
+
code.push(opcode, operand)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def track_evaluation!
|
|
128
|
+
@tracks_evaluation = true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def track_dynamic_scope!
|
|
132
|
+
@tracks_dynamic_scope = true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def finish(code)
|
|
136
|
+
flags = 0
|
|
137
|
+
flags |= TRACKS_EVALUATION if @tracks_evaluation
|
|
138
|
+
flags |= TRACKS_DYNAMIC_SCOPE if @tracks_dynamic_scope
|
|
139
|
+
code[0] = flags
|
|
140
|
+
@code = code.freeze
|
|
141
|
+
freeze
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def recursive_anchor?
|
|
145
|
+
@recursive_anchor
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def tracks_dynamic_scope?
|
|
149
|
+
@tracks_dynamic_scope
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
class Compiler
|
|
154
|
+
def initialize(graph)
|
|
155
|
+
@graph = graph
|
|
156
|
+
@programs = {}.compare_by_identity
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def compile(node)
|
|
160
|
+
program = @programs[node]
|
|
161
|
+
return program if program
|
|
162
|
+
|
|
163
|
+
program = Program.new(node)
|
|
164
|
+
@programs[node] = program
|
|
165
|
+
program.finish(compile_code(node, program))
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def compile_all
|
|
169
|
+
@graph.nodes.each { |node| compile(node) }
|
|
170
|
+
self
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def resolve(program, reference)
|
|
174
|
+
compile(@graph.resolve(program.node, reference))
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
private def compile_child(node, parent)
|
|
178
|
+
child = compile(node)
|
|
179
|
+
parent.track_dynamic_scope! if child.tracks_dynamic_scope?
|
|
180
|
+
child
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private def compile_code(node, program)
|
|
184
|
+
schema = node.schema
|
|
185
|
+
return program.emit([0], :boolean, schema) if schema.equal?(true) || schema.equal?(false)
|
|
186
|
+
return [0] unless schema.is_a?(Hash)
|
|
187
|
+
|
|
188
|
+
code = [0]
|
|
189
|
+
if schema.key?("$ref")
|
|
190
|
+
program.track_dynamic_scope!
|
|
191
|
+
program.emit(code, :ref, compile_reference(schema["$ref"]))
|
|
192
|
+
return code unless node.dialect.ref_siblings?
|
|
193
|
+
end
|
|
194
|
+
if schema.key?("$recursiveRef")
|
|
195
|
+
program.track_dynamic_scope!
|
|
196
|
+
program.emit(code, :recursive_ref, compile_reference(schema["$recursiveRef"]))
|
|
197
|
+
end
|
|
198
|
+
if schema.key?("$dynamicRef")
|
|
199
|
+
program.track_dynamic_scope!
|
|
200
|
+
program.emit(code, :dynamic_ref, compile_reference(schema["$dynamicRef"]))
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
mask = node.keyword_mask
|
|
204
|
+
compile_type(code, program, schema["type"]) if (mask & KEYWORD_TYPE) != 0 && schema.key?("type")
|
|
205
|
+
if (mask & KEYWORD_ENUM) != 0
|
|
206
|
+
program.emit(code, :enum, snapshot(schema["enum"])) if schema.key?("enum")
|
|
207
|
+
program.emit(code, :const, snapshot(schema["const"])) if schema.key?("const")
|
|
208
|
+
end
|
|
209
|
+
compile_combiners(code, program, node, schema) if (mask & KEYWORD_COMBINER) != 0
|
|
210
|
+
program.emit(code, :number, compile_number(schema)) if (mask & KEYWORD_NUMBER) != 0
|
|
211
|
+
if (mask & KEYWORD_STRING) != 0 || node.format
|
|
212
|
+
program.emit(code, :string, compile_string(node, schema))
|
|
213
|
+
end
|
|
214
|
+
if (mask & KEYWORD_ARRAY) != 0
|
|
215
|
+
rules = compile_array(node, schema, program)
|
|
216
|
+
program.track_evaluation! if rules.unevaluated
|
|
217
|
+
program.emit(code, :array, rules)
|
|
218
|
+
end
|
|
219
|
+
if (mask & KEYWORD_OBJECT) != 0
|
|
220
|
+
rules = compile_object(node, schema, program)
|
|
221
|
+
program.track_evaluation! if rules.unevaluated
|
|
222
|
+
program.emit(code, :object, rules)
|
|
223
|
+
end
|
|
224
|
+
fuse_type_instruction(code)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
private def fuse_type_instruction(code)
|
|
228
|
+
index = 1
|
|
229
|
+
while index + 3 < code.length
|
|
230
|
+
type_opcode = code[index]
|
|
231
|
+
rules_opcode = code[index + 2]
|
|
232
|
+
fused_opcode = case type_opcode
|
|
233
|
+
when :type_object then :typed_object if rules_opcode == :object
|
|
234
|
+
when :type_array then :typed_array if rules_opcode == :array
|
|
235
|
+
when :type_string then :typed_string if rules_opcode == :string
|
|
236
|
+
when :type_number then :typed_number if rules_opcode == :number
|
|
237
|
+
when :type_integer then :typed_integer if rules_opcode == :number
|
|
238
|
+
end
|
|
239
|
+
if fused_opcode
|
|
240
|
+
code[index] = fused_opcode
|
|
241
|
+
code[index + 1] = code[index + 3]
|
|
242
|
+
code.slice!(index + 2, 2)
|
|
243
|
+
break
|
|
244
|
+
end
|
|
245
|
+
index += 2
|
|
246
|
+
end
|
|
247
|
+
code
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
private def compile_combiners(code, program, node, schema)
|
|
251
|
+
%w[allOf anyOf oneOf].each do |keyword|
|
|
252
|
+
next unless schema.key?(keyword)
|
|
253
|
+
|
|
254
|
+
children = schema[keyword].each_index.map do |index|
|
|
255
|
+
compile_child(node.child(keyword, index), program)
|
|
256
|
+
end
|
|
257
|
+
program.emit(code, keyword.to_sym, children)
|
|
258
|
+
end
|
|
259
|
+
program.emit(code, :not, compile_child(node.child("not"), program)) if schema.key?("not")
|
|
260
|
+
return unless schema.key?("if")
|
|
261
|
+
|
|
262
|
+
program.emit(
|
|
263
|
+
code,
|
|
264
|
+
:conditional,
|
|
265
|
+
ConditionalRules.new(
|
|
266
|
+
compile_child(node.child("if"), program),
|
|
267
|
+
schema.key?("then") ? compile_child(node.child("then"), program) : nil,
|
|
268
|
+
schema.key?("else") ? compile_child(node.child("else"), program) : nil
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
private def compile_number(schema)
|
|
274
|
+
mask = 0
|
|
275
|
+
mask |= MAXIMUM if schema.key?("maximum")
|
|
276
|
+
mask |= MINIMUM if schema.key?("minimum")
|
|
277
|
+
mask |= EXCLUSIVE_MAXIMUM if schema.key?("exclusiveMaximum")
|
|
278
|
+
mask |= EXCLUSIVE_MINIMUM if schema.key?("exclusiveMinimum")
|
|
279
|
+
mask |= MULTIPLE_OF if schema.key?("multipleOf")
|
|
280
|
+
NumberRules.new(
|
|
281
|
+
mask,
|
|
282
|
+
compile_decimal(schema["maximum"]),
|
|
283
|
+
compile_decimal(schema["minimum"]),
|
|
284
|
+
compile_decimal(schema["exclusiveMaximum"]),
|
|
285
|
+
compile_decimal(schema["exclusiveMinimum"]),
|
|
286
|
+
compile_decimal(schema["multipleOf"])
|
|
287
|
+
)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
private def compile_reference(reference)
|
|
291
|
+
value = snapshot(reference)
|
|
292
|
+
separator = value.index("#")
|
|
293
|
+
fragment = separator ? -value[(separator + 1)..] : nil
|
|
294
|
+
ReferenceRules.new(value, fragment)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
private def compile_type(code, program, types)
|
|
298
|
+
return program.emit(code, TYPE_OPCODES.fetch(types), nil) unless types.is_a?(Array)
|
|
299
|
+
|
|
300
|
+
mask = types.reduce(0) { |result, type| result | TYPE_BITS.fetch(type) }
|
|
301
|
+
program.emit(code, :types, TypeRules.new(mask, snapshot(types)))
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
private def compile_string(node, schema)
|
|
305
|
+
StringRules.new(
|
|
306
|
+
schema["maxLength"],
|
|
307
|
+
schema["minLength"],
|
|
308
|
+
snapshot(schema["pattern"]),
|
|
309
|
+
node.format,
|
|
310
|
+
node.dialect.format_assertion?,
|
|
311
|
+
schema["contentEncoding"] == "base64",
|
|
312
|
+
schema["contentMediaType"] == "application/json"
|
|
313
|
+
)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
private def compile_array(node, schema, program)
|
|
317
|
+
prefix_items = if schema["prefixItems"].is_a?(Array)
|
|
318
|
+
schema["prefixItems"].each_index.map do |index|
|
|
319
|
+
compile_child(node.child("prefixItems", index), program)
|
|
320
|
+
end.freeze
|
|
321
|
+
end
|
|
322
|
+
items = if schema["items"].is_a?(Array)
|
|
323
|
+
schema["items"].each_index.map do |index|
|
|
324
|
+
compile_child(node.child("items", index), program)
|
|
325
|
+
end.freeze
|
|
326
|
+
elsif !schema["items"].nil?
|
|
327
|
+
compile_child(node.child("items"), program)
|
|
328
|
+
end
|
|
329
|
+
ArrayRules.new(
|
|
330
|
+
schema["maxItems"],
|
|
331
|
+
schema["minItems"],
|
|
332
|
+
schema["uniqueItems"],
|
|
333
|
+
prefix_items,
|
|
334
|
+
items,
|
|
335
|
+
items.is_a?(Array),
|
|
336
|
+
schema.key?("additionalItems") ? compile_child(node.child("additionalItems"), program) : nil,
|
|
337
|
+
schema.key?("contains") ? compile_child(node.child("contains"), program) : nil,
|
|
338
|
+
schema.fetch("minContains", 1),
|
|
339
|
+
schema.fetch("maxContains", Float::INFINITY),
|
|
340
|
+
node.dialect.keywords.key?("minContains"),
|
|
341
|
+
schema.key?("unevaluatedItems") ? compile_child(node.child("unevaluatedItems"), program) : nil
|
|
342
|
+
)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
private def compile_object(node, schema, program)
|
|
346
|
+
ObjectRules.new(
|
|
347
|
+
schema["maxProperties"],
|
|
348
|
+
schema["minProperties"],
|
|
349
|
+
snapshot(schema["required"]),
|
|
350
|
+
compile_map(node, schema, "properties", program),
|
|
351
|
+
compile_optional_map(node, schema, "patternProperties", program),
|
|
352
|
+
schema.key?("additionalProperties") ? compile_child(node.child("additionalProperties"), program) : nil,
|
|
353
|
+
schema.key?("propertyNames") ? compile_child(node.child("propertyNames"), program) : nil,
|
|
354
|
+
compile_dependencies(node, schema, program),
|
|
355
|
+
schema.fetch("dependentRequired", {}).map do |name, required_names|
|
|
356
|
+
[snapshot(name), snapshot(required_names)].freeze
|
|
357
|
+
end.then { |entries| entries.empty? ? nil : entries.freeze },
|
|
358
|
+
compile_optional_map(node, schema, "dependentSchemas", program),
|
|
359
|
+
schema.key?("unevaluatedProperties") ? compile_child(node.child("unevaluatedProperties"), program) : nil
|
|
360
|
+
)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
private def compile_map(node, schema, keyword, program)
|
|
364
|
+
schema.fetch(keyword, {}).each_key.to_h do |name|
|
|
365
|
+
[snapshot(name), compile_child(node.child(keyword, name), program)]
|
|
366
|
+
end.freeze
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
private def compile_optional_map(node, schema, keyword, program)
|
|
370
|
+
compiled = compile_map(node, schema, keyword, program)
|
|
371
|
+
compiled unless compiled.empty?
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
private def compile_dependencies(node, schema, program)
|
|
375
|
+
compiled = schema.fetch("dependencies", {}).map do |name, dependency|
|
|
376
|
+
compiled = if dependency.is_a?(Array)
|
|
377
|
+
snapshot(dependency)
|
|
378
|
+
else
|
|
379
|
+
compile_child(node.child("dependencies", name), program)
|
|
380
|
+
end
|
|
381
|
+
[snapshot(name), compiled].freeze
|
|
382
|
+
end
|
|
383
|
+
compiled.freeze unless compiled.empty?
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Keep aliases inside one operand while copying away from caller-owned
|
|
387
|
+
# containers. A fresh identity table also rechecks later compilations.
|
|
388
|
+
private def snapshot(value, copies = {}.compare_by_identity)
|
|
389
|
+
return copies[value] if copies.key?(value)
|
|
390
|
+
|
|
391
|
+
case value
|
|
392
|
+
when Hash
|
|
393
|
+
copies[value] = value.to_h { |key, item| [snapshot(key, copies), snapshot(item, copies)] }.freeze
|
|
394
|
+
when Array
|
|
395
|
+
copies[value] = value.map { |item| snapshot(item, copies) }.freeze
|
|
396
|
+
when String
|
|
397
|
+
-value
|
|
398
|
+
else
|
|
399
|
+
value
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
private def compile_decimal(value)
|
|
404
|
+
return value if value.nil? || value.is_a?(Integer) || value.is_a?(Rational)
|
|
405
|
+
|
|
406
|
+
Internal::Decimal.parse(value)
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|