lutaml-model 0.8.19 → 0.9.1

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.

Potentially problematic release.


This version of lutaml-model might be problematic. Click here for more details.

Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +1 -1
  3. data/.github/workflows/rake.yml +1 -0
  4. data/.rubocop_todo.yml +133 -72
  5. data/bench/bench_common.rb +6 -3
  6. data/bench/bench_niso.rb +1 -2
  7. data/lib/compat/opal/generate_boot.rb +3 -1
  8. data/lib/lutaml/key_value/transform.rb +4 -1
  9. data/lib/lutaml/key_value/transformation/value_serializer.rb +0 -2
  10. data/lib/lutaml/model/attribute.rb +23 -2
  11. data/lib/lutaml/model/global_context.rb +4 -2
  12. data/lib/lutaml/model/schema/class_loader.rb +2 -1
  13. data/lib/lutaml/model/schema/definitions/namespace.rb +2 -1
  14. data/lib/lutaml/model/schema/file_writer.rb +5 -2
  15. data/lib/lutaml/model/schema/renderers/base.rb +2 -1
  16. data/lib/lutaml/model/schema/renderers/mappings.rb +9 -3
  17. data/lib/lutaml/model/schema/renderers/member_decls.rb +9 -3
  18. data/lib/lutaml/model/schema/renderers/model.rb +16 -4
  19. data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +2 -1
  20. data/lib/lutaml/model/schema/renderers.rb +2 -1
  21. data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +6 -2
  22. data/lib/lutaml/model/schema/rnc_compiler.rb +3 -1
  23. data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +35 -15
  24. data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +4 -2
  25. data/lib/lutaml/model/schema/rng_compiler.rb +10 -5
  26. data/lib/lutaml/model/schema/xml_compiler/element_order.rb +2 -1
  27. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +4 -2
  28. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +39 -13
  29. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +29 -10
  30. data/lib/lutaml/model/schema/xml_compiler.rb +10 -5
  31. data/lib/lutaml/model/serialize/attribute_definition.rb +1 -0
  32. data/lib/lutaml/model/serialize/initialization.rb +9 -1
  33. data/lib/lutaml/model/serialize/model_import.rb +1 -0
  34. data/lib/lutaml/model/serialize.rb +5 -4
  35. data/lib/lutaml/model/union.rb +5 -2
  36. data/lib/lutaml/model/version.rb +1 -1
  37. data/lib/lutaml/xml/mapping_rule.rb +55 -1
  38. data/lib/lutaml/xml/model_transform.rb +53 -5
  39. data/lib/lutaml/xml/schema/xsd.rb +0 -2
  40. data/lib/lutaml/xml/schema/xsd_schema.rb +2 -1
  41. data/lib/lutaml/xml/transformation/element_builder.rb +2 -1
  42. data/lib/lutaml/xml/transformation/ordered_applier.rb +2 -1
  43. data/lutaml-model.gemspec +4 -1
  44. data/spec/lutaml/model/attribute_apply_value_map_spec.rb +26 -13
  45. data/spec/lutaml/model/attribute_spec.rb +8 -3
  46. data/spec/lutaml/model/custom_from_empty_values_spec.rb +74 -0
  47. data/spec/lutaml/model/defaults_spec.rb +45 -0
  48. data/spec/lutaml/model/json_spec.rb +1 -2
  49. data/spec/lutaml/model/serialize/builder_spec.rb +6 -3
  50. data/spec/lutaml/model/transform_apply_value_map_spec.rb +6 -3
  51. data/spec/lutaml/model/union_attribute_spec.rb +4 -2
  52. data/spec/lutaml/xml/attribute_namespace_disjointness_spec.rb +125 -0
  53. data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +17 -6
  54. data/spec/lutaml/xml/schema/rng_compiler_spec.rb +15 -6
  55. data/spec/lutaml/xml/w3c_types_spec.rb +3 -6
  56. metadata +5 -3
@@ -183,6 +183,34 @@ module Lutaml
183
183
  result
184
184
  end
185
185
 
186
+ # Match names an attribute rule consumes during parsing, mirroring how
187
+ # serialization qualifies the same attribute (resolve_namespace):
188
+ # a rule serialized with a prefix matches only attributes in its
189
+ # namespace; a rule serialized unprefixed matches only namespace-less
190
+ # attributes.
191
+ #
192
+ # Performance: memoized on the rule with zero-allocation cache hits.
193
+ # The inputs are constant for a given rule, register and parent model,
194
+ # so every element of a collection reuses the first resolution.
195
+ def attribute_match_names(attr, register, parent_ns_class, form_default,
196
+ default_namespace)
197
+ if defined?(@_amn_names) && @_amn_register == register &&
198
+ @_amn_parent_ns == parent_ns_class &&
199
+ @_amn_form_default == form_default &&
200
+ @_amn_default_ns == default_namespace
201
+ return @_amn_names
202
+ end
203
+
204
+ result = compute_attribute_match_names(attr, register, parent_ns_class,
205
+ form_default, default_namespace)
206
+ @_amn_register = register
207
+ @_amn_parent_ns = parent_ns_class
208
+ @_amn_form_default = form_default
209
+ @_amn_default_ns = default_namespace
210
+ @_amn_names = result
211
+ result
212
+ end
213
+
186
214
  def namespaced_name(parent_namespace = nil, name = self.name)
187
215
  if name.to_s == "lang"
188
216
  ::Lutaml::Model::Utils.blank?(prefix) ? name.to_s : "#{prefix}:#{name}"
@@ -274,6 +302,30 @@ module Lutaml
274
302
 
275
303
  private
276
304
 
305
+ # Extracted body of attribute_match_names for caching
306
+ def compute_attribute_match_names(attr, register, parent_ns_class,
307
+ form_default, default_namespace)
308
+ ns_info = resolve_namespace(
309
+ attr: attr,
310
+ register: register,
311
+ parent_ns_class: parent_ns_class,
312
+ form_default: form_default,
313
+ )
314
+ uri = ns_info[:uri]
315
+ # Schema-level attribute_form_default keeps the historical plain-name
316
+ # parse: dependents (e.g. ogc-gml) declare :qualified but their
317
+ # documents and expectations rely on unprefixed attributes parsing.
318
+ # Explicit namespaces, type namespaces and explicit form: rules
319
+ # mirror serialization strictly.
320
+ strict_match = uri && ns_info[:prefix] &&
321
+ !ns_info[:unqualified_same_ns] && !ns_info[:via_schema_default]
322
+ if strict_match
323
+ ["#{uri}:#{name}"].freeze
324
+ else
325
+ namespaced_names(default_namespace)
326
+ end
327
+ end
328
+
277
329
  # Extracted body of namespaced_names for caching
278
330
  def compute_namespaced_names(parent_namespace)
279
331
  if multiple_mappings?
@@ -427,7 +479,9 @@ form_default = :unqualified)
427
479
  # When schema specifies attributeFormDefault="qualified", attributes
428
480
  # must be qualified (prefixed) with the parent element's namespace
429
481
  if form_default == :qualified && parent_ns_class
430
- return build_namespace_result_from_class(parent_ns_class)
482
+ result = build_namespace_result_from_class(parent_ns_class)
483
+ result[:via_schema_default] = true
484
+ return result
431
485
  end
432
486
 
433
487
  # 4. No namespace (W3C default for unprefixed attributes)
@@ -509,8 +509,13 @@ _effective_register)
509
509
  local_name = rn[(last_colon_index + 1)..]
510
510
 
511
511
  matched_attr = doc.root.attributes.values.find do |attr|
512
- # Match by the attribute's namespaced_name, unprefixed_name, or by extracting
513
- # local name from the attribute's key (handles unresolved prefixes).
512
+ # A namespace-less, prefix-less attribute cannot belong to the
513
+ # rule's namespace, so it must not satisfy the fallback
514
+ # (lutaml-model#744). Attributes with undeclared prefixes keep
515
+ # their prefix in the raw name and stay eligible below.
516
+ next false if attr.namespace.nil? &&
517
+ attr.namespace_prefix.nil? && !attr.name.include?(":")
518
+
514
519
  attr.namespaced_name == local_name ||
515
520
  attr.unprefixed_name == local_name ||
516
521
  (attr.namespace.nil? && ((colon = attr.name.rindex(":")) ? attr.name[(colon + 1)..] : attr.name) == local_name)
@@ -544,9 +549,14 @@ _effective_register)
544
549
  rule_namespace = rule_namespace_set ? rule.namespace : nil
545
550
  options[:default_namespace]
546
551
 
547
- # Enhanced namespace resolution with type support
548
- rule_names = resolve_rule_names_with_type(rule, attr, options,
549
- effective_register, attr_type)
552
+ rule_names = if rule.attribute?
553
+ resolve_attribute_rule_names(rule, attr, options,
554
+ effective_register,
555
+ instance, instance_is_serialize)
556
+ else
557
+ resolve_rule_names_with_type(rule, attr, options,
558
+ effective_register, attr_type)
559
+ end
550
560
 
551
561
  return value_for_xml_attribute(doc, rule, rule_names) if rule.attribute?
552
562
 
@@ -879,6 +889,44 @@ effective_register = lutaml_register)
879
889
  end
880
890
  end
881
891
 
892
+ # Match names for an attribute rule, mirroring how serialization
893
+ # qualifies the same attribute (MappingRule#attribute_match_names):
894
+ # a rule serialized with a prefix matches only attributes in its
895
+ # namespace; a rule serialized unprefixed matches only namespace-less
896
+ # attributes.
897
+ def resolve_attribute_rule_names(rule, attr, options, effective_register,
898
+ instance, instance_is_serialize)
899
+ return rule.namespaced_names(options[:default_namespace]) if attr.nil? ||
900
+ rule.multiple_mappings?
901
+
902
+ if instance_is_serialize
903
+ parent_ns_class, form_default = parent_attribute_context(instance)
904
+ else
905
+ parent_ns_class = nil
906
+ form_default = :unqualified
907
+ end
908
+ rule.attribute_match_names(attr, effective_register, parent_ns_class,
909
+ form_default,
910
+ options[:default_namespace])
911
+ end
912
+
913
+ # [namespace_class, attribute_form_default] for the instance's model,
914
+ # keyed on the model class. Attribute rules of one element share the
915
+ # model, so a 1-entry cache avoids a mappings_for lookup per rule.
916
+ def parent_attribute_context(instance)
917
+ klass = instance.class
918
+ if @parent_ns_klass == klass && @parent_ns_value
919
+ return @parent_ns_value
920
+ end
921
+
922
+ ns_class = klass.mappings_for(:xml)&.namespace_class
923
+ value = [ns_class,
924
+ ns_class&.attribute_form_default || :unqualified]
925
+ @parent_ns_klass = klass
926
+ @parent_ns_value = value
927
+ value
928
+ end
929
+
882
930
  # Resolve rule names with type namespace support
883
931
  #
884
932
  # @param rule [MappingRule] the mapping rule
@@ -141,9 +141,7 @@ module Lutaml
141
141
  # Trigger autoloads for all XSD model classes to ensure they are
142
142
  # registered before parsing begins. This is necessary because
143
143
  # type resolution during parsing looks up classes in the register.
144
- # rubocop:disable Style/RedundantFetchOverride
145
144
  XSD_AUTOLOAD_CLASSES.each { |c| const_get(c) unless c == :VERSION }
146
- # rubocop:enable Style/RedundantFetchOverride
147
145
 
148
146
  # Validate XSD schema structure before parsing (unless disabled)
149
147
  if validate_schema && !nested_schema
@@ -410,7 +410,8 @@ attr_name)
410
410
  def self.get_attribute_xsd_type(attr, attr_type, register,
411
411
  _mapping_rule = nil)
412
412
  if attr.union?
413
- raise Lutaml::Model::UnionSchemaUnsupportedError.new(attr.name, "XSD")
413
+ raise Lutaml::Model::UnionSchemaUnsupportedError.new(attr.name,
414
+ "XSD")
414
415
  end
415
416
 
416
417
  # 1. Check for deprecated attribute-level xsd_type override
@@ -143,7 +143,8 @@ parent_element_form_default)
143
143
  # Get transformation for the actual class. Unions resolve the member
144
144
  # from the value's own class, like polymorphism.
145
145
  child_transformation = if union
146
- value.class.transformation_for(:xml, register)
146
+ value.class.transformation_for(:xml,
147
+ register)
147
148
  elsif is_polymorphic || is_polymorphic_subtype
148
149
  actual_class.transformation_for(:xml,
149
150
  register)
@@ -326,7 +326,8 @@ compiled_rules, mapping, processed_text_nodes)
326
326
  compiled_rules
327
327
  end
328
328
 
329
- emitted_counts = element_order_coverage(model_instance, compiled_rules)
329
+ emitted_counts = element_order_coverage(model_instance,
330
+ compiled_rules)
330
331
 
331
332
  rules_to_apply.each do |rule|
332
333
  mapping_type = rule.option(:mapping_type)
data/lutaml-model.gemspec CHANGED
@@ -18,7 +18,10 @@ Gem::Specification.new do |spec|
18
18
 
19
19
  spec.bindir = "exe"
20
20
  spec.require_paths = ["lib"]
21
- spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
21
+ # lib/ uses anonymous argument forwarding in call position, which parses
22
+ # only on Ruby 3.2+ (verified: 6 files fail to parse on 3.1). Without this
23
+ # floor, bundler resolves 0.8.x on 3.1 and it dies at require time.
24
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
22
25
 
23
26
  # Specify which files should be added to the gem when it is released.
24
27
  # The `git ls-files -z` loads the files in the
@@ -18,7 +18,8 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
18
18
 
19
19
  it "maps nil through value_map[:nil] => :empty to [] (collection)" do
20
20
  expect(
21
- attribute(:string, collection: true).apply_value_map(nil, { nil: :empty }),
21
+ attribute(:string, collection: true).apply_value_map(nil,
22
+ { nil: :empty }),
22
23
  ).to eq([])
23
24
  end
24
25
 
@@ -34,13 +35,15 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
34
35
  custom_collection_class = Class.new(Lutaml::Model::Collection) do
35
36
  instances :items, member_class
36
37
  end
37
- attr = described_class.new("items", member_class, collection: custom_collection_class)
38
+ attr = described_class.new("items", member_class,
39
+ collection: custom_collection_class)
38
40
  result = attr.apply_value_map(nil, { nil: :empty })
39
41
  expect(result).to be_a(custom_collection_class)
40
42
  end
41
43
 
42
44
  it "maps nil through value_map[:nil] => :omitted to UninitializedClass" do
43
- expect(attribute(:string).apply_value_map(nil, { nil: :omitted })).to eq(uninit)
45
+ expect(attribute(:string).apply_value_map(nil,
46
+ { nil: :omitted })).to eq(uninit)
44
47
  end
45
48
  end
46
49
 
@@ -50,12 +53,14 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
50
53
  end
51
54
 
52
55
  it "maps '' through value_map[:empty] => :empty preserving '' for scalar" do
53
- expect(attribute(:string).apply_value_map("", { empty: :empty })).to eq("")
56
+ expect(attribute(:string).apply_value_map("",
57
+ { empty: :empty })).to eq("")
54
58
  end
55
59
 
56
60
  it "maps [] through value_map[:empty] => :empty preserving [] for collection" do
57
61
  expect(
58
- attribute(:string, collection: true).apply_value_map([], { empty: :empty }),
62
+ attribute(:string, collection: true).apply_value_map([],
63
+ { empty: :empty }),
59
64
  ).to eq([])
60
65
  end
61
66
 
@@ -69,11 +74,13 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
69
74
 
70
75
  describe "omitted handling" do
71
76
  it "maps UninitializedClass through value_map[:omitted] => :nil" do
72
- expect(attribute(:string).apply_value_map(uninit, { omitted: :nil })).to be_nil
77
+ expect(attribute(:string).apply_value_map(uninit,
78
+ { omitted: :nil })).to be_nil
73
79
  end
74
80
 
75
81
  it "maps UninitializedClass through value_map[:omitted] => :empty" do
76
- expect(attribute(:string).apply_value_map(uninit, { omitted: :empty })).to eq("")
82
+ expect(attribute(:string).apply_value_map(uninit,
83
+ { omitted: :empty })).to eq("")
77
84
  end
78
85
 
79
86
  it "does NOT route UninitializedClass through the :empty branch" do
@@ -81,18 +88,21 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
81
88
  # utils.rb:132-137. Ensure dispatch order matches the originals: uninit
82
89
  # values go to :omitted, not :empty.
83
90
  expect(
84
- attribute(:string).apply_value_map(uninit, { empty: :nil, omitted: :empty }),
91
+ attribute(:string).apply_value_map(uninit,
92
+ { empty: :nil, omitted: :empty }),
85
93
  ).to eq("")
86
94
  end
87
95
  end
88
96
 
89
97
  describe "Boolean bare-form (Boolean attribute type required)" do
90
98
  it "returns false directly for Boolean attribute, value_map[:empty] = false" do
91
- expect(attribute(:boolean).apply_value_map("", { empty: false })).to be(false)
99
+ expect(attribute(:boolean).apply_value_map("",
100
+ { empty: false })).to be(false)
92
101
  end
93
102
 
94
103
  it "returns true directly for Boolean attribute, value_map[:omitted] = true" do
95
- expect(attribute(:boolean).apply_value_map(uninit, { omitted: true })).to be(true)
104
+ expect(attribute(:boolean).apply_value_map(uninit,
105
+ { omitted: true })).to be(true)
96
106
  end
97
107
 
98
108
  it "does NOT apply bare-boolean form to a non-Boolean attribute (documented tightening)" do
@@ -106,15 +116,18 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
106
116
 
107
117
  describe "Boolean nested-form (value_map[:from][:empty] / [:omitted])" do
108
118
  it "returns false from value_map[:from][:empty] unconditionally for Boolean attribute" do
109
- expect(attribute(:boolean).apply_value_map("", { from: { empty: false } })).to be(false)
119
+ expect(attribute(:boolean).apply_value_map("",
120
+ { from: { empty: false } })).to be(false)
110
121
  end
111
122
 
112
123
  it "returns true from value_map[:from][:omitted] unconditionally for Boolean attribute" do
113
- expect(attribute(:boolean).apply_value_map(uninit, { from: { omitted: true } })).to be(true)
124
+ expect(attribute(:boolean).apply_value_map(uninit,
125
+ { from: { omitted: true } })).to be(true)
114
126
  end
115
127
 
116
128
  it "returns false from nested form even for non-Boolean attribute (no type gate)" do
117
- expect(attribute(:string).apply_value_map("", { from: { empty: false } })).to be(false)
129
+ expect(attribute(:string).apply_value_map("",
130
+ { from: { empty: false } })).to be(false)
118
131
  end
119
132
  end
120
133
 
@@ -358,7 +358,8 @@ RSpec.describe Lutaml::Model::Attribute do
358
358
  it "executes the proc in the instance context (reads instance attribute)" do
359
359
  instance = model_class.new(name: "Alice")
360
360
  attribute = model_class.attributes[:greeting]
361
- expect(attribute.default_value(register, instance)).to eq("Hello, Alice")
361
+ expect(attribute.default_value(register,
362
+ instance)).to eq("Hello, Alice")
362
363
  end
363
364
  end
364
365
  end
@@ -413,7 +414,9 @@ RSpec.describe Lutaml::Model::Attribute do
413
414
  end
414
415
  instance = model_class.new(prefix: "Dr")
415
416
  attr = model_class.attributes[:name]
416
- expect { attr.validate_value!(nil, register, instance_object: instance) }
417
+ expect do
418
+ attr.validate_value!(nil, register, instance_object: instance)
419
+ end
417
420
  .to raise_error(Lutaml::Model::InvalidValueError) do |error|
418
421
  # The error carries the value that was used for validation.
419
422
  # If instance_exec ran, value is "Dr_default"; if not, error
@@ -427,7 +430,9 @@ RSpec.describe Lutaml::Model::Attribute do
427
430
  it "short-circuits without raising" do
428
431
  uninit = Lutaml::Model::UninitializedClass.instance
429
432
  attr = described_class.new("name", :string)
430
- expect { attr.validate_value!(uninit, register, instance_object: model) }
433
+ expect do
434
+ attr.validate_value!(uninit, register, instance_object: model)
435
+ end
431
436
  .not_to raise_error
432
437
  end
433
438
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string: true
2
+
3
+ require "spec_helper"
4
+
5
+ # lutaml-model#746: an empty value ("", [], {}) is a PRESENT value per
6
+ # docs/_guides/missing-values-handling.adoc and must reach custom `from`
7
+ # methods. Only nil (non-existent) and an omitted key (undefined) skip them.
8
+ module CustomFromEmptyValuesSpec
9
+ class Tagged < Lutaml::Model::Serializable
10
+ attribute :received, :string
11
+
12
+ key_value do
13
+ map "tags", to: :received, with: { to: :tags_to, from: :tags_from }
14
+ end
15
+
16
+ def tags_to(model, doc)
17
+ doc["tags"] = model.received
18
+ end
19
+
20
+ def tags_from(model, value)
21
+ model.received = value.inspect
22
+ end
23
+ end
24
+
25
+ class Mapped < Lutaml::Model::Serializable
26
+ attribute :received, :string
27
+
28
+ key_value do
29
+ map "fields", to: :received, with: { to: :fields_to, from: :fields_from }
30
+ end
31
+
32
+ def fields_to(model, doc)
33
+ doc["fields"] = model.received
34
+ end
35
+
36
+ def fields_from(model, value)
37
+ model.received = value.inspect
38
+ end
39
+ end
40
+ end
41
+
42
+ RSpec.describe "custom from methods and empty values" do
43
+ it "receives an empty collection" do
44
+ expect(CustomFromEmptyValuesSpec::Tagged.from_yaml("tags: []").received)
45
+ .to eq("[]")
46
+ end
47
+
48
+ it "receives an empty string" do
49
+ expect(CustomFromEmptyValuesSpec::Tagged.from_yaml('tags: ""').received)
50
+ .to eq("\"\"")
51
+ end
52
+
53
+ it "receives an empty mapping" do
54
+ expect(CustomFromEmptyValuesSpec::Mapped.from_yaml("fields: {}").received)
55
+ .to eq("{}")
56
+ end
57
+
58
+ it "receives empty values from JSON too" do
59
+ expect(CustomFromEmptyValuesSpec::Tagged.from_json(%({"tags": []})).received)
60
+ .to eq("[]")
61
+ expect(CustomFromEmptyValuesSpec::Tagged.from_json(%({"tags": ""})).received)
62
+ .to eq("\"\"")
63
+ end
64
+
65
+ it "still skips the method for a nil value" do
66
+ expect(CustomFromEmptyValuesSpec::Tagged.from_yaml("tags:").received)
67
+ .to be_nil
68
+ end
69
+
70
+ it "still skips the method for an omitted key" do
71
+ expect(CustomFromEmptyValuesSpec::Tagged.from_yaml("{}").received)
72
+ .to be_nil
73
+ end
74
+ end
@@ -146,6 +146,33 @@ module DefaultsSpec
146
146
 
147
147
  class InstanceAwareGrandChild < InstanceAwareChild
148
148
  end
149
+
150
+ # Class-method defaults must keep resolving as they did before 0.8.0
151
+ # (lutaml-model#745): default lambdas are evaluated against the instance,
152
+ # with the model class as fallback for methods the instance does not have.
153
+ class ClassMethodDefault < Lutaml::Model::Serializable
154
+ attribute :os, :string, default: -> { detect_os }
155
+
156
+ def self.detect_os
157
+ "from-class-method"
158
+ end
159
+ end
160
+
161
+ class ClassMethodDefaultWithArgs < Lutaml::Model::Serializable
162
+ attribute :label, :string, default: -> { build_label("x") }
163
+
164
+ def self.build_label(prefix)
165
+ "#{prefix}-label"
166
+ end
167
+ end
168
+
169
+ class InstanceMethodDefault < Lutaml::Model::Serializable
170
+ attribute :value, :string, default: -> { instance_helper }
171
+
172
+ def instance_helper
173
+ "from-instance-method"
174
+ end
175
+ end
149
176
  end
150
177
 
151
178
  RSpec.describe DefaultsSpec::Glaze do
@@ -346,3 +373,21 @@ RSpec.describe DefaultsSpec::Glaze do
346
373
  end
347
374
  end
348
375
  end
376
+
377
+ RSpec.describe DefaultsSpec::ClassMethodDefault do
378
+ it "resolves class methods from default lambdas" do
379
+ expect(described_class.new.os).to eq("from-class-method")
380
+ end
381
+ end
382
+
383
+ RSpec.describe DefaultsSpec::ClassMethodDefaultWithArgs do
384
+ it "passes arguments through to class methods" do
385
+ expect(described_class.new.label).to eq("x-label")
386
+ end
387
+ end
388
+
389
+ RSpec.describe DefaultsSpec::InstanceMethodDefault do
390
+ it "still resolves instance methods in default lambdas" do
391
+ expect(described_class.new.value).to eq("from-instance-method")
392
+ end
393
+ end
@@ -8,9 +8,8 @@ RSpec.describe Lutaml::Model::Json do
8
8
  let(:lib_path) { File.expand_path("../../../lib", __dir__) }
9
9
 
10
10
  it "loads require 'lutaml/json' without preloading lutaml/model" do
11
- # rubocop:disable Style/CommandLiteral
11
+ # rubocop:disable-next Style/CommandLiteral
12
12
  result = `#{RbConfig.ruby} -I#{lib_path} -e 'require "lutaml/json"; puts :ok' 2>&1`
13
- # rubocop:enable Style/CommandLiteral
14
13
 
15
14
  expect($?.success?).to be(true), result
16
15
  expect(result).to include("ok")
@@ -112,7 +112,8 @@ RSpec.describe "Lutaml::Model::Serialize::Builder" do
112
112
  <items>c</items>
113
113
  </container>
114
114
  XML
115
- expect(obj.element_order.map(&:name)).to eq(%w[singular items items items])
115
+ expect(obj.element_order.map(&:name)).to eq(%w[singular items items
116
+ items])
116
117
  end
117
118
 
118
119
  it "all direct setters: emits in call order" do
@@ -298,7 +299,8 @@ RSpec.describe "Lutaml::Model::Serialize::Builder" do
298
299
  # so both assignments are recorded. The serializer emits the current
299
300
  # value of the attribute via element_order in recorded order; the
300
301
  # safety net guarantees no data loss.
301
- expect(obj.element_order.map(&:name)).to eq(%w[items items items items items])
302
+ expect(obj.element_order.map(&:name)).to eq(%w[items items items items
303
+ items])
302
304
  xml = obj.to_xml
303
305
  # The serialized output must contain all current items
304
306
  expect(xml).to include("<items>c</items>")
@@ -317,7 +319,8 @@ RSpec.describe "Lutaml::Model::Serialize::Builder" do
317
319
  xml = obj.to_xml
318
320
 
319
321
  if !obj.singular.nil? && !obj.singular.to_s.empty?
320
- expect(xml).to include("<singular>"), "singular was dropped from output"
322
+ expect(xml).to include("<singular>"),
323
+ "singular was dropped from output"
321
324
  end
322
325
  return if obj.items.nil? || obj.items.empty?
323
326
 
@@ -33,7 +33,8 @@ RSpec.describe Lutaml::Model::Transform, "#apply_value_map (nil attr)" do
33
33
  end
34
34
 
35
35
  it "passes initialized values through unchanged" do
36
- expect(transform.apply_value_map("hello", default_vmap, nil)).to eq("hello")
36
+ expect(transform.apply_value_map("hello", default_vmap,
37
+ nil)).to eq("hello")
37
38
  end
38
39
  end
39
40
 
@@ -50,11 +51,13 @@ RSpec.describe Lutaml::Model::Transform, "#apply_value_map (nil attr)" do
50
51
  # custom method runs), and resolved :nil/:empty consistently for empty inputs.
51
52
  describe "non-:nil options with nil attr" do
52
53
  it "maps a nil value to uninitialized via nil: :omitted" do
53
- expect(transform.apply_value_map(nil, { nil: :omitted }, nil)).to eq(uninit)
54
+ expect(transform.apply_value_map(nil, { nil: :omitted },
55
+ nil)).to eq(uninit)
54
56
  end
55
57
 
56
58
  it "maps an empty value to uninitialized via empty: :omitted" do
57
- expect(transform.apply_value_map("", { empty: :omitted }, nil)).to eq(uninit)
59
+ expect(transform.apply_value_map("", { empty: :omitted },
60
+ nil)).to eq(uninit)
58
61
  end
59
62
 
60
63
  it "maps an empty value to nil via empty: :nil" do
@@ -311,7 +311,8 @@ RSpec.describe "Union-typed attributes (issue #190)" do
311
311
  it "serializes a model member built in plain Ruby identically to a deserialized one" do
312
312
  built = UnionAttributeSpec::Ceramic.new(
313
313
  firing_temperature:
314
- UnionAttributeSpec::TemperatureWithUnit.new(number: 1300.0, unit: "C"),
314
+ UnionAttributeSpec::TemperatureWithUnit.new(number: 1300.0,
315
+ unit: "C"),
315
316
  )
316
317
  round = UnionAttributeSpec::Ceramic.from_yaml(built.to_yaml)
317
318
  expect(round.firing_temperature)
@@ -424,7 +425,8 @@ RSpec.describe "Union-typed attributes (issue #190)" do
424
425
  end
425
426
 
426
427
  it "rejects unsupported scalar member types at definition time" do
427
- %i[hash symbol time time_without_date date date_time].each do |unsupported|
428
+ %i[hash symbol time time_without_date date
429
+ date_time].each do |unsupported|
428
430
  expect do
429
431
  Class.new(Lutaml::Model::Serializable) do
430
432
  attribute :bad, [unsupported, :string]