lutaml-model 0.8.127 → 0.8.129
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/README.adoc +10 -2
- data/docs/_pages/value_types.adoc +102 -0
- data/lib/lutaml/key_value/transform.rb +112 -4
- data/lib/lutaml/model/schema/generator/property.rb +60 -9
- data/lib/lutaml/model/schema/xml_compiler.rb +4 -3
- data/lib/lutaml/model/type/date_time_with_timezone.rb +4 -0
- data/lib/lutaml/model/type/date_with_timezone.rb +4 -0
- data/lib/lutaml/model/type/day_time_duration.rb +11 -7
- data/lib/lutaml/model/type/email_address.rb +6 -0
- data/lib/lutaml/model/type/hostname.rb +6 -0
- data/lib/lutaml/model/type/ip_v4_address.rb +4 -0
- data/lib/lutaml/model/type/ip_v6_address.rb +22 -17
- data/lib/lutaml/model/type/metaschema_pattern.rb +5 -7
- data/lib/lutaml/model/type/non_negative_integer.rb +1 -0
- data/lib/lutaml/model/type/positive_integer.rb +1 -0
- data/lib/lutaml/model/type/token.rb +2 -0
- data/lib/lutaml/model/type/uri_reference.rb +6 -0
- data/lib/lutaml/model/type/uuid.rb +2 -0
- data/lib/lutaml/model/type/value.rb +9 -0
- data/lib/lutaml/model/type/year_month_duration.rb +4 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/schema/xsd/list.rb +1 -0
- data/spec/lutaml/model/metaschema_types_spec.rb +143 -0
- data/spec/lutaml/model/schema/xml_compiler_runtime_spec.rb +52 -0
- data/spec/lutaml/xml/schema/xsd/list_simple_type_spec.rb +30 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6903f41ec929cb64ce2450b6979f832e0a832f8afbbe82a134af3db157d2fdd8
|
|
4
|
+
data.tar.gz: 3d2f61efc2e609ade7d440994d318c1b190255c243f33ec96538de2a4cda5774
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f82b33a1e8ac555aa6c756007da45a99dc3e506fd58c1f780446c112397314b3a76b0978566dc3c3279651e76701f0537b87dd1c700accaeb77f789497e20dc
|
|
7
|
+
data.tar.gz: 1100e41da62097636496a5fe31d1ac6a77f55ac491a8a3313eb96dfa4ceda55c52ba92166eda902f07bceae1ace17fd2224c2190c9b9792c5ca76ff4ce29293f
|
data/README.adoc
CHANGED
|
@@ -857,8 +857,16 @@ Every type has a corresponding Ruby class and a serialization format type.
|
|
|
857
857
|
|
|
858
858
|
The NIST Metaschema simple data types are supported as first-class value
|
|
859
859
|
types (https://pages.nist.gov/metaschema/specification/datatypes/). Each
|
|
860
|
-
type carries the normative metaschema-datatypes pattern
|
|
861
|
-
|
|
860
|
+
type carries the normative metaschema-datatypes pattern, answers `valid?`
|
|
861
|
+
for a value, and declares it as a restriction facet: attributes typed
|
|
862
|
+
with these types are validated by `model.validate`, and schema
|
|
863
|
+
generation emits the pattern/bounds as restriction facets in every
|
|
864
|
+
supported schema language — `xs:pattern`/`xs:minInclusive` in generated
|
|
865
|
+
XSD, `pattern`/`minimum` plus the NIST `format` keywords (`ipv4`,
|
|
866
|
+
`ipv6`, `email`, `uri-reference`, `duration`, `date`, `date-time`) in
|
|
867
|
+
generated JSON and YAML schemas. Casting follows the lenient
|
|
868
|
+
base-type behavior; TOML has no schema language, so its surface for
|
|
869
|
+
these types is the cast/serialize lane.
|
|
862
870
|
|
|
863
871
|
.Source types
|
|
864
872
|
[cols="3,4,2"]
|
|
@@ -35,6 +35,19 @@ Every type has a corresponding Ruby class and a serialization format type.
|
|
|
35
35
|
| `:qname` | `Lutaml::Model::Type::QName` | `xs:QName` | `string` | `string` | `"prefix:localName"`
|
|
36
36
|
| `:base64_binary` | `String` | `xs:base64Binary` | `string` | `string` | `"SGVsbG8gV29ybGQ="`
|
|
37
37
|
| `:hex_binary` | `String` | `xs:hexBinary` | `string` | `string` | `"48656c6c6f"`
|
|
38
|
+
| `:email_address` | `String` | `xs:string` | `string` | `string` | `"user@example.com"`
|
|
39
|
+
| `:hostname` | `String` | `xs:string` | `string` | `string` | `"example.com"`
|
|
40
|
+
| `:ip_v4_address` | `String` | `xs:string` | `string` | `string` | `"192.168.0.1"`
|
|
41
|
+
| `:ip_v6_address` | `String` | `xs:string` | `string` | `string` | `"2001:db8::2:1"`
|
|
42
|
+
| `:token` | `String` | `xs:string` | `string` | `string` | `"label_3.v2"`
|
|
43
|
+
| `:uri_reference` | `String` | `xs:anyURI` | `string` | `string` | `"path/to/resource"`
|
|
44
|
+
| `:uuid` | `String` | `xs:string` | `string` | `string` | `"a3bb189e-8bf9-4888-9912-ace4e6543002"`
|
|
45
|
+
| `:non_negative_integer` | `Integer` | `xs:nonNegativeInteger` | `number` | `integer` | `7`
|
|
46
|
+
| `:positive_integer` | `Integer` | `xs:positiveInteger` | `number` | `integer` | `7`
|
|
47
|
+
| `:date_with_timezone` | `Date`/`DateTime` | `xs:date` | `string` | `string` | `"2019-12-02-08:00"`
|
|
48
|
+
| `:date_time_with_timezone` | `DateTime` | `xs:dateTime` | `string` | `string` | `"2019-12-02T16:39:57-08:00"`
|
|
49
|
+
| `:day_time_duration` | `Lutaml::Model::Type::Duration` | `xs:dayTimeDuration` | `string` | `string` | `"P1DT12H45M"`
|
|
50
|
+
| `:year_month_duration` | `Lutaml::Model::Type::Duration` | `xs:yearMonthDuration` | `string` | `string` | `"P1Y6M"`
|
|
38
51
|
| `:hash` | `Hash` | complex element | object | map | `{key: "value"}`
|
|
39
52
|
| (nil value) | `nil` | `xs:anyType` | `null` | `null` | `null`
|
|
40
53
|
| `{ ref: [Model, :attr] }` | `Reference` | `xs:string` | `string` | `string` | `"model-id"`
|
|
@@ -120,6 +133,56 @@ puts event.start_date.class
|
|
|
120
133
|
|
|
121
134
|
|
|
122
135
|
[[xml-namespace-types]]
|
|
136
|
+
=== NIST Metaschema types
|
|
137
|
+
|
|
138
|
+
The NIST Metaschema simple data types are supported as first-class value
|
|
139
|
+
types (https://pages.nist.gov/metaschema/specification/datatypes/). Each
|
|
140
|
+
type carries the normative metaschema-datatypes pattern, answers `valid?`
|
|
141
|
+
for a value, and declares it as a restriction facet: attributes typed
|
|
142
|
+
with these types are validated by `model.validate`, and schema
|
|
143
|
+
generation emits the pattern/bounds as restriction facets in every
|
|
144
|
+
supported schema language — `xs:pattern`/`xs:minInclusive` in generated
|
|
145
|
+
XSD, `pattern`/`minimum` plus the NIST `format` keywords (`ipv4`,
|
|
146
|
+
`ipv6`, `email`, `uri-reference`, `duration`, `date`, `date-time`) in
|
|
147
|
+
generated JSON and YAML schemas. Casting follows the lenient
|
|
148
|
+
base-type behavior; TOML has no schema language, so its surface for
|
|
149
|
+
these types is the cast/serialize lane.
|
|
150
|
+
|
|
151
|
+
.Metaschema type codes and their restrictions
|
|
152
|
+
[cols="3,4,2"]
|
|
153
|
+
|===
|
|
154
|
+
| Type code | Metaschema datatype | Restriction
|
|
155
|
+
|
|
156
|
+
| `:email_address` | `email-address` | contains an at-sign
|
|
157
|
+
| `:hostname` | `hostname` | non-empty, trimmed
|
|
158
|
+
| `:ip_v4_address` | `ip-v4-address` | dotted-quad IPv4
|
|
159
|
+
| `:ip_v6_address` | `ip-v6-address` | RFC3513 IPv6 (incl. zones, embedded IPv4)
|
|
160
|
+
| `:token` | `token` | NMTOKEN-like letter/number/punctuation run
|
|
161
|
+
| `:uri_reference` | `uri-reference` | relative or absolute URI reference
|
|
162
|
+
| `:uuid` | `uuid` | RFC4122 UUID, version 4 or 5
|
|
163
|
+
| `:non_negative_integer` | `non-negative-integer` | integer >= 0
|
|
164
|
+
| `:positive_integer` | `positive-integer` | integer >= 1
|
|
165
|
+
| `:date_with_timezone` | `date-with-timezone` | date with `Z` or offset
|
|
166
|
+
| `:date_time_with_timezone` | `date-time-with-timezone` | date-time with `Z` or offset
|
|
167
|
+
| `:day_time_duration` | `day-time-duration` | duration without years/months
|
|
168
|
+
| `:year_month_duration` | `year-month-duration` | duration without time components
|
|
169
|
+
|===
|
|
170
|
+
|
|
171
|
+
.Using metaschema type attributes
|
|
172
|
+
[example]
|
|
173
|
+
====
|
|
174
|
+
[source,ruby]
|
|
175
|
+
----
|
|
176
|
+
class Asset < Lutaml::Model::Serializable
|
|
177
|
+
attribute :id, :uuid
|
|
178
|
+
attribute :owner, :email_address
|
|
179
|
+
attribute :host, :hostname
|
|
180
|
+
attribute :address, :ip_v4_address
|
|
181
|
+
attribute :renewal, :year_month_duration
|
|
182
|
+
end
|
|
183
|
+
----
|
|
184
|
+
====
|
|
185
|
+
|
|
123
186
|
=== Additional XSD types
|
|
124
187
|
|
|
125
188
|
Lutaml::Model supports additional XSD types for specialized data handling:
|
|
@@ -1325,6 +1388,45 @@ All built-in types have default XSD type mappings:
|
|
|
1325
1388
|
| [`Type::HexBinary`](lib/lutaml/model/type/hex_binary.rb)
|
|
1326
1389
|
| `xs:hexBinary`
|
|
1327
1390
|
|
|
1391
|
+
| [`Type::EmailAddress`](lib/lutaml/model/type/email_address.rb)
|
|
1392
|
+
| `xs:string`
|
|
1393
|
+
|
|
1394
|
+
| [`Type::Hostname`](lib/lutaml/model/type/hostname.rb)
|
|
1395
|
+
| `xs:string`
|
|
1396
|
+
|
|
1397
|
+
| [`Type::IpV4Address`](lib/lutaml/model/type/ip_v4_address.rb)
|
|
1398
|
+
| `xs:string`
|
|
1399
|
+
|
|
1400
|
+
| [`Type::IpV6Address`](lib/lutaml/model/type/ip_v6_address.rb)
|
|
1401
|
+
| `xs:string`
|
|
1402
|
+
|
|
1403
|
+
| [`Type::Token`](lib/lutaml/model/type/token.rb)
|
|
1404
|
+
| `xs:string`
|
|
1405
|
+
|
|
1406
|
+
| [`Type::UriReference`](lib/lutaml/model/type/uri_reference.rb)
|
|
1407
|
+
| `xs:anyURI`
|
|
1408
|
+
|
|
1409
|
+
| [`Type::Uuid`](lib/lutaml/model/type/uuid.rb)
|
|
1410
|
+
| `xs:string`
|
|
1411
|
+
|
|
1412
|
+
| [`Type::NonNegativeInteger`](lib/lutaml/model/type/non_negative_integer.rb)
|
|
1413
|
+
| `xs:nonNegativeInteger`
|
|
1414
|
+
|
|
1415
|
+
| [`Type::PositiveInteger`](lib/lutaml/model/type/positive_integer.rb)
|
|
1416
|
+
| `xs:positiveInteger`
|
|
1417
|
+
|
|
1418
|
+
| [`Type::DateWithTimezone`](lib/lutaml/model/type/date_with_timezone.rb)
|
|
1419
|
+
| `xs:date`
|
|
1420
|
+
|
|
1421
|
+
| [`Type::DateTimeWithTimezone`](lib/lutaml/model/type/date_time_with_timezone.rb)
|
|
1422
|
+
| `xs:dateTime`
|
|
1423
|
+
|
|
1424
|
+
| [`Type::DayTimeDuration`](lib/lutaml/model/type/day_time_duration.rb)
|
|
1425
|
+
| `xs:dayTimeDuration`
|
|
1426
|
+
|
|
1427
|
+
| [`Type::YearMonthDuration`](lib/lutaml/model/type/year_month_duration.rb)
|
|
1428
|
+
| `xs:yearMonthDuration`
|
|
1429
|
+
|
|
1328
1430
|
| [`Type::Hash`](lib/lutaml/model/type/hash.rb)
|
|
1329
1431
|
| `xs:anyType`
|
|
1330
1432
|
|
|
@@ -215,18 +215,26 @@ module Lutaml
|
|
|
215
215
|
|
|
216
216
|
mapping = model_class.mappings_for(format, register)
|
|
217
217
|
return false if mapping.nil?
|
|
218
|
-
# The catch-all collect runs on the interpretive walk only —
|
|
219
|
-
# the compiled hydration must not skip unknown keys silently.
|
|
220
|
-
return false if mapping.unmapped_attribute
|
|
221
218
|
|
|
222
219
|
attrs = model_class.attributes(register)
|
|
220
|
+
# The catch-all compiles as a row: unclaimed document keys are
|
|
221
|
+
# collected into the target attribute after the rules hydrate,
|
|
222
|
+
# mirroring the interpretive collect's position and merge. A
|
|
223
|
+
# typo'd target raises here, exactly as the walk raises at
|
|
224
|
+
# first parse.
|
|
225
|
+
unmapped_target = mapping.unmapped_attribute
|
|
226
|
+
if unmapped_target && !attrs.key?(unmapped_target)
|
|
227
|
+
raise ArgumentError,
|
|
228
|
+
"map_unmapped target :#{unmapped_target} is not a " \
|
|
229
|
+
"declared attribute of #{model_class}"
|
|
230
|
+
end
|
|
223
231
|
rows = nil
|
|
232
|
+
claimed = nil
|
|
224
233
|
mapping.mappings(register).each do |rule|
|
|
225
234
|
eligible = !rule.name.nil? && !rule.multiple_mappings? &&
|
|
226
235
|
rule.delegate.nil? &&
|
|
227
236
|
!rule.has_custom_method_for_deserialization? &&
|
|
228
237
|
!rule.raw_mapping? && !rule.root_mapping? &&
|
|
229
|
-
!rule.hash_mappings && rule.child_mappings.nil? &&
|
|
230
238
|
rule.when_attribute.empty? &&
|
|
231
239
|
!(if rule.polymorphic.is_a?(::Hash)
|
|
232
240
|
!rule.polymorphic.empty?
|
|
@@ -252,6 +260,24 @@ module Lutaml
|
|
|
252
260
|
return false if Lutaml::Model::GlobalContext.context(register)
|
|
253
261
|
.substitution_for(type).any?
|
|
254
262
|
|
|
263
|
+
if rule.hash_mappings
|
|
264
|
+
# #1003: compiled child-map row. The compiled ChildMapPlan
|
|
265
|
+
# reproduces translate_mappings' per-child lookups; shapes
|
|
266
|
+
# the compile cannot cover (unmappable child attribute,
|
|
267
|
+
# singular target, non-model child type) keep the whole
|
|
268
|
+
# model interpretive.
|
|
269
|
+
return false unless attr.collection? &&
|
|
270
|
+
type.is_a?(Class) && type.include?(Lutaml::Model::Serialize)
|
|
271
|
+
|
|
272
|
+
child_plan = compile_group_child_plan(type, format, register,
|
|
273
|
+
rule.hash_mappings)
|
|
274
|
+
return false unless child_plan
|
|
275
|
+
|
|
276
|
+
rows ||= []
|
|
277
|
+
rows << [rule, attr, :child_map, child_plan, nil, nil]
|
|
278
|
+
next
|
|
279
|
+
end
|
|
280
|
+
|
|
255
281
|
if type.is_a?(Class) && type.include?(Lutaml::Model::Serialize)
|
|
256
282
|
child_register = Lutaml::Model::Register
|
|
257
283
|
.resolve_for_child(type, register)
|
|
@@ -272,10 +298,45 @@ module Lutaml
|
|
|
272
298
|
return false
|
|
273
299
|
end
|
|
274
300
|
end
|
|
301
|
+
if unmapped_target
|
|
302
|
+
claimed = {}
|
|
303
|
+
mapping.mappings(register).each do |rule|
|
|
304
|
+
Array(rule.name).each { |name| claimed[name.to_s] = true } if rule.name
|
|
305
|
+
end
|
|
306
|
+
claimed.freeze
|
|
307
|
+
(rows ||= []) << [nil, nil, :unmapped, unmapped_target, claimed,
|
|
308
|
+
nil]
|
|
309
|
+
end
|
|
275
310
|
# No eligible rows at all (empty mapping) — nothing to gain.
|
|
276
311
|
rows
|
|
277
312
|
end
|
|
278
313
|
|
|
314
|
+
# Class-level twin of the walk's child-map compile: the registry
|
|
315
|
+
# lookups (child rules, per-attribute wire names, path casts)
|
|
316
|
+
# resolve once per plan instead of once per node per child. nil
|
|
317
|
+
# keeps the model interpretive (preserving the walk's raise
|
|
318
|
+
# timing for declaration errors).
|
|
319
|
+
def self.compile_group_child_plan(child_class, format, register,
|
|
320
|
+
child_mappings)
|
|
321
|
+
child_rules = child_class.mappings_for(format, register)
|
|
322
|
+
wire_names = {}
|
|
323
|
+
paths = {}
|
|
324
|
+
child_mappings.each do |attr_name, path|
|
|
325
|
+
attr_rule = child_rules.find_by_to!(attr_name)
|
|
326
|
+
wire_names[attr_name] = attr_rule.from.to_s
|
|
327
|
+
paths[attr_name] = case path
|
|
328
|
+
when :key then :key
|
|
329
|
+
when :value then :value
|
|
330
|
+
else Array(path).map(&:to_s).freeze
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
ChildMapPlan.new(child_mappings, wire_names, paths,
|
|
334
|
+
child_mappings.values == [:key], child_rules,
|
|
335
|
+
child_class)
|
|
336
|
+
rescue Lutaml::Model::NoMappingFoundError
|
|
337
|
+
nil
|
|
338
|
+
end
|
|
339
|
+
|
|
279
340
|
# Build values bottom-up, then ONE instance per model: present
|
|
280
341
|
# keys through the casting setters, ABSENT rules through the real
|
|
281
342
|
# per-rule walk (their extractor/defaults/sentinel semantics are
|
|
@@ -289,7 +350,13 @@ module Lutaml
|
|
|
289
350
|
# Lazy: the present-key hot path never pushes, and this runs
|
|
290
351
|
# once per hydrated model instance.
|
|
291
352
|
absent = nil
|
|
353
|
+
unmapped = nil
|
|
292
354
|
rows.each do |rule, attr, kind, type, child_rows, child_register|
|
|
355
|
+
if kind == :unmapped
|
|
356
|
+
unmapped = [type, child_rows]
|
|
357
|
+
next
|
|
358
|
+
end
|
|
359
|
+
|
|
293
360
|
# One hash pass per row: fetch with a sentinel distinguishes
|
|
294
361
|
# absent keys from present-nil values (both read as nil), so
|
|
295
362
|
# the separate string_or_symbol_key? probe goes away.
|
|
@@ -299,6 +366,33 @@ module Lutaml
|
|
|
299
366
|
next
|
|
300
367
|
end
|
|
301
368
|
|
|
369
|
+
if kind == :child_map
|
|
370
|
+
# Compiled translate_mappings (#1003): per-child lookups
|
|
371
|
+
# are plan constants. Children hydrate through their own
|
|
372
|
+
# compiled walk (the mappings: option routes them there);
|
|
373
|
+
# parent/root wiring rides the shared children pass. A
|
|
374
|
+
# non-Hash value falls back to the walk, which owns that
|
|
375
|
+
# error.
|
|
376
|
+
return nil unless v.is_a?(::Hash)
|
|
377
|
+
|
|
378
|
+
built = []
|
|
379
|
+
keys_only = type.keys_only && v.values.all?(::Hash)
|
|
380
|
+
v.each do |child_key, child_value|
|
|
381
|
+
child_hash = {}
|
|
382
|
+
type.child_mappings.each_key do |attr_name|
|
|
383
|
+
child_hash[type.wire_names[attr_name]] =
|
|
384
|
+
extract_compiled_child_value(type.paths[attr_name],
|
|
385
|
+
child_key, child_value)
|
|
386
|
+
end
|
|
387
|
+
child_hash.update(child_value) if keys_only
|
|
388
|
+
built << self.class.data_to_model(type.attr_type,
|
|
389
|
+
child_hash, format, { mappings: type.child_rules })
|
|
390
|
+
end
|
|
391
|
+
setters << [rule.kv_setter, built]
|
|
392
|
+
children.concat(built)
|
|
393
|
+
next
|
|
394
|
+
end
|
|
395
|
+
|
|
302
396
|
if kind == :scalar
|
|
303
397
|
setters << [rule.kv_setter, v]
|
|
304
398
|
next
|
|
@@ -345,6 +439,20 @@ module Lutaml
|
|
|
345
439
|
child.lutaml_parent = instance
|
|
346
440
|
child.lutaml_root ||= instance.lutaml_root || instance
|
|
347
441
|
end
|
|
442
|
+
if unmapped
|
|
443
|
+
target, claimed = unmapped
|
|
444
|
+
extras = nil
|
|
445
|
+
doc.each do |key, value|
|
|
446
|
+
next if claimed.key?(key.to_s)
|
|
447
|
+
|
|
448
|
+
(extras ||= {})[key] = value
|
|
449
|
+
end
|
|
450
|
+
if extras
|
|
451
|
+
existing = instance.public_send(target)
|
|
452
|
+
merged = existing.is_a?(::Hash) ? existing.merge(extras) : extras
|
|
453
|
+
instance.public_send(:"#{target}=", merged)
|
|
454
|
+
end
|
|
455
|
+
end
|
|
348
456
|
instance
|
|
349
457
|
end
|
|
350
458
|
|
|
@@ -96,8 +96,35 @@ module Lutaml
|
|
|
96
96
|
def get_constraints(attr)
|
|
97
97
|
constraints = {}
|
|
98
98
|
|
|
99
|
-
# Add pattern validation
|
|
100
|
-
|
|
99
|
+
# Add pattern validation (attribute option conjunctive with the
|
|
100
|
+
# resolved type's restriction facets; a single JSON Schema
|
|
101
|
+
# pattern cannot express conjunction, mirroring the XSD emitter)
|
|
102
|
+
patterns = constraint_patterns(attr)
|
|
103
|
+
if patterns.size > 1
|
|
104
|
+
raise Lutaml::Model::Error,
|
|
105
|
+
"Cannot export #{patterns.size} conjunctive patterns on " \
|
|
106
|
+
"#{attr.name} to a single JSON Schema pattern: sibling " \
|
|
107
|
+
"patterns are alternatives (OR), and JSON Schema cannot " \
|
|
108
|
+
"express conjunction (AND) without allOf, which is not " \
|
|
109
|
+
"yet supported."
|
|
110
|
+
end
|
|
111
|
+
# Attribute-option patterns export verbatim (author contract);
|
|
112
|
+
# type-facet patterns carry Ruby \A/\z anchoring, which ECMA
|
|
113
|
+
# regexes lack — express the full-match intent with ^...$.
|
|
114
|
+
if (pattern, from_facet = patterns.first)
|
|
115
|
+
constraints["pattern"] =
|
|
116
|
+
from_facet ? json_schema_pattern(pattern) : pattern.source
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Add the resolved type's restriction facets and format
|
|
120
|
+
type = attr.type(register)
|
|
121
|
+
if type.is_a?(Class) && type <= Lutaml::Model::Type::Value
|
|
122
|
+
facets = type.facets
|
|
123
|
+
constraints["minimum"] = facets[:min_inclusive] if facets[:min_inclusive]
|
|
124
|
+
constraints["maximum"] = facets[:max_inclusive] if facets[:max_inclusive]
|
|
125
|
+
constraints["exclusiveMinimum"] = facets[:min_exclusive] if facets[:min_exclusive]
|
|
126
|
+
constraints["exclusiveMaximum"] = facets[:max_exclusive] if facets[:max_exclusive]
|
|
127
|
+
end
|
|
101
128
|
|
|
102
129
|
# Add default value
|
|
103
130
|
resolved = attr.default_value(register, nil)
|
|
@@ -108,21 +135,45 @@ module Lutaml
|
|
|
108
135
|
# Add enumeration values
|
|
109
136
|
constraints["enum"] = attr.enum_values if attr.enum?
|
|
110
137
|
|
|
138
|
+
# Add the format keyword for types that declare one (e.g. the
|
|
139
|
+
# NIST Metaschema types: ipv4, ipv6, email, uri-reference)
|
|
140
|
+
format = type.json_schema_format if type.is_a?(Class) &&
|
|
141
|
+
type <= Lutaml::Model::Type::Value
|
|
142
|
+
constraints["format"] = format if format
|
|
143
|
+
|
|
111
144
|
constraints
|
|
112
145
|
end
|
|
113
146
|
|
|
147
|
+
def constraint_patterns(attr)
|
|
148
|
+
patterns = []
|
|
149
|
+
patterns << [attr.pattern, false] if attr.pattern
|
|
150
|
+
type = attr.type(register)
|
|
151
|
+
if type.is_a?(Class) && type <= Lutaml::Model::Type::Value
|
|
152
|
+
type.facets[:pattern].to_a.each { |re| patterns << [re, true] }
|
|
153
|
+
end
|
|
154
|
+
patterns
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# JSON Schema (ECMA) regexes have no \A/\z; full-match intent is
|
|
158
|
+
# expressed with ^...$ anchoring (the NIST JSON forms do the same).
|
|
159
|
+
def json_schema_pattern(regexp)
|
|
160
|
+
source = regexp.source
|
|
161
|
+
source = source.delete_prefix('\\A').delete_suffix('\\z')
|
|
162
|
+
"^#{source}$"
|
|
163
|
+
end
|
|
164
|
+
|
|
114
165
|
def polymorphic?(attr)
|
|
115
166
|
Utils.present?(attr.options[:polymorphic])
|
|
116
167
|
end
|
|
117
168
|
|
|
118
169
|
def get_type(type)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
170
|
+
return "integer" if type.is_a?(Class) && type <= Lutaml::Model::Type::Integer
|
|
171
|
+
return "boolean" if type.is_a?(Class) && type <= Lutaml::Model::Type::Boolean
|
|
172
|
+
return "number" if type.is_a?(Class) && type <= Lutaml::Model::Type::Float
|
|
173
|
+
return "number" if type.is_a?(Class) && type <= Lutaml::Model::Type::Decimal
|
|
174
|
+
return "object" if type.is_a?(Class) && type <= Lutaml::Model::Type::Hash
|
|
175
|
+
|
|
176
|
+
"string" # Strings and all other scalar types
|
|
126
177
|
end
|
|
127
178
|
end
|
|
128
179
|
end
|
|
@@ -71,9 +71,10 @@ module Lutaml
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
def as_models(schema, options: {})
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
# The schema builder resolves its own DOM builder from the
|
|
75
|
+
# configured adapter type (falling back to nokogiri); a stale
|
|
76
|
+
# adapter-class name check here rejected every caller after
|
|
77
|
+
# Config.xml_adapter became symbol-based (#251).
|
|
77
78
|
|
|
78
79
|
parsed_schema = Lutaml::Xml::Schema::Xsd.parse(
|
|
79
80
|
schema, location: options[:location]
|
|
@@ -5,23 +5,27 @@ module Lutaml
|
|
|
5
5
|
module Type
|
|
6
6
|
# NIST Metaschema day-time-duration datatype (#107): an ISO 8601
|
|
7
7
|
# duration expressed in days, hours, minutes, and seconds (no
|
|
8
|
-
# years or months), per the xs:dayTimeDuration form.
|
|
8
|
+
# years or months), per the xs:dayTimeDuration form. Built by
|
|
9
|
+
# interpolation with no formatting whitespace, so the pattern's
|
|
10
|
+
# source is export-clean for xs:pattern facets.
|
|
9
11
|
#
|
|
10
12
|
# @example Using DayTimeDuration type
|
|
11
13
|
# attribute :elapsed, :day_time_duration
|
|
12
14
|
class DayTimeDuration < Duration
|
|
13
15
|
ELAPSED_TIME =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
PATTERN = /
|
|
18
|
-
\A-?P(?:[0-9]+D(?:T#{ELAPSED_TIME.source})?|T#{ELAPSED_TIME.source})\z
|
|
19
|
-
/x
|
|
16
|
+
"(?:[0-9]+H(?:[0-9]+M)?(?:[0-9]+(?:\\.[0-9]+)?S)?" \
|
|
17
|
+
"|[0-9]+M(?:[0-9]+(?:\\.[0-9]+)?S)?" \
|
|
18
|
+
"|[0-9]+(?:\\.[0-9]+)?S)"
|
|
19
|
+
PATTERN = /\A-?P(?:[0-9]+D(?:T#{ELAPSED_TIME})?|T#{ELAPSED_TIME})\z/
|
|
20
20
|
|
|
21
21
|
def self.valid?(value)
|
|
22
22
|
value.is_a?(::String) ? value.match?(PATTERN) : !cast(value).nil?
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def self.json_schema_format
|
|
26
|
+
"duration"
|
|
27
|
+
end
|
|
28
|
+
|
|
25
29
|
def self.default_xsd_type
|
|
26
30
|
"xs:dayTimeDuration"
|
|
27
31
|
end
|
|
@@ -14,6 +14,10 @@ module Lutaml
|
|
|
14
14
|
OCTET = /(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])/
|
|
15
15
|
PATTERN = /\A#{OCTET.source}\.#{OCTET.source}\.#{OCTET.source}\.#{OCTET.source}\z/
|
|
16
16
|
|
|
17
|
+
def self.json_schema_format
|
|
18
|
+
"ipv4"
|
|
19
|
+
end
|
|
20
|
+
|
|
17
21
|
def self.default_xsd_type
|
|
18
22
|
"xs:string"
|
|
19
23
|
end
|
|
@@ -6,7 +6,9 @@ module Lutaml
|
|
|
6
6
|
# NIST Metaschema ip-v6-address datatype (#107): RFC3513 IPv6 text
|
|
7
7
|
# form, per the metaschema-datatypes pattern (the generalized
|
|
8
8
|
# StackOverflow IPv6 expression with metaschema customizations,
|
|
9
|
-
# including link-local zones and embedded IPv4).
|
|
9
|
+
# including link-local zones and embedded IPv4). Built by
|
|
10
|
+
# interpolation with no formatting whitespace, so the pattern's
|
|
11
|
+
# source is export-clean for xs:pattern facets.
|
|
10
12
|
#
|
|
11
13
|
# @example Using IpV6Address type
|
|
12
14
|
# attribute :address, :ip_v6_address
|
|
@@ -16,22 +18,25 @@ module Lutaml
|
|
|
16
18
|
HEX4 = "[0-9a-fA-F]{1,4}"
|
|
17
19
|
OCTET = "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])"
|
|
18
20
|
EMBEDDED_V4 = "(#{OCTET}\\.){3,3}#{OCTET}".freeze
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
21
|
+
GROUPS = "(#{HEX4}:){7,7}#{HEX4}" \
|
|
22
|
+
"|([0-9a-fA-F]{1,4}:){1,7}:" \
|
|
23
|
+
"|([0-9a-fA-F]{1,4}:){1,6}:#{HEX4}" \
|
|
24
|
+
"|([0-9a-fA-F]{1,4}:){1,5}(:#{HEX4}){1,2}" \
|
|
25
|
+
"|([0-9a-fA-F]{1,4}:){1,4}(:#{HEX4}){1,3}" \
|
|
26
|
+
"|([0-9a-fA-F]{1,4}:){1,3}(:#{HEX4}){1,4}" \
|
|
27
|
+
"|([0-9a-fA-F]{1,4}:){1,2}(:#{HEX4}){1,5}" \
|
|
28
|
+
"|#{HEX4}:((:#{HEX4}){1,6})" \
|
|
29
|
+
"|:((:#{HEX4}){1,7}|:)" \
|
|
30
|
+
"|[fF][eE]80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}" \
|
|
31
|
+
"|::([fF]{4}(:0{1,4}){0,1}:){0,1}#{EMBEDDED_V4}" \
|
|
32
|
+
"|([0-9a-fA-F]{1,4}:){1,4}:#{EMBEDDED_V4}".freeze
|
|
33
|
+
PATTERN = /\A(#{GROUPS})\z/
|
|
34
|
+
|
|
35
|
+
pattern PATTERN
|
|
36
|
+
|
|
37
|
+
def self.json_schema_format
|
|
38
|
+
"ipv6"
|
|
39
|
+
end
|
|
35
40
|
|
|
36
41
|
def self.default_xsd_type
|
|
37
42
|
"xs:string"
|
|
@@ -4,17 +4,15 @@ module Lutaml
|
|
|
4
4
|
module Model
|
|
5
5
|
module Type
|
|
6
6
|
# Pattern machinery shared by the NIST Metaschema string types (#107).
|
|
7
|
-
# Each including class defines a PATTERN regexp
|
|
8
|
-
#
|
|
7
|
+
# Each including class defines a PATTERN regexp (also declared as the
|
|
8
|
+
# type's `pattern` facet, feeding runtime validation and XSD
|
|
9
|
+
# restriction emission); values cast to strings and `valid?` checks
|
|
10
|
+
# full-match against it.
|
|
9
11
|
module MetaschemaPattern
|
|
10
|
-
def pattern
|
|
11
|
-
self::PATTERN
|
|
12
|
-
end
|
|
13
|
-
|
|
14
12
|
def valid?(value)
|
|
15
13
|
return false if value.nil?
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
self::PATTERN.match?(value.to_s)
|
|
18
16
|
end
|
|
19
17
|
|
|
20
18
|
def cast(value, _options = {})
|
|
@@ -126,6 +126,15 @@ from: nil)
|
|
|
126
126
|
"conflicting `#{key}` facets: #{existing} and #{incoming}"
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
# The JSON Schema `format` keyword for this type, or nil. The NIST
|
|
130
|
+
# Metaschema types declare the format the metaschema JSON
|
|
131
|
+
# representations use (ipv4, ipv6, email, uri-reference, duration,
|
|
132
|
+
# date, date-time); core types default to nil and their schemas
|
|
133
|
+
# carry no format keyword.
|
|
134
|
+
def json_schema_format
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
129
138
|
# Constrain ordered values via min/maxInclusive.
|
|
130
139
|
def inclusive(min: nil, max: nil)
|
|
131
140
|
min = cast_facet_value(min)
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -221,4 +221,147 @@ RSpec.describe "NIST Metaschema simple data types" do
|
|
|
221
221
|
.to eq("xs:yearMonthDuration")
|
|
222
222
|
expect(Lutaml::Model::Type::Uuid.default_xsd_type).to eq("xs:string")
|
|
223
223
|
end
|
|
224
|
+
|
|
225
|
+
describe "wiring into validation and schema emission" do
|
|
226
|
+
it "declares the patterns and bounds as Layer-2 facets" do
|
|
227
|
+
expect(Lutaml::Model::Type::Uuid.facets[:pattern])
|
|
228
|
+
.to include(Lutaml::Model::Type::Uuid::PATTERN)
|
|
229
|
+
expect(Lutaml::Model::Type::NonNegativeInteger.facets[:min_inclusive])
|
|
230
|
+
.to eq(0)
|
|
231
|
+
expect(Lutaml::Model::Type::PositiveInteger.facets[:min_inclusive])
|
|
232
|
+
.to eq(1)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it "validates metaschema-typed attributes through model validation" do
|
|
236
|
+
stub_const("WiredNode", Class.new(Lutaml::Model::Serializable) do
|
|
237
|
+
attribute :id, :uuid
|
|
238
|
+
attribute :contact, :email_address
|
|
239
|
+
attribute :retries, :positive_integer
|
|
240
|
+
|
|
241
|
+
json do
|
|
242
|
+
map "id", to: :id
|
|
243
|
+
map "contact", to: :contact
|
|
244
|
+
map "retries", to: :retries
|
|
245
|
+
end
|
|
246
|
+
end)
|
|
247
|
+
|
|
248
|
+
good = WiredNode.new(id: "a3bb189e-8bf9-4888-9912-ace4e6543002",
|
|
249
|
+
contact: "ops@example.com", retries: 2)
|
|
250
|
+
expect(good.validate).to be_empty
|
|
251
|
+
|
|
252
|
+
bad = WiredNode.new(id: "not-a-uuid", contact: "no-at-sign", retries: 0)
|
|
253
|
+
messages = bad.validate.map(&:message)
|
|
254
|
+
|
|
255
|
+
expect(messages).to all(be_a(String))
|
|
256
|
+
expect(messages.join("\n")).to include("id")
|
|
257
|
+
expect(messages.join("\n")).to include("contact")
|
|
258
|
+
expect(messages.join("\n")).to include("retries")
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
it "exports the metaschema restrictions in generated XSD" do
|
|
262
|
+
stub_const("XsdNode", Class.new(Lutaml::Model::Serializable) do
|
|
263
|
+
attribute :id, :uuid
|
|
264
|
+
attribute :contact, :email_address
|
|
265
|
+
attribute :retries, :positive_integer
|
|
266
|
+
|
|
267
|
+
xml do
|
|
268
|
+
element "node"
|
|
269
|
+
map_attribute "id", to: :id
|
|
270
|
+
map_element "contact", to: :contact
|
|
271
|
+
map_element "retries", to: :retries
|
|
272
|
+
end
|
|
273
|
+
end)
|
|
274
|
+
|
|
275
|
+
xsd = Lutaml::Model::Schema.to_xsd(XsdNode)
|
|
276
|
+
|
|
277
|
+
expect(xsd).to include("[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[45]")
|
|
278
|
+
expect(xsd).to include(".+@.+")
|
|
279
|
+
expect(xsd).to include('minInclusive value="1"')
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it "exports the NIST JSON representations in generated JSON and YAML schemas" do
|
|
283
|
+
stub_const("JsonWiredNode", Class.new(Lutaml::Model::Serializable) do
|
|
284
|
+
attribute :id, :uuid
|
|
285
|
+
attribute :contact, :email_address
|
|
286
|
+
attribute :address, :ip_v4_address
|
|
287
|
+
attribute :retries, :positive_integer
|
|
288
|
+
attribute :term, :year_month_duration
|
|
289
|
+
|
|
290
|
+
json do
|
|
291
|
+
map "id", to: :id
|
|
292
|
+
map "contact", to: :contact
|
|
293
|
+
map "address", to: :address
|
|
294
|
+
map "retries", to: :retries
|
|
295
|
+
map "term", to: :term
|
|
296
|
+
end
|
|
297
|
+
end)
|
|
298
|
+
|
|
299
|
+
properties = JSON.parse(Lutaml::Model::Schema.to_json(JsonWiredNode))
|
|
300
|
+
.fetch("$defs").fetch("JsonWiredNode").fetch("properties")
|
|
301
|
+
|
|
302
|
+
expect(properties.fetch("id").fetch("pattern"))
|
|
303
|
+
.to eq("^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[45][0-9A-Fa-f]{3}-" \
|
|
304
|
+
"[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}$")
|
|
305
|
+
expect(properties.fetch("contact")).to include("format" => "email")
|
|
306
|
+
expect(properties.fetch("address")).to include("format" => "ipv4")
|
|
307
|
+
expect(Array(properties.fetch("retries").fetch("type")))
|
|
308
|
+
.to include("integer")
|
|
309
|
+
expect(properties.fetch("retries")).to include("minimum" => 1)
|
|
310
|
+
expect(properties.fetch("term")).to include("format" => "duration")
|
|
311
|
+
|
|
312
|
+
yaml_properties = YAML.safe_load(
|
|
313
|
+
Lutaml::Model::Schema.to_yaml(JsonWiredNode),
|
|
314
|
+
).fetch("$defs").fetch("JsonWiredNode").fetch("properties")
|
|
315
|
+
|
|
316
|
+
expect(yaml_properties.fetch("id")["pattern"]).to include("[45]")
|
|
317
|
+
expect(yaml_properties.fetch("retries")["minimum"]).to eq(1)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
it "round-trips metaschema-typed models through every format" do
|
|
321
|
+
stub_const("FormatNode", Class.new(Lutaml::Model::Serializable) do
|
|
322
|
+
attribute :id, :uuid
|
|
323
|
+
attribute :host, :hostname
|
|
324
|
+
attribute :address, :ip_v4_address
|
|
325
|
+
attribute :renewal, :year_month_duration
|
|
326
|
+
|
|
327
|
+
xml do
|
|
328
|
+
element "node"
|
|
329
|
+
map_attribute "id", to: :id
|
|
330
|
+
map_element "host", to: :host
|
|
331
|
+
map_element "address", to: :address
|
|
332
|
+
map_element "renewal", to: :renewal
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
json do
|
|
336
|
+
map "id", to: :id
|
|
337
|
+
map "host", to: :host
|
|
338
|
+
map "address", to: :address
|
|
339
|
+
map "renewal", to: :renewal
|
|
340
|
+
end
|
|
341
|
+
end)
|
|
342
|
+
|
|
343
|
+
node = FormatNode.new(id: "a3bb189e-8bf9-4888-9912-ace4e6543002",
|
|
344
|
+
host: "example.com", address: "10.0.0.7",
|
|
345
|
+
renewal: "P1Y6M")
|
|
346
|
+
|
|
347
|
+
xml_round_trip = FormatNode.from_xml(node.to_xml)
|
|
348
|
+
expect(xml_round_trip).to eq(node)
|
|
349
|
+
|
|
350
|
+
json_round_trip = FormatNode.from_json(node.to_json)
|
|
351
|
+
expect(json_round_trip).to eq(node)
|
|
352
|
+
|
|
353
|
+
yaml_round_trip = FormatNode.from_yaml(node.to_yaml)
|
|
354
|
+
expect(yaml_round_trip).to eq(node)
|
|
355
|
+
|
|
356
|
+
toml_round_trip = FormatNode.from_toml(node.to_toml)
|
|
357
|
+
expect(toml_round_trip).to eq(node)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
it "keeps the metaschema pattern sources export-clean" do
|
|
361
|
+
[Lutaml::Model::Type::IpV6Address,
|
|
362
|
+
Lutaml::Model::Type::DayTimeDuration].each do |type|
|
|
363
|
+
expect(type::PATTERN.source).not_to match(/\s/)
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
224
367
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/schema"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
# Runtime XSD-to-classes compilation (#251): Schema.from_xml compiles a
|
|
8
|
+
# schema into generated Serializable classes, and with load_classes: the
|
|
9
|
+
# classes are available in-process for direct parsing and serialization —
|
|
10
|
+
# no generated files on disk.
|
|
11
|
+
RSpec.describe "runtime XSD class loading" do
|
|
12
|
+
let(:xsd) do
|
|
13
|
+
<<~XSD
|
|
14
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
15
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
16
|
+
<xs:complexType name="EntryType">
|
|
17
|
+
<xs:sequence>
|
|
18
|
+
<xs:element name="name" type="xs:string"/>
|
|
19
|
+
<xs:element name="year" type="xs:integer"/>
|
|
20
|
+
</xs:sequence>
|
|
21
|
+
<xs:attribute name="id" type="xs:string" use="required"/>
|
|
22
|
+
</xs:complexType>
|
|
23
|
+
<xs:element name="entry" type="EntryType"/>
|
|
24
|
+
</xs:schema>
|
|
25
|
+
XSD
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
let(:instance_xml) do
|
|
29
|
+
<<~XML
|
|
30
|
+
<entry id="e1"><name>Alpha</name><year>2024</year></entry>
|
|
31
|
+
XML
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "compiles a schema, loads the classes, and round-trips an instance" do
|
|
35
|
+
Dir.mktmpdir do |dir|
|
|
36
|
+
Lutaml::Model::Schema.from_xml(
|
|
37
|
+
xsd, output_dir: File.join(dir, "runtime_pilot_models"),
|
|
38
|
+
load_classes: true
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
doc = RuntimePilotModels::EntryType.from_xml(instance_xml)
|
|
42
|
+
|
|
43
|
+
expect(doc.name).to eq("Alpha")
|
|
44
|
+
expect(doc.year).to eq(2024)
|
|
45
|
+
expect(doc.id).to eq("e1")
|
|
46
|
+
|
|
47
|
+
expect(doc.to_xml).to include("<name>Alpha</name>")
|
|
48
|
+
expect(doc.to_xml).to include("<year>2024</year>")
|
|
49
|
+
expect(doc.to_xml).to include('id="e1"')
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "xs:list nested simpleType mapping" do
|
|
6
|
+
it "maps the inline simpleType of a list from the parsed XSD" do
|
|
7
|
+
xsd = <<~XSD
|
|
8
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
9
|
+
<xs:element name="r">
|
|
10
|
+
<xs:simpleType>
|
|
11
|
+
<xs:list>
|
|
12
|
+
<xs:simpleType>
|
|
13
|
+
<xs:restriction base="xs:string">
|
|
14
|
+
<xs:enumeration value="a"/>
|
|
15
|
+
</xs:restriction>
|
|
16
|
+
</xs:simpleType>
|
|
17
|
+
</xs:list>
|
|
18
|
+
</xs:simpleType>
|
|
19
|
+
</xs:element>
|
|
20
|
+
</xs:schema>
|
|
21
|
+
XSD
|
|
22
|
+
|
|
23
|
+
schema = Lutaml::Xml::Schema::Xsd.parse(xsd)
|
|
24
|
+
list = schema.element.first.simple_type.list
|
|
25
|
+
|
|
26
|
+
expect(list.simple_type).to be_a(Lutaml::Xml::Schema::Xsd::SimpleType)
|
|
27
|
+
values = Array(list.simple_type.restriction.enumeration).map(&:value)
|
|
28
|
+
expect(values).to eq(["a"])
|
|
29
|
+
end
|
|
30
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.129
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -1837,6 +1837,7 @@ files:
|
|
|
1837
1837
|
- spec/lutaml/model/schema/renderers/namespace_spec.rb
|
|
1838
1838
|
- spec/lutaml/model/schema/renderers/restricted_type_spec.rb
|
|
1839
1839
|
- spec/lutaml/model/schema/renderers/union_spec.rb
|
|
1840
|
+
- spec/lutaml/model/schema/xml_compiler_runtime_spec.rb
|
|
1840
1841
|
- spec/lutaml/model/schema/xsd_schema_spec.rb
|
|
1841
1842
|
- spec/lutaml/model/schema/yaml_schema_spec.rb
|
|
1842
1843
|
- spec/lutaml/model/sequence_choice_compositor_spec.rb
|
|
@@ -2054,6 +2055,7 @@ files:
|
|
|
2054
2055
|
- spec/lutaml/xml/schema/xsd/liquid_methods/element_spec.rb
|
|
2055
2056
|
- spec/lutaml/xml/schema/xsd/liquid_methods/schema_spec.rb
|
|
2056
2057
|
- spec/lutaml/xml/schema/xsd/liquid_methods/simple_content_spec.rb
|
|
2058
|
+
- spec/lutaml/xml/schema/xsd/list_simple_type_spec.rb
|
|
2057
2059
|
- spec/lutaml/xml/schema/xsd/namespace_uri_remapping_spec.rb
|
|
2058
2060
|
- spec/lutaml/xml/schema/xsd/schema_helper_methods_spec.rb
|
|
2059
2061
|
- spec/lutaml/xml/schema/xsd/schema_mapping_spec.rb
|