lutaml-model 0.8.38 → 0.8.40

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad19014fa289dad72ab50f6908d425ed7708b4f7046432ac05426b65087fc713
4
- data.tar.gz: 4f2d95722e22e73b3691e6eec7903d3967ec1d9cf9ea2b641b02b77cc59911ef
3
+ metadata.gz: 597b465123fe390703bdb760a4f6e18e4069b55bea8998633e2a4c0c7f5fcb9c
4
+ data.tar.gz: e848597e1ad2cef05670d8dc8de6f7480bf028fde627021c5a61f554ad99a2f0
5
5
  SHA512:
6
- metadata.gz: 575ac30c297c4d8059bccb27b42b1d1e942ffef3352072f1ebdbff55039073cffb228a2e6873c55afd940bc78aaac1830c5375df0b37ac1b4eb498cea3a73fe2
7
- data.tar.gz: 4d1c80a2a3fd04e5c461aec33ba2248547ae5f6c877990f457d23160c272a4f67e17ef4ab05ad450265f5c2aa8c5c637deead3c27b20a2ea1978ab4815d6d397
6
+ metadata.gz: 231f9601a9ce76ae516a6435b2c7185e15c2cc6680d0aefbad27ef328b25f67f0ed2635be8319eff6c3a64338f3bd2f9855ab1daa51058018345eb77cf3aa0d9
7
+ data.tar.gz: 88af6f65b2fadf91990f6f6f10a03a799007acda351d87a059f00325c85514291fbaccfd99580dd1daac3090bd61f4aa1ec26efffd058438c7e6abdd9a53cea4
@@ -0,0 +1,71 @@
1
+ = Custom serialization methods
2
+
3
+ :toc:
4
+
5
+ Lutaml::Model maps serialization constructs (`map_element`, `map_attribute`, `map_content`, key-value `map`) to *model attributes*. When the wire shape and the model shape disagree, custom methods bridge the gap at the mapping site instead of hand-rolled `to_h`/`from_h` code.
6
+
7
+ == `with:` — per-rule custom methods
8
+
9
+ Declare both directions on the rule. The methods live on the model (instance methods); the names are symbols.
10
+
11
+ [source,ruby]
12
+ ----
13
+ class Requirement
14
+ include Lutaml::Model::Serialize
15
+
16
+ attribute :guidance, :string
17
+ attribute :purpose, :string
18
+
19
+ xml do
20
+ element "requirement"
21
+ map_element "component", to: :guidance,
22
+ with: { from: :component_from_guidance, to: :component_to_guidance }
23
+ end
24
+
25
+ def component_from_guidance(model, value, ctx = nil)
26
+ # `from` receives (model, value[, context]); assign the model attribute
27
+ model.guidance = value
28
+ end
29
+
30
+ def component_to_guidance(model, parent, doc = nil, ctx = nil)
31
+ # `to` receives (model, parent[, doc[, context]]); write into parent/doc
32
+ parent.add_element("component", model.guidance, type: "guidance")
33
+ end
34
+ end
35
+ ----
36
+
37
+ Notes:
38
+
39
+ * The `from` method *assigns* the attribute (it receives the whole model); the `to` method *writes* the output document.
40
+ * The optional trailing `context` parameter receives the options hash passed to `Model.from_xml(data, context: {...})` / `model.to_xml(context: {...})` — it is forwarded when the method declares it (lutaml-model#550).
41
+ * Callables work in key-value mappings: `with: { from: ->(value) { ... } }` maps and assigns the return value; an exact-arity-2 callable receives the context as its second argument.
42
+
43
+ == `transform:` — value-level transforms
44
+
45
+ `transform:` converts values (not documents). A Hash form (`transform: { from: ->(v) {...}, to: ->(v) {...} }`) wraps the value in both directions without touching assignment; a `ValueTransformer` subclass receives `from(value, format)` / `to(value, format)` and is shared by every rule that references it.
46
+
47
+ == Type-level serialization
48
+
49
+ A *type class* (a `Lutaml::Model::Type::Value` subclass) can carry its own format behavior, so reuse lives on the type rather than the parent mapping:
50
+
51
+ [source,ruby]
52
+ ----
53
+ class XmiNsType < Lutaml::Model::Type::String
54
+ xml do
55
+ namespace XmiNs # bind the type's own namespace
56
+ end
57
+ end
58
+ ----
59
+
60
+ `Lutaml::Model::Type::Value.register_format_type_serializer(format, type_class, to:, from:)` registers per-format serialize/deserialize procs for a type class; resolution walks the hierarchy.
61
+
62
+ == `of_*` and the conversion entry points
63
+
64
+ `Model.of_<format>(parsed_document, options)` is the documented seam between adapter parsing and mapping application: the adapter (`from_<format>`) parses the wire data into a document object, and `of_<format>` (class method) applies the mappings to build instances. It is public API for tooling that already holds a parsed document — e.g. re-applying mappings to a reparsed tree — and is what `from_<format>` delegates to.
65
+
66
+ The per-instance counterparts are `to_<format>` (serialize through the mappings) and `as_<format>` (render without register-specific side effects). Prefer the generated `from_*/to_*` methods; reach for `of_*` only when you interpose between parse and mapping.
67
+
68
+ == Common recipes
69
+
70
+ * One wire element, several attributes dispatched by an attribute value (`component/@type`): one `with:` pair per attribute whose `from` inspects the dispatch attribute and whose `to` emits only for its type. (Polymorphic *class* dispatch has first-class support via `polymorphic:`.)
71
+ * Key remapping, nesting shifts, delimiter splitting: prefer `transform:`, `child_mappings`, and `root`/`prefix` options before custom methods — the mapping stays declarative and reviewable.
@@ -19,6 +19,7 @@ Task-oriented guides for accomplishing specific goals with Lutaml::Model.
19
19
 
20
20
  == Key-value serialization
21
21
 
22
+ * link:../custom-methods[Custom Serialization Methods] - with:/transform:, type-level serialization, of_* entry points
22
23
  * link:../keyvalue-serialization[Key-Value Serialization] - JSON/YAML/TOML/Hash
23
24
  * link:../collection-serialization[Collection Serialization] - JSONL and YAML Stream
24
25
 
@@ -286,7 +286,6 @@ format)
286
286
  # only nil (non-existent) skips it (lutaml-model#746).
287
287
  return if value.nil?
288
288
 
289
- warn "PROBE-264 keys=#{options.keys.inspect} ctx=#{options[:context].inspect}"
290
289
  return rule.deserialize(instance, value, attributes, model_class,
291
290
  options[:context])
292
291
  end
@@ -208,6 +208,15 @@ module Lutaml
208
208
  # @param args [Array] Arguments (ignored for options)
209
209
  # @param block [Proc] Block (ignored for options)
210
210
  # @return [Object] The option value or nil
211
+ # Static per rule (see MappingRule#custom_method_only?). Compiled
212
+ # rules are frozen, so the answer is recomputed with a single
213
+ # to_s instead of the historical two.
214
+ def custom_method_only?
215
+ name = attribute_name.to_s
216
+ (name.start_with?("__") && name.end_with?("__")) ||
217
+ (!custom_methods.empty? && attribute_type.nil?)
218
+ end
219
+
211
220
  def method_missing(method_name, *args, &block)
212
221
  # Check if this is an option key
213
222
  if options.key?(method_name)
@@ -307,6 +307,18 @@ context = nil)
307
307
  end
308
308
  end
309
309
 
310
+ # Static per rule: placeholder `__name__` targets and inferred
311
+ # custom-method targets without an attribute type never change —
312
+ # the applier asked this per element and paid two to_s strings
313
+ # each time (240k allocations on the ISO-13849 serialize profile).
314
+ # Rules may be frozen, so no per-instance memo: one to_s instead
315
+ # of two is the frozen-safe diet.
316
+ def custom_method_only?
317
+ name = attribute_name.to_s
318
+ (name.start_with?("__") && name.end_with?("__")) ||
319
+ (has_custom_methods? && attribute_type.nil?)
320
+ end
321
+
310
322
  def has_custom_method_for_serialization?
311
323
  !custom_methods.empty? && custom_methods[:to]
312
324
  end
@@ -129,6 +129,7 @@ module Lutaml
129
129
  end
130
130
  end
131
131
  elsif attr.unresolved_type == Lutaml::Model::Type::Reference
132
+ Lutaml::Model::Store.reference_types_in_use!
132
133
  define_reference_methods(name, register_id)
133
134
  else
134
135
  define_regular_attribute_methods(name, attr)
@@ -264,9 +265,12 @@ module Lutaml
264
265
  # class_eval'd bodies cannot close over locals; a hidden
265
266
  # define_method accessor holds the Attribute handle.
266
267
  attr_reader_method = :"__attribute_definition_#{name}"
267
- unless method_defined?(attr_reader_method, false)
268
- define_method(attr_reader_method) { attr }
269
- end
268
+ reader_defined = if Lutaml::Model.opal?
269
+ method_defined?(attr_reader_method)
270
+ else
271
+ method_defined?(attr_reader_method, false)
272
+ end
273
+ define_method(attr_reader_method) { attr } unless reader_defined
270
274
 
271
275
  if attr.collection?
272
276
  # class_eval interpolates, e.g.:
@@ -33,6 +33,11 @@ module Lutaml
33
33
  # The getter materializes a real Array on first access.
34
34
  LAZY_EMPTY_COLLECTION = [].freeze
35
35
 
36
+ # Ivar symbols for lazy collections, memoized per attribute name:
37
+ # the "@name" interpolation allocated twice per collection read
38
+ # (491k on the ISO-13849 serialize profile).
39
+ LAZY_IVAR = {}.compare_by_identity
40
+
36
41
  # Sentinel distinguishing "getter called with no argument" from
37
42
  # builder-syntax `g.attr(value)` where value may be nil. Compiled
38
43
  # getters default to it instead of a `*args` splat.
@@ -406,11 +411,16 @@ module Lutaml
406
411
  # @param attribute_name [Symbol] the collection attribute
407
412
  # @return [Object] the stored collection
408
413
  def materialize_lazy_collection(attribute_name)
409
- current = instance_variable_get(:"@#{attribute_name}")
414
+ ivar = LAZY_IVAR[attribute_name]
415
+ unless ivar
416
+ ivar = :"@#{attribute_name}"
417
+ LAZY_IVAR[attribute_name] = ivar
418
+ end
419
+ current = instance_variable_get(ivar)
410
420
  return current unless current.equal?(LAZY_EMPTY_COLLECTION)
411
421
  return current if frozen?
412
422
 
413
- instance_variable_set(:"@#{attribute_name}", [])
423
+ instance_variable_set(ivar, [])
414
424
  end
415
425
 
416
426
  # Hand back a reference collection the caller can push onto.
@@ -33,11 +33,30 @@ module Lutaml
33
33
  @instance ||= new
34
34
  end
35
35
 
36
+ # lutaml-model#808: Ruby 3.3's GC has a WeakMap-mark hazard under
37
+ # heavy insert churn (freed slot dereferenced at wmap mark time).
38
+ # Registration is only ever consumed by `ref:` resolution — when
39
+ # no Reference-typed attribute exists anywhere, nothing can
40
+ # resolve, so registration is skipped entirely and the WeakMap
41
+ # stays quiet. Set the first time an attribute with a Reference
42
+ # type is defined.
43
+ # Not memoized `||=` — the setter flips it to true for the
44
+ # process lifetime and reads must see that.
45
+ def reference_types_in_use?
46
+ @reference_types_in_use ? true : false
47
+ end
48
+
49
+ def reference_types_in_use!
50
+ @reference_types_in_use = true
51
+ end
52
+
36
53
  def reset!
37
54
  @instance = new
38
55
  end
39
56
 
40
57
  def register(object)
58
+ return unless reference_types_in_use?
59
+
41
60
  instance.register(object)
42
61
  end
43
62
 
@@ -19,7 +19,10 @@ module Lutaml
19
19
  end
20
20
 
21
21
  value = value.to_s
22
- value = value.gsub(/[\t\n\r]/, " ") unless mode == :preserve
22
+ # tr, not gsub: the regexp engine is measurable on hot text
23
+ # paths; :collapse keeps squeeze+strip (two C passes, no
24
+ # intermediate regexp state).
25
+ value = value.tr("\t\n\r", " ") unless mode == :preserve
23
26
  value = value.squeeze(" ").strip if mode == :collapse
24
27
 
25
28
  unless options.equal?(EMPTY_OPTIONS)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.38"
5
+ VERSION = "0.8.40"
6
6
  end
7
7
  end
@@ -299,13 +299,23 @@ module Lutaml
299
299
  # Create element
300
300
  xml.create_and_add_element(tag_name, attributes: attributes,
301
301
  prefix: prefix) do |inner_xml|
302
- # Handle raw content (map_all directive)
302
+ # Handle raw content (map_all directive). lutaml-model#223:
303
+ # with coexisting mappings the raw remainder is the TAIL —
304
+ # mapped children render first through the normal paths
305
+ # below, then the fragment is appended. Standalone map_all
306
+ # (raw is the whole content) keeps the historical
307
+ # raw-only rendering by fronting the fragment and skipping
308
+ # children only when there is nothing mapped.
303
309
  has_raw_content = false
304
310
  if element.is_a?(Lutaml::Xml::DataModel::XmlElement)
305
311
  raw_content = element.raw_content
306
312
  if raw_content && !raw_content.to_s.empty?
307
- inner_xml.add_xml_fragment(inner_xml, raw_content.to_s)
308
- has_raw_content = true
313
+ if element.children.empty?
314
+ inner_xml.add_xml_fragment(inner_xml, raw_content.to_s)
315
+ has_raw_content = true
316
+ else
317
+ (tail_raw_fragments ||= []) << raw_content.to_s
318
+ end
309
319
  end
310
320
  end
311
321
 
@@ -343,6 +353,9 @@ module Lutaml
343
353
  end
344
354
  end
345
355
  end
356
+ (tail_raw_fragments || []).each do |fragment|
357
+ inner_xml.add_xml_fragment(inner_xml, fragment)
358
+ end
346
359
  end
347
360
  end
348
361
 
@@ -268,11 +268,20 @@ module Lutaml
268
268
  attributes["xmlns"] = "" if needs_xmlns_blank
269
269
 
270
270
  xml.create_and_add_element(qualified_name, attributes: attributes) do
271
+ tail_raw = nil
271
272
  if xml_element.is_a?(Lutaml::Xml::DataModel::XmlElement)
272
273
  raw_content = xml_element.raw_content
273
274
  if raw_content && !raw_content.to_s.empty?
274
- xml.add_xml_fragment(xml, raw_content.to_s)
275
- return
275
+ # lutaml-model#223: with coexisting map_all, the raw
276
+ # remainder is the tail — mapped children render first.
277
+ # Standalone map_all (no mapped children) keeps the
278
+ # historical raw-only rendering.
279
+ if xml_element.children.empty?
280
+ xml.add_xml_fragment(xml, raw_content.to_s)
281
+ return
282
+ end
283
+
284
+ tail_raw = raw_content.to_s
276
285
  end
277
286
  end
278
287
 
@@ -306,6 +315,8 @@ module Lutaml
306
315
  end
307
316
  end
308
317
 
318
+ xml.add_xml_fragment(xml, tail_raw) if tail_raw
319
+
309
320
  if xml_element.text_content
310
321
  if xml_element.cdata
311
322
  xml.cdata(xml_element.text_content.to_s)
@@ -109,6 +109,13 @@ module Lutaml
109
109
  children.map(&:to_xml).join
110
110
  end
111
111
 
112
+ # The element including its own tags — the building block for
113
+ # scoped raw capture (#223): an unclaimed child contributes its
114
+ # whole subtree verbatim.
115
+ def inner_xml_or_self
116
+ to_xml
117
+ end
118
+
112
119
  private
113
120
 
114
121
  def build_element_xml(builder)
@@ -979,9 +979,14 @@ module Lutaml
979
979
  "is allowed with #{TYPES[:all_content]}"
980
980
  end
981
981
 
982
- if !(elements.empty? && content_mapping.nil?) && type == TYPES[:all_content]
983
- raise StandardError,
984
- "#{TYPES[:all_content]} is not allowed with other mappings"
982
+ if type == TYPES[:all_content] &&
983
+ !(elements.empty? && content_mapping.nil?)
984
+ # lutaml-model#223: map_all may coexist with mapped siblings.
985
+ # The raw capture receives only the UNCLAIMED content — the
986
+ # verbatim subtree minus children matched by element rules.
987
+ # The historical exclusivity raised here; the coexistence
988
+ # contract is enforced by scoped capture on parse and
989
+ # remainder-after-mapped emission on serialize.
985
990
  end
986
991
  end
987
992
 
@@ -18,9 +18,13 @@ module Lutaml
18
18
  # rules — attribute, derived/valid flags, and #765 group-skip —
19
19
  # keyed on the mapping's finalize version so late changes rebuild.
20
20
  # The loop body used to re-derive these per parsed instance.
21
- # Concurrent::Map has no compare_by_identity; class identity is
22
- # the key semantic here.
23
- RULE_RECORDS = Concurrent::Map.new # rubocop:disable Lint/HashCompareByIdentity
21
+ # Concurrent::Map under threaded MRI, plain Hash under Opal
22
+ # (Concurrent is unavailable there); class identity keys either.
23
+ RULE_RECORDS = if Lutaml::Model.opal?
24
+ {}
25
+ else
26
+ Concurrent::Map.new # rubocop:disable Lint/HashCompareByIdentity
27
+ end
24
28
 
25
29
  # Namespaced rule name -> [local_name, rule_uri]. Pure string
26
30
  # splitting, deterministic per spelling, shared across parses.
@@ -51,14 +55,17 @@ module Lutaml
51
55
  # ISO-13849 profile). Memoize one instance per (element, class)
52
56
  # for the whole parse; the memo travels in the propagated
53
57
  # deserialization options.
54
- memo = options[:__lutaml_child_memo] ||= {}
55
- memo_key = [data.object_id, model_class.object_id]
56
- if (existing = memo[memo_key])
58
+ # Nested identity maps keyed on the element object itself —
59
+ # flat object_id pairs could collide after GC id reuse on
60
+ # long parses (#808 audit).
61
+ memo = options[:__lutaml_child_memo] ||= {}.compare_by_identity
62
+ per_class = (memo[data] ||= {}.compare_by_identity)
63
+ if (existing = per_class[model_class])
57
64
  return existing
58
65
  end
59
66
 
60
67
  instance = model_class.allocate_for_deserialization(child_register)
61
- memo[memo_key] = instance
68
+ per_class[model_class] = instance
62
69
  else
63
70
  instance = model_class.new
64
71
  register_accessor_methods_for(instance, child_register)
@@ -341,7 +348,7 @@ module Lutaml
341
348
  end
342
349
 
343
350
  value = if rule.raw_mapping?
344
- doc.root.inner_xml
351
+ scoped_raw_inner_xml(doc, xml_mapping)
345
352
  elsif rule.content_mapping?
346
353
  rule.cdata ? doc.cdata : doc.text
347
354
  elsif (group = grouped_plain_rules[rule_to]) &&
@@ -709,6 +716,23 @@ _effective_register)
709
716
  # lutaml-model#154: decode HTML entities for mappings that opted in
710
717
  # (`html_entities` in the xml block). Values stay untouched
711
718
  # otherwise — XML keeps undefined entities literal by design.
719
+ # lutaml-model#223: with coexisting map_all, the raw capture is
720
+ # the verbatim subtree MINUS the children claimed by element
721
+ # rules (they hydrate their own attributes). Standalone map_all
722
+ # (no element rules) keeps the full inner_xml.
723
+ def scoped_raw_inner_xml(doc, xml_mapping)
724
+ claimed = []
725
+ xml_mapping&.elements&.each do |element_rule|
726
+ Array(element_rule.name).each { |n| claimed << n.to_s }
727
+ end
728
+ return doc.root.inner_xml if claimed.empty?
729
+
730
+ doc.root.element_children.reject do |child|
731
+ claimed.include?(child.unprefixed_name) ||
732
+ claimed.include?(child.name)
733
+ end.map(&:inner_xml_or_self).join
734
+ end
735
+
712
736
  def decode_html_entities_value(value, enabled)
713
737
  return value if value.nil? || !value.is_a?(String)
714
738
  return value unless enabled
@@ -724,8 +748,11 @@ _effective_register)
724
748
 
725
749
  mapping = model_class.mappings_for(:xml, register)
726
750
  version = mapping&.rule_records_version.to_i
727
- per_class = RULE_RECORDS[model_class.object_id] || # rubocop:disable Lint/HashCompareByIdentity
728
- (RULE_RECORDS[model_class.object_id] = Concurrent::Map.new) # rubocop:disable Lint/HashCompareByIdentity,Layout/MultilineAssignmentLayout
751
+ per_class = RULE_RECORDS[model_class.object_id] # rubocop:disable Lint/HashCompareByIdentity
752
+ unless per_class
753
+ per_class = Lutaml::Model.opal? ? {} : Concurrent::Map.new
754
+ RULE_RECORDS[model_class.object_id] = per_class # rubocop:disable Lint/HashCompareByIdentity
755
+ end
729
756
  cached = per_class[register]
730
757
  return cached.records if cached && cached.version == version
731
758
 
@@ -227,13 +227,7 @@ register_id)
227
227
  # @param rule [CompiledRule] The rule
228
228
  # @return [Boolean] true if custom method only
229
229
  def custom_method_only?(rule)
230
- # Check if attribute name is a placeholder (e.g., :__content__)
231
- return true if rule.attribute_name.to_s.start_with?("__") &&
232
- rule.attribute_name.to_s.end_with?("__")
233
-
234
- # Also check if rule has custom methods but attribute_type is nil
235
- # This handles cases where we inferred an attribute name for custom methods
236
- rule.has_custom_methods? && rule.attribute_type.nil?
230
+ rule.custom_method_only?
237
231
  end
238
232
 
239
233
  # Extract value for a rule
@@ -185,28 +185,32 @@ module MapAllSpec
185
185
  end
186
186
  end
187
187
 
188
- describe "invalid mapping combinations" do
189
- it "raises error when combining map_all with other mappings" do
190
- expect do
191
- InvalidDocument.json do
188
+ describe "coexistence with other mappings (#223)" do
189
+ it "captures only unclaimed content alongside mapped elements" do
190
+ doc_class = Class.new(Lutaml::Model::Serializable) do
191
+ attribute :title, :string
192
+ attribute :rest, :string
193
+
194
+ xml do
195
+ element "eref"
192
196
  map_element "title", to: :title
197
+ map_all to: :rest
193
198
  end
194
- end.to raise_error(
195
- StandardError,
196
- "map_all is not allowed with other mappings",
197
- )
198
- end
199
+ end
199
200
 
200
- it "raises error when combining other mappings are used with map_all" do
201
- expect do
202
- InvalidDocument.yaml do
203
- map_element "title", to: :title
204
- map_all to: :content
205
- end
206
- end.to raise_error(
207
- StandardError,
208
- "map_all is not allowed with other mappings",
209
- )
201
+ source = %(<eref><title>T</title><p>para</p><note>N</note></eref>)
202
+ parsed = doc_class.from_xml(source)
203
+ expect(parsed.title).to eq("T")
204
+ expect(parsed.rest).to eq("<p>para</p><note>N</note>")
205
+
206
+ out = doc_class.to_xml(parsed)
207
+ expect(out).to include("<title>T</title>")
208
+ expect(out).to include("<p>para</p>")
209
+ expect(out).to include("<note>N</note>")
210
+
211
+ reparsed = doc_class.from_xml(out)
212
+ expect(reparsed.title).to eq("T")
213
+ expect(reparsed.rest).to eq("<p>para</p><note>N</note>")
210
214
  end
211
215
  end
212
216
  end
@@ -3,6 +3,13 @@
3
3
  require "spec_helper"
4
4
 
5
5
  RSpec.describe Lutaml::Model::Store do
6
+ before do
7
+ # Registration only runs when a Reference-typed attribute exists
8
+ # somewhere (#808); the internal specs opt in explicitly.
9
+ described_class.reference_types_in_use!
10
+ described_class.clear
11
+ end
12
+
6
13
  let(:model_class) do
7
14
  Class.new(Lutaml::Model::Serializable) do
8
15
  attribute :id, :string
@@ -10,7 +17,6 @@ RSpec.describe Lutaml::Model::Store do
10
17
  end
11
18
  end
12
19
 
13
- before { described_class.clear }
14
20
  after { described_class.clear }
15
21
 
16
22
  describe "#register and #resolve" do
@@ -974,7 +974,6 @@ RSpec.describe Lutaml::Xml::Mapping do
974
974
  end
975
975
 
976
976
  context "when mixed content is true and child is content_mapping" do
977
- let(:map_all_error) { "map_all is not allowed with other mappings" }
978
977
  let(:invalid_element) { "must be defined without namespace" }
979
978
  let(:mfenced) do
980
979
  <<~XML
@@ -994,7 +993,7 @@ RSpec.describe Lutaml::Xml::Mapping do
994
993
  XML
995
994
  end
996
995
 
997
- it "raises error when map_all used in content_mapping without custom methods" do
996
+ it "allows map_all alongside element mappings (#223)" do
998
997
  original_mapping = XmlMappingSpec::MmlMath.mappings[:xml].deep_dup
999
998
 
1000
999
  begin
@@ -1005,7 +1004,7 @@ RSpec.describe Lutaml::Xml::Mapping do
1005
1004
  XmlMappingSpec::MmlMath.xml do
1006
1005
  map_all to: :all_content
1007
1006
  end
1008
- end.to raise_error(StandardError, map_all_error)
1007
+ end.not_to raise_error
1009
1008
  ensure
1010
1009
  XmlMappingSpec::MmlMath.instance_variable_set(:@mappings,
1011
1010
  { xml: original_mapping })
@@ -1013,16 +1012,16 @@ RSpec.describe Lutaml::Xml::Mapping do
1013
1012
  end
1014
1013
  end
1015
1014
 
1016
- it "can be defined after any other mapping" do
1015
+ it "still validates its own arguments when combined (#223)" do
1017
1016
  original_mapping = XmlMappingSpec::MmlMath.mappings[:xml].deep_dup
1018
1017
 
1019
1018
  begin
1020
1019
  expect do
1021
1020
  XmlMappingSpec::MmlMath.xml do
1022
- map_all to: :all_content
1023
- namespace XmiNewNamespace
1021
+ map_all to: :all_content, namespace: "http://explicit.ns"
1024
1022
  end
1025
- end.to raise_error(StandardError, map_all_error)
1023
+ end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError,
1024
+ /namespace is not allowed/)
1026
1025
  ensure
1027
1026
  XmlMappingSpec::MmlMath.instance_variable_set(:@mappings,
1028
1027
  { xml: original_mapping })
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.38
4
+ version: 0.8.40
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-18 00:00:00.000000000 Z
11
+ date: 2026-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -202,6 +202,7 @@ files:
202
202
  - docs/_guides/character-encoding.adoc
203
203
  - docs/_guides/collection-serialization.adoc
204
204
  - docs/_guides/creating-xsd.adoc
205
+ - docs/_guides/custom-methods.adoc
205
206
  - docs/_guides/document-validation.adoc
206
207
  - docs/_guides/index.adoc
207
208
  - docs/_guides/jsonld-serialization.adoc