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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +52 -0
  3. data/docs/_guides/keyvalue-serialization.adoc +75 -0
  4. data/lib/compat/opal/lutaml_model_boot.rb +15 -0
  5. data/lib/lutaml/key_value/transform.rb +126 -26
  6. data/lib/lutaml/model/error/unknown_mapping_error.rb +18 -0
  7. data/lib/lutaml/model/serialize/format_conversion.rb +115 -2
  8. data/lib/lutaml/model/serialize/transformation_builder.rb +4 -3
  9. data/lib/lutaml/model/transform.rb +29 -4
  10. data/lib/lutaml/model/transformation_registry.rb +7 -5
  11. data/lib/lutaml/model/type/date_time_with_timezone.rb +24 -0
  12. data/lib/lutaml/model/type/date_with_timezone.rb +24 -0
  13. data/lib/lutaml/model/type/day_time_duration.rb +31 -0
  14. data/lib/lutaml/model/type/email_address.rb +22 -0
  15. data/lib/lutaml/model/type/hostname.rb +24 -0
  16. data/lib/lutaml/model/type/ip_v4_address.rb +23 -0
  17. data/lib/lutaml/model/type/ip_v6_address.rb +42 -0
  18. data/lib/lutaml/model/type/metaschema_pattern.rb +36 -0
  19. data/lib/lutaml/model/type/non_negative_integer.rb +24 -0
  20. data/lib/lutaml/model/type/positive_integer.rb +24 -0
  21. data/lib/lutaml/model/type/token.rb +23 -0
  22. data/lib/lutaml/model/type/uri_reference.rb +23 -0
  23. data/lib/lutaml/model/type/uuid.rb +23 -0
  24. data/lib/lutaml/model/type/year_month_duration.rb +25 -0
  25. data/lib/lutaml/model/type.rb +27 -0
  26. data/lib/lutaml/model/version.rb +1 -1
  27. data/lib/lutaml/model.rb +2 -0
  28. data/lib/lutaml/xml/adapter/xml_serializer.rb +8 -6
  29. data/lib/lutaml/xml/model_transform.rb +8 -4
  30. data/lib/lutaml/xml/schema/xsd_schema.rb +30 -9
  31. data/lib/lutaml/xml/transformation.rb +3 -2
  32. data/spec/lutaml/key_value/child_map_plan_spec.rb +169 -0
  33. data/spec/lutaml/model/metaschema_types_spec.rb +224 -0
  34. data/spec/lutaml/model/serialization_profiles_spec.rb +276 -0
  35. 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
- def self.cached_transform(context, register)
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
- key = transformation_key(model_class, format, register)
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 = get_or_build_mapping(model_class, format, register)
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
@@ -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 =
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.125"
5
+ VERSION = "0.8.127"
6
6
  end
7
7
  end
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(:xml, register).transform(root,
153
- options)
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[:mapper_class]&.mappings_for(:xml) ||
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(:xml, lutaml_register)
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
- xml_mapping = mappings_for(:xml, effective_register)
275
- mappings = options[:mappings] || xml_mapping.mappings(effective_register)
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 if options[:mappings].nil?
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]