lutaml-model 0.8.11 → 0.8.13

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 (149) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/opal.yml +31 -0
  3. data/.rspec-opal +5 -0
  4. data/.rubocop_todo.yml +45 -34
  5. data/README.adoc +126 -104
  6. data/RELEASE_NOTES.adoc +3 -3
  7. data/benchmark/quick_benchmark.rb +2 -2
  8. data/benchmark/serialization_benchmark.rb +4 -4
  9. data/docs/_guides/advanced-mapping.adoc +1 -1
  10. data/docs/_guides/character-encoding.adoc +3 -3
  11. data/docs/_guides/index.adoc +4 -0
  12. data/docs/_guides/missing-values-handling.adoc +6 -6
  13. data/docs/_guides/ooxml-examples.adoc +7 -7
  14. data/docs/_guides/opal.adoc +221 -0
  15. data/docs/_guides/value-transformations.adoc +7 -7
  16. data/docs/_guides/xml/namespace-presentation.adoc +1 -1
  17. data/docs/_guides/xml/namespace-semantics.adoc +15 -15
  18. data/docs/_guides/xml/type-namespaces.adoc +9 -9
  19. data/docs/_guides/xml-mapping.adoc +32 -26
  20. data/docs/_guides/xml-namespace-qualification.adoc +4 -4
  21. data/docs/_guides/xml-namespaces.adoc +2 -2
  22. data/docs/_guides/xml_mappings/04_xml_namespace_class.adoc +18 -18
  23. data/docs/_guides/xml_mappings/05_common_patterns.adoc +16 -16
  24. data/docs/_guides/xml_mappings/06_migration_guide.adoc +5 -5
  25. data/docs/_guides/xml_mappings/07_best_practices.adoc +13 -12
  26. data/docs/_migrations/0-8-0-namespace-restructuring.adoc +2 -2
  27. data/docs/_pages/attributes.adoc +2 -2
  28. data/docs/_pages/collections.adoc +26 -20
  29. data/docs/_pages/configuration.adoc +9 -4
  30. data/docs/_pages/consolidation-mapping.adoc +4 -4
  31. data/docs/_pages/importable_models.adoc +14 -13
  32. data/docs/_pages/index.adoc +1 -0
  33. data/docs/_pages/quick-start.adoc +1 -1
  34. data/docs/_pages/serialization_adapters.adoc +3 -2
  35. data/docs/_pages/value_types.adoc +10 -10
  36. data/docs/_references/custom_registers.adoc +7 -7
  37. data/docs/_references/format-independent-features.adoc +4 -4
  38. data/docs/_references/instance-serialization.adoc +1 -1
  39. data/docs/_references/parent-root-context.adoc +3 -3
  40. data/docs/_tutorials/basic-model-definition.adoc +1 -1
  41. data/docs/_tutorials/first-xml-serialization.adoc +4 -4
  42. data/docs/_tutorials/lutaml-xml-architecture.adoc +4 -4
  43. data/docs/_tutorials/validation-basics.adoc +1 -1
  44. data/docs/_tutorials/working-with-collections.adoc +2 -2
  45. data/docs/_tutorials/xml-namespaces-basics.adoc +1 -1
  46. data/docs/_tutorials/xml-schema-primer-style-guide.adoc +29 -29
  47. data/docs/cli_compare.adoc +1 -1
  48. data/docs/index.adoc +2 -1
  49. data/docs/namespace-management.adoc +14 -14
  50. data/lib/lutaml/hash_format/adapter/mapping.rb +2 -4
  51. data/lib/lutaml/json/adapter/mapping.rb +2 -4
  52. data/lib/lutaml/jsonl/adapter/mapping.rb +2 -4
  53. data/lib/lutaml/key_value/adapter/hash/mapping.rb +2 -4
  54. data/lib/lutaml/key_value/adapter/json/mapping.rb +2 -4
  55. data/lib/lutaml/key_value/adapter/jsonl/mapping.rb +2 -4
  56. data/lib/lutaml/key_value/adapter/toml/mapping.rb +2 -4
  57. data/lib/lutaml/key_value/adapter/yaml/mapping.rb +2 -4
  58. data/lib/lutaml/key_value/adapter/yamls/mapping.rb +2 -4
  59. data/lib/lutaml/key_value/mapping.rb +35 -10
  60. data/lib/lutaml/model/adapter_resolver.rb +5 -8
  61. data/lib/lutaml/model/collection.rb +11 -11
  62. data/lib/lutaml/model/error/no_root_mapping_error.rb +6 -5
  63. data/lib/lutaml/model/error/no_root_namespace_error.rb +6 -5
  64. data/lib/lutaml/model/error/type_only_mapping_error.rb +13 -0
  65. data/lib/lutaml/model/error/type_only_namespace_error.rb +12 -0
  66. data/lib/lutaml/model/mapping/mapping.rb +12 -0
  67. data/lib/lutaml/model/version.rb +1 -1
  68. data/lib/lutaml/model.rb +3 -0
  69. data/lib/lutaml/toml/adapter/mapping.rb +2 -4
  70. data/lib/lutaml/xml/adapter/base_adapter.rb +0 -9
  71. data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +0 -1
  72. data/lib/lutaml/xml/adapter/oga_adapter.rb +0 -1
  73. data/lib/lutaml/xml/adapter/ox_adapter.rb +0 -1
  74. data/lib/lutaml/xml/adapter/rexml_adapter.rb +0 -1
  75. data/lib/lutaml/xml/adapter/xml_serializer.rb +42 -22
  76. data/lib/lutaml/xml/adapter.rb +4 -0
  77. data/lib/lutaml/xml/builder/base.rb +64 -25
  78. data/lib/lutaml/xml/builder/nokogiri.rb +0 -2
  79. data/lib/lutaml/xml/builder/oga.rb +0 -2
  80. data/lib/lutaml/xml/builder/ox.rb +0 -2
  81. data/lib/lutaml/xml/builder/rexml.rb +0 -2
  82. data/lib/lutaml/xml/builder.rb +1 -0
  83. data/lib/lutaml/xml/configurable.rb +2 -2
  84. data/lib/lutaml/xml/declaration_handler.rb +3 -105
  85. data/lib/lutaml/xml/mapping.rb +3 -3
  86. data/lib/lutaml/xml/schema/xsd/documentation.rb +1 -1
  87. data/lib/lutaml/xml/schema/xsd.rb +5 -4
  88. data/lib/lutaml/xml/schema.rb +8 -5
  89. data/lib/lutaml/xml/serialization/collection_ext.rb +7 -7
  90. data/lib/lutaml/xml/serialization/format_conversion.rb +1 -1
  91. data/lib/lutaml/xml/serialization/instance_methods.rb +1 -1
  92. data/lib/lutaml/xml/xml_orderable.rb +17 -0
  93. data/lib/lutaml/xml.rb +9 -13
  94. data/lib/lutaml/yaml/adapter/mapping.rb +2 -4
  95. data/lib/lutaml/yamls/adapter/mapping.rb +7 -3
  96. data/lib/tasks/memory_profile.rb +2 -2
  97. data/lib/tasks/performance_benchmark.rb +5 -5
  98. data/lutaml-model.gemspec +1 -1
  99. data/spec/lutaml/key_value/transformation/rule_compiler_spec.rb +1 -1
  100. data/spec/lutaml/key_value/transformation/value_serializer_spec.rb +1 -1
  101. data/spec/lutaml/model/attribute_collection_spec.rb +1 -1
  102. data/spec/lutaml/model/cli_spec.rb +1 -1
  103. data/spec/lutaml/model/collection_spec.rb +1 -1
  104. data/spec/lutaml/model/collection_validation_spec.rb +6 -6
  105. data/spec/lutaml/model/consolidation_spec.rb +8 -8
  106. data/spec/lutaml/model/custom_collection_spec.rb +3 -3
  107. data/spec/lutaml/model/default_register_spec.rb +23 -23
  108. data/spec/lutaml/model/delegation_spec.rb +3 -10
  109. data/spec/lutaml/model/derived_attribute_serialization_spec.rb +1 -1
  110. data/spec/lutaml/model/dynamic_attribute_spec.rb +2 -2
  111. data/spec/lutaml/model/enum_spec.rb +1 -1
  112. data/spec/lutaml/model/group_spec.rb +12 -12
  113. data/spec/lutaml/model/lazy_collection_spec.rb +4 -4
  114. data/spec/lutaml/model/mixed_content_spec.rb +2 -2
  115. data/spec/lutaml/model/namespace_versioning_spec.rb +4 -4
  116. data/spec/lutaml/model/opal_smoke_spec.rb +117 -0
  117. data/spec/lutaml/model/processing_instruction_spec.rb +11 -11
  118. data/spec/lutaml/model/register_methods_spec.rb +2 -2
  119. data/spec/lutaml/model/render_empty_spec.rb +1 -1
  120. data/spec/lutaml/model/serialize_perf_guard_spec.rb +1 -1
  121. data/spec/lutaml/model/transform_dynamic_attributes_spec.rb +1 -1
  122. data/spec/lutaml/model/transformation_builder_spec.rb +2 -2
  123. data/spec/lutaml/model/xml_decoupling_spec.rb +3 -3
  124. data/spec/lutaml/model/xsd_patterns_spec.rb +2 -3
  125. data/spec/lutaml/xml/adapter/order_spec.rb +1 -1
  126. data/spec/lutaml/xml/clear_parse_state_spec.rb +1 -1
  127. data/spec/lutaml/xml/content_model_validation_spec.rb +4 -2
  128. data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +5 -5
  129. data/spec/lutaml/xml/enhanced_mapping_spec.rb +2 -1
  130. data/spec/lutaml/xml/entity_fragmentation_spec.rb +5 -5
  131. data/spec/lutaml/xml/indent_spec.rb +109 -0
  132. data/spec/lutaml/xml/line_ending_spec.rb +66 -0
  133. data/spec/lutaml/xml/mapping_finalization_guard_spec.rb +2 -2
  134. data/spec/lutaml/xml/model_transform_guard_spec.rb +4 -4
  135. data/spec/lutaml/xml/namespace_alias_spec.rb +4 -4
  136. data/spec/lutaml/xml/namespace_aware_parsing_spec.rb +3 -3
  137. data/spec/lutaml/xml/namespace_bound_element_roundtrip_spec.rb +2 -2
  138. data/spec/lutaml/xml/namespace_format_preservation_spec.rb +1 -1
  139. data/spec/lutaml/xml/namespace_inheritance_spec.rb +3 -3
  140. data/spec/lutaml/xml/namespace_preservation_spec.rb +5 -5
  141. data/spec/lutaml/xml/opal_xml_spec.rb +145 -0
  142. data/spec/lutaml/xml/pipeline_integration_spec.rb +145 -0
  143. data/spec/lutaml/xml/schema_primer_spec.rb +5 -5
  144. data/spec/lutaml/xml/transformation_spec.rb +20 -20
  145. data/spec/lutaml/xml/type_namespace/collector_spec.rb +1 -1
  146. data/spec/lutaml/xml/type_namespace/planner_spec.rb +3 -3
  147. data/spec/lutaml/xml/xml_spec.rb +64 -13
  148. data/spec/support/opal.rb +6 -0
  149. metadata +16 -4
data/RELEASE_NOTES.adoc CHANGED
@@ -26,7 +26,7 @@ The `namespace_scope` directive allows you to control WHERE namespace declaratio
26
26
  ----
27
27
  class Vcard < Lutaml::Model::Serializable
28
28
  xml do
29
- root "vCard"
29
+ element "vCard"
30
30
  namespace VcardNamespace
31
31
 
32
32
  # Consolidate namespaces at root
@@ -145,7 +145,7 @@ class SomeModel < Lutaml::Model::Serializable
145
145
  attribute :coll, :string, collection: true
146
146
 
147
147
  xml do
148
- root "some-model"
148
+ element "some-model"
149
149
  map_element 'collection', to: :coll
150
150
  end
151
151
  end
@@ -209,7 +209,7 @@ class SomeModel < Lutaml::Model::Serializable
209
209
  attribute :coll, :string, collection: true
210
210
 
211
211
  xml do
212
- root "some-model"
212
+ element "some-model"
213
213
  map_element 'collection', to: :coll, render_nil: :omit
214
214
  end
215
215
 
@@ -30,7 +30,7 @@ class Item < Lutaml::Model::Serializable
30
30
  attribute :value, :float
31
31
 
32
32
  xml do
33
- root "item"
33
+ element "item"
34
34
  map_attribute "id", to: :id
35
35
  map_element "name", to: :name
36
36
  map_element "value", to: :value
@@ -47,7 +47,7 @@ class Container < Lutaml::Model::Serializable
47
47
  attribute :items, Item, collection: true
48
48
 
49
49
  xml do
50
- root "container"
50
+ element "container"
51
51
  map_element "item", to: :items
52
52
  end
53
53
 
@@ -49,7 +49,7 @@ class SimpleModel < Lutaml::Model::Serializable
49
49
  attribute :created_at, :date_time
50
50
 
51
51
  xml do
52
- root "simple"
52
+ element "simple"
53
53
  map_attribute "id", to: :id
54
54
  map_element "name", to: :name
55
55
  map_element "active", to: :active
@@ -79,7 +79,7 @@ class AddressModel < Lutaml::Model::Serializable
79
79
  attribute :postal_code, :string
80
80
 
81
81
  xml do
82
- root "address"
82
+ element "address"
83
83
  map_element "street", to: :street
84
84
  map_element "city", to: :city
85
85
  map_element "country", to: :country
@@ -106,7 +106,7 @@ class PersonModel < Lutaml::Model::Serializable
106
106
  attribute :tags, :string, collection: true
107
107
 
108
108
  xml do
109
- root "person"
109
+ element "person"
110
110
  map_attribute "id", to: :id
111
111
  map_element "first_name", to: :first_name
112
112
  map_element "last_name", to: :last_name
@@ -136,7 +136,7 @@ class DepartmentModel < Lutaml::Model::Serializable
136
136
  attribute :employees, PersonModel, collection: true
137
137
 
138
138
  xml do
139
- root "department"
139
+ element "department"
140
140
  map_element "name", to: :name
141
141
  map_element "code", to: :code
142
142
  map_element "employee", to: :employees
@@ -47,7 +47,7 @@ class CustomModel < Lutaml::Model::Serializable
47
47
  end
48
48
 
49
49
  xml do
50
- root "CustomModel"
50
+ element "CustomModel"
51
51
  map_element ["name", "custom-name"], with: { to: :name_to_xml, from: :name_from_xml }
52
52
  map_element ["color", "shade"], with: { to: :color_to_xml, from: :color_from_xml }
53
53
  map_attribute ["id", "identifier"], to: :id
@@ -79,7 +79,7 @@ class JapaneseCeramic < Lutaml::Model::Serializable
79
79
  attribute :description, :string
80
80
 
81
81
  xml do
82
- root 'JapaneseCeramic'
82
+ element "JapaneseCeramic"
83
83
  map_attribute 'glazeType', to: :glaze_type
84
84
  map_element 'description', to: :description
85
85
  end
@@ -140,7 +140,7 @@ class Ceramic < Lutaml::Model::Serializable
140
140
  attribute :temperature, :integer
141
141
 
142
142
  xml do
143
- root 'ceramic'
143
+ element "ceramic"
144
144
  map_element 'potter', to: :potter
145
145
  map_content to: :description
146
146
  end
@@ -191,7 +191,7 @@ class JapaneseCeramic < Lutaml::Model::Serializable
191
191
  attribute :description, :string
192
192
 
193
193
  xml do
194
- root 'JapaneseCeramic'
194
+ element "JapaneseCeramic"
195
195
  map_attribute 'glazeType', to: :glaze_type
196
196
  map_element 'description', to: :description
197
197
  end
@@ -46,6 +46,10 @@ See link:../references/rdf-namespaces[RDF Namespaces] for namespace classes,
46
46
  * link:../consolidation-mapping[Consolidation Mapping] - Group sibling elements into structured models
47
47
  * link:../document-validation[Document Validation] - Document-level validation with rules, profiles, and remediation
48
48
 
49
+ == Runtime environments
50
+
51
+ * link:../opal[Opal Usage Guide] - Run Lutaml::Model in the browser via Opal (Ruby to JavaScript)
52
+
49
53
  == By task
50
54
 
51
55
  === I want to serialize to XML
@@ -1246,7 +1246,7 @@ class SomeModel < Lutaml::Model::Serializable
1246
1246
  attribute :coll, :string, collection: true
1247
1247
 
1248
1248
  xml do
1249
- root "some-model"
1249
+ element "some-model"
1250
1250
  map_element 'collection', to: :coll, render_nil: :omit
1251
1251
  end
1252
1252
 
@@ -1285,7 +1285,7 @@ class SomeModel < Lutaml::Model::Serializable
1285
1285
  attribute :coll, :string, collection: true
1286
1286
 
1287
1287
  xml do
1288
- root "some-model"
1288
+ element "some-model"
1289
1289
  map_element 'collection', to: :coll, render_nil: :as_nil
1290
1290
  end
1291
1291
 
@@ -1327,7 +1327,7 @@ class SomeModel < Lutaml::Model::Serializable
1327
1327
  attribute :coll, :string, collection: true
1328
1328
 
1329
1329
  xml do
1330
- root "some-model"
1330
+ element "some-model"
1331
1331
  map_element 'collection', to: :coll, render_nil: :as_blank # <1>
1332
1332
  end
1333
1333
 
@@ -1424,7 +1424,7 @@ class SomeModel < Lutaml::Model::Serializable
1424
1424
  attribute :coll, :string, collection: true
1425
1425
 
1426
1426
  xml do
1427
- root "some-model"
1427
+ element "some-model"
1428
1428
  map_element 'collection', to: :coll, render_empty: :omit
1429
1429
  end
1430
1430
 
@@ -1461,7 +1461,7 @@ class SomeModel < Lutaml::Model::Serializable
1461
1461
  attribute :coll, :string, collection: true
1462
1462
 
1463
1463
  xml do
1464
- root "some-model"
1464
+ element "some-model"
1465
1465
  map_element 'collection', to: :coll, render_empty: :as_nil
1466
1466
  end
1467
1467
 
@@ -1503,7 +1503,7 @@ class SomeModel < Lutaml::Model::Serializable
1503
1503
  attribute :coll, :string, collection: true
1504
1504
 
1505
1505
  xml do
1506
- root "some-model"
1506
+ element "some-model"
1507
1507
  map_element 'collection', to: :coll, render_empty: :as_blank # <1>
1508
1508
  end
1509
1509
 
@@ -101,7 +101,7 @@ class PersonName < Lutaml::Model::Serializable
101
101
  attribute :suffix, :string
102
102
 
103
103
  xml do
104
- root "personName"
104
+ element "personName"
105
105
  map_element "givenName", to: :given_name
106
106
  map_element "surname", to: :surname
107
107
  map_attribute "prefix", to: :prefix
@@ -114,7 +114,7 @@ class Contact < Lutaml::Model::Serializable
114
114
  attribute :person_name, PersonName
115
115
 
116
116
  xml do
117
- root "ContactInfo"
117
+ element "ContactInfo"
118
118
  map_element "personName", to: :person_name
119
119
  end
120
120
  end
@@ -233,7 +233,7 @@ class DctermsCreated < Lutaml::Model::Serializable
233
233
  attribute :type, XsiTypeType
234
234
 
235
235
  xml do
236
- root "created"
236
+ element "created"
237
237
  namespace DctermsNamespace
238
238
  map_attribute "type", to: :type
239
239
  map_content to: :value
@@ -246,7 +246,7 @@ class DctermsModified < Lutaml::Model::Serializable
246
246
  attribute :type, XsiTypeType
247
247
 
248
248
  xml do
249
- root "modified"
249
+ element "modified"
250
250
  namespace DctermsNamespace
251
251
  map_attribute "type", to: :type
252
252
  map_content to: :value
@@ -263,7 +263,7 @@ class CoreProperties < Lutaml::Model::Serializable
263
263
  attribute :modified, DctermsModified
264
264
 
265
265
  xml do
266
- root "coreProperties"
266
+ element "coreProperties"
267
267
  map_element "title", to: :title
268
268
  map_element "creator", to: :creator
269
269
  map_element "lastModifiedBy", to: :last_modified_by
@@ -420,7 +420,7 @@ class Settings < Lutaml::Model::Serializable
420
420
  attribute :chart_tracking, :boolean
421
421
 
422
422
  xml do
423
- root "settings"
423
+ element "settings"
424
424
  namespace WNamespace
425
425
  namespace_scope [W14Namespace, W15Namespace]
426
426
 
@@ -655,7 +655,7 @@ class Settings < Lutaml::Model::Serializable
655
655
  attribute :w15_doc_id, W15DocId
656
656
 
657
657
  xml do
658
- root "settings"
658
+ element "settings"
659
659
  namespace WNamespace
660
660
  namespace_scope [W14Namespace, W15Namespace]
661
661
 
@@ -0,0 +1,221 @@
1
+ ---
2
+ title: Opal Usage Guide
3
+ parent: Guides
4
+ nav_order: 99
5
+ ---
6
+
7
+ = Opal Usage Guide
8
+
9
+ :toc:
10
+ :toclevels: 3
11
+
12
+ == Overview
13
+
14
+ https://opalrb.com[Opal] is a Ruby-to-JavaScript compiler that allows Ruby code to run in the browser. Lutaml::Model supports running under Opal, enabling XML/JSON/YAML serialization in client-side applications.
15
+
16
+ The XML parsing layer is provided by the https://github.com/lutaml/moxml[moxml] gem (v0.2+), which detects the Opal runtime and uses the REXML adapter automatically.
17
+
18
+ == How it works
19
+
20
+ Opal compiles Ruby source code to JavaScript. Under Opal:
21
+
22
+ . `RUBY_ENGINE` equals `"opal"`
23
+ . `Lutaml::Model::RuntimeCompatibility.opal?` returns `true`
24
+ . Adapter selection is adjusted automatically
25
+ . Gems with C extensions (Nokogiri, Ox, Oj, tomlib) are not available
26
+ . REXML is used for XML because Opal reimplements `strscan` and `stringio` in its stdlib, enabling REXML (pure Ruby) to compile cleanly to JavaScript
27
+
28
+ No configuration is needed -- the runtime is detected and adapters are selected automatically.
29
+
30
+ == Supported features
31
+
32
+ === Fully supported
33
+
34
+ * Model definition with `attribute`, types, collections, defaults
35
+ * XML serialization/deserialization (via REXML adapter)
36
+ ** Element and attribute mapping
37
+ ** Nested elements and collections
38
+ ** Mixed content
39
+ ** Namespaces (parsing and serialization)
40
+ ** CDATA sections
41
+ ** Processing instructions
42
+ ** Comments
43
+ ** Entity references
44
+ ** XML declarations and doctypes
45
+ * JSON serialization/deserialization (via standard adapter)
46
+ * YAML serialization/deserialization (via standard adapter)
47
+ * Hash transformation
48
+ * Custom types
49
+ * Model import (`import_model`)
50
+ * Value transformations
51
+
52
+ === Not available
53
+
54
+ [cols="1,3",options="header"]
55
+ |===
56
+ | Feature | Reason
57
+ | XSD schema generation | Requires Nokogiri
58
+ | RELAX NG generation | Requires Nokogiri
59
+ | XML schema compilation | Requires Nokogiri + native parsing
60
+ | TOML serialization | Both tomlib and toml-rb require native extensions
61
+ | Oj / MultiJson adapters | Require native extensions
62
+ | Ox / Nokogiri / Oga adapters | Require native extensions or C extensions
63
+ | XPath queries | REXML XPath requires features not yet in Opal's stdlib
64
+ | Liquid templating | Uses file-system-based template loading (Phase 1: skipped)
65
+ | Canon XML equivalence | Uses Nokogiri for XML parsing
66
+ |===
67
+
68
+ == Setup
69
+
70
+ === Gemfile
71
+
72
+ Add Opal gems to your Gemfile:
73
+
74
+ [source,ruby]
75
+ ----
76
+ gem "lutaml-model"
77
+
78
+ group :opal do
79
+ gem "opal", "~> 1.8"
80
+ gem "opal-rspec", "~> 1.0"
81
+ gem "opal-sprockets"
82
+ end
83
+ ----
84
+
85
+ === Rake task
86
+
87
+ Add an Opal RSpec task to your Rakefile:
88
+
89
+ [source,ruby]
90
+ ----
91
+ begin
92
+ require "opal/rspec/rake_task"
93
+ rescue LoadError
94
+ # Opal not available
95
+ end
96
+
97
+ namespace :spec do
98
+ if defined?(Opal::RSpec::RakeTask)
99
+ desc "Run Opal (JavaScript) tests"
100
+ Opal::RSpec::RakeTask.new(:opal) do |server, runner|
101
+ server.append_path "lib"
102
+ runner.default_path = "spec"
103
+ runner.pattern = "spec/**/*_spec.{rb,opal}"
104
+ end
105
+ end
106
+ end
107
+ ----
108
+
109
+ === Test configuration
110
+
111
+ Create `spec/support/opal.rb` for Opal-specific test patches:
112
+
113
+ [source,ruby]
114
+ ----
115
+ # frozen_string_literal: true
116
+
117
+ if RUBY_ENGINE == "opal"
118
+ Lutaml::Model::Config.xml_adapter_type = :rexml
119
+ end
120
+ ----
121
+
122
+ Create `.rspec-opal`:
123
+
124
+ ----
125
+ --default-path=spec
126
+ --pattern='spec/**/*_spec.{rb,opal}'
127
+ -I lib
128
+ --opal-opt=-g,lutaml-model
129
+ -I spec
130
+ --require=spec_helper
131
+ --require=support/opal
132
+ ----
133
+
134
+ === CI workflow
135
+
136
+ Add `.github/workflows/opal.yml`:
137
+
138
+ [source,yaml]
139
+ ----
140
+ name: opal
141
+ on:
142
+ push:
143
+ branches: [main]
144
+ pull_request:
145
+
146
+ jobs:
147
+ test:
148
+ runs-on: ubuntu-latest
149
+ steps:
150
+ - uses: actions/checkout@v4
151
+ with:
152
+ submodules: "recursive"
153
+ - uses: ruby/setup-ruby@v1
154
+ with:
155
+ ruby-version: "3.3"
156
+ bundler-cache: true
157
+ - uses: actions/setup-node@v4
158
+ with:
159
+ node-version: "18"
160
+ - name: Run Opal tests
161
+ run: bundle exec rake spec:opal
162
+ ----
163
+
164
+ == Example: XML round-trip in the browser
165
+
166
+ [source,ruby]
167
+ ----
168
+ class Person
169
+ include Lutaml::Model::Serialize
170
+
171
+ attribute :name, :string
172
+ attribute :age, :integer
173
+
174
+ xml do
175
+ element "person"
176
+ map_element "name", to: :name
177
+ map_element "age", to: :age
178
+ end
179
+ end
180
+
181
+ # Parse XML
182
+ person = Person.from_xml('<person><name>Alice</name><age>30</age></person>')
183
+ person.name # => "Alice"
184
+ person.age # => 30
185
+
186
+ # Serialize to XML
187
+ person.to_xml # => "<person><name>Alice</name><age>30</age></person>"
188
+
189
+ # JSON and YAML also work
190
+ person.to_json # => '{"name":"Alice","age":30}'
191
+ person.to_yaml # => "---\nname: Alice\nage: 30\n"
192
+ ----
193
+
194
+ == Architecture
195
+
196
+ Under Opal, the library uses a JRuby-like pattern for dependency management:
197
+
198
+ . *Dependencies stay in the gemspec.* Gems like Nokogiri, Ox, and Oga remain listed as dependencies -- they are simply not loadable under Opal.
199
+ . *Requires are guarded with `RUBY_ENGINE`.* Code that depends on native gems uses `RUBY_ENGINE == "opal"` checks instead of silent `rescue LoadError`.
200
+ . *No gem splitting.* The same gem works on both MRI and Opal.
201
+
202
+ Key components:
203
+ * `Lutaml::Model::RuntimeCompatibility` -- detects the runtime (opal, windows, native)
204
+ * `Lutaml::Model::AdapterResolver` -- selects adapters based on runtime capabilities
205
+ * `Moxml::Config::OPAL_DEFAULT_ADAPTER` -- set to `:rexml`
206
+ * `Moxml::Adapter::OPAL_AVAILABLE_ADAPTERS` -- set to `%i[rexml]`
207
+
208
+ == Dependencies
209
+
210
+ The following gems in the lutaml ecosystem support Opal:
211
+
212
+ * **lutaml-model** -- Core model library (this gem)
213
+ * **moxml** (v0.2+) -- XML parsing abstraction with REXML adapter for Opal
214
+ * **canon** -- XML comparison (uses moxml under Opal; comparison features work but Nokogiri-specific features do not)
215
+
216
+ == Limitations
217
+
218
+ * **No XPath** -- REXML's XPath module has dependencies not yet reimplemented in Opal's stdlib.
219
+ * **No schema generation** -- XSD, RELAX NG generation, and schema compilation require Nokogiri.
220
+ * **No TOML** -- Both TOML adapters (tomlib, toml-rb) require native extensions.
221
+ * **No Liquid** -- Template rendering via Liquid is skipped in Opal (file-system dependency). May be addressed in a future phase using liquidjs via Opal's JavaScript bridge.
@@ -275,7 +275,7 @@ class Event < Lutaml::Model::Serializable
275
275
  attribute :event_date, MultiFormatDate
276
276
 
277
277
  xml do
278
- root "event"
278
+ element "event"
279
279
  map_element "eventDate", to: :event_date
280
280
  end
281
281
 
@@ -402,7 +402,7 @@ class Document < Lutaml::Model::Serializable
402
402
  }
403
403
 
404
404
  xml do
405
- root "document"
405
+ element "document"
406
406
  map_element "pubDate", to: :publication_date
407
407
  end
408
408
 
@@ -505,7 +505,7 @@ class Document < Lutaml::Model::Serializable
505
505
 
506
506
  # XML wants YYYYMMDD
507
507
  xml do
508
- root "document"
508
+ element "document"
509
509
  map_element "pubDate", to: :publication_date, transform: {
510
510
  export: ->(date) { date&.strftime("%Y%m%d") },
511
511
  import: ->(str) {
@@ -749,7 +749,7 @@ class Product < Lutaml::Model::Serializable
749
749
  end
750
750
 
751
751
  xml do
752
- root "product"
752
+ element "product"
753
753
  map_element "name", to: :name, transform: {
754
754
  export: ->(value) { "XML:#{value}" },
755
755
  import: ->(value) { value.gsub("XML:", "") }
@@ -898,7 +898,7 @@ class Event < Lutaml::Model::Serializable
898
898
  attribute :name, :string
899
899
 
900
900
  xml do
901
- root "event"
901
+ element "event"
902
902
  map_element "eventDate", to: :event_date
903
903
  map_element "name", to: :name
904
904
  end
@@ -1029,7 +1029,7 @@ class Schedule < Lutaml::Model::Serializable
1029
1029
  attribute :activity, :string
1030
1030
 
1031
1031
  xml do
1032
- root "schedule"
1032
+ element "schedule"
1033
1033
  map_element "date", to: :week_date
1034
1034
  map_element "activity", to: :activity
1035
1035
  end
@@ -1100,7 +1100,7 @@ class BlogPost < Lutaml::Model::Serializable
1100
1100
 
1101
1101
  # XML API requires YYYYMMDD
1102
1102
  xml do
1103
- root "post"
1103
+ element "post"
1104
1104
  map_element "publishDate", to: :published_on, transform: {
1105
1105
  export: ->(date) { date&.strftime("%Y%m%d") },
1106
1106
  import: ->(str) {
@@ -185,7 +185,7 @@ class NamespacedItem < Lutaml::Model::Serializable
185
185
  attribute :alt_name, SecondNamespacedName # SecondNamespace
186
186
 
187
187
  xml do
188
- root "item"
188
+ element "item"
189
189
  namespace SecondNamespace # Root uses SecondNamespace
190
190
  map_element "name", to: :name
191
191
  map_element "alt_name", to: :alt_name
@@ -43,7 +43,7 @@ class NativeItem < Lutaml::Model::Serializable
43
43
  attribute :name, :string # Native type with no inherent namespace
44
44
 
45
45
  xml do
46
- root "first_item"
46
+ element "first_item"
47
47
  namespace FirstItemNamespace
48
48
  map_element "name", to: :name
49
49
  end
@@ -97,7 +97,7 @@ class NamespacedItem < Lutaml::Model::Serializable
97
97
  attribute :alt_name, SecondNamespacedName # Has SecondNamespace
98
98
 
99
99
  xml do
100
- root "second_item"
100
+ element "second_item"
101
101
  namespace SecondNamespace
102
102
  map_element "name", to: :name
103
103
  map_element "alt_name", to: :alt_name
@@ -127,7 +127,7 @@ class NestedItem < Lutaml::Model::Serializable
127
127
  attribute :value, :string
128
128
 
129
129
  xml do
130
- root "nested"
130
+ element "nested"
131
131
  namespace FirstNamespace
132
132
  map_element "value", to: :value
133
133
  end
@@ -137,7 +137,7 @@ class Container < Lutaml::Model::Serializable
137
137
  attribute :item, NestedItem
138
138
 
139
139
  xml do
140
- root "container"
140
+ element "container"
141
141
  namespace SecondNamespace
142
142
  map_element "item", to: :item
143
143
  end
@@ -162,7 +162,7 @@ end
162
162
  [source,ruby]
163
163
  ----
164
164
  xml do
165
- root "item"
165
+ element "item"
166
166
  namespace MyNamespace
167
167
  map_attribute "id", to: :id # Unqualified (no namespace)
168
168
  map_element "name", to: :name # Qualified (uses MyNamespace)
@@ -200,7 +200,7 @@ class Item < Lutaml::Model::Serializable
200
200
  attribute :value, :integer
201
201
 
202
202
  xml do
203
- root "item"
203
+ element "item"
204
204
  namespace MyNamespace
205
205
  map_attribute "id", to: :id
206
206
  map_attribute "value", to: :value
@@ -280,7 +280,7 @@ class Spacing < Lutaml::Model::Serializable
280
280
  attribute :before, :integer
281
281
 
282
282
  xml do
283
- root "spacing"
283
+ element "spacing"
284
284
  namespace WordProcessingML
285
285
  map_attribute "val", to: :val
286
286
  map_attribute "after", to: :after
@@ -330,7 +330,7 @@ class Item < Lutaml::Model::Serializable
330
330
  attribute :explicit, :string
331
331
 
332
332
  xml do
333
- root "item"
333
+ element "item"
334
334
  namespace Namespace1
335
335
  map_attribute "normal", to: :normal # Uses form_default (unqualified)
336
336
  map_attribute "explicit", to: :explicit, namespace: Namespace2 # Explicit wins
@@ -397,7 +397,7 @@ class NativeItemNames < Lutaml::Model::Serializable
397
397
  attribute :name, :string, collection: true
398
398
 
399
399
  xml do
400
- root "item_names"
400
+ element "item_names"
401
401
  namespace FirstItemNamespace
402
402
  map_element "name", to: :name
403
403
  end
@@ -422,7 +422,7 @@ class NativeItemCollection < Lutaml::Model::Serializable
422
422
  attribute :items, NativeItem, collection: true
423
423
 
424
424
  xml do
425
- root "items"
425
+ element "items"
426
426
  namespace FirstItemNamespace
427
427
  map_element "item", to: :items
428
428
  end
@@ -441,7 +441,7 @@ class Container < Lutaml::Model::Serializable
441
441
  attribute :items, BaseItem, collection: true, polymorphic: [TypeA, TypeB]
442
442
 
443
443
  xml do
444
- root "container"
444
+ element "container"
445
445
  map_element "item", to: :items
446
446
  end
447
447
  end
@@ -557,7 +557,7 @@ class Child < Lutaml::Model::Serializable
557
557
  attribute :value, :string
558
558
 
559
559
  xml do
560
- root "child"
560
+ element "child"
561
561
  # No namespace declared - will inherit from parent
562
562
  map_element "value", to: :value
563
563
  end
@@ -567,7 +567,7 @@ class Parent < Lutaml::Model::Serializable
567
567
  attribute :child, Child
568
568
 
569
569
  xml do
570
- root "parent"
570
+ element "parent"
571
571
  namespace ParentNamespace
572
572
  map_element "child", to: :child
573
573
  end
@@ -594,7 +594,7 @@ class Wrapper < Lutaml::Model::Serializable
594
594
  attribute :items, NamespacedItem, collection: true
595
595
 
596
596
  xml do
597
- root "wrapper"
597
+ element "wrapper"
598
598
  namespace WrapperNamespace
599
599
  map_element "item", to: :items
600
600
  end
@@ -635,7 +635,7 @@ class PlainItem < Lutaml::Model::Serializable
635
635
  attribute :name, :string
636
636
 
637
637
  xml do
638
- root "item"
638
+ element "item"
639
639
  # No namespace declared
640
640
  map_element "name", to: :name
641
641
  end