lutaml-model 0.8.3 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +23 -23
  3. data/README.adoc +213 -1
  4. data/docs/_guides/document-validation.adoc +303 -0
  5. data/docs/_guides/index.adoc +1 -0
  6. data/docs/_guides/xml-mapping.adoc +9 -1
  7. data/docs/_guides/xml_mappings/07_best_practices.adoc +36 -0
  8. data/docs/_guides/xml_mappings/08_troubleshooting.adoc +89 -0
  9. data/docs/_tutorials/lutaml-xml-architecture.adoc +6 -1
  10. data/lib/lutaml/model/attribute.rb +19 -1
  11. data/lib/lutaml/model/error/liquid_drop_already_registered_error.rb +11 -0
  12. data/lib/lutaml/model/error/ordered_content_mapping_error.rb +17 -0
  13. data/lib/lutaml/model/global_context.rb +1 -0
  14. data/lib/lutaml/model/liquefiable.rb +12 -15
  15. data/lib/lutaml/model/mapping/mapping_rule.rb +10 -2
  16. data/lib/lutaml/model/mapping_hash.rb +1 -1
  17. data/lib/lutaml/model/services/transformer.rb +67 -32
  18. data/lib/lutaml/model/transform.rb +41 -4
  19. data/lib/lutaml/model/uninitialized_class.rb +11 -5
  20. data/lib/lutaml/model/validation/concerns/has_issues.rb +27 -0
  21. data/lib/lutaml/model/validation/context.rb +36 -0
  22. data/lib/lutaml/model/validation/issue.rb +62 -0
  23. data/lib/lutaml/model/validation/layer_result.rb +34 -0
  24. data/lib/lutaml/model/validation/profile.rb +66 -0
  25. data/lib/lutaml/model/validation/registry.rb +60 -0
  26. data/lib/lutaml/model/validation/remediation.rb +33 -0
  27. data/lib/lutaml/model/validation/remediation_result.rb +20 -0
  28. data/lib/lutaml/model/validation/report.rb +39 -0
  29. data/lib/lutaml/model/validation/rule.rb +59 -0
  30. data/lib/lutaml/model/validation.rb +2 -1
  31. data/lib/lutaml/model/validation_framework.rb +77 -0
  32. data/lib/lutaml/model/version.rb +1 -1
  33. data/lib/lutaml/model.rb +4 -0
  34. data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +9 -2
  35. data/lib/lutaml/xml/adapter/oga_adapter.rb +11 -3
  36. data/lib/lutaml/xml/adapter/ox_adapter.rb +5 -2
  37. data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -3
  38. data/lib/lutaml/xml/adapter_element.rb +26 -2
  39. data/lib/lutaml/xml/data_model.rb +14 -0
  40. data/lib/lutaml/xml/document.rb +3 -0
  41. data/lib/lutaml/xml/element.rb +8 -2
  42. data/lib/lutaml/xml/mapping.rb +9 -0
  43. data/lib/lutaml/xml/model_transform.rb +42 -0
  44. data/lib/lutaml/xml/schema/xsd/base.rb +4 -1
  45. data/lib/lutaml/xml/serialization/instance_methods.rb +3 -1
  46. data/lib/lutaml/xml/transformation/ordered_applier.rb +46 -2
  47. data/lib/lutaml/xml/transformation.rb +40 -1
  48. data/lib/lutaml/xml/xml_element.rb +8 -7
  49. data/lutaml-model.gemspec +1 -1
  50. data/spec/lutaml/model/attribute_default_cache_spec.rb +58 -0
  51. data/spec/lutaml/model/liquefiable_spec.rb +22 -6
  52. data/spec/lutaml/model/liquid_compatibility_spec.rb +442 -0
  53. data/spec/lutaml/model/ordered_content_spec.rb +5 -5
  54. data/spec/lutaml/model/services/transformer_spec.rb +43 -0
  55. data/spec/lutaml/model/transform_cache_spec.rb +62 -0
  56. data/spec/lutaml/model/transform_dynamic_attributes_spec.rb +41 -0
  57. data/spec/lutaml/model/uninitialized_class_deep_dup_spec.rb +39 -0
  58. data/spec/lutaml/model/uninitialized_class_spec.rb +14 -2
  59. data/spec/lutaml/model/validation/concerns/has_issues_spec.rb +76 -0
  60. data/spec/lutaml/model/validation/context_spec.rb +60 -0
  61. data/spec/lutaml/model/validation/issue_spec.rb +77 -0
  62. data/spec/lutaml/model/validation/layer_result_spec.rb +66 -0
  63. data/spec/lutaml/model/validation/profile_spec.rb +134 -0
  64. data/spec/lutaml/model/validation/registry_spec.rb +94 -0
  65. data/spec/lutaml/model/validation/remediation_result_spec.rb +23 -0
  66. data/spec/lutaml/model/validation/remediation_spec.rb +72 -0
  67. data/spec/lutaml/model/validation/report_spec.rb +58 -0
  68. data/spec/lutaml/model/validation/rule_spec.rb +134 -0
  69. data/spec/lutaml/model/validation/uninitialized_class_validate_spec.rb +29 -0
  70. data/spec/lutaml/model/validation/validation_error_spec.rb +29 -0
  71. data/spec/lutaml/model/validation/validation_framework_spec.rb +110 -0
  72. data/spec/lutaml/xml/content_model_validation_spec.rb +157 -0
  73. data/spec/lutaml/xml/mapping_spec.rb +12 -7
  74. metadata +46 -7
  75. data/spec/fixtures/liquid_templates/_ceramics.liquid +0 -3
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/validation_framework"
5
+
6
+ RSpec.describe Lutaml::Model::Validation::Remediation do
7
+ subject(:remediation) { described_class.new }
8
+
9
+ it "returns nil id by default" do
10
+ expect(remediation.id).to be_nil
11
+ end
12
+
13
+ it "returns nil targets by default" do
14
+ expect(remediation.targets).to be_nil
15
+ end
16
+
17
+ it "is always applicable" do
18
+ expect(remediation.applicable?(nil, nil)).to be true
19
+ end
20
+
21
+ it "raises NotImplementedError from base fix" do
22
+ expect { remediation.fix(nil, nil) }
23
+ .to raise_error(NotImplementedError, /must be implemented/)
24
+ end
25
+
26
+ it "returns nil preview by default" do
27
+ expect(remediation.preview(nil, nil)).to be_nil
28
+ end
29
+
30
+ describe "custom subclass" do
31
+ let(:custom_remediation) do
32
+ Class.new(described_class) do
33
+ def id = "REM-001"
34
+ def targets = ["DOC-020"]
35
+
36
+ def applicable?(_context, report)
37
+ report.any? { |i| i.code == "DOC-020" }
38
+ end
39
+
40
+ def fix(_context, _report)
41
+ Lutaml::Model::Validation::RemediationResult.new(
42
+ success: true,
43
+ message: "Fixed DOC-020",
44
+ fixed_codes: ["DOC-020"],
45
+ )
46
+ end
47
+ end
48
+ end
49
+
50
+ it "overrides id and targets" do
51
+ rem = custom_remediation.new
52
+ expect(rem.id).to eq("REM-001")
53
+ expect(rem.targets).to eq(["DOC-020"])
54
+ end
55
+
56
+ it "checks applicability" do
57
+ rem = custom_remediation.new
58
+ issue = Lutaml::Model::Validation::Issue.new(
59
+ severity: "error", code: "DOC-020", message: "bad",
60
+ )
61
+ expect(rem.applicable?(nil, [issue])).to be true
62
+ expect(rem.applicable?(nil, [])).to be false
63
+ end
64
+
65
+ it "returns successful fix" do
66
+ rem = custom_remediation.new
67
+ result = rem.fix(nil, nil)
68
+ expect(result.success).to be true
69
+ expect(result.fixed_codes).to eq(["DOC-020"])
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/validation_framework"
5
+
6
+ RSpec.describe Lutaml::Model::Validation::Report do
7
+ subject(:report) do
8
+ described_class.new(
9
+ source: "test.xml",
10
+ valid: false,
11
+ duration_ms: 50,
12
+ layers: [layer],
13
+ )
14
+ end
15
+
16
+ let(:issue) do
17
+ Lutaml::Model::Validation::Issue.new(
18
+ severity: "error", code: "TEST-003", message: "Broken",
19
+ )
20
+ end
21
+
22
+ let(:layer) do
23
+ Lutaml::Model::Validation::LayerResult.new(
24
+ name: "Check", status: "fail", issues: [issue],
25
+ )
26
+ end
27
+
28
+ it "serializes to JSON" do
29
+ parsed = JSON.parse(report.to_json)
30
+ expect(parsed["source"]).to eq("test.xml")
31
+ expect(parsed["valid"]).to be(false)
32
+ expect(parsed["timestamp"]).not_to be_nil
33
+ end
34
+
35
+ it "auto-sets timestamp on initialization" do
36
+ expect(report.timestamp).to match(/\d{4}-\d{2}-\d{2}T/)
37
+ end
38
+
39
+ describe "issue aggregation via HasIssues" do
40
+ it "aggregates issues from all layers" do
41
+ expect(report.issues.length).to eq(1)
42
+ end
43
+
44
+ it "filters errors" do
45
+ expect(report.errors.length).to eq(1)
46
+ end
47
+
48
+ it "returns empty warnings when none present" do
49
+ expect(report.warnings).to be_empty
50
+ end
51
+ end
52
+
53
+ it "handles empty layers" do
54
+ empty_report = described_class.new(source: "empty.xml", valid: true)
55
+ expect(empty_report.issues).to be_empty
56
+ expect(empty_report.errors).to be_empty
57
+ end
58
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/validation_framework"
5
+
6
+ RSpec.describe Lutaml::Model::Validation::Rule do
7
+ subject(:rule) { described_class.new }
8
+
9
+ describe "defaults" do
10
+ it "returns nil code" do
11
+ expect(rule.code).to be_nil
12
+ end
13
+
14
+ it "returns :general category" do
15
+ expect(rule.category).to eq(:general)
16
+ end
17
+
18
+ it "returns error severity" do
19
+ expect(rule.severity).to eq("error")
20
+ end
21
+
22
+ it "is always applicable" do
23
+ expect(rule.applicable?(nil)).to be true
24
+ expect(rule.applicable?({})).to be true
25
+ end
26
+
27
+ it "returns empty issues from check" do
28
+ expect(rule.check(nil)).to eq([])
29
+ end
30
+
31
+ it "is not deferred" do
32
+ expect(rule.needs_deferred?).to be false
33
+ end
34
+
35
+ it "returns nil from collect" do
36
+ expect(rule.collect(:element, nil)).to be_nil
37
+ end
38
+
39
+ it "returns empty array from complete" do
40
+ expect(rule.complete(nil)).to eq([])
41
+ end
42
+ end
43
+
44
+ describe "custom subclass" do
45
+ let(:custom_rule_class) do
46
+ Class.new(described_class) do
47
+ def code = "CUSTOM-001"
48
+ def category = :custom
49
+ def severity = "warning"
50
+
51
+ def applicable?(context)
52
+ context&.dig(:enabled) != false
53
+ end
54
+
55
+ def check(_context)
56
+ [issue("Found a problem")]
57
+ end
58
+ end
59
+ end
60
+
61
+ it "overrides defaults" do
62
+ rule = custom_rule_class.new
63
+ expect(rule.code).to eq("CUSTOM-001")
64
+ expect(rule.category).to eq(:custom)
65
+ expect(rule.severity).to eq("warning")
66
+ end
67
+
68
+ it "produces issues via helper" do
69
+ rule = custom_rule_class.new
70
+ issues = rule.check(nil)
71
+ expect(issues.length).to eq(1)
72
+ expect(issues.first.code).to eq("CUSTOM-001")
73
+ expect(issues.first.severity).to eq("warning")
74
+ expect(issues.first.message).to eq("Found a problem")
75
+ end
76
+
77
+ it "respects applicable?" do
78
+ rule = custom_rule_class.new
79
+ expect(rule.applicable?({ enabled: true })).to be true
80
+ expect(rule.applicable?({ enabled: false })).to be false
81
+ end
82
+
83
+ it "allows issue helper to override severity and code" do
84
+ klass = Class.new(described_class) do
85
+ def code = "OVERRIDE"
86
+ def severity = "error"
87
+
88
+ def check(_context)
89
+ [issue("msg", severity: "info", code: "OTHER")]
90
+ end
91
+ end
92
+ issues = klass.new.check(nil)
93
+ expect(issues.first.severity).to eq("info")
94
+ expect(issues.first.code).to eq("OTHER")
95
+ end
96
+ end
97
+
98
+ describe "streaming subclass" do
99
+ let(:streaming_rule_class) do
100
+ Class.new(described_class) do
101
+ def code = "STREAM-001"
102
+ def needs_deferred? = true
103
+
104
+ def collect(element, context)
105
+ state = context.rule_state(code)
106
+ state[:items] ||= []
107
+ state[:items] << element
108
+ end
109
+
110
+ def complete(context)
111
+ state = context.rule_state(code)
112
+ return [] unless state[:items]&.any?
113
+
114
+ [issue("Collected #{state[:items].length} items")]
115
+ end
116
+ end
117
+ end
118
+
119
+ it "signals deferred collection" do
120
+ rule = streaming_rule_class.new
121
+ expect(rule.needs_deferred?).to be true
122
+ end
123
+
124
+ it "collects elements and reports in complete" do
125
+ ctx = Lutaml::Model::Validation::Context.new
126
+ rule = streaming_rule_class.new
127
+ rule.collect("item_a", ctx)
128
+ rule.collect("item_b", ctx)
129
+ issues = rule.complete(ctx)
130
+ expect(issues.length).to eq(1)
131
+ expect(issues.first.message).to eq("Collected 2 items")
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Lutaml::Model::Validation with UninitializedClass" do
6
+ before do
7
+ stub_const("ValidateWithUninitializedModel", Class.new(Lutaml::Model::Serializable) do
8
+ attribute :name, :string
9
+ attribute :role, :string, values: %w[admin guest], default: -> { "guest" }
10
+ end)
11
+ end
12
+
13
+ it "does not crash when validating with uninitialized attributes" do
14
+ model = ValidateWithUninitializedModel.new
15
+ # name is uninitialized, but validate should not crash
16
+ expect { model.validate }.not_to raise_error
17
+ end
18
+
19
+ it "still catches value constraint violations" do
20
+ model = ValidateWithUninitializedModel.new(role: "hacker")
21
+ errors = model.validate
22
+ expect(errors).not_to be_empty
23
+ end
24
+
25
+ it "returns empty errors for valid instance" do
26
+ model = ValidateWithUninitializedModel.new(name: "ok", role: "admin")
27
+ expect(model.validate).to be_empty
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/validation_framework"
5
+
6
+ RSpec.describe Lutaml::Model::Validation::ValidationError do
7
+ it "is a StandardError" do
8
+ expect(described_class).to be < StandardError
9
+ end
10
+
11
+ it "stores message" do
12
+ error = described_class.new("Something broke")
13
+ expect(error.message).to eq("Something broke")
14
+ end
15
+
16
+ it "stores issues" do
17
+ issue = Lutaml::Model::Validation::Issue.new(
18
+ severity: "error", code: "E-001", message: "bad",
19
+ )
20
+ error = described_class.new("Failed", issues: [issue])
21
+ expect(error.issues.length).to eq(1)
22
+ expect(error.issues.first.code).to eq("E-001")
23
+ end
24
+
25
+ it "defaults issues to empty array" do
26
+ error = described_class.new("Failed")
27
+ expect(error.issues).to eq([])
28
+ end
29
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/validation_framework"
5
+
6
+ RSpec.describe Lutaml::Model::Validation, ".validate / .validate!" do
7
+ let(:registry) { described_class.new_registry }
8
+
9
+ let(:rule_a_class) do
10
+ Class.new(Lutaml::Model::Validation::Rule) do
11
+ def code = "E2E-001"
12
+ def severity = "error"
13
+
14
+ def check(context)
15
+ context[:items].empty? ? [issue("No items found")] : []
16
+ end
17
+ end
18
+ end
19
+
20
+ let(:rule_b_class) do
21
+ Class.new(Lutaml::Model::Validation::Rule) do
22
+ def code = "E2E-002"
23
+ def severity = "warning"
24
+
25
+ def check(context)
26
+ context[:items].length > 100 ? [issue("Too many items")] : []
27
+ end
28
+ end
29
+ end
30
+
31
+ before do
32
+ registry.register(rule_a_class)
33
+ registry.register(rule_b_class)
34
+ end
35
+
36
+ describe ".validate" do
37
+ it "finds no issues with valid data" do
38
+ issues = described_class.validate({ items: (1..50).to_a }, registry)
39
+ expect(issues).to be_empty
40
+ end
41
+
42
+ it "finds errors for empty items" do
43
+ issues = described_class.validate({ items: [] }, registry)
44
+ expect(issues.length).to eq(1)
45
+ expect(issues.first.code).to eq("E2E-001")
46
+ expect(issues.first).to be_error
47
+ end
48
+
49
+ it "finds warnings for too many items" do
50
+ issues = described_class.validate({ items: (1..101).to_a }, registry)
51
+ expect(issues.length).to eq(1)
52
+ expect(issues.first.code).to eq("E2E-002")
53
+ expect(issues.first).to be_warning
54
+ end
55
+
56
+ it "skips inapplicable rules" do
57
+ skip_rule = Class.new(Lutaml::Model::Validation::Rule) do
58
+ def code = "SKIP"
59
+ def applicable?(_ctx) = false
60
+ def check(_ctx) = [issue("should not appear")]
61
+ end
62
+ registry.register(skip_rule)
63
+ issues = described_class.validate({ items: [1] }, registry)
64
+ expect(issues).to be_empty
65
+ end
66
+
67
+ it "does not crash with contexts that support add_error" do
68
+ Lutaml::Model::Validation::Context.new
69
+ # Use plain hash for data — rules expect [:items]
70
+ issues = described_class.validate({ items: [] }, registry)
71
+ expect(issues.length).to eq(1)
72
+ # Plain hash doesn't accumulate, but validate still works
73
+ end
74
+ end
75
+
76
+ describe ".validate!" do
77
+ it "raises on errors" do
78
+ expect do
79
+ described_class.validate!({ items: [] }, registry)
80
+ end.to raise_error(Lutaml::Model::Validation::ValidationError, /E2E-001/)
81
+ end
82
+
83
+ it "exposes issues on ValidationError" do
84
+ described_class.validate!({ items: [] }, registry)
85
+ rescue Lutaml::Model::Validation::ValidationError => e
86
+ expect(e.issues.length).to eq(1)
87
+ expect(e.issues.first.code).to eq("E2E-001")
88
+ end
89
+
90
+ it "does not raise when only warnings" do
91
+ expect do
92
+ described_class.validate!({ items: (1..101).to_a }, registry)
93
+ end.not_to raise_error
94
+ end
95
+
96
+ it "does not raise when no issues" do
97
+ expect do
98
+ described_class.validate!({ items: (1..50).to_a }, registry)
99
+ end.not_to raise_error
100
+ end
101
+ end
102
+
103
+ describe ".new_registry" do
104
+ it "returns a fresh Registry instance" do
105
+ reg = described_class.new_registry
106
+ expect(reg).to be_a(Lutaml::Model::Validation::Registry)
107
+ expect(reg.size).to eq(0)
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,157 @@
1
+ # spec/lutaml/xml/content_model_validation_spec.rb
2
+
3
+ require "spec_helper"
4
+ require_relative "../../../lib/lutaml/model"
5
+
6
+ RSpec.describe "Content model validation" do
7
+ describe "OrderedContentMappingError" do
8
+ it "raises when ordered + map_content" do
9
+ expect do
10
+ Class.new(Lutaml::Model::Serializable) do
11
+ attribute :content, :string
12
+
13
+ xml do
14
+ element "test"
15
+ ordered
16
+ map_content to: :content
17
+ end
18
+
19
+ def self.name
20
+ "OrderedWithContentTest"
21
+ end
22
+ end
23
+ end.to raise_error(
24
+ Lutaml::Model::OrderedContentMappingError,
25
+ /Element-only content model.*does not support `map_content`/,
26
+ )
27
+ end
28
+
29
+ it "raises when root with ordered: true and map_content" do
30
+ expect do
31
+ Class.new(Lutaml::Model::Serializable) do
32
+ attribute :content, :string
33
+
34
+ xml do
35
+ root "test", ordered: true
36
+ map_content to: :content
37
+ end
38
+
39
+ def self.name
40
+ "RootOrderedWithContentTest"
41
+ end
42
+ end
43
+ end.to raise_error(Lutaml::Model::OrderedContentMappingError)
44
+ end
45
+
46
+ it "does not raise when ordered without map_content" do
47
+ expect do
48
+ Class.new(Lutaml::Model::Serializable) do
49
+ attribute :child, :string
50
+
51
+ xml do
52
+ element "test"
53
+ ordered
54
+ map_element "child", to: :child
55
+ end
56
+
57
+ def self.name
58
+ "OrderedWithoutContentTest"
59
+ end
60
+ end
61
+ end.not_to raise_error
62
+ end
63
+
64
+ it "does not raise when mixed_content with map_content (collection)" do
65
+ expect do
66
+ Class.new(Lutaml::Model::Serializable) do
67
+ attribute :content, :string, collection: true
68
+
69
+ xml do
70
+ element "test"
71
+ mixed_content
72
+ map_content to: :content
73
+ end
74
+
75
+ def self.name
76
+ "MixedWithContentTest"
77
+ end
78
+ end
79
+ end.not_to raise_error
80
+ end
81
+
82
+ it "does not raise when root with mixed: true and map_content (collection)" do
83
+ expect do
84
+ Class.new(Lutaml::Model::Serializable) do
85
+ attribute :content, :string, collection: true
86
+
87
+ xml do
88
+ root "test", mixed: true
89
+ map_content to: :content
90
+ end
91
+
92
+ def self.name
93
+ "RootMixedWithContentTest"
94
+ end
95
+ end
96
+ end.not_to raise_error
97
+ end
98
+
99
+ it "does not raise when default (no ordered/mixed) with map_content" do
100
+ expect do
101
+ Class.new(Lutaml::Model::Serializable) do
102
+ attribute :content, :string
103
+
104
+ xml do
105
+ element "test"
106
+ map_content to: :content
107
+ end
108
+
109
+ def self.name
110
+ "DefaultWithContentTest"
111
+ end
112
+ end
113
+ end.not_to raise_error
114
+ end
115
+ end
116
+
117
+ describe "MixedContentCollectionError" do
118
+ it "raises when mixed_content + map_content to non-collection attribute" do
119
+ expect do
120
+ Class.new(Lutaml::Model::Serializable) do
121
+ attribute :content, :string
122
+
123
+ xml do
124
+ element "test"
125
+ mixed_content
126
+ map_content to: :content
127
+ end
128
+
129
+ def self.name
130
+ "MixedContentNonCollectionTest"
131
+ end
132
+ end
133
+ end.to raise_error(
134
+ Lutaml::Model::MixedContentCollectionError,
135
+ /Mixed content requires.*to be a string collection/,
136
+ )
137
+ end
138
+
139
+ it "does not raise when mixed_content + map_content to collection attribute" do
140
+ expect do
141
+ Class.new(Lutaml::Model::Serializable) do
142
+ attribute :content, :string, collection: true
143
+
144
+ xml do
145
+ element "test"
146
+ mixed_content
147
+ map_content to: :content
148
+ end
149
+
150
+ def self.name
151
+ "MixedContentCollectionTest"
152
+ end
153
+ end
154
+ end.not_to raise_error
155
+ end
156
+ end
157
+ end
@@ -755,15 +755,20 @@ RSpec.describe Lutaml::Xml::Mapping do
755
755
  end
756
756
 
757
757
  it "element_order should be correct" do
758
- expect(parsed.element_order).to eq(expected_order)
758
+ # Filter out whitespace-only text nodes for comparison with expected_order
759
+ non_ws_order = parsed.element_order.reject do |e|
760
+ e.type == "Text" && e.text_content&.strip&.empty?
761
+ end
762
+ expect(non_ws_order).to eq(expected_order)
759
763
  end
760
764
 
761
- it "element_order omits whitespace-only text nodes" do
762
- # Moxml's Nokogiri adapter filters whitespace-only text nodes between
763
- # elements, matching the behavior of Oga/Ox/Rexml adapters. This test
764
- # locks in that behavior so a regression is detected if moxml changes.
765
- text_entries = parsed.element_order.select { |e| e.type == "Text" }
766
- expect(text_entries).to be_empty
765
+ it "element_order includes whitespace-only text nodes" do
766
+ # Whitespace-only text nodes between elements are preserved in element_order
767
+ # for mixed-content round-trip fidelity.
768
+ text_entries = parsed.element_order.select do |e|
769
+ e.type == "Text" && e.text_content&.strip&.empty?
770
+ end
771
+ expect(text_entries).not_to be_empty
767
772
  end
768
773
 
769
774
  it "to_xml should be correct" do