lutaml-model 0.8.33 → 0.8.34
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 +5 -0
- data/Gemfile +4 -0
- data/README.adoc +39 -0
- data/docs/conversion_caching.adoc +143 -0
- data/lib/compat/opal/lutaml_model_boot.rb +15 -0
- data/lib/lutaml/model/attribute.rb +89 -0
- data/lib/lutaml/model/config.rb +13 -0
- data/lib/lutaml/model/configuration.rb +23 -0
- data/lib/lutaml/model/error/fraction_digits_error.rb +20 -0
- data/lib/lutaml/model/error/length_error.rb +19 -0
- data/lib/lutaml/model/error/max_exclusive_error.rb +19 -0
- data/lib/lutaml/model/error/max_inclusive_error.rb +20 -0
- data/lib/lutaml/model/error/max_length_error.rb +20 -0
- data/lib/lutaml/model/error/min_exclusive_error.rb +19 -0
- data/lib/lutaml/model/error/min_inclusive_error.rb +20 -0
- data/lib/lutaml/model/error/min_length_error.rb +20 -0
- data/lib/lutaml/model/error/restriction_error.rb +8 -0
- data/lib/lutaml/model/error/total_digits_error.rb +20 -0
- data/lib/lutaml/model/error/type/max_exclusive_error.rb +20 -0
- data/lib/lutaml/model/error/type/min_exclusive_error.rb +20 -0
- data/lib/lutaml/model/error/type.rb +2 -0
- data/lib/lutaml/model/restriction_validation.rb +437 -0
- data/lib/lutaml/model/schema/definitions/restricted_type.rb +4 -2
- data/lib/lutaml/model/schema/renderers/restricted_type.rb +134 -4
- data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +3 -0
- data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +9 -1
- data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +2 -0
- data/lib/lutaml/model/schema/templates.rb +2 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +39 -7
- data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +3 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +18 -7
- data/lib/lutaml/model/serialize/builder.rb +4 -5
- data/lib/lutaml/model/serialize/conversion_caching.rb +124 -0
- data/lib/lutaml/model/serialize/enum_handling.rb +50 -11
- data/lib/lutaml/model/serialize/format_conversion.rb +46 -38
- data/lib/lutaml/model/serialize/initialization.rb +4 -2
- data/lib/lutaml/model/serialize.rb +61 -0
- data/lib/lutaml/model/services/type/validator.rb +23 -0
- data/lib/lutaml/model/type/string.rb +28 -3
- data/lib/lutaml/model/type/value.rb +231 -0
- data/lib/lutaml/model/type.rb +2 -0
- data/lib/lutaml/model/validation.rb +2 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +16 -0
- data/lib/lutaml/xml/model_transform.rb +4 -1
- data/lib/lutaml/xml/schema/xsd/max_exclusive.rb +1 -1
- data/lib/lutaml/xml/schema/xsd/min_exclusive.rb +1 -1
- data/lib/lutaml/xml/schema/xsd_schema.rb +183 -11
- data/lib/lutaml/xml/transformation/order_reconciler.rb +267 -0
- data/lib/lutaml/xml/transformation/ordered_applier.rb +80 -122
- data/lib/lutaml/xml/transformation/rule_compiler.rb +2 -0
- data/lib/lutaml/xml/transformation.rb +2 -0
- data/lib/lutaml/xml/transformation_support.rb +1 -0
- data/spec/fixtures/xml/restriction_facets.xsd +46 -0
- data/spec/lutaml/model/attribute_restriction_spec.rb +472 -0
- data/spec/lutaml/model/attribute_spec.rb +7 -2
- data/spec/lutaml/model/collection_reader_liveness_spec.rb +516 -0
- data/spec/lutaml/model/conversion_caching_spec.rb +280 -0
- data/spec/lutaml/model/lazy_collection_spec.rb +28 -3
- data/spec/lutaml/model/parsed_model_mutation_spec.rb +486 -0
- data/spec/lutaml/model/phantom_value_spec.rb +799 -0
- data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +1 -0
- data/spec/lutaml/model/schema/xsd_schema_spec.rb +340 -695
- data/spec/lutaml/model/serialize_perf_guard_spec.rb +53 -1
- data/spec/lutaml/model/type_digit_facet_spec.rb +229 -0
- data/spec/lutaml/model/type_facet_restriction_spec.rb +667 -0
- data/spec/lutaml/model/type_white_space_facet_spec.rb +132 -0
- data/spec/lutaml/xml/schema/compiler_spec.rb +302 -0
- data/spec/lutaml/xml/schema/facet_round_trip_spec.rb +153 -0
- metadata +27 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
class MinLengthError < RestrictionError
|
|
6
|
+
def initialize(attr_name, value, min_length)
|
|
7
|
+
@attr_name = attr_name
|
|
8
|
+
@value = value
|
|
9
|
+
@min_length = min_length
|
|
10
|
+
|
|
11
|
+
super()
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
"#{@attr_name} length (#{@value.to_s.length}) is less than " \
|
|
16
|
+
"the minimum required length #{@min_length}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
class TotalDigitsError < RestrictionError
|
|
6
|
+
def initialize(attr_name, value, total_digits)
|
|
7
|
+
@attr_name = attr_name
|
|
8
|
+
@value = value
|
|
9
|
+
@total_digits = total_digits
|
|
10
|
+
|
|
11
|
+
super()
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
"#{@attr_name} (#{@value}) exceeds the maximum of " \
|
|
16
|
+
"#{@total_digits} total digits"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
class MaxExclusiveError < Error
|
|
7
|
+
def initialize(value, max_bound)
|
|
8
|
+
@value = value
|
|
9
|
+
@max_bound = max_bound
|
|
10
|
+
|
|
11
|
+
super()
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
"Value #{@value} is not less than the exclusive maximum #{@max_bound}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
class MinExclusiveError < Error
|
|
7
|
+
def initialize(value, min_bound)
|
|
8
|
+
@value = value
|
|
9
|
+
@min_bound = min_bound
|
|
10
|
+
|
|
11
|
+
super()
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
"Value #{@value} is not greater than the exclusive minimum #{@min_bound}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -7,6 +7,8 @@ module Lutaml
|
|
|
7
7
|
autoload :InvalidValueError, "#{__dir__}/type/invalid_value_error"
|
|
8
8
|
autoload :MinBoundError, "#{__dir__}/type/min_bound_error"
|
|
9
9
|
autoload :MaxBoundError, "#{__dir__}/type/max_bound_error"
|
|
10
|
+
autoload :MinExclusiveError, "#{__dir__}/type/min_exclusive_error"
|
|
11
|
+
autoload :MaxExclusiveError, "#{__dir__}/type/max_exclusive_error"
|
|
10
12
|
autoload :PatternNotMatchedError,
|
|
11
13
|
"#{__dir__}/type/pattern_not_matched_error"
|
|
12
14
|
autoload :MinLengthError, "#{__dir__}/type/min_length_error"
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
# Module for enforcing value restrictions on attributes.
|
|
6
|
+
#
|
|
7
|
+
# Provides the lazy validation seam used by `Attribute#validate_value!`:
|
|
8
|
+
# a configuration check (applicability of the effective facets to the
|
|
9
|
+
# resolved type) and a value check (ordered bounds / string length) that
|
|
10
|
+
# reuses the shared Type validators and translates their errors into
|
|
11
|
+
# model-level errors carrying the attribute name.
|
|
12
|
+
#
|
|
13
|
+
# The effective facet set is the conjunctive merge of Layer-1 attribute
|
|
14
|
+
# options (lowered to canonical keys) and the Layer-2 facets declared on a
|
|
15
|
+
# `Type::Value` subclass; the tightest constraint per key wins.
|
|
16
|
+
module RestrictionValidation
|
|
17
|
+
# Verify the effective facets are applicable to the resolved type. The
|
|
18
|
+
# type is only known after register resolution, so this runs at
|
|
19
|
+
# validation time and raises ahead of the value checks.
|
|
20
|
+
def validate_restriction_configuration!(resolved_type)
|
|
21
|
+
facets = effective_facets(resolved_type)
|
|
22
|
+
|
|
23
|
+
if @options.key?(:signed) && !numeric_type?(resolved_type)
|
|
24
|
+
raise ArgumentError,
|
|
25
|
+
"Invalid options for `#{name}`: " \
|
|
26
|
+
"`signed` is only allowed for numeric types"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if ordered_facets?(facets) && !ordered_type?(resolved_type)
|
|
30
|
+
raise ArgumentError,
|
|
31
|
+
"Invalid options for `#{name}`: " \
|
|
32
|
+
"`min`, `max`, `inclusive` and `exclusive` are only allowed " \
|
|
33
|
+
"for numeric types or temporal (date/time) types"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if length_facets?(facets) && !string_type?(resolved_type)
|
|
37
|
+
raise ArgumentError,
|
|
38
|
+
"Invalid options for `#{name}`: " \
|
|
39
|
+
"`min_length` and `max_length` are only allowed for :string type"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if facets.key?(:pattern) && !string_type?(resolved_type)
|
|
43
|
+
raise ArgumentError,
|
|
44
|
+
"Invalid options for `#{name}`: " \
|
|
45
|
+
"`pattern` is only allowed for string-derived types"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if digit_facets?(facets) && !digit_type?(resolved_type)
|
|
49
|
+
raise ArgumentError,
|
|
50
|
+
"Invalid options for `#{name}`: " \
|
|
51
|
+
"`total_digits` and `fraction_digits` are only allowed " \
|
|
52
|
+
"for :integer and :decimal types"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Enforce the effective facets against the value, translating the reused
|
|
57
|
+
# validator's Type::* errors into model-level errors that carry the
|
|
58
|
+
# attribute name so `validate` can collect them.
|
|
59
|
+
def validate_restriction_values!(value, resolved_type)
|
|
60
|
+
facets = effective_facets(resolved_type)
|
|
61
|
+
return if facets.empty?
|
|
62
|
+
return if value.nil? || Utils.uninitialized?(value)
|
|
63
|
+
|
|
64
|
+
unless collection? && collection_instance?(value)
|
|
65
|
+
return validate_element_facets!(value, resolved_type, facets)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
value.each do |element|
|
|
69
|
+
next if element.nil? || Utils.uninitialized?(element)
|
|
70
|
+
|
|
71
|
+
validate_element_facets!(element, resolved_type, facets)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Effective facet set for schema emission: the validator's merge plus the
|
|
76
|
+
# pre-#191 `values:`/`pattern:` options. Those are enforced at runtime by
|
|
77
|
+
# a separate path (so they are absent from the validation facet set); fold
|
|
78
|
+
# them in here so the exported XSD is not weaker than what is enforced.
|
|
79
|
+
def effective_restriction_facets(resolved_type)
|
|
80
|
+
facets = effective_facets(resolved_type)
|
|
81
|
+
facets = merge_values_option(facets, resolved_type) if @options.key?(:values)
|
|
82
|
+
facets = merge_pattern_option(facets) if @options.key?(:pattern)
|
|
83
|
+
# Folding in the values: option can empty an enumeration (a disjoint
|
|
84
|
+
# set enforced by both layers); that is an unsatisfiable restriction,
|
|
85
|
+
# not an unrestricted one, so reject it as the merged interval does.
|
|
86
|
+
reject_empty_enumeration!(facets)
|
|
87
|
+
facets
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# Value facets apply to a single value: the whole value for a singular
|
|
93
|
+
# attribute, or each present element for a collection. Cardinality (item
|
|
94
|
+
# count) is governed separately by `collection:` in the `&&`-chain.
|
|
95
|
+
def validate_element_facets!(value, resolved_type, facets)
|
|
96
|
+
if ordered_type?(resolved_type)
|
|
97
|
+
validate_inclusive_bounds!(value, facets)
|
|
98
|
+
validate_exclusive_bounds!(value, facets)
|
|
99
|
+
end
|
|
100
|
+
if string_type?(resolved_type)
|
|
101
|
+
# Length and pattern facets operate on the lexical (serialized) form:
|
|
102
|
+
# a string-derived type whose cast value is not a ::String (e.g.
|
|
103
|
+
# Type::Uri holding a ::URI) has no #length/#match?, so measuring or
|
|
104
|
+
# matching the raw cast object would raise. Serialize once, share it.
|
|
105
|
+
lexical = resolved_type.serialize(value)
|
|
106
|
+
validate_length_bounds!(value, lexical, facets)
|
|
107
|
+
validate_patterns!(value, lexical, facets)
|
|
108
|
+
end
|
|
109
|
+
validate_digit_bounds!(value, facets) if digit_type?(resolved_type)
|
|
110
|
+
validate_enumeration!(value, facets)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Conjunctive merge of Layer-1 options and Layer-2 type facets, both in
|
|
114
|
+
# canonical-key form. Ordered bounds and enumeration members are cast to
|
|
115
|
+
# the resolved type per layer (before the merge, so `tighter_facet`/`&`
|
|
116
|
+
# compare homogeneous values). An empty resulting interval is a config
|
|
117
|
+
# error.
|
|
118
|
+
def effective_facets(resolved_type)
|
|
119
|
+
facets = option_facets(resolved_type)
|
|
120
|
+
.merge(type_facets(resolved_type)) do |key, a, b|
|
|
121
|
+
Type::Value.tighter_facet(key, a, b)
|
|
122
|
+
end
|
|
123
|
+
ensure_consistent_interval!(facets)
|
|
124
|
+
facets
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def option_facets(resolved_type)
|
|
128
|
+
facets = {}
|
|
129
|
+
facets[:min_inclusive] = cast_bound(@options[:min], resolved_type) if @options.key?(:min)
|
|
130
|
+
facets[:max_inclusive] = cast_bound(@options[:max], resolved_type) if @options.key?(:max)
|
|
131
|
+
if @options[:signed] == false
|
|
132
|
+
zero = cast_bound(0, resolved_type)
|
|
133
|
+
facets[:min_inclusive] = [facets[:min_inclusive], zero].compact.max
|
|
134
|
+
end
|
|
135
|
+
facets[:min_length] = @options[:min_length] if @options.key?(:min_length)
|
|
136
|
+
facets[:max_length] = @options[:max_length] if @options.key?(:max_length)
|
|
137
|
+
# An explicit `min: nil` / `max: nil` means "no bound": drop the nil
|
|
138
|
+
# key so it neither emits an empty <xs:restriction> nor masks a genuinely
|
|
139
|
+
# empty interval during consolidation.
|
|
140
|
+
facets.compact
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Layer-2 facets are already cast to the type at declaration; only the
|
|
144
|
+
# Layer-1 option bounds (below) still need casting.
|
|
145
|
+
def type_facets(resolved_type)
|
|
146
|
+
return {} unless castable_type?(resolved_type)
|
|
147
|
+
|
|
148
|
+
resolved_type.facets
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Fold the Layer-1 `values:` option into the schema enumeration. Runtime
|
|
152
|
+
# enforces both sets conjunctively, so a Layer-2 enumeration is
|
|
153
|
+
# intersected with the values option (both cast to the type so the
|
|
154
|
+
# intersection compares like values).
|
|
155
|
+
def merge_values_option(facets, resolved_type)
|
|
156
|
+
members = Array(@options[:values]).map { |v| cast_bound(v, resolved_type) }
|
|
157
|
+
existing = facets[:enumeration]
|
|
158
|
+
facets.merge(enumeration: existing ? existing & members : members)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Fold the Layer-1 `pattern:` option into the schema pattern list (the
|
|
162
|
+
# emitter fails fast if that yields conjunctive patterns XSD cannot OR).
|
|
163
|
+
def merge_pattern_option(facets)
|
|
164
|
+
option = @options[:pattern]
|
|
165
|
+
regexp = option.is_a?(Regexp) ? option : Regexp.new(option)
|
|
166
|
+
facets.merge(pattern: Array(facets[:pattern]) + [regexp])
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def cast_bound(value, resolved_type)
|
|
170
|
+
return value unless castable_type?(resolved_type)
|
|
171
|
+
|
|
172
|
+
cast_facet_value(value, resolved_type)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Cast a bound/enumeration literal to the resolved type. A non-nil literal
|
|
176
|
+
# that casts to nil is an unparseable restriction (e.g. `min: "abc"` on an
|
|
177
|
+
# integer); raise rather than silently drop the constraint.
|
|
178
|
+
def cast_facet_value(value, resolved_type)
|
|
179
|
+
cast = resolved_type.cast(value)
|
|
180
|
+
return cast if !cast.nil? || value.nil?
|
|
181
|
+
|
|
182
|
+
raise ArgumentError,
|
|
183
|
+
"Invalid restrictions for `#{name}`: " \
|
|
184
|
+
"#{value.inspect} is not a valid #{resolved_type} value"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def castable_type?(resolved_type)
|
|
188
|
+
resolved_type.is_a?(Class) && resolved_type <= Type::Value
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def ensure_consistent_interval!(facets)
|
|
192
|
+
consolidate_min_bounds!(facets)
|
|
193
|
+
consolidate_max_bounds!(facets)
|
|
194
|
+
reject_empty_ordered_interval!(facets)
|
|
195
|
+
reject_empty_interval!(facets, :min_length, :max_length)
|
|
196
|
+
reject_exact_length_conflict!(facets)
|
|
197
|
+
reject_empty_enumeration!(facets)
|
|
198
|
+
reject_fraction_exceeding_total!(facets)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# A same-side inclusive+exclusive pair is a conjunction (both must hold),
|
|
202
|
+
# so the tighter bound binds and the looser one is dropped. Lower side:
|
|
203
|
+
# `>= inc AND > exc`; inclusive binds only when it excludes strictly more
|
|
204
|
+
# (`inc > exc`), otherwise the exclusive bound wins (tie included, since
|
|
205
|
+
# `> n` is tighter than `>= n`).
|
|
206
|
+
def consolidate_min_bounds!(facets)
|
|
207
|
+
inc = facets[:min_inclusive]
|
|
208
|
+
exc = facets[:min_exclusive]
|
|
209
|
+
return if inc.nil? || exc.nil?
|
|
210
|
+
|
|
211
|
+
facets.delete(inc > exc ? :min_exclusive : :min_inclusive)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Upper side: `<= inc AND < exc`; inclusive binds only when it excludes
|
|
215
|
+
# strictly more (`inc < exc`), otherwise the exclusive bound wins (tie
|
|
216
|
+
# included, since `< n` is tighter than `<= n`).
|
|
217
|
+
def consolidate_max_bounds!(facets)
|
|
218
|
+
inc = facets[:max_inclusive]
|
|
219
|
+
exc = facets[:max_exclusive]
|
|
220
|
+
return if inc.nil? || exc.nil?
|
|
221
|
+
|
|
222
|
+
facets.delete(inc < exc ? :max_exclusive : :max_inclusive)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def reject_fraction_exceeding_total!(facets)
|
|
226
|
+
total = facets[:total_digits]
|
|
227
|
+
fraction = facets[:fraction_digits]
|
|
228
|
+
return if total.nil? || fraction.nil? || fraction <= total
|
|
229
|
+
|
|
230
|
+
raise ArgumentError,
|
|
231
|
+
"Invalid restrictions for `#{name}`: " \
|
|
232
|
+
"fraction_digits (#{fraction}) exceeds total_digits (#{total})"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def reject_empty_enumeration!(facets)
|
|
236
|
+
enumeration = facets[:enumeration]
|
|
237
|
+
return if enumeration.nil? || !enumeration.empty?
|
|
238
|
+
|
|
239
|
+
raise ArgumentError,
|
|
240
|
+
"Invalid restrictions for `#{name}`: " \
|
|
241
|
+
"enumeration allows no values"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Reject an empty interval formed by the merged ordered bounds. After
|
|
245
|
+
# consolidation at most one lower and one upper bound remain; a shared
|
|
246
|
+
# endpoint is valid only when both bounds are inclusive (single point).
|
|
247
|
+
def reject_empty_ordered_interval!(facets)
|
|
248
|
+
lo_key = facets.key?(:min_inclusive) ? :min_inclusive : :min_exclusive
|
|
249
|
+
hi_key = facets.key?(:max_inclusive) ? :max_inclusive : :max_exclusive
|
|
250
|
+
lo = facets[lo_key]
|
|
251
|
+
hi = facets[hi_key]
|
|
252
|
+
return if lo.nil? || hi.nil?
|
|
253
|
+
|
|
254
|
+
if lo > hi
|
|
255
|
+
raise ArgumentError,
|
|
256
|
+
"Invalid restrictions for `#{name}`: " \
|
|
257
|
+
"#{lo_key} (#{lo}) exceeds #{hi_key} (#{hi})"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
return unless lo == hi
|
|
261
|
+
return if lo_key == :min_inclusive && hi_key == :max_inclusive
|
|
262
|
+
|
|
263
|
+
raise ArgumentError,
|
|
264
|
+
"Invalid restrictions for `#{name}`: " \
|
|
265
|
+
"#{lo_key} (#{lo}) and #{hi_key} (#{hi}) define an empty interval"
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def reject_empty_interval!(facets, min_key, max_key)
|
|
269
|
+
min = facets[min_key]
|
|
270
|
+
max = facets[max_key]
|
|
271
|
+
return if min.nil? || max.nil? || min <= max
|
|
272
|
+
|
|
273
|
+
raise ArgumentError,
|
|
274
|
+
"Invalid restrictions for `#{name}`: " \
|
|
275
|
+
"#{min_key} (#{min}) exceeds #{max_key} (#{max})"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def reject_exact_length_conflict!(facets)
|
|
279
|
+
length = facets[:length]
|
|
280
|
+
min = facets[:min_length]
|
|
281
|
+
max = facets[:max_length]
|
|
282
|
+
return if length.nil?
|
|
283
|
+
return if (min.nil? || length >= min) && (max.nil? || length <= max)
|
|
284
|
+
|
|
285
|
+
raise ArgumentError,
|
|
286
|
+
"Invalid restrictions for `#{name}`: " \
|
|
287
|
+
"length (#{length}) is outside [#{min}, #{max}]"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def ordered_facets?(facets)
|
|
291
|
+
facets.key?(:min_inclusive) || facets.key?(:max_inclusive) ||
|
|
292
|
+
facets.key?(:min_exclusive) || facets.key?(:max_exclusive)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def length_facets?(facets)
|
|
296
|
+
facets.key?(:min_length) || facets.key?(:max_length) ||
|
|
297
|
+
facets.key?(:length)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def digit_facets?(facets)
|
|
301
|
+
facets.key?(:total_digits) || facets.key?(:fraction_digits)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Digit facets are countable only on integer- and decimal-derived types.
|
|
305
|
+
# Float is intentionally excluded: its binary representation makes the
|
|
306
|
+
# significant-digit count unreliable.
|
|
307
|
+
def digit_type?(resolved_type)
|
|
308
|
+
resolved_type <= Type::Integer || resolved_type <= Type::Decimal
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def ordered_type?(resolved_type)
|
|
312
|
+
numeric_type?(resolved_type) || temporal_type?(resolved_type)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def numeric_type?(resolved_type)
|
|
316
|
+
resolved_type <= Type::Integer ||
|
|
317
|
+
resolved_type <= Type::Decimal ||
|
|
318
|
+
resolved_type <= Type::Float
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Ordered facets apply to temporal types whose cast value has a reliable
|
|
322
|
+
# total order (Date/Time/DateTime respond to <=>). Duration and
|
|
323
|
+
# TimeWithoutDate are intentionally excluded: their cast value is a
|
|
324
|
+
# lexical String, so comparing them as ordered bounds would be incorrect.
|
|
325
|
+
def temporal_type?(resolved_type)
|
|
326
|
+
resolved_type <= Type::Date ||
|
|
327
|
+
resolved_type <= Type::Time ||
|
|
328
|
+
resolved_type <= Type::DateTime
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def string_type?(resolved_type)
|
|
332
|
+
resolved_type <= Type::String
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def validate_inclusive_bounds!(value, facets)
|
|
336
|
+
bounds = { min: facets[:min_inclusive],
|
|
337
|
+
max: facets[:max_inclusive] }.compact
|
|
338
|
+
return if bounds.empty?
|
|
339
|
+
|
|
340
|
+
Services::Type::Validator.validate_min_max_bounds!(value, bounds)
|
|
341
|
+
rescue Lutaml::Model::Type::MinBoundError
|
|
342
|
+
raise Lutaml::Model::MinInclusiveError.new(name, value, bounds[:min])
|
|
343
|
+
rescue Lutaml::Model::Type::MaxBoundError
|
|
344
|
+
raise Lutaml::Model::MaxInclusiveError.new(name, value, bounds[:max])
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def validate_exclusive_bounds!(value, facets)
|
|
348
|
+
min = facets[:min_exclusive]
|
|
349
|
+
max = facets[:max_exclusive]
|
|
350
|
+
return if min.nil? && max.nil?
|
|
351
|
+
|
|
352
|
+
Services::Type::Validator.validate_exclusive_bounds!(value, facets)
|
|
353
|
+
rescue Lutaml::Model::Type::MinExclusiveError
|
|
354
|
+
raise Lutaml::Model::MinExclusiveError.new(name, value, min)
|
|
355
|
+
rescue Lutaml::Model::Type::MaxExclusiveError
|
|
356
|
+
raise Lutaml::Model::MaxExclusiveError.new(name, value, max)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# `lexical` is the serialized form measured for length; `value` is the
|
|
360
|
+
# original cast value carried in the model error (as the pattern path does).
|
|
361
|
+
def validate_length_bounds!(value, lexical, facets)
|
|
362
|
+
validate_exact_length!(value, lexical, facets[:length])
|
|
363
|
+
min = facets[:min_length]
|
|
364
|
+
max = facets[:max_length]
|
|
365
|
+
Services::Type::Validator::String.validate_min_length!(lexical, min) if min
|
|
366
|
+
Services::Type::Validator::String.validate_max_length!(lexical, max) if max
|
|
367
|
+
rescue Lutaml::Model::Type::MinLengthError
|
|
368
|
+
raise Lutaml::Model::MinLengthError.new(name, value, min)
|
|
369
|
+
rescue Lutaml::Model::Type::MaxLengthError
|
|
370
|
+
raise Lutaml::Model::MaxLengthError.new(name, value, max)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def validate_exact_length!(value, lexical, length)
|
|
374
|
+
return if length.nil? || lexical.length == length
|
|
375
|
+
|
|
376
|
+
raise Lutaml::Model::LengthError.new(name, value, length)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def validate_digit_bounds!(value, facets)
|
|
380
|
+
total_limit = facets[:total_digits]
|
|
381
|
+
fraction_limit = facets[:fraction_digits]
|
|
382
|
+
return if total_limit.nil? && fraction_limit.nil?
|
|
383
|
+
|
|
384
|
+
total, fraction = digit_counts(value)
|
|
385
|
+
if total_limit && total > total_limit
|
|
386
|
+
raise Lutaml::Model::TotalDigitsError.new(name, value, total_limit)
|
|
387
|
+
end
|
|
388
|
+
return unless fraction_limit && fraction > fraction_limit
|
|
389
|
+
|
|
390
|
+
raise Lutaml::Model::FractionDigitsError.new(name, value, fraction_limit)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Count digits of an already-cast numeric value using XSD semantics
|
|
394
|
+
# (xs:totalDigits / xs:fractionDigits). From the fixed-notation string
|
|
395
|
+
# drop the sign and strip trailing zeros from the fraction part; the
|
|
396
|
+
# significand is the integer part concatenated with that fraction, with
|
|
397
|
+
# leading zeros removed. total = significant digits in the significand;
|
|
398
|
+
# fraction = remaining fraction digits. A leading zero before the point
|
|
399
|
+
# (values < 1) is insignificant, so `0.05` is [1, 2]; zero is [0, 0].
|
|
400
|
+
#
|
|
401
|
+
# @return [Array(Integer, Integer)] [total_digits, fraction_digits]
|
|
402
|
+
def digit_counts(value)
|
|
403
|
+
fixed = value.is_a?(::Integer) ? value.to_s : value.to_s("F")
|
|
404
|
+
int_part, frac_part = fixed.delete("-").split(".")
|
|
405
|
+
significant_frac = frac_part.to_s.sub(/0+\z/, "")
|
|
406
|
+
significand = (int_part + significant_frac).sub(/\A0+/, "")
|
|
407
|
+
[significand.length, significant_frac.length]
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def validate_enumeration!(value, facets)
|
|
411
|
+
allowed = facets[:enumeration]
|
|
412
|
+
return if allowed.nil?
|
|
413
|
+
|
|
414
|
+
Services::Type::Validator.validate_values!(value, allowed)
|
|
415
|
+
rescue Lutaml::Model::Type::InvalidValueError
|
|
416
|
+
raise Lutaml::Model::InvalidValueError.new(name, value, allowed)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Each accumulated pattern must match against the lexical form (already
|
|
420
|
+
# serialized by the caller) so non-::String string-derived types are
|
|
421
|
+
# matched on their serialized text rather than their cast object; the
|
|
422
|
+
# model error still carries the original `value`.
|
|
423
|
+
def validate_patterns!(value, lexical, facets)
|
|
424
|
+
patterns = facets[:pattern]
|
|
425
|
+
return if patterns.nil?
|
|
426
|
+
|
|
427
|
+
patterns.each do |pattern|
|
|
428
|
+
Services::Type::Validator::String.validate_pattern!(
|
|
429
|
+
lexical, pattern: pattern
|
|
430
|
+
)
|
|
431
|
+
rescue Lutaml::Model::Type::PatternNotMatchedError
|
|
432
|
+
raise Lutaml::Model::PatternNotMatchedError.new(name, pattern, value)
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
@@ -7,16 +7,18 @@ module Lutaml
|
|
|
7
7
|
# Restricted simple type — `class X < Lutaml::Model::Type::Y`
|
|
8
8
|
# with a cast body that applies facets and delegates to super.
|
|
9
9
|
class RestrictedType
|
|
10
|
-
attr_accessor :class_name, :parent_class, :facets,
|
|
10
|
+
attr_accessor :class_name, :parent_class, :base_class, :facets,
|
|
11
11
|
:transform_facet, :required_files,
|
|
12
12
|
:keep_register_when_namespaced, :namespace_class_name
|
|
13
13
|
|
|
14
14
|
def initialize(class_name:, facets:, parent_class: nil,
|
|
15
|
-
|
|
15
|
+
base_class: nil, transform_facet: nil,
|
|
16
|
+
required_files: [],
|
|
16
17
|
keep_register_when_namespaced: false,
|
|
17
18
|
namespace_class_name: nil)
|
|
18
19
|
@class_name = class_name
|
|
19
20
|
@parent_class = parent_class
|
|
21
|
+
@base_class = base_class
|
|
20
22
|
@facets = facets
|
|
21
23
|
@transform_facet = transform_facet
|
|
22
24
|
@required_files = required_files
|