lutaml-model 0.8.4 → 0.8.6

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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +5 -0
  3. data/.rubocop.yml +18 -0
  4. data/.rubocop_todo.yml +91 -22
  5. data/Gemfile +2 -0
  6. data/README.adoc +114 -2
  7. data/docs/_guides/index.adoc +18 -0
  8. data/docs/_guides/jsonld-serialization.adoc +217 -0
  9. data/docs/_guides/rdf-serialization.adoc +344 -0
  10. data/docs/_guides/turtle-serialization.adoc +224 -0
  11. data/docs/_migrations/0-8-0-namespace-restructuring.adoc +90 -0
  12. data/docs/_pages/serialization_adapters.adoc +31 -0
  13. data/docs/_references/index.adoc +1 -0
  14. data/docs/_references/rdf-namespaces.adoc +243 -0
  15. data/docs/index.adoc +3 -2
  16. data/lib/lutaml/jsonld/adapter.rb +23 -0
  17. data/lib/lutaml/jsonld/context.rb +69 -0
  18. data/lib/lutaml/jsonld/term_definition.rb +39 -0
  19. data/lib/lutaml/jsonld/transform.rb +174 -0
  20. data/lib/lutaml/jsonld.rb +23 -0
  21. data/lib/lutaml/model/format_registry.rb +10 -1
  22. data/lib/lutaml/model/serialize/format_conversion.rb +17 -1
  23. data/lib/lutaml/model/version.rb +1 -1
  24. data/lib/lutaml/model.rb +6 -0
  25. data/lib/lutaml/rdf/error.rb +7 -0
  26. data/lib/lutaml/rdf/iri.rb +44 -0
  27. data/lib/lutaml/rdf/language_tagged.rb +11 -0
  28. data/lib/lutaml/rdf/literal.rb +62 -0
  29. data/lib/lutaml/rdf/mapping.rb +71 -0
  30. data/lib/lutaml/rdf/mapping_rule.rb +35 -0
  31. data/lib/lutaml/rdf/member_rule.rb +13 -0
  32. data/lib/lutaml/rdf/namespace.rb +58 -0
  33. data/lib/lutaml/rdf/namespace_set.rb +69 -0
  34. data/lib/lutaml/rdf/namespaces/dcterms_namespace.rb +12 -0
  35. data/lib/lutaml/rdf/namespaces/owl_namespace.rb +12 -0
  36. data/lib/lutaml/rdf/namespaces/rdf_namespace.rb +14 -0
  37. data/lib/lutaml/rdf/namespaces/rdfs_namespace.rb +12 -0
  38. data/lib/lutaml/rdf/namespaces/skos_namespace.rb +12 -0
  39. data/lib/lutaml/rdf/namespaces/xsd_namespace.rb +12 -0
  40. data/lib/lutaml/rdf/namespaces.rb +14 -0
  41. data/lib/lutaml/rdf/transform.rb +36 -0
  42. data/lib/lutaml/rdf.rb +19 -0
  43. data/lib/lutaml/turtle/adapter.rb +35 -0
  44. data/lib/lutaml/turtle/mapping.rb +7 -0
  45. data/lib/lutaml/turtle/transform.rb +158 -0
  46. data/lib/lutaml/turtle.rb +22 -0
  47. data/lib/lutaml/xml/adapter/adapter_helpers.rb +1 -42
  48. data/lib/lutaml/xml/adapter/base_adapter.rb +48 -458
  49. data/lib/lutaml/xml/adapter/namespace_data.rb +0 -17
  50. data/lib/lutaml/xml/adapter/namespace_uri_collector.rb +71 -0
  51. data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +5 -1110
  52. data/lib/lutaml/xml/adapter/oga_adapter.rb +6 -846
  53. data/lib/lutaml/xml/adapter/ox_adapter.rb +7 -884
  54. data/lib/lutaml/xml/adapter/plan_based_builder.rb +929 -0
  55. data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -864
  56. data/lib/lutaml/xml/adapter/xml_parser.rb +86 -0
  57. data/lib/lutaml/xml/adapter/xml_serializer.rb +291 -0
  58. data/lib/lutaml/xml/adapter.rb +0 -1
  59. data/lib/lutaml/xml/adapter_element.rb +7 -1
  60. data/lib/lutaml/xml/builder/base.rb +0 -1
  61. data/lib/lutaml/xml/data_model.rb +9 -1
  62. data/lib/lutaml/xml/document.rb +3 -1
  63. data/lib/lutaml/xml/element.rb +13 -10
  64. data/lib/lutaml/xml/serialization/format_conversion.rb +19 -42
  65. data/lib/lutaml/xml/serialization/instance_methods.rb +26 -35
  66. data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +34 -55
  67. data/lib/lutaml/xml/transformation/rule_applier.rb +1 -1
  68. data/lib/lutaml/xml/xml_element.rb +24 -20
  69. data/spec/lutaml/integration/edge_cases_spec.rb +109 -0
  70. data/spec/lutaml/integration/multi_format_spec.rb +106 -0
  71. data/spec/lutaml/integration/round_trip_spec.rb +170 -0
  72. data/spec/lutaml/jsonld/adapter_spec.rb +46 -0
  73. data/spec/lutaml/jsonld/context_spec.rb +114 -0
  74. data/spec/lutaml/jsonld/term_definition_spec.rb +55 -0
  75. data/spec/lutaml/jsonld/transform_spec.rb +211 -0
  76. data/spec/lutaml/rdf/graph_serialization_spec.rb +137 -0
  77. data/spec/lutaml/rdf/iri_spec.rb +73 -0
  78. data/spec/lutaml/rdf/literal_spec.rb +98 -0
  79. data/spec/lutaml/rdf/mapping_spec.rb +164 -0
  80. data/spec/lutaml/rdf/member_rule_spec.rb +17 -0
  81. data/spec/lutaml/rdf/namespace_set_spec.rb +115 -0
  82. data/spec/lutaml/rdf/namespace_spec.rb +241 -0
  83. data/spec/lutaml/rdf/rdf_transform_spec.rb +82 -0
  84. data/spec/lutaml/turtle/adapter_spec.rb +47 -0
  85. data/spec/lutaml/turtle/mapping_spec.rb +123 -0
  86. data/spec/lutaml/turtle/transform_spec.rb +273 -0
  87. data/spec/lutaml/xml/adapter/base_adapter_regression_spec.rb +151 -0
  88. data/spec/lutaml/xml/adapter/order_spec.rb +150 -0
  89. data/spec/lutaml/xml/clear_parse_state_spec.rb +139 -0
  90. data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +0 -2
  91. data/spec/lutaml/xml/schema/compiler_spec.rb +75 -69
  92. data/spec/lutaml/xml/transformation/custom_method_wrapper_spec.rb +213 -14
  93. metadata +58 -3
  94. data/lib/lutaml/xml/adapter/xml_serialization.rb +0 -145
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "#clear_xml_parse_state!" do
6
+ before do
7
+ Lutaml::Model::GlobalContext.clear_caches
8
+ Lutaml::Model::TransformationRegistry.instance.clear
9
+ Lutaml::Model::GlobalRegister.instance.reset
10
+ end
11
+
12
+ let(:ns_class) do
13
+ Class.new(Lutaml::Xml::Namespace) do
14
+ uri "http://example.com/items"
15
+ prefix_default "a"
16
+ end
17
+ end
18
+
19
+ let(:model_class) do
20
+ ns = ns_class
21
+ Class.new(Lutaml::Model::Serializable) do
22
+ attribute :item, :string
23
+ attribute :count, :integer
24
+
25
+ xml do
26
+ root "root"
27
+ namespace ns
28
+ map_element "item", to: :item
29
+ map_attribute "count", to: :count
30
+ end
31
+ end
32
+ end
33
+
34
+ let(:xml_with_ns) do
35
+ <<~XML
36
+ <root xmlns:xyz="http://example.com/items" count="3">
37
+ <xyz:item>hello</xyz:item>
38
+ </root>
39
+ XML
40
+ end
41
+
42
+ describe "clearing parse state" do
43
+ it "clears import_declaration_plan set by :eager mode" do
44
+ model = model_class.from_xml(xml_with_ns, import_declaration_plan: :eager)
45
+ expect(model.import_declaration_plan).to be_a(Lutaml::Xml::DeclarationPlan)
46
+ model.clear_xml_parse_state!
47
+ expect(model.import_declaration_plan).to be_nil
48
+ end
49
+
50
+ it "clears pending_plan_root_element set by :lazy mode" do
51
+ model = model_class.from_xml(xml_with_ns)
52
+ expect(model.pending_plan_root_element).not_to be_nil
53
+ model.clear_xml_parse_state!
54
+ expect(model.pending_plan_root_element).to be_nil
55
+ end
56
+ end
57
+
58
+ describe "return value" do
59
+ it "returns self for chaining" do
60
+ model = model_class.from_xml(xml_with_ns)
61
+ expect(model.clear_xml_parse_state!).to equal(model)
62
+ end
63
+ end
64
+
65
+ describe "idempotency" do
66
+ it "is safe to call multiple times" do
67
+ model = model_class.from_xml(xml_with_ns)
68
+ expect { 3.times { model.clear_xml_parse_state! } }.not_to raise_error
69
+ end
70
+
71
+ it "is safe on freshly created instances with no parse state" do
72
+ model = model_class.new(item: "test", count: 1)
73
+ expect { model.clear_xml_parse_state! }.not_to raise_error
74
+ end
75
+ end
76
+
77
+ describe "user-facing attributes" do
78
+ it "does not clear element_order" do
79
+ model = model_class.from_xml(xml_with_ns)
80
+ original_order = model.element_order
81
+ model.clear_xml_parse_state!
82
+ expect(model.element_order).to eq(original_order)
83
+ end
84
+
85
+ it "does not clear encoding" do
86
+ model = model_class.from_xml(xml_with_ns, encoding: "UTF-8")
87
+ model.clear_xml_parse_state!
88
+ expect(model.encoding).to eq("UTF-8")
89
+ end
90
+
91
+ it "does not clear model attributes" do
92
+ model = model_class.from_xml(xml_with_ns)
93
+ expect(model.item).to eq("hello")
94
+ expect(model.count).to eq(3)
95
+ model.clear_xml_parse_state!
96
+ expect(model.item).to eq("hello")
97
+ expect(model.count).to eq(3)
98
+ end
99
+ end
100
+
101
+ describe "re-serialization after clearing" do
102
+ it "allows to_xml after clearing parse state" do
103
+ model = model_class.from_xml(xml_with_ns, import_declaration_plan: :eager)
104
+ model.clear_xml_parse_state!
105
+ result = model.to_xml
106
+ expect(result).to include("hello")
107
+ end
108
+
109
+ it "allows to_xml after clearing lazy parse state" do
110
+ model = model_class.from_xml(xml_with_ns)
111
+ model.clear_xml_parse_state!
112
+ result = model.to_xml
113
+ expect(result).to include("hello")
114
+ end
115
+
116
+ it "reflects model modifications after clearing" do
117
+ model = model_class.from_xml(xml_with_ns)
118
+ model.item = "modified"
119
+ model.clear_xml_parse_state!
120
+ result = model.to_xml
121
+ expect(result).to include("modified")
122
+ end
123
+ end
124
+
125
+ describe "Uniword parse-modify-clear-reserialize workflow" do
126
+ it "clears stale namespace state across multiple parts" do
127
+ part1 = model_class.from_xml(xml_with_ns, import_declaration_plan: :eager)
128
+ part2 = model_class.from_xml(xml_with_ns, import_declaration_plan: :eager)
129
+
130
+ part1.item = "reconciled"
131
+ part2.item = "also reconciled"
132
+
133
+ [part1, part2].each(&:clear_xml_parse_state!)
134
+
135
+ expect(part1.to_xml).to include("reconciled")
136
+ expect(part2.to_xml).to include("also reconciled")
137
+ end
138
+ end
139
+ end
@@ -375,7 +375,6 @@ RSpec.describe "Doubly-defined namespace prefixes" do
375
375
  it "builds plan immediately during parsing" do
376
376
  model = model_class.from_xml(prefixed_xml,
377
377
  import_declaration_plan: :eager)
378
- expect(model.pending_namespace_data).to be_nil
379
378
  expect(model.import_declaration_plan).to be_a(Lutaml::Xml::DeclarationPlan)
380
379
  end
381
380
  end
@@ -384,7 +383,6 @@ RSpec.describe "Doubly-defined namespace prefixes" do
384
383
  it "never builds plan" do
385
384
  model = model_class.from_xml(prefixed_xml,
386
385
  import_declaration_plan: :skip)
387
- expect(model.pending_namespace_data).to be_nil
388
386
  expect(model.import_declaration_plan).to be_nil
389
387
  end
390
388
  end
@@ -51,18 +51,14 @@ RSpec.describe Lutaml::Model::Schema::XmlCompiler do
51
51
  context "with valid xml schema, it generates the models" do
52
52
  before do
53
53
  described_class.to_models(schema, output_dir: dir,
54
- create_files: true, module_namespace: nil)
55
- Dir.each_child(dir) do |child|
56
- require_relative File.expand_path("#{dir}/#{child}")
57
- end
54
+ create_files: true, module_namespace: "MathTestSpec")
55
+ require File.join(dir, "mathtestspec_registry.rb")
56
+ MathTestSpec.register_all
58
57
  end
59
58
 
60
59
  after do
61
60
  FileUtils.rm_rf(dir)
62
- # Clean up dynamically generated classes to prevent test pollution
63
- Object.send(:remove_const, :CTMathTest) if defined?(CTMathTest)
64
- Object.send(:remove_const, :StInteger255) if defined?(StInteger255)
65
- Object.send(:remove_const, :Long) if defined?(Long)
61
+ Object.send(:remove_const, :MathTestSpec) if defined?(MathTestSpec)
66
62
  end
67
63
 
68
64
  let(:dir) { Dir.mktmpdir }
@@ -87,40 +83,30 @@ RSpec.describe Lutaml::Model::Schema::XmlCompiler do
87
83
  INVALID_XML_EXAMPLE
88
84
  end
89
85
 
90
- it "validates if the files exist in the directory" do
91
- expect(File).to exist("#{dir}/ct_math_test.rb")
92
- expect(File).to exist("#{dir}/st_integer255.rb")
93
- expect(File).to exist("#{dir}/long.rb")
86
+ it "validates if the files exist in the module directory" do
87
+ module_dir = File.join(dir, "mathtestspec")
88
+ expect(File).to exist("#{module_dir}/ct_math_test.rb")
89
+ expect(File).to exist("#{module_dir}/st_integer255.rb")
90
+ expect(File).to exist("#{module_dir}/long.rb")
94
91
  end
95
92
 
96
93
  it "validates if the CTMathTest class is loaded" do
97
- expect(defined?(CTMathTest)).to eq("constant")
94
+ expect(defined?(MathTestSpec::CTMathTest)).to eq("constant")
98
95
  end
99
96
 
100
97
  it "creates the model files, requires them, and tests them with valid and invalid xml" do
101
- expect(CTMathTest.from_xml(valid_value_xml_example).to_xml).to be_xml_equivalent_to(valid_value_xml_example)
98
+ expect(MathTestSpec::CTMathTest.from_xml(valid_value_xml_example).to_xml).to be_xml_equivalent_to(valid_value_xml_example)
102
99
  end
103
100
 
104
101
  it "raises error when processing invalid example" do
105
102
  expect do
106
- CTMathTest.from_xml(invalid_value_xml_example)
103
+ MathTestSpec::CTMathTest.from_xml(invalid_value_xml_example)
107
104
  end.to raise_error(Lutaml::Model::Type::MinBoundError)
108
105
  end
109
106
  end
110
107
 
111
108
  context "when processing examples from classes/files generated by valid xml schema" do
112
- before do
113
- Dir.mktmpdir do |dir|
114
- described_class.to_models(
115
- File.read("spec/fixtures/xml/math_document_schema.xsd"),
116
- output_dir: dir,
117
- create_files: true,
118
- module_namespace: nil,
119
- )
120
- require_relative "#{dir}/math_document"
121
- end
122
- end
123
-
109
+ let!(:math_doc_dir) { Dir.mktmpdir }
124
110
  let(:valid_example) do
125
111
  File.read("spec/fixtures/xml/valid_math_document.xml")
126
112
  end
@@ -128,42 +114,39 @@ RSpec.describe Lutaml::Model::Schema::XmlCompiler do
128
114
  File.read("spec/fixtures/xml/invalid_math_document.xml")
129
115
  end
130
116
 
117
+ before do
118
+ described_class.to_models(
119
+ File.read("spec/fixtures/xml/math_document_schema.xsd"),
120
+ output_dir: math_doc_dir,
121
+ create_files: true,
122
+ module_namespace: "MathDocSpec",
123
+ )
124
+ require File.join(math_doc_dir, "mathdocspec_registry.rb")
125
+ MathDocSpec.register_all
126
+ end
127
+
128
+ after do
129
+ FileUtils.rm_rf(math_doc_dir)
130
+ Object.send(:remove_const, :MathDocSpec) if defined?(MathDocSpec)
131
+ end
132
+
131
133
  it "does not raise error with valid example and creates files" do
132
- expect(defined?(MathDocument)).to eq("constant")
133
- parsed = MathDocument.from_xml(valid_example)
134
+ expect(defined?(MathDocSpec::MathDocument)).to eq("constant")
135
+ parsed = MathDocSpec::MathDocument.from_xml(valid_example)
134
136
  expect(parsed.title).to eql("Example Title")
135
137
  expect(parsed.ipv4_address).to eql("192.168.1.1")
136
138
  expect(parsed.to_xml).to be_xml_equivalent_to(valid_example)
137
139
  end
138
140
 
139
141
  it "raises PatternNotMatchedError" do
140
- expect(defined?(MathDocument)).to eq("constant")
141
- expect { MathDocument.from_xml(invalid_example) }
142
+ expect(defined?(MathDocSpec::MathDocument)).to eq("constant")
143
+ expect { MathDocSpec::MathDocument.from_xml(invalid_example) }
142
144
  .to raise_error(Lutaml::Model::Type::PatternNotMatchedError)
143
145
  end
144
146
  end
145
147
 
146
148
  context "when processing example from lutaml-model#260" do
147
- before do
148
- # Remove any existing Address constant to prevent test pollution
149
- Object.send(:remove_const, :Address) if defined?(Address)
150
-
151
- Dir.mktmpdir do |dir|
152
- described_class.to_models(
153
- File.read("spec/fixtures/xml/address_example_260.xsd"),
154
- output_dir: dir,
155
- create_files: true,
156
- module_namespace: nil,
157
- )
158
- load "#{dir}/address.rb"
159
- end
160
- end
161
-
162
- after do
163
- # Clean up the Address constant to prevent test pollution
164
- Object.send(:remove_const, :Address) if defined?(Address)
165
- end
166
-
149
+ let!(:address_dir) { Dir.mktmpdir }
167
150
  let(:address) do
168
151
  <<~ADD
169
152
  <Address>
@@ -174,43 +157,66 @@ RSpec.describe Lutaml::Model::Schema::XmlCompiler do
174
157
  ADD
175
158
  end
176
159
 
177
- it "matches parsed xml with input" do
178
- expect(defined?(Address)).to eq("constant")
179
- expect(Address.from_xml(address).to_xml).to be_xml_equivalent_to(address)
160
+ before do
161
+ described_class.to_models(
162
+ File.read("spec/fixtures/xml/address_example_260.xsd"),
163
+ output_dir: address_dir,
164
+ create_files: true,
165
+ module_namespace: "CompilerSpec260",
166
+ )
167
+ require File.join(address_dir, "compilerspec260_registry.rb")
168
+ CompilerSpec260.register_all
180
169
  end
181
- end
182
170
 
183
- context "when processing example from files generated by schema -> product_catalog.xsd" do
184
- before do
185
- Dir.mktmpdir do |dir|
186
- described_class.to_models(
187
- File.read("spec/fixtures/xml/product_catalog.xsd"),
188
- output_dir: dir,
189
- create_files: true,
190
- module_namespace: nil,
191
- )
192
- require_relative "#{dir}/product_catalog"
171
+ after do
172
+ FileUtils.rm_rf(address_dir)
173
+ if defined?(CompilerSpec260)
174
+ Object.send(:remove_const,
175
+ :CompilerSpec260)
193
176
  end
194
177
  end
195
178
 
179
+ it "matches parsed xml with input" do
180
+ expect(defined?(CompilerSpec260::Address)).to eq("constant")
181
+ expect(CompilerSpec260::Address.from_xml(address).to_xml).to be_xml_equivalent_to(address)
182
+ end
183
+ end
184
+
185
+ context "when processing example from files generated by schema -> product_catalog.xsd" do
186
+ let!(:catalog_dir) { Dir.mktmpdir }
196
187
  let(:product_catalog) do
197
188
  File.read("spec/fixtures/xml/examples/valid_catalog.xml")
198
189
  end
199
-
200
190
  let(:nested_category) do
201
191
  File.read("spec/fixtures/xml/examples/nested_categories.xml")
202
192
  end
203
193
 
194
+ before do
195
+ described_class.to_models(
196
+ File.read("spec/fixtures/xml/product_catalog.xsd"),
197
+ output_dir: catalog_dir,
198
+ create_files: true,
199
+ module_namespace: "CatalogSpec",
200
+ )
201
+ require File.join(catalog_dir, "catalogspec_registry.rb")
202
+ CatalogSpec.register_all
203
+ end
204
+
205
+ after do
206
+ FileUtils.rm_rf(catalog_dir)
207
+ Object.send(:remove_const, :CatalogSpec) if defined?(CatalogSpec)
208
+ end
209
+
204
210
  it "confirms the ProductCatalog class is required" do
205
- expect(defined?(ProductCatalog)).to eq("constant")
211
+ expect(defined?(CatalogSpec::ProductCatalog)).to eq("constant")
206
212
  end
207
213
 
208
214
  it "confirms that the from_xml and to_xml methods successfully handles xml for valid_catalog.xml" do
209
- expect(ProductCatalog.from_xml(product_catalog).to_xml).to be_a(String)
215
+ expect(CatalogSpec::ProductCatalog.from_xml(product_catalog).to_xml).to be_a(String)
210
216
  end
211
217
 
212
218
  it "confirms that the from_xml and to_xml methods successfully handles xml for nested_categories.xml" do
213
- expect(ProductCatalog.from_xml(nested_category).to_xml).to be_a(String)
219
+ expect(CatalogSpec::ProductCatalog.from_xml(nested_category).to_xml).to be_a(String)
214
220
  end
215
221
  end
216
222
 
@@ -4,30 +4,229 @@ require "spec_helper"
4
4
  require "lutaml/xml"
5
5
 
6
6
  RSpec.describe Lutaml::Xml::CustomMethodWrapper do
7
- describe "#add_element" do
8
- it "parses XML fragments through Moxml when Opal is active" do
9
- parent = Lutaml::Xml::DataModel::XmlElement.new("parent")
10
- wrapper = described_class.new(parent, nil)
7
+ let(:parent) { Lutaml::Xml::DataModel::XmlElement.new("parent") }
8
+ let(:wrapper) { described_class.new(parent) }
9
+
10
+ describe "#create_element" do
11
+ it "returns a new XmlElement with the given name" do
12
+ el = wrapper.create_element("foo")
13
+ expect(el).to be_a(Lutaml::Xml::DataModel::XmlElement)
14
+ expect(el.name).to eq("foo")
15
+ end
16
+
17
+ it "does not add the element to any parent" do
18
+ wrapper.create_element("orphan")
19
+ expect(parent.children).to be_empty
20
+ end
21
+ end
11
22
 
12
- allow(Lutaml::Model::RuntimeCompatibility).to receive(:opal?)
13
- .and_return(true)
23
+ describe "#add_element" do
24
+ it "adds an XmlElement child with single-argument form" do
25
+ child = Lutaml::Xml::DataModel::XmlElement.new("child")
26
+ result = wrapper.add_element(child)
27
+ expect(result).to eq(child)
28
+ expect(parent.children).to eq([child])
29
+ end
14
30
 
15
- wrapper.add_element(
16
- parent,
17
- "<a title=\"copyright\">one</a><b>two</b>",
18
- )
31
+ it "adds an XmlElement child with two-argument form" do
32
+ child = Lutaml::Xml::DataModel::XmlElement.new("child")
33
+ wrapper.add_element(parent, child)
34
+ expect(parent.children).to eq([child])
35
+ end
19
36
 
20
- expect(parent.raw_content).to be_nil
37
+ it "parses XML string fragments via Moxml" do
38
+ wrapper.add_element(parent, "<a title=\"copyright\">one</a><b>two</b>")
21
39
  expect(parent.children.map(&:name)).to eq(%w[a b])
22
40
  expect(parent.children[0].attributes.first.value).to eq("copyright")
23
41
  expect(parent.children[0].text_content).to eq("one")
24
42
  expect(parent.children[1].text_content).to eq("two")
25
43
  end
44
+
45
+ it "raises TypeError for unsupported types" do
46
+ foreign = instance_double(Object)
47
+ expect do
48
+ wrapper.add_element(parent, foreign)
49
+ end.to raise_error(TypeError,
50
+ /add_element expects a String or XmlElement/)
51
+ end
52
+
53
+ it "parses nested XML elements recursively" do
54
+ wrapper.add_element(parent, "<outer><inner>text</inner></outer>")
55
+ outer = parent.children.first
56
+ expect(outer.name).to eq("outer")
57
+ expect(outer.children.first.name).to eq("inner")
58
+ expect(outer.children.first.text_content).to eq("text")
59
+ end
60
+ end
61
+
62
+ describe "#add_text" do
63
+ it "sets text_content on the given element" do
64
+ el = Lutaml::Xml::DataModel::XmlElement.new("p")
65
+ wrapper.add_text(el, "hello")
66
+ expect(el.text_content).to eq("hello")
67
+ end
68
+
69
+ it "sets text on current context when element is nil" do
70
+ wrapper.add_text(nil, "fallback")
71
+ expect(parent.text_content).to eq("fallback")
72
+ end
73
+
74
+ it "sets text on current context when wrapper itself is passed" do
75
+ wrapper.add_text(wrapper, "via-doc")
76
+ expect(parent.text_content).to eq("via-doc")
77
+ end
78
+ end
79
+
80
+ describe "#add_attribute" do
81
+ it "adds an attribute to the element" do
82
+ el = Lutaml::Xml::DataModel::XmlElement.new("node")
83
+ wrapper.add_attribute(el, "id", "42")
84
+ expect(el.attributes.size).to eq(1)
85
+ expect(el.attributes.first.name).to eq("id")
86
+ expect(el.attributes.first.value).to eq("42")
87
+ end
88
+
89
+ it "coerces name and value to strings" do
90
+ el = Lutaml::Xml::DataModel::XmlElement.new("node")
91
+ wrapper.add_attribute(el, :key, 123)
92
+ expect(el.attributes.first.name).to eq("key")
93
+ expect(el.attributes.first.value).to eq("123")
94
+ end
95
+ end
96
+
97
+ describe "#create_and_add_element" do
98
+ it "creates and adds element to current context" do
99
+ el = wrapper.create_and_add_element("item")
100
+ expect(el.name).to eq("item")
101
+ expect(parent.children).to eq([el])
102
+ end
103
+
104
+ it "creates element with attributes" do
105
+ el = wrapper.create_and_add_element("item", attributes: { id: "1" })
106
+ expect(el.attributes.first.name).to eq("id")
107
+ expect(el.attributes.first.value).to eq("1")
108
+ end
109
+
110
+ it "yields ElementWrapper in block form" do
111
+ wrapper.create_and_add_element("outer") do |w|
112
+ w.add_text(w, "content")
113
+ end
114
+ outer = parent.children.first
115
+ expect(outer.text_content).to eq("content")
116
+ end
117
+
118
+ it "supports nested create_and_add_element in block" do
119
+ wrapper.create_and_add_element("outer") do |w|
120
+ w.create_and_add_element("inner") do |iw|
121
+ iw.add_text(iw, "deep")
122
+ end
123
+ end
124
+ outer = parent.children.first
125
+ inner = outer.children.first
126
+ expect(inner.name).to eq("inner")
127
+ expect(inner.text_content).to eq("deep")
128
+ end
129
+
130
+ it "restores context after block exits" do
131
+ wrapper.create_and_add_element("outer") do |_w|
132
+ # context is now "outer"
133
+ end
134
+ # context should be back to parent
135
+ el = wrapper.create_and_add_element("after")
136
+ expect(parent.children).to include(el)
137
+ end
138
+ end
139
+
140
+ describe "#push_context / #pop_context" do
141
+ it "manages a stack of context elements" do
142
+ child = Lutaml::Xml::DataModel::XmlElement.new("child")
143
+ wrapper.push_context(child)
144
+ expect(wrapper.current_context).to eq(child)
145
+ wrapper.pop_context
146
+ expect(wrapper.current_context).to eq(parent)
147
+ end
148
+
149
+ it "does not pop below the root context" do
150
+ wrapper.pop_context
151
+ expect(wrapper.current_context).to eq(parent)
152
+ end
153
+ end
154
+
155
+ describe ".build_element" do
156
+ it "creates an element with attributes" do
157
+ el = described_class.build_element("tag", { a: "1", b: "2" })
158
+ expect(el.name).to eq("tag")
159
+ expect(el.attributes.map(&:name)).to eq(%w[a b])
160
+ end
161
+
162
+ it "handles nil attributes hash" do
163
+ el = described_class.build_element("tag", nil)
164
+ expect(el.attributes).to be_empty
165
+ end
166
+ end
167
+
168
+ describe Lutaml::Xml::CustomMethodWrapper::ElementWrapper do
169
+ let(:element) { Lutaml::Xml::DataModel::XmlElement.new("el") }
170
+ let(:ew) { described_class.new(element) }
171
+
172
+ describe "#add_text" do
173
+ it "sets text and cdata flag" do
174
+ ew.add_text(ew, "data", cdata: true)
175
+ expect(element.text_content).to eq("data")
176
+ expect(element.cdata).to be true
177
+ end
178
+
179
+ it "handles cdata as hash format" do
180
+ ew.add_text(ew, "data", cdata: { cdata: true })
181
+ expect(element.cdata).to be true
182
+ end
183
+
184
+ it "defaults cdata to false" do
185
+ ew.add_text(ew, "data")
186
+ expect(element.cdata).to be false
187
+ end
188
+ end
189
+
190
+ describe "#create_and_add_element" do
191
+ it "adds child to wrapped element" do
192
+ child = ew.create_and_add_element("sub")
193
+ expect(element.children).to eq([child])
194
+ end
195
+
196
+ it "yields nested wrapper in block form" do
197
+ ew.create_and_add_element("sub") do |sub|
198
+ sub.add_text(sub, "text")
199
+ end
200
+ expect(element.children.first.text_content).to eq("text")
201
+ end
202
+ end
203
+ end
204
+
205
+ describe "XmlElement#add_child type guard" do
206
+ it "accepts XmlElement" do
207
+ child = Lutaml::Xml::DataModel::XmlElement.new("ok")
208
+ expect { parent.add_child(child) }.not_to raise_error
209
+ end
210
+
211
+ it "accepts String" do
212
+ expect { parent.add_child("text") }.not_to raise_error
213
+ end
214
+
215
+ it "accepts XmlComment" do
216
+ comment = Lutaml::Xml::DataModel::XmlComment.new("a comment")
217
+ expect { parent.add_child(comment) }.not_to raise_error
218
+ end
219
+
220
+ it "rejects foreign types" do
221
+ expect do
222
+ parent.add_child(42)
223
+ end.to raise_error(TypeError, /XmlElement#add_child expects/)
224
+ end
26
225
  end
27
226
 
28
227
  describe "RuleApplier integration" do
29
228
  it "loads the wrapper through the public XML autoload path" do
30
- parent = Lutaml::Xml::DataModel::XmlElement.new("parent")
229
+ parent_el = Lutaml::Xml::DataModel::XmlElement.new("parent")
31
230
  model_class = Class.new do
32
231
  def custom_to_xml(_model, parent, wrapper)
33
232
  wrapper.add_text(parent, "ok")
@@ -40,9 +239,9 @@ RSpec.describe Lutaml::Xml::CustomMethodWrapper do
40
239
  public :apply_custom_method
41
240
  end.new
42
241
 
43
- applier.apply_custom_method(parent, rule, model_class, Object.new)
242
+ applier.apply_custom_method(parent_el, rule, model_class, Object.new)
44
243
 
45
- expect(parent.text_content).to eq("ok")
244
+ expect(parent_el.text_content).to eq("ok")
46
245
  end
47
246
  end
48
247
  end