lutaml-model 0.8.22 → 0.8.23

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: a30fcd552dca587769736a81de33b66909c592cbc5ed40f696d34fbaf7de887f
4
- data.tar.gz: b15506fd1398cad525dcac0a624166cdc23dedf7faa3e282b1aae10c583b405a
3
+ metadata.gz: 22cdfc7334bab60076fc98d5153c5770789f2670dc41eeabfee4df3dc6265f99
4
+ data.tar.gz: fcb7ed45d08960bc739e7b0401771c96cc295a79f0d5236d60c0b8c7edfc54bb
5
5
  SHA512:
6
- metadata.gz: 061ea9253cc7e425f9591e5f57407935eb6b52b1ee69fc5b289b672a0109f8aa3850097d0dd3971613b6010c5e8d83b5425e3447bebe0ae6724916fbdc7a3173
7
- data.tar.gz: 9280bc8adfdeaa2e6167c57379d0d40a7c8c27a4dffb420c8f8b6468b893df8ae72207d5fddef2885d4954ba4c45918a1f9cd3746ef2194aed3b09bae46b19f9
6
+ metadata.gz: c2642f2aba35a1eff18d1cc69c3dd44c95f88c80ebb8b76937c10202f0f2527e9c8365be6730fdbd69ee34b44641707714e4d6e0bd2c1cb062176a8d735b7398
7
+ data.tar.gz: 5cb5c0079ac4d8cb578cab080641e2d4f3d6a6445c1f212080aba556133b8b707afdf86a8508a4df488c859b3fabbe52adf26f324ddaecd1988f4d79338dc2b9
@@ -717,6 +717,37 @@ end
717
717
  ----
718
718
  ====
719
719
 
720
+ ===== Mapping several element names to one attribute
721
+
722
+ Several `map_element` rules may target the same collection attribute to
723
+ accept multiple spellings of one element (e.g. a legacy and a modern
724
+ name). Parsing merges the content of every spelling **in document
725
+ order**; serialization emits the value once, under the **first declared**
726
+ spelling.
727
+
728
+ [source,ruby]
729
+ ----
730
+ class Editorial < Lutaml::Model::Serializable
731
+ attribute :groups, Group, collection: true
732
+
733
+ xml do
734
+ element "metadata"
735
+ map_element "editorial-group", to: :groups
736
+ map_element "editorialgroup", to: :groups # legacy spelling
737
+ end
738
+ end
739
+
740
+ Editorial.from_xml(
741
+ "<metadata><editorialgroup>…</editorialgroup><editorial-group>…</editorial-group></metadata>"
742
+ ).groups
743
+ # => [group(legacy), group(modern)] — document order
744
+ ----
745
+
746
+ NOTE: The merge applies to plain element rules on collection attributes.
747
+ Rules with custom deserialization methods, `raw`, `content`, or `cdata`
748
+ are not merged — each still assigns separately, last rule winning as
749
+ before.
750
+
720
751
  ===== Using elements in different namespaces
721
752
 
722
753
  ====== General
@@ -17,6 +17,13 @@ module Lutaml
17
17
  end
18
18
 
19
19
  # rubocop:disable Style/ArgumentsForwarding -- anonymous * requires Ruby 3.2+
20
+ # Oj has its own option namespace and its own escape_mode. Handing it
21
+ # the stdlib generator's options overrides that configuration, so it
22
+ # takes none of them -- which is what it received before.
23
+ def ignores_generator_options?
24
+ true
25
+ end
26
+
20
27
  def to_json(*args)
21
28
  require "oj"
22
29
  # Handle KeyValueElement input (new symmetric architecture)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require_relative "../generator_options"
4
5
 
5
6
  module Lutaml
6
7
  module Json
@@ -9,12 +10,16 @@ module Lutaml
9
10
  FORMAT_SYMBOL = :json
10
11
 
11
12
  def self.parse(json, _options = {})
12
- JSON.parse(json, create_additions: false)
13
+ JSON.parse(json)
13
14
  end
14
15
 
15
- def to_json(*args)
16
- options = args.first || {}
16
+ # This adapter hands its payload to the stdlib generator, so a
17
+ # JSON::State carrying the outer formatting is meaningful here.
18
+ def accepts_generator_state?
19
+ true
20
+ end
17
21
 
22
+ def to_json(*args)
18
23
  # Handle KeyValueElement input (new symmetric architecture)
19
24
  attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
20
25
  # Unwrap __root__ wrapper to get actual content
@@ -24,10 +29,17 @@ module Lutaml
24
29
  @attributes
25
30
  end
26
31
 
32
+ unless GeneratorOptions.lutaml_options?(args.first)
33
+ return JSON.generate(attributes_to_serialize, args.first)
34
+ end
35
+
36
+ options = args.first || {}
37
+ generator_options = GeneratorOptions.filter(options)
38
+
27
39
  if options[:pretty]
28
- JSON.pretty_generate(attributes_to_serialize, *args)
40
+ JSON.pretty_generate(attributes_to_serialize, generator_options)
29
41
  else
30
- JSON.generate(attributes_to_serialize, *args)
42
+ JSON.generate(attributes_to_serialize, generator_options)
31
43
  end
32
44
  end
33
45
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Lutaml
6
+ module Json
7
+ # Filters the options LutaML threads through serialization down to the
8
+ # subset the JSON generator accepts.
9
+ #
10
+ # json 2.x silently ignored unknown generator options; json 3.0 raises
11
+ # ArgumentError on them, so LutaML's own options (:register, :pretty and
12
+ # any caller-supplied extras) have to be stripped before they reach
13
+ # JSON.generate.
14
+ module GeneratorOptions
15
+ # Options the generator accepts on both json 2.x and 3.x. Measured
16
+ # against 2.9.1, 2.15.2, 2.19.9, 2.20.0, 2.21.1, 2.21.2 and 3.0.0.
17
+ # :escape_slash is NOT here because json 3.0 removed it; on 2.x it still
18
+ # arrives via DERIVED_PERMITTED. :allow_duplicate_key IS here because the
19
+ # generator accepts it on every version while JSON::State never exposed
20
+ # it as an accessor, so the derived half alone would miss it.
21
+ BASE_PERMITTED = %i[
22
+ allow_duplicate_key allow_nan array_nl as_json ascii_only
23
+ buffer_initial_length depth indent max_nesting object_nl script_safe
24
+ sort_keys space space_before strict
25
+ ].freeze
26
+
27
+ # Additive, so a json release that adds an option needs no change here.
28
+ # Opal's JSON shim has no State class, hence the guard.
29
+ DERIVED_PERMITTED =
30
+ if defined?(::JSON::State)
31
+ accessors = ::JSON::State.instance_methods(false).grep(/\A[a-z_][a-z0-9_]*=\z/)
32
+ accessors.map { |name| name.to_s.chomp("=").to_sym }
33
+ else
34
+ []
35
+ end.freeze
36
+
37
+ PERMITTED = (BASE_PERMITTED | DERIVED_PERMITTED).freeze
38
+
39
+ # json 3.0 removed :escape_slash, which was only ever an alias of
40
+ # :script_safe. Dropping it would silently stop escaping slashes, so it
41
+ # is translated instead of discarded.
42
+ RENAMED = { escape_slash: :script_safe }.freeze
43
+
44
+ def self.filter(options)
45
+ return {} unless options.is_a?(::Hash)
46
+
47
+ options.each_with_object({}) do |(key, value), kept|
48
+ key = RENAMED.fetch(key, key) unless PERMITTED.include?(key)
49
+ kept[key] = value if PERMITTED.include?(key)
50
+ end
51
+ end
52
+
53
+ # Ruby's JSON generator calls #to_json with a JSON::State whenever a
54
+ # document is nested inside another JSON.generate call. That is the
55
+ # generator's own state, not a LutaML options hash, and json 3.0 removed
56
+ # JSON::State#[], so it must never be read like one.
57
+ def self.lutaml_options?(argument)
58
+ argument.nil? || argument.is_a?(::Hash)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -14,7 +14,7 @@ module Lutaml
14
14
  next if line.strip.empty?
15
15
 
16
16
  begin
17
- results << JSON.parse(line, create_additions: false)
17
+ results << JSON.parse(line)
18
18
  rescue JSON::ParserError => e
19
19
  warn "Skipping invalid line: #{e.message}"
20
20
  end
@@ -1,21 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require_relative "../json/generator_options"
4
5
 
5
6
  module Lutaml
6
7
  module JsonLd
7
8
  class Adapter < Lutaml::KeyValue::Document
8
9
  def self.parse(jsonld_string, _options = {})
9
- JSON.parse(jsonld_string, create_additions: false)
10
+ JSON.parse(jsonld_string)
10
11
  end
11
12
 
12
13
  def to_jsonld(*args)
13
- options = args.first || {}
14
14
  data = @attributes
15
+
16
+ unless Lutaml::Json::GeneratorOptions.lutaml_options?(args.first)
17
+ return JSON.generate(data, args.first)
18
+ end
19
+
20
+ options = args.first || {}
21
+ generator_options = Lutaml::Json::GeneratorOptions.filter(options)
22
+
15
23
  if options[:pretty]
16
- JSON.pretty_generate(data, *args)
24
+ JSON.pretty_generate(data, generator_options)
17
25
  else
18
- JSON.generate(data, *args)
26
+ JSON.generate(data, generator_options)
19
27
  end
20
28
  end
21
29
  end
@@ -21,6 +21,13 @@ module Lutaml
21
21
  end
22
22
 
23
23
  # rubocop:disable Style/ArgumentsForwarding -- anonymous * requires Ruby 3.2+
24
+ # Oj has its own option namespace and its own escape_mode. Handing it
25
+ # the stdlib generator's options overrides that configuration, so it
26
+ # takes none of them -- which is what it received before.
27
+ def ignores_generator_options?
28
+ true
29
+ end
30
+
24
31
  def to_json(*args)
25
32
  require "oj"
26
33
  # Handle KeyValueElement input (new symmetric architecture)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require_relative "../../../json/generator_options"
4
5
 
5
6
  # Backward compatibility - delegates to Lutaml::Json::Adapter
6
7
  # @deprecated Use Lutaml::Json::Adapter::StandardAdapter instead
@@ -13,12 +14,16 @@ module Lutaml
13
14
  FORMAT_SYMBOL = :json
14
15
 
15
16
  def self.parse(json, _options = {})
16
- JSON.parse(json, create_additions: false)
17
+ JSON.parse(json)
17
18
  end
18
19
 
19
- def to_json(*args)
20
- options = args.first || {}
20
+ # This adapter hands its payload to the stdlib generator, so a
21
+ # JSON::State carrying the outer formatting is meaningful here.
22
+ def accepts_generator_state?
23
+ true
24
+ end
21
25
 
26
+ def to_json(*args)
22
27
  # Handle KeyValueElement input (new symmetric architecture)
23
28
  attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
24
29
  # Unwrap __root__ wrapper to get actual content
@@ -28,10 +33,17 @@ module Lutaml
28
33
  @attributes
29
34
  end
30
35
 
36
+ unless Lutaml::Json::GeneratorOptions.lutaml_options?(args.first)
37
+ return JSON.generate(attributes_to_serialize, args.first)
38
+ end
39
+
40
+ options = args.first || {}
41
+ generator_options = Lutaml::Json::GeneratorOptions.filter(options)
42
+
31
43
  if options[:pretty]
32
- JSON.pretty_generate(attributes_to_serialize, *args)
44
+ JSON.pretty_generate(attributes_to_serialize, generator_options)
33
45
  else
34
- JSON.generate(attributes_to_serialize, *args)
46
+ JSON.generate(attributes_to_serialize, generator_options)
35
47
  end
36
48
  end
37
49
  end
@@ -15,7 +15,7 @@ module Lutaml
15
15
  next if line.strip.empty?
16
16
 
17
17
  begin
18
- results << JSON.parse(line, create_additions: false)
18
+ results << JSON.parse(line)
19
19
  rescue JSON::ParserError => e
20
20
  warn "Skipping invalid line: #{e.message}"
21
21
  end
@@ -308,6 +308,10 @@ module Lutaml
308
308
  end
309
309
 
310
310
  def to(format, instance, options = {})
311
+ # Wrap before the branch, not inside it: only XML reaches the
312
+ # unwrapped path today, but a raw JSON::State must never survive
313
+ # into either arm.
314
+ options = Lutaml::Model::Serialize.wrap_generator_state(options)
311
315
  mappings = mappings_for(format)
312
316
 
313
317
  if mappings.no_root? && collection_unwrapped_to?(format)
@@ -456,6 +460,7 @@ lutaml_register: Lutaml::Model::Config.default_register)
456
460
  end
457
461
 
458
462
  def to_format(format, options = {})
463
+ options = Lutaml::Model::Serialize.wrap_generator_state(options)
459
464
  super(format, options.merge(collection: true))
460
465
  end
461
466
 
@@ -17,7 +17,7 @@ module Lutaml
17
17
  end
18
18
 
19
19
  def self.parse(data, _options = {})
20
- document_class.parse(data, create_additions: false)
20
+ document_class.parse(data)
21
21
  end
22
22
  end
23
23
  end
@@ -192,24 +192,55 @@ module Lutaml
192
192
  # is always preserved when available, regardless of this option.
193
193
  # @return [String] The serialized output
194
194
  def to(format, instance, options = {})
195
+ # Ruby's JSON generator hands #to_json its own JSON::State rather
196
+ # than an options hash. It carries no LutaML options, but it does
197
+ # carry the surrounding indent context, so it is forwarded to the
198
+ # adapter unchanged instead of being read like a Hash -- json 3.0
199
+ # removed JSON::State#[].
200
+ options = Serialize.wrap_generator_state(options)
201
+ generator_state = options.delete(Serialize::GENERATOR_STATE_KEY)
202
+
195
203
  Instrumentation.instrument(:to, model: name, format: format) do
196
- adapter_override = options.is_a?(Hash) && options.delete(:adapter)
197
- if adapter_override && options.is_a?(Hash)
198
- options[:_adapter_override] =
199
- true
200
- end
204
+ adapter_override = options.delete(:adapter)
205
+ options[:_adapter_override] = true if adapter_override
201
206
  value = public_send(:"as_#{format}", instance, options)
202
207
  adapter = resolve_adapter(format, adapter_override)
203
208
 
204
209
  # Hook for format-specific options preparation (e.g., XML prefix/namespace/declaration)
205
210
  options = prepare_to_options(format, instance, options)
206
211
 
207
- adapter.new(value, register: options[:register]).public_send(
208
- :"to_#{format}", options
212
+ document = adapter.new(value, register: options[:register])
213
+
214
+ document.public_send(
215
+ :"to_#{format}",
216
+ forward_options(document, generator_state, options),
209
217
  )
210
218
  end
211
219
  end
212
220
 
221
+ # Main's behaviour differs per ADAPTER, not per option, so this follows
222
+ # the adapter rather than trying to translate option names:
223
+ # stdlib honours script_safe / ascii_only / pretty -> give it the state
224
+ # Oj ignores them all and uses its own escape_mode -> give it none
225
+ # others reach the stdlib generator underneath -> give them the options
226
+ def forward_options(document, generator_state, options)
227
+ return options if generator_state.nil?
228
+
229
+ if declares?(document, :accepts_generator_state?)
230
+ generator_state
231
+ elsif declares?(document, :ignores_generator_options?)
232
+ options
233
+ elsif generator_state.respond_to?(:to_h)
234
+ options.merge(generator_state.to_h)
235
+ else
236
+ options
237
+ end
238
+ end
239
+
240
+ def declares?(document, predicate)
241
+ document.respond_to?(predicate) && document.public_send(predicate)
242
+ end
243
+
213
244
  # Hook for format-specific options preparation before serialization.
214
245
  # XML overrides to handle prefix, namespace overrides, declaration plan.
215
246
  #
@@ -225,7 +225,25 @@ module Lutaml
225
225
  self.class.as_yaml(self)
226
226
  end
227
227
 
228
+ # Ruby's JSON generator hands #to_json its own JSON::State instead of an
229
+ # options hash. It carries no LutaML options, but it does carry the
230
+ # surrounding indent context, so it travels IN BAND under a private key
231
+ # rather than replacing the options hash. Wrapping rather than returning
232
+ # early keeps every later step -- register propagation, root-mapping
233
+ # validation, Collection's `collection: true` merge -- working on a real
234
+ # Hash. json 3.0 removed JSON::State#[] and rejects unknown keys in
235
+ # State#merge, so nothing may treat it as a Hash.
236
+ GENERATOR_STATE_KEY = :_generator_state
237
+
238
+ def self.wrap_generator_state(options)
239
+ return options if options.is_a?(::Hash)
240
+
241
+ { GENERATOR_STATE_KEY => options }
242
+ end
243
+
228
244
  def to_format(format, options = {})
245
+ options = Lutaml::Model::Serialize.wrap_generator_state(options)
246
+
229
247
  # Hook for format-specific validation (e.g., XML root mapping check)
230
248
  validate_root_mapping!(format, options)
231
249
 
@@ -37,9 +37,14 @@ module Lutaml
37
37
  @format = format
38
38
  @register = register
39
39
  @compiled_rules = compile_rules(mapping_dsl)
40
+ after_compile
40
41
  freeze
41
42
  end
42
43
 
44
+ # Hook for format subclasses to derive rule-set-level data after
45
+ # compilation, before the transformation is frozen and shared.
46
+ def after_compile; end
47
+
43
48
  # Transform a model instance into format-specific representation
44
49
  #
45
50
  # @abstract Subclasses must implement this method
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.22"
5
+ VERSION = "0.8.23"
6
6
  end
7
7
  end
@@ -1071,6 +1071,30 @@ module Lutaml
1071
1071
  result
1072
1072
  end
1073
1073
 
1074
+ # lutaml-model#765: plain element rules grouped by target attribute
1075
+ # name, for spelling-tolerance mapping (several element names feeding
1076
+ # one attribute). Cached per register so the parse hot path only pays
1077
+ # a hash lookup. Grouping uses rule-level properties only — the
1078
+ # transform applies the collection-attribute filter.
1079
+ #
1080
+ # @return [Hash{Symbol, String => Array<MappingRule>}]
1081
+ def plain_element_rules_by_attr(register_id = nil)
1082
+ reg_key = register_id || :default
1083
+ @plain_element_rules_by_attr ||= {}
1084
+ @plain_element_rules_by_attr[reg_key] ||= begin
1085
+ grouped = {}
1086
+ mappings(reg_key).each do |r|
1087
+ next if r.attribute? || r.raw_mapping? || r.content_mapping? ||
1088
+ r.cdata || r.has_custom_method_for_deserialization? ||
1089
+ (r.transform.is_a?(Hash) && !r.transform.empty?) ||
1090
+ r.transform.is_a?(Class)
1091
+
1092
+ (grouped[r.to] ||= []) << r
1093
+ end
1094
+ grouped
1095
+ end
1096
+ end
1097
+
1074
1098
  def importable_mappings
1075
1099
  @importable_mappings ||= []
1076
1100
  end
@@ -258,6 +258,18 @@ module Lutaml
258
258
  end
259
259
  end
260
260
 
261
+ # lutaml-model#765: several plain element rules may target one
262
+ # collection attribute (spelling tolerance, e.g. editorial-group vs
263
+ # editorialgroup). The first rule of such a group performs matching
264
+ # widened to every spelling so content merges in document order;
265
+ # the remaining rules are no-ops so their assignment cannot
266
+ # silently overwrite the merged value.
267
+ # Grouping is cached on the mapping DSL (per register); only the
268
+ # collection filter runs here.
269
+ grouped_plain_rules = xml_mapping.plain_element_rules_by_attr(
270
+ effective_register,
271
+ )
272
+
261
273
  mappings.each do |rule|
262
274
  # Performance: Cache rule properties accessed multiple times
263
275
  rule.name
@@ -268,6 +280,11 @@ module Lutaml
268
280
  attr = attribute_for_rule(rule)
269
281
  next if attr&.derived?
270
282
 
283
+ if (group = grouped_plain_rules[rule_to]) &&
284
+ group.size > 1 && attr&.collection? && !group.first.equal?(rule)
285
+ next
286
+ end
287
+
271
288
  raise "Attribute '#{rule_to}' not found in #{context}" unless valid_rule?(
272
289
  rule, attr
273
290
  )
@@ -285,6 +302,26 @@ module Lutaml
285
302
  doc.root.inner_xml
286
303
  elsif rule.content_mapping?
287
304
  rule.cdata ? doc.cdata : doc.text
305
+ elsif (group = grouped_plain_rules[rule_to]) &&
306
+ group.size > 1 && attr&.collection?
307
+ # First rule of a #765 group: match every spelling of
308
+ # the group so the children merge in document order.
309
+ if child_names_set &&
310
+ group.drop(1).none? do |s|
311
+ child_matches_rule?(s, child_names_set,
312
+ default_namespace)
313
+ end && !child_matches_rule?(rule, child_names_set,
314
+ default_namespace)
315
+ ::Lutaml::Model::UninitializedClass.instance
316
+ else
317
+ extra_names = group.drop(1).flat_map do |s|
318
+ resolve_rule_names_with_type(s, attr, new_opts,
319
+ effective_register,
320
+ attr&.type(effective_register))
321
+ end
322
+ value_for_rule(session, rule, new_opts, attr,
323
+ extra_names)
324
+ end
288
325
  elsif child_names_set && !rule.attribute? &&
289
326
  !child_matches_rule?(rule, child_names_set,
290
327
  default_namespace)
@@ -551,7 +588,8 @@ _effective_register)
551
588
  nil
552
589
  end
553
590
 
554
- def value_for_rule(session, rule, options, cached_attr = nil)
591
+ def value_for_rule(session, rule, options, cached_attr = nil,
592
+ extra_rule_names = nil)
555
593
  doc = session.doc
556
594
  instance = session.instance
557
595
  instance_is_serialize = session.instance_is_serialize
@@ -576,6 +614,12 @@ _effective_register)
576
614
  resolve_rule_names_with_type(rule, attr, options,
577
615
  effective_register, attr_type)
578
616
  end
617
+ # lutaml-model#765: widened matching for the first rule of a
618
+ # shared-attribute group — children of sibling spellings must be
619
+ # selected too, in document order. Deduped: sibling resolution can
620
+ # yield the same spelling twice, and the child index would then
621
+ # return the same children again.
622
+ rule_names = rule_names.concat(extra_rule_names).uniq if extra_rule_names
579
623
 
580
624
  return value_for_xml_attribute(doc, rule, rule_names) if rule.attribute?
581
625
 
@@ -643,6 +687,13 @@ _effective_register)
643
687
  children = nil # fall through to select for alias matching
644
688
  else
645
689
  children = indexed.flatten(1)
690
+ # #765 widened matching: rule_names spans several spellings, so
691
+ # the flatten above yields rule-name order; restore document
692
+ # order for the merged group.
693
+ if extra_rule_names
694
+ position = element_children.each_with_index.to_h
695
+ children = children.sort_by { |c| position[c] }
696
+ end
646
697
  end
647
698
  else
648
699
  children = nil
@@ -264,6 +264,8 @@ module Lutaml
264
264
  root, model_instance, options,
265
265
  compiled_rules, model_class, register_id
266
266
  ) do |action, rule, value, set_xsi_nil|
267
+ next if action == :apply_rule && duplicate_element_rules[rule]
268
+
267
269
  rule_options = options.merge(current_model: model_instance)
268
270
  case action
269
271
  when :apply_rule
@@ -291,6 +293,32 @@ module Lutaml
291
293
  # @param root [XmlElement] Root element
292
294
  # @param model_instance [Object] The model instance
293
295
  # @param options [Hash] Options
296
+ # Reader for the precomputed skip set (#765).
297
+ def duplicate_element_rules
298
+ @duplicate_element_rules
299
+ end
300
+
301
+ # lutaml-model#765: several element rules may target one attribute
302
+ # (spelling tolerance, e.g. editorial-group vs editorialgroup). The
303
+ # extra spellings exist for parse tolerance only — serialization
304
+ # emits the value once, under the first declared spelling. Derived
305
+ # once at compile time (transformations are frozen and shared).
306
+ def after_compile
307
+ seen = {}
308
+ @duplicate_element_rules = {}
309
+ compiled_rules.each do |rule|
310
+ next unless rule.option(:mapping_type) == :element
311
+
312
+ key = rule.attribute_name.to_s
313
+ if seen.key?(key)
314
+ @duplicate_element_rules[rule] = true
315
+ else
316
+ seen[key] = true
317
+ end
318
+ end
319
+ @duplicate_element_rules.freeze
320
+ end
321
+
294
322
  def apply_standard_rules(root, model_instance, options)
295
323
  attr_order = model_instance.is_a?(Lutaml::Model::Serialize) &&
296
324
  model_instance.attribute_order
@@ -303,6 +331,7 @@ module Lutaml
303
331
 
304
332
  rules.each do |rule|
305
333
  next unless valid_mapping?(rule, options)
334
+ next if duplicate_element_rules[rule]
306
335
 
307
336
  rule_options = options.merge(current_model: model_instance)
308
337
  apply_rule(root, rule, model_instance, rule_options, model_class,
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/json/adapter/standard_adapter"
5
+
6
+ RSpec.describe Lutaml::Json::Adapter::StandardAdapter do
7
+ let(:attributes) { { "name" => "John", "age" => 30 } }
8
+
9
+ describe ".parse" do
10
+ # json 3.0 removed create_additions and now raises ArgumentError on any
11
+ # unknown keyword, so the parser must be called without it.
12
+ it "parses without passing removed generator keywords to the json gem" do
13
+ expect(described_class.parse('{"name":"John","age":30}'))
14
+ .to eq(attributes)
15
+ end
16
+ end
17
+
18
+ describe "#to_json" do
19
+ subject(:document) { described_class.new(attributes) }
20
+
21
+ it "serializes with no options" do
22
+ expect(document.to_json).to eq('{"name":"John","age":30}')
23
+ end
24
+
25
+ # :register and :pretty are LutaML's own options. json 2.x ignored unknown
26
+ # options; json 3.0 raises ArgumentError, so they must be stripped before
27
+ # they reach JSON.generate.
28
+ it "strips LutaML options that the json generator does not accept" do
29
+ expect(document.to_json(register: :default_register))
30
+ .to eq('{"name":"John","age":30}')
31
+ end
32
+
33
+ it "strips LutaML options when generating pretty output" do
34
+ expect(document.to_json(pretty: true, register: :default_register))
35
+ .to eq(%({\n "name": "John",\n "age": 30\n}))
36
+ end
37
+
38
+ # An ALLOWLIST and a denylist of LutaML's own option names behave
39
+ # identically for :register and :pretty. Only a key nobody enumerated
40
+ # tells them apart -- and json 3.0 raises on it.
41
+ it "strips a caller option nobody enumerated" do
42
+ expect(document.to_json(some_unknown_option: 1))
43
+ .to eq('{"name":"John","age":30}')
44
+ end
45
+
46
+ # json 3.0 removed :escape_slash. Dropping it would silently stop escaping
47
+ # slashes; it is the old alias of :script_safe, so it is translated.
48
+ it "honours escape_slash on every json version" do
49
+ expect(described_class.new({ "a" => "x/y" }).to_json(escape_slash: true))
50
+ .to eq('{"a":"x\\/y"}')
51
+ end
52
+
53
+ # The filter is an allowlist, not a passthrough: genuine generator options
54
+ # must still reach JSON.generate.
55
+ it "forwards options the json generator does accept" do
56
+ expect(document.to_json(register: :default_register, space: " "))
57
+ .to eq('{"name": "John","age": 30}')
58
+ end
59
+ end
60
+
61
+ describe "nesting inside another JSON.generate call" do
62
+ subject(:document) { described_class.new(attributes) }
63
+
64
+ # Ruby's generator hands #to_json a JSON::State, not an options hash.
65
+ # json 3.0 removed JSON::State#[], so reading :pretty off it raises.
66
+ it "serializes when nested in a Hash passed to JSON.generate" do
67
+ expect(JSON.generate({ "doc" => document }))
68
+ .to eq('{"doc":{"name":"John","age":30}}')
69
+ end
70
+
71
+ it "serializes when nested in an Array passed to JSON.generate" do
72
+ expect(JSON.generate([document])).to eq('[{"name":"John","age":30}]')
73
+ end
74
+
75
+ # A DEFAULT state renders compact, which is also what a DISCARDED state
76
+ # produces, so the two examples above cannot tell them apart. These carry
77
+ # formatting, so dropping the state changes the output.
78
+ it "inherits the outer indent when nested in a pretty Hash" do
79
+ expect(JSON.pretty_generate({ "doc" => document }))
80
+ .to eq(%({\n "doc": {\n "name": "John",\n "age": 30\n }\n}))
81
+ end
82
+
83
+ it "inherits the outer indent when nested in a pretty Array" do
84
+ expect(JSON.pretty_generate([document]))
85
+ .to eq(%([\n {\n "name": "John",\n "age": 30\n }\n]))
86
+ end
87
+
88
+ it "honours a configured JSON::State handed to #to_json directly" do
89
+ # A DEFAULT state renders compact, which is also what a discarded state
90
+ # produces -- so the state has to carry formatting for this to mean
91
+ # anything.
92
+ state = JSON::State.new(indent: " ", object_nl: "\n", space: " ")
93
+ expect(document.to_json(state))
94
+ .to eq(%({\n "name": "John",\n "age": 30\n}))
95
+ end
96
+ end
97
+
98
+ describe Lutaml::Json::GeneratorOptions do
99
+ # The derived half of the allowlist reads JSON::State, which Opal's JSON
100
+ # shim does not define. BASE_PERMITTED is what the filter falls back to
101
+ # there, so it has to stand on its own.
102
+ # BASE_PERMITTED is the whole allowlist on Opal, whose JSON shim has no
103
+ # State class for DERIVED_PERMITTED to read. So it has to stand alone
104
+ # against the real generator, not merely be a subset of something larger.
105
+ it "permits only options the running generator actually accepts" do
106
+ rejected = described_class::BASE_PERMITTED.reject do |key|
107
+ JSON.generate({ "a" => 1 }, key => nil)
108
+ true
109
+ rescue ArgumentError => e
110
+ # "unknown keyword" means the generator does not know this option at
111
+ # all. Any other complaint -- a type error about the nil value we
112
+ # passed -- means it knows the option and only dislikes the value.
113
+ !e.message.include?("unknown keyword")
114
+ rescue StandardError
115
+ true
116
+ end
117
+
118
+ expect(rejected).to eq([])
119
+ end
120
+
121
+ # Opal's JSON shim defines no State class, so DERIVED_PERMITTED is empty
122
+ # there and the whole module must still load. Asserting that
123
+ # BASE_PERMITTED contains some symbols does not exercise the guard; only
124
+ # loading the file with JSON::State absent does.
125
+ it "loads and still permits the base set when JSON::State is absent" do
126
+ hide_const("JSON::State")
127
+
128
+ mod = Module.new
129
+ mod.module_eval(
130
+ File.read(File.expand_path("../../../../lib/lutaml/json/generator_options.rb", __dir__)),
131
+ )
132
+ permitted = mod.const_get(:Lutaml).const_get(:Json)
133
+ .const_get(:GeneratorOptions)::PERMITTED
134
+
135
+ expect(permitted).to eq(described_class::BASE_PERMITTED)
136
+ end
137
+
138
+ it "carries the formatting options pretty output depends on" do
139
+ expect(described_class::BASE_PERMITTED)
140
+ .to include(:indent, :object_nl, :space, :space_before)
141
+ end
142
+
143
+ it "rejects a non-Hash options argument" do
144
+ expect(described_class.filter(nil)).to eq({})
145
+ end
146
+
147
+ it "treats nil as LutaML options" do
148
+ expect(described_class.lutaml_options?(nil)).to be(true)
149
+ end
150
+
151
+ it "treats a Hash as LutaML options" do
152
+ expect(described_class.lutaml_options?({ pretty: true })).to be(true)
153
+ end
154
+
155
+ it "does not treat a JSON::State as LutaML options" do
156
+ expect(described_class.lutaml_options?(JSON::State.new)).to be(false)
157
+ end
158
+ end
159
+ end
@@ -43,4 +43,23 @@ RSpec.describe Lutaml::JsonLd::Adapter do
43
43
  round_tripped = JSON.parse(result)
44
44
  expect(round_tripped).to eq(jsonld_hash.transform_keys(&:to_s))
45
45
  end
46
+
47
+ describe "a JSON::State handed to #to_jsonld" do
48
+ subject(:document) { described_class.new({ "@id" => "urn:x", "n" => 1 }) }
49
+
50
+ # Ruby's generator only ever calls #to_json, so a state reaches #to_jsonld
51
+ # only from a direct caller. json 3.0 removed JSON::State#[], so the state
52
+ # must be forwarded to the payload rather than read as an options hash.
53
+ it "honours a state that carries formatting" do
54
+ state = JSON::State.new(indent: " ", object_nl: "\n", space: " ")
55
+
56
+ expect(document.to_jsonld(state))
57
+ .to eq(%({\n "@id": "urn:x",\n "n": 1\n}))
58
+ end
59
+
60
+ it "renders compactly for a default state" do
61
+ expect(document.to_jsonld(JSON::State.new))
62
+ .to eq('{"@id":"urn:x","n":1}')
63
+ end
64
+ end
46
65
  end
@@ -644,7 +644,7 @@ RSpec.describe Lutaml::Model::Collection do
644
644
  collection = title_collection_class.from_json(json_data)
645
645
 
646
646
  # JSON.parse should only be called once on the main array, not on individual strings
647
- expect(JSON).to have_received(:parse).once.with(json_data, anything)
647
+ expect(JSON).to have_received(:parse).once.with(json_data)
648
648
  expect(collection.titles.map(&:content)).to eq(["Title One", "Title Two",
649
649
  "Title Three"])
650
650
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # A Collection overrides #to_format and merges `collection: true` into the
6
+ # options BEFORE the base implementation runs. JSON::State#merge is #configure,
7
+ # which rejects unknown keys on json 3.0, so the state has to be wrapped first.
8
+ RSpec.describe "a collection nested inside JSON.generate" do
9
+ before do
10
+ stub_const("NestedItem", Class.new(Lutaml::Model::Serializable) do
11
+ attribute :n, :string
12
+ end)
13
+ stub_const("NestedItems", Class.new(Lutaml::Model::Collection) do
14
+ instances :items, NestedItem
15
+ end)
16
+ end
17
+
18
+ let(:collection) { NestedItems.new([NestedItem.new(n: "a")]) }
19
+
20
+ it "serializes inside a Hash" do
21
+ expect(JSON.generate({ "c" => collection })).to eq('{"c":[{"n":"a"}]}')
22
+ end
23
+
24
+ it "inherits the outer indent inside a pretty Hash" do
25
+ expect(JSON.pretty_generate({ "c" => collection }))
26
+ .to eq(%({\n "c": [\n {\n "n": "a"\n }\n ]\n}))
27
+ end
28
+
29
+ # The class-level entry point normalises separately from the instance one,
30
+ # and deleting that normaliser leaves every instance example green.
31
+ it "normalises a state passed to the class-level entry point" do
32
+ # array_nl is what puts the newlines in an array; without it the state
33
+ # is not a pretty state at all and the example proves nothing.
34
+ state = JSON::State.new(indent: " ", object_nl: "\n", array_nl: "\n",
35
+ space: " ")
36
+
37
+ expect(NestedItems.to_json(collection, state))
38
+ .to eq(%([\n {\n "n": "a"\n }\n]))
39
+ end
40
+
41
+ it "still serializes directly" do
42
+ expect(collection.to_json).to eq('[{"n":"a"}]')
43
+ end
44
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "oj"
5
+ require "multi_json"
6
+
7
+ # What a nested model should do with the outer generator's options differs per
8
+ # ADAPTER, not per option. Reference values are unmodified main on json 2.21.2.
9
+ #
10
+ # stdlib honours script_safe / ascii_only / pretty -> receives the state
11
+ # Oj ignores them and uses its own escape_mode -> receives none of them
12
+ #
13
+ # Four earlier attempts at this got it wrong by reasoning about option names
14
+ # instead of measuring each adapter, so every row here is a measured cell.
15
+ RSpec.describe "generator options reaching a nested model" do
16
+ before do
17
+ stub_const("EscapeProbe", Class.new(Lutaml::Model::Serializable) do
18
+ attribute :s, :string
19
+ end)
20
+ end
21
+
22
+ let(:model) { EscapeProbe.new(s: "</script>é") }
23
+
24
+ describe "the stdlib adapter" do
25
+ it "honours script_safe" do
26
+ expect(JSON.generate({ "m" => model }, script_safe: true))
27
+ .to eq('{"m":{"s":"<\\/script>é"}}')
28
+ end
29
+
30
+ it "honours ascii_only" do
31
+ expect(JSON.generate({ "m" => model }, ascii_only: true))
32
+ .to eq('{"m":{"s":"</script>\\u00e9"}}')
33
+ end
34
+
35
+ it "honours the outer indent" do
36
+ expect(JSON.pretty_generate({ "m" => model }))
37
+ .to eq(%({\n "m": {\n "s": "</script>é"\n }\n}))
38
+ end
39
+ end
40
+
41
+ describe "the Oj adapter" do
42
+ around do |example|
43
+ previous = Oj.default_options
44
+ Oj.default_options = { escape_mode: :xss_safe }
45
+ example.run
46
+ Oj.default_options = previous
47
+ end
48
+
49
+ # Oj must keep its OWN escaping. Forwarding the generator's options -- all
50
+ # of them, only the caller-set ones, or just ascii_only -- each reset
51
+ # escape_mode and silently emitted a literal </script>.
52
+ it "keeps its configured escape_mode when no option is passed" do
53
+ result = Lutaml::Model::Config.with_adapter(json: :oj) do
54
+ JSON.generate({ "m" => model })
55
+ end
56
+
57
+ expect(result).to eq('{"m":{"s":"\\u003c\\/script\\u003e\\u00e9"}}')
58
+ end
59
+
60
+ it "keeps its configured escape_mode when ascii_only is passed" do
61
+ result = Lutaml::Model::Config.with_adapter(json: :oj) do
62
+ JSON.generate({ "m" => model }, ascii_only: true)
63
+ end
64
+
65
+ expect(result).to eq('{"m":{"s":"\\u003c\\/script\\u003e\\u00e9"}}')
66
+ end
67
+
68
+ it "keeps its configured escape_mode when script_safe is passed" do
69
+ result = Lutaml::Model::Config.with_adapter(json: :oj) do
70
+ JSON.generate({ "m" => model }, script_safe: true)
71
+ end
72
+
73
+ expect(result).to eq('{"m":{"s":"\\u003c\\/script\\u003e\\u00e9"}}')
74
+ end
75
+ end
76
+
77
+ # MultiJson's json_gem backend IS the stdlib generator, so unlike Oj it does
78
+ # honour these options and must still receive them. That BACKEND cannot run
79
+ # under json 3.0 -- it sends create_additions, which json removed -- so these
80
+ # are asserted on the versions where it works. multi_json itself is fine
81
+ # under json 3.0 on its Oj backend; only json_gem is affected.
82
+ describe "the MultiJson adapter", if: Gem::Version.new(JSON::VERSION) < Gem::Version.new("3.0.0") do
83
+ before { MultiJson.use(:json_gem) }
84
+
85
+ it "still receives script_safe" do
86
+ result = Lutaml::Model::Config.with_adapter(json: :multi_json) do
87
+ JSON.generate({ "m" => model }, script_safe: true)
88
+ end
89
+
90
+ expect(result).to eq('{"m":{"s":"<\\/script>é"}}')
91
+ end
92
+
93
+ it "still receives ascii_only" do
94
+ result = Lutaml::Model::Config.with_adapter(json: :multi_json) do
95
+ JSON.generate({ "m" => model }, ascii_only: true)
96
+ end
97
+
98
+ expect(result).to eq('{"m":{"s":"</script>\\u00e9"}}')
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # json 3.0 removed JSON::State#[]. Ruby's generator passes a JSON::State to
6
+ # #to_json whenever a model is nested inside another JSON.generate call, so the
7
+ # model layer must not read option keys off it.
8
+ RSpec.describe "a model nested inside JSON.generate" do
9
+ before do
10
+ stub_const("JsonStateNestingModel", Class.new(Lutaml::Model::Serializable) do
11
+ attribute :name, :string
12
+ attribute :age, :integer
13
+ end)
14
+ end
15
+
16
+ let(:model) { JsonStateNestingModel.new(name: "John", age: 30) }
17
+
18
+ it "serializes inside a Hash" do
19
+ expect(JSON.generate({ "person" => model }))
20
+ .to eq('{"person":{"name":"John","age":30}}')
21
+ end
22
+
23
+ it "serializes inside an Array" do
24
+ expect(JSON.generate([model])).to eq('[{"name":"John","age":30}]')
25
+ end
26
+
27
+ # The generator's JSON::State carries the surrounding indent context. It has
28
+ # to reach the adapter, not be discarded, or a nested model silently renders
29
+ # compact inside an otherwise pretty document. Reference values are the
30
+ # output of unmodified main on json 2.20.0.
31
+ it "inherits the surrounding indent inside a pretty Hash" do
32
+ expect(JSON.pretty_generate({ "person" => model }))
33
+ .to eq(%({\n "person": {\n "name": "John",\n "age": 30\n }\n}))
34
+ end
35
+
36
+ it "inherits the surrounding indent inside a pretty Array" do
37
+ expect(JSON.pretty_generate([model]))
38
+ .to eq(%([\n {\n "name": "John",\n "age": 30\n }\n]))
39
+ end
40
+
41
+ it "still honours LutaML's own options when called directly" do
42
+ expect(model.to_json(pretty: true))
43
+ .to eq(%({\n "name": "John",\n "age": 30\n}))
44
+ end
45
+
46
+ # A non-Hash options argument must be replaced, not merely captured -- a
47
+ # falsy one (nil, false) previously stayed in place and the next line called
48
+ # .delete on it.
49
+ it "does not raise on a nil options argument" do
50
+ expect(model.to_json(nil)).to eq('{"name":"John","age":30}')
51
+ end
52
+
53
+ it "does not raise on a false options argument" do
54
+ expect(model.to_json(false)).to eq('{"name":"John","age":30}')
55
+ end
56
+
57
+ # A non-Hash options argument reaches every format, not just JSON, so the
58
+ # normalisation must not skip format-specific validation on the way past.
59
+ # A type-only model has no root mapping and must refuse to serialise alone.
60
+ it "still applies XML root validation to a non-Hash argument" do
61
+ stub_const("TypeOnlyModel", Class.new(Lutaml::Model::Serializable) do
62
+ attribute :n, :string
63
+ end)
64
+
65
+ expect { TypeOnlyModel.new(n: "a").to_xml(nil) }
66
+ .to raise_error(Lutaml::Model::TypeOnlyMappingError)
67
+ end
68
+
69
+ # Every example above goes through an INSTANCE entry point. The class-level
70
+ # one normalises separately, and deleting that normaliser leaves them all
71
+ # green.
72
+ it "normalises a state passed to the class-level entry point" do
73
+ state = JSON::State.new(indent: " ", object_nl: "\n", space: " ")
74
+
75
+ expect(JsonStateNestingModel.to_json(model, state))
76
+ .to eq(%({\n "name": "John",\n "age": 30\n}))
77
+ end
78
+
79
+ it "leaves non-JSON formats untouched" do
80
+ expect(model.to_yaml).to eq("---\nname: John\nage: 30\n")
81
+ end
82
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # lutaml-model#765: two element rules mapped to one collection attribute
6
+ # (spelling tolerance, e.g. metanorma's editorial-group/editorialgroup).
7
+ # Parse must merge the spellings' content in document order — never drop
8
+ # the first silently — and serialization must emit the value once, under
9
+ # the first declared spelling.
10
+ module SharedElementRulesSpec
11
+ class Both < Lutaml::Model::Serializable
12
+ attribute :items, :string, collection: true
13
+
14
+ xml do
15
+ root "r"
16
+ map_element "a", to: :items
17
+ map_element "b", to: :items
18
+ end
19
+ end
20
+ end
21
+
22
+ RSpec.describe "multiple element rules on one attribute" do
23
+ it "appends both spellings in document order" do
24
+ expect(SharedElementRulesSpec::Both.from_xml("<r><a>one</a><b>two</b></r>").items)
25
+ .to eq(["one", "two"])
26
+ end
27
+
28
+ it "appends in document order when the second spelling comes first" do
29
+ expect(SharedElementRulesSpec::Both.from_xml("<r><b>two</b><a>one</a></r>").items)
30
+ .to eq(["two", "one"])
31
+ end
32
+
33
+ it "keeps document order across interleaved spellings" do
34
+ xml = "<r><a>1</a><b>2</b><a>3</a><b>4</b></r>"
35
+ expect(SharedElementRulesSpec::Both.from_xml(xml).items).to eq(%w[1 2 3 4])
36
+ end
37
+
38
+ it "parses a document that only uses the first spelling" do
39
+ expect(SharedElementRulesSpec::Both.from_xml("<r><a>one</a></r>").items)
40
+ .to eq(["one"])
41
+ end
42
+
43
+ it "parses a document that only uses the second spelling" do
44
+ expect(SharedElementRulesSpec::Both.from_xml("<r><b>two</b></r>").items)
45
+ .to eq(["two"])
46
+ end
47
+
48
+ it "serializes the value once, under the first declared spelling" do
49
+ xml = SharedElementRulesSpec::Both.new(items: %w[one two]).to_xml
50
+ expect(xml.scan(/<(a|b)>/).flatten).to eq(%w[a a])
51
+ expect(xml).to include("<a>one</a>").and include("<a>two</a>")
52
+ end
53
+
54
+ it "round-trips through both spellings without losing content" do
55
+ doc = SharedElementRulesSpec::Both.from_xml("<r><a>one</a><b>two</b></r>")
56
+ reparsed = SharedElementRulesSpec::Both.from_xml(doc.to_xml)
57
+ expect(reparsed.items).to eq(%w[one two])
58
+ end
59
+
60
+ it "keeps the mapping usable when the attribute also has a default" do
61
+ both = SharedElementRulesSpec::Both.from_xml("<r/>")
62
+ expect(both.items).to eq([])
63
+ end
64
+ end
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.22
4
+ version: 0.8.23
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-08-29 00:00:00.000000000 Z
11
+ date: 2026-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -321,6 +321,7 @@ files:
321
321
  - lib/lutaml/json/adapter/standard_adapter.rb
322
322
  - lib/lutaml/json/adapter/transform.rb
323
323
  - lib/lutaml/json/format.rb
324
+ - lib/lutaml/json/generator_options.rb
324
325
  - lib/lutaml/json/schema.rb
325
326
  - lib/lutaml/json/schema/json_schema.rb
326
327
  - lib/lutaml/json/type/serializers.rb
@@ -1638,6 +1639,7 @@ files:
1638
1639
  - spec/lutaml/integration/edge_cases_spec.rb
1639
1640
  - spec/lutaml/integration/multi_format_spec.rb
1640
1641
  - spec/lutaml/integration/round_trip_spec.rb
1642
+ - spec/lutaml/json/adapter/standard_adapter_spec.rb
1641
1643
  - spec/lutaml/jsonld/adapter_spec.rb
1642
1644
  - spec/lutaml/key_value/transformation/collection_serializer_spec.rb
1643
1645
  - spec/lutaml/key_value/transformation/rule_compiler_spec.rb
@@ -1657,6 +1659,7 @@ files:
1657
1659
  - spec/lutaml/model/collection_index_spec.rb
1658
1660
  - spec/lutaml/model/collection_mutation_spec.rb
1659
1661
  - spec/lutaml/model/collection_spec.rb
1662
+ - spec/lutaml/model/collection_state_nesting_spec.rb
1660
1663
  - spec/lutaml/model/collection_validation_spec.rb
1661
1664
  - spec/lutaml/model/comparable_model_spec.rb
1662
1665
  - spec/lutaml/model/compiled_rule_spec.rb
@@ -1677,6 +1680,7 @@ files:
1677
1680
  - spec/lutaml/model/enum_spec.rb
1678
1681
  - spec/lutaml/model/except_spec.rb
1679
1682
  - spec/lutaml/model/finalization_cache_spec.rb
1683
+ - spec/lutaml/model/generator_defaults_spec.rb
1680
1684
  - spec/lutaml/model/global_context_spec.rb
1681
1685
  - spec/lutaml/model/global_register_spec.rb
1682
1686
  - spec/lutaml/model/group_spec.rb
@@ -1686,6 +1690,7 @@ files:
1686
1690
  - spec/lutaml/model/inheritance_spec.rb
1687
1691
  - spec/lutaml/model/json_adapter_spec.rb
1688
1692
  - spec/lutaml/model/json_spec.rb
1693
+ - spec/lutaml/model/json_state_nesting_spec.rb
1689
1694
  - spec/lutaml/model/jsonl/standard_adapter_spec.rb
1690
1695
  - spec/lutaml/model/jsonl_spec.rb
1691
1696
  - spec/lutaml/model/key_value_data_model/key_value_element_spec.rb
@@ -1916,6 +1921,7 @@ files:
1916
1921
  - spec/lutaml/xml/schema/xsd/xsd_spec.rb
1917
1922
  - spec/lutaml/xml/schema_primer_spec.rb
1918
1923
  - spec/lutaml/xml/serializable_namespace_spec.rb
1924
+ - spec/lutaml/xml/shared_element_rules_spec.rb
1919
1925
  - spec/lutaml/xml/transformation/custom_method_wrapper_spec.rb
1920
1926
  - spec/lutaml/xml/transformation_spec.rb
1921
1927
  - spec/lutaml/xml/type_namespace/collector_spec.rb