lutaml-model 0.8.125 → 0.8.127
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 +52 -0
- data/docs/_guides/keyvalue-serialization.adoc +75 -0
- data/lib/compat/opal/lutaml_model_boot.rb +15 -0
- data/lib/lutaml/key_value/transform.rb +126 -26
- data/lib/lutaml/model/error/unknown_mapping_error.rb +18 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +115 -2
- data/lib/lutaml/model/serialize/transformation_builder.rb +4 -3
- data/lib/lutaml/model/transform.rb +29 -4
- data/lib/lutaml/model/transformation_registry.rb +7 -5
- data/lib/lutaml/model/type/date_time_with_timezone.rb +24 -0
- data/lib/lutaml/model/type/date_with_timezone.rb +24 -0
- data/lib/lutaml/model/type/day_time_duration.rb +31 -0
- data/lib/lutaml/model/type/email_address.rb +22 -0
- data/lib/lutaml/model/type/hostname.rb +24 -0
- data/lib/lutaml/model/type/ip_v4_address.rb +23 -0
- data/lib/lutaml/model/type/ip_v6_address.rb +42 -0
- data/lib/lutaml/model/type/metaschema_pattern.rb +36 -0
- data/lib/lutaml/model/type/non_negative_integer.rb +24 -0
- data/lib/lutaml/model/type/positive_integer.rb +24 -0
- data/lib/lutaml/model/type/token.rb +23 -0
- data/lib/lutaml/model/type/uri_reference.rb +23 -0
- data/lib/lutaml/model/type/uuid.rb +23 -0
- data/lib/lutaml/model/type/year_month_duration.rb +25 -0
- data/lib/lutaml/model/type.rb +27 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +2 -0
- data/lib/lutaml/xml/adapter/xml_serializer.rb +8 -6
- data/lib/lutaml/xml/model_transform.rb +8 -4
- data/lib/lutaml/xml/schema/xsd_schema.rb +30 -9
- data/lib/lutaml/xml/transformation.rb +3 -2
- data/spec/lutaml/key_value/child_map_plan_spec.rb +169 -0
- data/spec/lutaml/model/metaschema_types_spec.rb +224 -0
- data/spec/lutaml/model/serialization_profiles_spec.rb +276 -0
- metadata +20 -2
|
@@ -11,18 +11,23 @@ module Lutaml
|
|
|
11
11
|
|
|
12
12
|
def self.data_to_model(context, data, format, options = {})
|
|
13
13
|
register = options[:register] || Lutaml::Model::Config.default_register
|
|
14
|
-
transform = cached_transform(context, register)
|
|
14
|
+
transform = cached_transform(context, register, options[:mappings])
|
|
15
15
|
transform.data_to_model(data, format, options)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def self.model_to_data(context, model, format, options = {})
|
|
19
19
|
register = model.lutaml_register if model.is_a?(Lutaml::Model::Serialize)
|
|
20
20
|
register ||= Lutaml::Model::Config.default_register
|
|
21
|
-
transform = cached_transform(context, register)
|
|
21
|
+
transform = cached_transform(context, register, options[:mappings])
|
|
22
22
|
transform.model_to_data(model, format, options)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
# An explicit mapping (#176 `mapping:` profiles / `mappings:` seam)
|
|
26
|
+
# gets a dedicated transform so per-instance compiled state (walk
|
|
27
|
+
# plans, serialize plans) never mixes with the base mapping's.
|
|
28
|
+
def self.cached_transform(context, register, mappings = nil)
|
|
29
|
+
return override_transform(context, register, mappings) if mappings
|
|
30
|
+
|
|
26
31
|
@transform_cache ||= {}
|
|
27
32
|
cache_key = [context, register]
|
|
28
33
|
entry = @transform_cache[cache_key]
|
|
@@ -32,8 +37,25 @@ module Lutaml
|
|
|
32
37
|
@transform_cache[cache_key] = new(context, register)
|
|
33
38
|
end
|
|
34
39
|
|
|
40
|
+
def self.override_transform(context, register, mappings)
|
|
41
|
+
@override_transform_cache ||= {}
|
|
42
|
+
cache_key = [context, register, mappings.object_id]
|
|
43
|
+
entry = @override_transform_cache[cache_key]
|
|
44
|
+
return entry if entry
|
|
45
|
+
|
|
46
|
+
evict_override_if_needed if @override_transform_cache.size >= MAX_CACHE_SIZE
|
|
47
|
+
@override_transform_cache[cache_key] = new(context, register, mappings)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.evict_override_if_needed
|
|
51
|
+
keys = @override_transform_cache.keys.first(@override_transform_cache.size / 2)
|
|
52
|
+
keys.each { |k| @override_transform_cache.delete(k) }
|
|
53
|
+
end
|
|
54
|
+
private_class_method :evict_override_if_needed
|
|
55
|
+
|
|
35
56
|
def self.clear_cache!
|
|
36
57
|
@transform_cache&.clear
|
|
58
|
+
@override_transform_cache&.clear
|
|
37
59
|
end
|
|
38
60
|
|
|
39
61
|
def self.cache_size
|
|
@@ -59,9 +81,10 @@ module Lutaml
|
|
|
59
81
|
|
|
60
82
|
attr_reader :context, :lutaml_register
|
|
61
83
|
|
|
62
|
-
def initialize(context, register = nil)
|
|
84
|
+
def initialize(context, register = nil, mappings_override = nil)
|
|
63
85
|
@context = context
|
|
64
86
|
@lutaml_register = register || Lutaml::Model::Config.default_register
|
|
87
|
+
@lutaml_mapping_override = mappings_override
|
|
65
88
|
end
|
|
66
89
|
|
|
67
90
|
def attributes
|
|
@@ -106,6 +129,8 @@ module Lutaml
|
|
|
106
129
|
end
|
|
107
130
|
|
|
108
131
|
def mappings_for(format, register = nil)
|
|
132
|
+
return @lutaml_mapping_override if @lutaml_mapping_override
|
|
133
|
+
|
|
109
134
|
context.mappings_for(format, register)
|
|
110
135
|
end
|
|
111
136
|
|
|
@@ -113,8 +113,9 @@ module Lutaml
|
|
|
113
113
|
# @param format [Symbol] The format (:xml, :json, :yaml, :hash, :toml)
|
|
114
114
|
# @param register [Symbol, Register, nil] The register for type resolution
|
|
115
115
|
# @return [Transformation, nil] The transformation, or nil if cycle detected
|
|
116
|
-
def get_or_build_transformation(model_class, format, register
|
|
117
|
-
|
|
116
|
+
def get_or_build_transformation(model_class, format, register,
|
|
117
|
+
mapping: nil)
|
|
118
|
+
key = transformation_key(model_class, format, register, mapping)
|
|
118
119
|
current_thread = Thread.current
|
|
119
120
|
|
|
120
121
|
@mutex.synchronize do
|
|
@@ -144,7 +145,7 @@ module Lutaml
|
|
|
144
145
|
|
|
145
146
|
# Build transformation OUTSIDE the lock to avoid deadlock
|
|
146
147
|
# (building may trigger recursive calls to get_or_build_transformation)
|
|
147
|
-
mapping
|
|
148
|
+
mapping ||= get_or_build_mapping(model_class, format, register)
|
|
148
149
|
transformation = build_transformation(model_class, mapping, format,
|
|
149
150
|
register)
|
|
150
151
|
|
|
@@ -221,7 +222,7 @@ module Lutaml
|
|
|
221
222
|
# @param format [Symbol] The format
|
|
222
223
|
# @param register [Symbol, Register, nil] The register
|
|
223
224
|
# @return [Symbol] Unique cache key
|
|
224
|
-
def transformation_key(model_class, format, register)
|
|
225
|
+
def transformation_key(model_class, format, register, mapping = nil)
|
|
225
226
|
register_id = extract_register_id(register)
|
|
226
227
|
# Always include object_id to prevent cache pollution between classes
|
|
227
228
|
# with the same name (common in test fixtures)
|
|
@@ -233,7 +234,8 @@ module Lutaml
|
|
|
233
234
|
else
|
|
234
235
|
"#{class_name}@0x#{model_class.object_id.to_s(16)}"
|
|
235
236
|
end
|
|
236
|
-
:"#{class_id}#{format}#{register_id}"
|
|
237
|
+
key = :"#{class_id}#{format}#{register_id}"
|
|
238
|
+
mapping ? :"#{key}#m#{mapping.object_id}" : key
|
|
237
239
|
end
|
|
238
240
|
|
|
239
241
|
# Build unique key for mapping cache
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# NIST Metaschema date-time-with-timezone datatype (#107): a
|
|
7
|
+
# date-time that must carry a UTC offset or Z.
|
|
8
|
+
#
|
|
9
|
+
# @example Using DateTimeWithTimezone type
|
|
10
|
+
# attribute :generated_at, :date_time_with_timezone
|
|
11
|
+
class DateTimeWithTimezone < DateTime
|
|
12
|
+
TIMEZONE = /(Z|[+-]\d{2}:\d{2})\z/
|
|
13
|
+
|
|
14
|
+
def self.valid?(value)
|
|
15
|
+
value.is_a?(::String) ? value.match?(TIMEZONE) : !cast(value).nil?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.default_xsd_type
|
|
19
|
+
"xs:dateTime"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# NIST Metaschema date-with-timezone datatype (#107): a date that
|
|
7
|
+
# must carry a UTC offset or Z.
|
|
8
|
+
#
|
|
9
|
+
# @example Using DateWithTimezone type
|
|
10
|
+
# attribute :effective_on, :date_with_timezone
|
|
11
|
+
class DateWithTimezone < Date
|
|
12
|
+
TIMEZONE = /(Z|[+-]\d{2}:\d{2})\z/
|
|
13
|
+
|
|
14
|
+
def self.valid?(value)
|
|
15
|
+
value.is_a?(::String) ? value.match?(TIMEZONE) : !cast(value).nil?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.default_xsd_type
|
|
19
|
+
"xs:date"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# NIST Metaschema day-time-duration datatype (#107): an ISO 8601
|
|
7
|
+
# duration expressed in days, hours, minutes, and seconds (no
|
|
8
|
+
# years or months), per the xs:dayTimeDuration form.
|
|
9
|
+
#
|
|
10
|
+
# @example Using DayTimeDuration type
|
|
11
|
+
# attribute :elapsed, :day_time_duration
|
|
12
|
+
class DayTimeDuration < Duration
|
|
13
|
+
ELAPSED_TIME =
|
|
14
|
+
/(?:[0-9]+H(?:[0-9]+M)?(?:[0-9]+(?:\.[0-9]+)?S)?
|
|
15
|
+
|[0-9]+M(?:[0-9]+(?:\.[0-9]+)?S)?
|
|
16
|
+
|[0-9]+(?:\.[0-9]+)?S)/x
|
|
17
|
+
PATTERN = /
|
|
18
|
+
\A-?P(?:[0-9]+D(?:T#{ELAPSED_TIME.source})?|T#{ELAPSED_TIME.source})\z
|
|
19
|
+
/x
|
|
20
|
+
|
|
21
|
+
def self.valid?(value)
|
|
22
|
+
value.is_a?(::String) ? value.match?(PATTERN) : !cast(value).nil?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.default_xsd_type
|
|
26
|
+
"xs:dayTimeDuration"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# NIST Metaschema email-address datatype (#107): a string containing
|
|
7
|
+
# an at-sign, per the metaschema-datatypes pattern `.+@.+`.
|
|
8
|
+
#
|
|
9
|
+
# @example Using EmailAddress type
|
|
10
|
+
# attribute :contact, :email_address
|
|
11
|
+
class EmailAddress < String
|
|
12
|
+
extend MetaschemaPattern
|
|
13
|
+
|
|
14
|
+
PATTERN = /\A.+@.+\z/
|
|
15
|
+
|
|
16
|
+
def self.default_xsd_type
|
|
17
|
+
"xs:string"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# NIST Metaschema hostname datatype (#107): a non-empty string with
|
|
7
|
+
# no outer whitespace. The normative XSD carries no hostname pattern
|
|
8
|
+
# yet (the metaschema spec notes it is pending), so only the
|
|
9
|
+
# trimmed-string base constraint applies.
|
|
10
|
+
#
|
|
11
|
+
# @example Using Hostname type
|
|
12
|
+
# attribute :host, :hostname
|
|
13
|
+
class Hostname < String
|
|
14
|
+
extend MetaschemaPattern
|
|
15
|
+
|
|
16
|
+
PATTERN = /\A\S(.*\S)?\z/
|
|
17
|
+
|
|
18
|
+
def self.default_xsd_type
|
|
19
|
+
"xs:string"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# NIST Metaschema ip-v4-address datatype (#107): dotted-quad IPv4
|
|
7
|
+
# per the metaschema-datatypes pattern.
|
|
8
|
+
#
|
|
9
|
+
# @example Using IpV4Address type
|
|
10
|
+
# attribute :address, :ip_v4_address
|
|
11
|
+
class IpV4Address < String
|
|
12
|
+
extend MetaschemaPattern
|
|
13
|
+
|
|
14
|
+
OCTET = /(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])/
|
|
15
|
+
PATTERN = /\A#{OCTET.source}\.#{OCTET.source}\.#{OCTET.source}\.#{OCTET.source}\z/
|
|
16
|
+
|
|
17
|
+
def self.default_xsd_type
|
|
18
|
+
"xs:string"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# NIST Metaschema ip-v6-address datatype (#107): RFC3513 IPv6 text
|
|
7
|
+
# form, per the metaschema-datatypes pattern (the generalized
|
|
8
|
+
# StackOverflow IPv6 expression with metaschema customizations,
|
|
9
|
+
# including link-local zones and embedded IPv4).
|
|
10
|
+
#
|
|
11
|
+
# @example Using IpV6Address type
|
|
12
|
+
# attribute :address, :ip_v6_address
|
|
13
|
+
class IpV6Address < String
|
|
14
|
+
extend MetaschemaPattern
|
|
15
|
+
|
|
16
|
+
HEX4 = "[0-9a-fA-F]{1,4}"
|
|
17
|
+
OCTET = "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])"
|
|
18
|
+
EMBEDDED_V4 = "(#{OCTET}\\.){3,3}#{OCTET}".freeze
|
|
19
|
+
PATTERN = /
|
|
20
|
+
\A(
|
|
21
|
+
(#{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}
|
|
33
|
+
)\z
|
|
34
|
+
/x
|
|
35
|
+
|
|
36
|
+
def self.default_xsd_type
|
|
37
|
+
"xs:string"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# Pattern machinery shared by the NIST Metaschema string types (#107).
|
|
7
|
+
# Each including class defines a PATTERN regexp; values cast to
|
|
8
|
+
# strings and validate full-match against it.
|
|
9
|
+
module MetaschemaPattern
|
|
10
|
+
def pattern
|
|
11
|
+
self::PATTERN
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def valid?(value)
|
|
15
|
+
return false if value.nil?
|
|
16
|
+
|
|
17
|
+
pattern.match?(value.to_s)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def cast(value, _options = {})
|
|
21
|
+
return nil if value.nil?
|
|
22
|
+
return value if Utils.uninitialized?(value)
|
|
23
|
+
return value if value.is_a?(::String)
|
|
24
|
+
|
|
25
|
+
value.to_s
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def serialize(value)
|
|
29
|
+
return nil if value.nil?
|
|
30
|
+
|
|
31
|
+
value.to_s
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# NIST Metaschema non-negative-integer datatype (#107): an integer
|
|
7
|
+
# equal to or greater than 0 (xs:nonNegativeInteger).
|
|
8
|
+
#
|
|
9
|
+
# @example Using NonNegativeInteger type
|
|
10
|
+
# attribute :index, :non_negative_integer
|
|
11
|
+
class NonNegativeInteger < Integer
|
|
12
|
+
def self.valid?(value)
|
|
13
|
+
cast_value = cast(value)
|
|
14
|
+
|
|
15
|
+
cast_value.is_a?(::Integer) && cast_value >= 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.default_xsd_type
|
|
19
|
+
"xs:nonNegativeInteger"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# NIST Metaschema positive-integer datatype (#107): an integer
|
|
7
|
+
# greater than 0 (xs:positiveInteger).
|
|
8
|
+
#
|
|
9
|
+
# @example Using PositiveInteger type
|
|
10
|
+
# attribute :ordinal, :positive_integer
|
|
11
|
+
class PositiveInteger < Integer
|
|
12
|
+
def self.valid?(value)
|
|
13
|
+
cast_value = cast(value)
|
|
14
|
+
|
|
15
|
+
cast_value.is_a?(::Integer) && cast_value >= 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.default_xsd_type
|
|
19
|
+
"xs:positiveInteger"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# NIST Metaschema token datatype (#107): an XML NMTOKEN-like value —
|
|
7
|
+
# a letter or underscore followed by letters, numbers, and the
|
|
8
|
+
# punctuation `.-_` — per the metaschema-datatypes pattern.
|
|
9
|
+
#
|
|
10
|
+
# @example Using Token type
|
|
11
|
+
# attribute :label, :token
|
|
12
|
+
class Token < String
|
|
13
|
+
extend MetaschemaPattern
|
|
14
|
+
|
|
15
|
+
PATTERN = /\A(\p{L}|_)(\p{L}|\p{N}|[.\-_])*\z/
|
|
16
|
+
|
|
17
|
+
def self.default_xsd_type
|
|
18
|
+
"xs:string"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# NIST Metaschema uri-reference datatype (#107): a relative or
|
|
7
|
+
# absolute URI reference — any non-empty string without outer
|
|
8
|
+
# whitespace, unlike :uri which requires a scheme.
|
|
9
|
+
#
|
|
10
|
+
# @example Using UriReference type
|
|
11
|
+
# attribute :link, :uri_reference
|
|
12
|
+
class UriReference < String
|
|
13
|
+
extend MetaschemaPattern
|
|
14
|
+
|
|
15
|
+
PATTERN = /\A\S(.*\S)?\z/
|
|
16
|
+
|
|
17
|
+
def self.default_xsd_type
|
|
18
|
+
"xs:anyURI"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# NIST Metaschema uuid datatype (#107): an RFC4122 UUID, restricted
|
|
7
|
+
# to versions 4 and 5 and the standard variant, per the
|
|
8
|
+
# metaschema-datatypes pattern.
|
|
9
|
+
#
|
|
10
|
+
# @example Using Uuid type
|
|
11
|
+
# attribute :identifier, :uuid
|
|
12
|
+
class Uuid < String
|
|
13
|
+
extend MetaschemaPattern
|
|
14
|
+
|
|
15
|
+
PATTERN = %r{\A[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[45][0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}\z}
|
|
16
|
+
|
|
17
|
+
def self.default_xsd_type
|
|
18
|
+
"xs:string"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# NIST Metaschema year-month-duration datatype (#107): an ISO 8601
|
|
7
|
+
# duration expressed in years and months (no time components), per
|
|
8
|
+
# the xs:yearMonthDuration form — P1Y6M, -P9M, P10M.
|
|
9
|
+
#
|
|
10
|
+
# @example Using YearMonthDuration type
|
|
11
|
+
# attribute :term, :year_month_duration
|
|
12
|
+
class YearMonthDuration < Duration
|
|
13
|
+
PATTERN = /\A-?P(?:[0-9]+Y(?:[0-9]+M)?|[0-9]+M)\z/
|
|
14
|
+
|
|
15
|
+
def self.valid?(value)
|
|
16
|
+
value.is_a?(::String) ? value.match?(PATTERN) : !cast(value).nil?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.default_xsd_type
|
|
20
|
+
"xs:yearMonthDuration"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/lutaml/model/type.rb
CHANGED
|
@@ -24,6 +24,20 @@ module Lutaml
|
|
|
24
24
|
autoload :QName, "#{__dir__}/type/qname"
|
|
25
25
|
autoload :Base64Binary, "#{__dir__}/type/base64_binary"
|
|
26
26
|
autoload :HexBinary, "#{__dir__}/type/hex_binary"
|
|
27
|
+
autoload :MetaschemaPattern, "#{__dir__}/type/metaschema_pattern"
|
|
28
|
+
autoload :EmailAddress, "#{__dir__}/type/email_address"
|
|
29
|
+
autoload :Hostname, "#{__dir__}/type/hostname"
|
|
30
|
+
autoload :IpV4Address, "#{__dir__}/type/ip_v4_address"
|
|
31
|
+
autoload :IpV6Address, "#{__dir__}/type/ip_v6_address"
|
|
32
|
+
autoload :Token, "#{__dir__}/type/token"
|
|
33
|
+
autoload :UriReference, "#{__dir__}/type/uri_reference"
|
|
34
|
+
autoload :Uuid, "#{__dir__}/type/uuid"
|
|
35
|
+
autoload :NonNegativeInteger, "#{__dir__}/type/non_negative_integer"
|
|
36
|
+
autoload :PositiveInteger, "#{__dir__}/type/positive_integer"
|
|
37
|
+
autoload :DateWithTimezone, "#{__dir__}/type/date_with_timezone"
|
|
38
|
+
autoload :DateTimeWithTimezone, "#{__dir__}/type/date_time_with_timezone"
|
|
39
|
+
autoload :DayTimeDuration, "#{__dir__}/type/day_time_duration"
|
|
40
|
+
autoload :YearMonthDuration, "#{__dir__}/type/year_month_duration"
|
|
27
41
|
|
|
28
42
|
# Error classes (defined in error/type/ but under Type namespace)
|
|
29
43
|
autoload :InvalidValueError, "#{__dir__}/error/type/invalid_value_error"
|
|
@@ -55,6 +69,19 @@ module Lutaml
|
|
|
55
69
|
qname: "Lutaml::Model::Type::QName",
|
|
56
70
|
base64_binary: "Lutaml::Model::Type::Base64Binary",
|
|
57
71
|
hex_binary: "Lutaml::Model::Type::HexBinary",
|
|
72
|
+
email_address: "Lutaml::Model::Type::EmailAddress",
|
|
73
|
+
hostname: "Lutaml::Model::Type::Hostname",
|
|
74
|
+
ip_v4_address: "Lutaml::Model::Type::IpV4Address",
|
|
75
|
+
ip_v6_address: "Lutaml::Model::Type::IpV6Address",
|
|
76
|
+
token: "Lutaml::Model::Type::Token",
|
|
77
|
+
uri_reference: "Lutaml::Model::Type::UriReference",
|
|
78
|
+
uuid: "Lutaml::Model::Type::Uuid",
|
|
79
|
+
non_negative_integer: "Lutaml::Model::Type::NonNegativeInteger",
|
|
80
|
+
positive_integer: "Lutaml::Model::Type::PositiveInteger",
|
|
81
|
+
date_with_timezone: "Lutaml::Model::Type::DateWithTimezone",
|
|
82
|
+
date_time_with_timezone: "Lutaml::Model::Type::DateTimeWithTimezone",
|
|
83
|
+
day_time_duration: "Lutaml::Model::Type::DayTimeDuration",
|
|
84
|
+
year_month_duration: "Lutaml::Model::Type::YearMonthDuration",
|
|
58
85
|
}.freeze
|
|
59
86
|
|
|
60
87
|
BUILTIN_CLASSES =
|
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/model.rb
CHANGED
|
@@ -164,6 +164,8 @@ module Lutaml
|
|
|
164
164
|
"#{__dir__}/model/error/import_model_with_root_error"
|
|
165
165
|
autoload :UnsupportedVersionError,
|
|
166
166
|
"#{__dir__}/model/error/unsupported_version_error"
|
|
167
|
+
autoload :UnknownMappingError,
|
|
168
|
+
"#{__dir__}/model/error/unknown_mapping_error"
|
|
167
169
|
autoload :InvalidChoiceRangeError,
|
|
168
170
|
"#{__dir__}/model/error/invalid_choice_range_error"
|
|
169
171
|
autoload :UnknownSequenceMappingError,
|
|
@@ -144,18 +144,19 @@ module Lutaml
|
|
|
144
144
|
return root if root.is_a?(Lutaml::Xml::DataModel::XmlElement)
|
|
145
145
|
|
|
146
146
|
mapper_class = options[:mapper_class] || root.class
|
|
147
|
-
xml_mapping = mapper_class.mappings_for(:xml)
|
|
147
|
+
xml_mapping = options[:mappings] || mapper_class.mappings_for(:xml)
|
|
148
148
|
|
|
149
149
|
return nil if xml_mapping.raw_mapping&.custom_methods&.[](:to)
|
|
150
150
|
|
|
151
151
|
yield(root)
|
|
152
|
-
mapper_class.transformation_for(
|
|
153
|
-
|
|
152
|
+
mapper_class.transformation_for(
|
|
153
|
+
:xml, register, mapping: options[:mappings]
|
|
154
|
+
).transform(root, options)
|
|
154
155
|
end
|
|
155
156
|
|
|
156
157
|
def render_xml_element(xml, xml_element, original_model, options)
|
|
157
158
|
mapper_class = options[:mapper_class] || xml_element.class
|
|
158
|
-
mapping = mapper_class.mappings_for(:xml)
|
|
159
|
+
mapping = options[:mappings] || mapper_class.mappings_for(:xml)
|
|
159
160
|
plan = declaration_plan_for(
|
|
160
161
|
xml_element,
|
|
161
162
|
mapping,
|
|
@@ -171,7 +172,7 @@ module Lutaml
|
|
|
171
172
|
|
|
172
173
|
def render_legacy_model(xml, options)
|
|
173
174
|
mapper_class = options[:mapper_class] || root.class
|
|
174
|
-
xml_mapping = mapper_class.mappings_for(:xml)
|
|
175
|
+
xml_mapping = options[:mappings] || mapper_class.mappings_for(:xml)
|
|
175
176
|
plan = declaration_plan_for(root, xml_mapping, options, mapper_class)
|
|
176
177
|
|
|
177
178
|
build_element_with_plan(xml, root, plan, options)
|
|
@@ -191,7 +192,8 @@ module Lutaml
|
|
|
191
192
|
stored_plan = nil
|
|
192
193
|
|
|
193
194
|
if original_model
|
|
194
|
-
mapping_for_original = options[:
|
|
195
|
+
mapping_for_original = options[:mappings] ||
|
|
196
|
+
options[:mapper_class]&.mappings_for(:xml) ||
|
|
195
197
|
original_model.class.mappings_for(:xml)
|
|
196
198
|
original_ns_uris = collect_original_namespace_uris(
|
|
197
199
|
original_model, mapping_for_original
|
|
@@ -249,7 +249,9 @@ module Lutaml
|
|
|
249
249
|
# Check if model class has a pre-compiled transformation
|
|
250
250
|
model_class = model.class
|
|
251
251
|
if model_class.is_a?(Class) && model_class.include?(::Lutaml::Model::Serialize)
|
|
252
|
-
transformation = model_class.transformation_for(
|
|
252
|
+
transformation = model_class.transformation_for(
|
|
253
|
+
:xml, lutaml_register, mapping: @lutaml_mapping_override
|
|
254
|
+
)
|
|
253
255
|
|
|
254
256
|
# If transformation exists and is an Xml::Transformation, use it
|
|
255
257
|
if transformation.is_a?(::Lutaml::Xml::Transformation)
|
|
@@ -271,11 +273,13 @@ module Lutaml
|
|
|
271
273
|
instance_is_serialize = session.instance_is_serialize
|
|
272
274
|
|
|
273
275
|
# Performance: Resolve xml_mapping once — used for both rules and metadata
|
|
274
|
-
|
|
275
|
-
|
|
276
|
+
# An explicit mapping (#176 profiles) governs the rules and the
|
|
277
|
+
# metadata alike; its own catch-all rule applies.
|
|
278
|
+
xml_mapping = options[:mappings] || mappings_for(:xml, effective_register)
|
|
279
|
+
mappings = xml_mapping.mappings(effective_register)
|
|
276
280
|
# Catch-all rule (map_any_element): iterated like any other,
|
|
277
281
|
# claiming every child no explicit rule matches.
|
|
278
|
-
mappings += [xml_mapping.any_element_rule].compact
|
|
282
|
+
mappings += [xml_mapping.any_element_rule].compact
|
|
279
283
|
default_namespace = options[:default_namespace]
|
|
280
284
|
ordered_option = options[:ordered]
|
|
281
285
|
mixed_content_option = options[:mixed_content]
|