lutaml-model 0.5.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +2 -0
  3. data/.rubocop_todo.yml +86 -23
  4. data/Gemfile +2 -0
  5. data/README.adoc +1441 -220
  6. data/lib/lutaml/model/attribute.rb +33 -10
  7. data/lib/lutaml/model/choice.rb +56 -0
  8. data/lib/lutaml/model/config.rb +1 -0
  9. data/lib/lutaml/model/constants.rb +7 -0
  10. data/lib/lutaml/model/error/choice_lower_bound_error.rb +9 -0
  11. data/lib/lutaml/model/error/choice_upper_bound_error.rb +9 -0
  12. data/lib/lutaml/model/error/import_model_with_root_error.rb +9 -0
  13. data/lib/lutaml/model/error/incorrect_sequence_error.rb +9 -0
  14. data/lib/lutaml/model/error/invalid_choice_range_error.rb +20 -0
  15. data/lib/lutaml/model/error/no_root_mapping_error.rb +9 -0
  16. data/lib/lutaml/model/error/no_root_namespace_error.rb +9 -0
  17. data/lib/lutaml/model/error/type/invalid_value_error.rb +19 -0
  18. data/lib/lutaml/model/error/unknown_sequence_mapping_error.rb +9 -0
  19. data/lib/lutaml/model/error.rb +9 -0
  20. data/lib/lutaml/model/json_adapter/standard_json_adapter.rb +6 -1
  21. data/lib/lutaml/model/key_value_mapping.rb +34 -3
  22. data/lib/lutaml/model/key_value_mapping_rule.rb +4 -2
  23. data/lib/lutaml/model/liquefiable.rb +59 -0
  24. data/lib/lutaml/model/mapping_hash.rb +9 -1
  25. data/lib/lutaml/model/mapping_rule.rb +19 -2
  26. data/lib/lutaml/model/schema/templates/simple_type.rb +247 -0
  27. data/lib/lutaml/model/schema/xml_compiler.rb +762 -0
  28. data/lib/lutaml/model/schema.rb +5 -0
  29. data/lib/lutaml/model/schema_location.rb +7 -0
  30. data/lib/lutaml/model/sequence.rb +71 -0
  31. data/lib/lutaml/model/serialize.rb +139 -33
  32. data/lib/lutaml/model/toml_adapter/toml_rb_adapter.rb +1 -2
  33. data/lib/lutaml/model/type/decimal.rb +0 -4
  34. data/lib/lutaml/model/type/hash.rb +11 -11
  35. data/lib/lutaml/model/type/time.rb +3 -3
  36. data/lib/lutaml/model/utils.rb +19 -15
  37. data/lib/lutaml/model/validation.rb +12 -1
  38. data/lib/lutaml/model/version.rb +1 -1
  39. data/lib/lutaml/model/xml_adapter/builder/oga.rb +10 -7
  40. data/lib/lutaml/model/xml_adapter/builder/ox.rb +20 -13
  41. data/lib/lutaml/model/xml_adapter/element.rb +32 -0
  42. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +13 -9
  43. data/lib/lutaml/model/xml_adapter/oga/element.rb +14 -13
  44. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +86 -19
  45. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +19 -15
  46. data/lib/lutaml/model/xml_adapter/xml_document.rb +82 -25
  47. data/lib/lutaml/model/xml_adapter/xml_element.rb +57 -3
  48. data/lib/lutaml/model/xml_mapping.rb +53 -9
  49. data/lib/lutaml/model/xml_mapping_rule.rb +8 -6
  50. data/lib/lutaml/model.rb +2 -0
  51. data/lutaml-model.gemspec +5 -0
  52. data/spec/benchmarks/xml_parsing_benchmark_spec.rb +75 -0
  53. data/spec/ceramic_spec.rb +39 -0
  54. data/spec/fixtures/ceramic.rb +23 -0
  55. data/spec/fixtures/xml/address_example_260.xsd +9 -0
  56. data/spec/fixtures/xml/invalid_math_document.xml +4 -0
  57. data/spec/fixtures/xml/math_document_schema.xsd +56 -0
  58. data/spec/fixtures/xml/test_schema.xsd +53 -0
  59. data/spec/fixtures/xml/user.xsd +10 -0
  60. data/spec/fixtures/xml/valid_math_document.xml +4 -0
  61. data/spec/lutaml/model/cdata_spec.rb +4 -5
  62. data/spec/lutaml/model/choice_spec.rb +168 -0
  63. data/spec/lutaml/model/collection_spec.rb +1 -1
  64. data/spec/lutaml/model/custom_model_spec.rb +7 -21
  65. data/spec/lutaml/model/custom_serialization_spec.rb +74 -2
  66. data/spec/lutaml/model/defaults_spec.rb +3 -1
  67. data/spec/lutaml/model/delegation_spec.rb +7 -5
  68. data/spec/lutaml/model/enum_spec.rb +35 -0
  69. data/spec/lutaml/model/group_spec.rb +160 -0
  70. data/spec/lutaml/model/inheritance_spec.rb +25 -0
  71. data/spec/lutaml/model/key_value_mapping_spec.rb +27 -0
  72. data/spec/lutaml/model/liquefiable_spec.rb +121 -0
  73. data/spec/lutaml/model/map_all_spec.rb +188 -0
  74. data/spec/lutaml/model/mixed_content_spec.rb +95 -56
  75. data/spec/lutaml/model/multiple_mapping_spec.rb +22 -10
  76. data/spec/lutaml/model/schema/xml_compiler_spec.rb +1624 -0
  77. data/spec/lutaml/model/sequence_spec.rb +216 -0
  78. data/spec/lutaml/model/transformation_spec.rb +230 -0
  79. data/spec/lutaml/model/type_spec.rb +138 -31
  80. data/spec/lutaml/model/utils_spec.rb +32 -0
  81. data/spec/lutaml/model/with_child_mapping_spec.rb +2 -2
  82. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +11 -7
  83. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +52 -0
  84. data/spec/lutaml/model/xml_mapping_rule_spec.rb +51 -0
  85. data/spec/lutaml/model/xml_mapping_spec.rb +250 -112
  86. metadata +77 -2
data/README.adoc CHANGED
@@ -8,19 +8,27 @@ image:https://img.shields.io/gem/v/lutaml-model.svg[RubyGems Version]
8
8
 
9
9
  == Purpose
10
10
 
11
- Lutaml::Model is a lightweight library for serializing and deserializing Ruby
12
- objects to and from various formats such as JSON, XML, YAML, and TOML. It uses
13
- an adapter pattern to support multiple libraries for each format, providing
14
- flexibility and extensibility for your data modeling needs.
11
+ Lutaml::Model is the Ruby implementation of the LutaML modeling methodology,
12
+ for:
15
13
 
16
- NOTE: The Lutaml::Model modeling Ruby API is designed to be mostly compatible
17
- with the data modeling API of https://www.shalerb.org[Shale], a data modeller
18
- for Ruby.
19
- Lutaml::Model is meant to address advanced needs not currently addressed by
20
- Shale.
14
+ * creating information models in the LutaML language (or its Ruby DSL)
15
+ * serializing and deserializing LutaML information models
16
+ * accessing data instances of LutaML information models
17
+ * documenting LutaML information models
21
18
 
22
- NOTE: Instructions on how to migrate from Shale to Lutaml::Model are provided in
23
- <<migrate-from-shale>>.
19
+ It provides simple, flexible and comprehensive mechanisms for defining
20
+ information models with attributes and types, and the serialization of them
21
+ to/from serialization formats including JSON, XML, YAML, and TOML.
22
+
23
+ For serialization formats, it uses an adapter pattern to support multiple
24
+ libraries for each format, providing flexibility and extensibility for your data
25
+ modeling needs.
26
+
27
+ NOTE: The Lutaml::Model modeling Ruby DSL was originally designed to be mostly
28
+ compatible with the data modeling DSL of https://www.shalerb.org[Shale], a data
29
+ modeller for Ruby. Lutaml::Model is meant to address advanced needs not
30
+ currently addressed by Shale. Instructions on how to migrate from Shale to
31
+ Lutaml::Model are provided in <<migrate-from-shale>>.
24
32
 
25
33
 
26
34
  == Features
@@ -49,12 +57,12 @@ The Lutaml::Model data modelling approach is as follows:
49
57
  .Modeling relationships of a LutaML Model
50
58
  [source]
51
59
  ----
52
- Lutaml Model
60
+ LutaML Model
53
61
 
54
62
  Has many attributes
55
63
 
56
64
 
57
- Attribute
65
+ Attribute
58
66
 
59
67
  Has type of
60
68
 
@@ -96,57 +104,144 @@ Studio (Model)
96
104
  .Modeling relationships of a LutaML Model to serialization models
97
105
  [source]
98
106
  ----
99
- ╔═══════════════════════╗ ╔════════════════════════════╗
100
- Core Model ║ Serialization Models ║
101
- ╚═══════════════════════╝ ╚════════════════════════════╝
102
-
103
- ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
104
- ┆ Model ┆ ┆ XML Model ┆
105
- ┆ │ ┆ ┌────────────────┐ ┆ │ ┆
106
- ┆ ┌────────┴──┐ ┆ │ │ ┆ ┌──────┴──────┐ ┆
107
- ┆ │ │ ┆ │ Model │ ┆ │ │ ┆
108
- ┆ Models Value Types ┆──►│ Transformation │ ┆ Models Value Types ┆
109
- ┆ │ │ ┆ │ & │ ┆ │ │ ┆
110
- ┆ │ │ ┆ │ Mapping Rules │ ┆ │ │ ┆
111
- ┆ │ ┌──────┴──┐ ┆ │ │ ┆ ┌────┴────┐ ┌─┴─┐ ┆
112
- ┆ │ │ │ ┆ │ │ ┆ │ │ │ │ ┆
113
- ┆ │ String Integer ┆ └────────────────┘ ┆ Element Value xs:string ┆
114
- ┆ │ Date Float ┆ │ ┆ Attribute Type xs:date ┆
115
- ┆ │ Time Boolean ┆ ├──────► ┆ xs:boolean ┆
116
- ┆ │ ┆ │ ┆ xs:anyURI ┆
117
- ┆ └──────┐ ┆ │ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
107
+ ╔═══════════════════════╗ ╔════════════════════════════╗
108
+ LutaML Core Model ║ Serialization Models ║
109
+ ╚═══════════════════════╝ ╚════════════════════════════╝
110
+
111
+ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
112
+ ┆ Model ┆ ┆ XML Model ┆
113
+ ┆ │ ┆ ┌────────────────┐ ┆ │ ┆
114
+ ┆ ┌────────┴──┐ ┆ │ │ ┆ ┌──────┴──────┐ ┆
115
+ ┆ │ │ ┆ │ Model │ ┆ │ │ ┆
116
+ ┆ Models Value Types ┆──►│ Transformation │ ┆ Models Value Types ┆
117
+ ┆ │ │ ┆ │ & │ ┆ │ │ ┆
118
+ ┆ │ │ ┆ │ Mapping Rules │ ┆ │ │ ┆
119
+ ┆ │ ┌──────┴──┐ ┆ │ │ ┆ ┌────┴────┐ ┌─┴─┐ ┆
120
+ ┆ │ │ │ ┆ └────────────────┘ ┆ │ │ │ │ ┆
121
+ ┆ │ String Integer ┆┆ Element Value xs:string ┆
122
+ ┆ │ Date Float ┆ │ ┆ Attribute Type xs:date ┆
123
+ ┆ │ Time Boolean ┆ ├──────────►┆ xs:boolean ┆
124
+ ┆ │ ┆ │ ┆ xs:anyURI ┆
125
+ ┆ └──────┐ ┆ │ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
118
126
  ┆ │ ┆ │
119
- ┆ Contains ┆ │ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
120
- ┆ more Models ┆ │ ┆ JSON Model ┆
121
- ┆ (recursive) ┆ │ ┆ │ ┆
122
- ┆ ┆ │ ┆ ┌──────┴──────┐ ┆
123
- ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯ └───────► ┆ │ │ ┆
124
- ┆ Models Value Types ┆
125
- ┆ │ │ ┆
126
- ┆ │ │ ┆
127
- ┆ ┌────┴───┐ ┌───┴──┐ ┆
128
- ┆ │ │ │ │ ┆
129
- ┆ object array number string ┆
130
- ┆ value boolean null ┆
127
+ ┆ Contains ┆ │ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
128
+ ┆ more Models ┆ │ ┆ JSON Model ┆
129
+ ┆ (recursive) ┆ │ ┆ │ ┆
130
+ ┆ ┆ │ ┆ ┌──────┴──────┐ ┆
131
+ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯ └──────────►┆ │ │ ┆
132
+ ┆ Models Value Types ┆
133
+ ┆ │ │ ┆
134
+ ┆ │ │ ┆
135
+ ┆ ┌────┴───┐ ┌───┴──┐ ┆
136
+ ┆ │ │ │ │ ┆
137
+ ┆ object array number string ┆
138
+ ┆ value boolean null ┆
131
139
  ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
132
140
  ----
133
141
 
142
+ .Model transformation of a LutaML Model to another LutaML Model
143
+ [source]
144
+ ----
145
+ ╔═══════════════════════╗ ╔══════════════════╗ ╔═══════════════════════╗
146
+ ║LutaML Model Class FOO ║ ║LutaML Transformer║ ║LutaML Model Class BAR ║
147
+ ╚═══════════════════════╝ ╚══════════════════╝ ╚═══════════════════════╝
148
+
149
+ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
150
+ ┆ Model ┆ ┆ Model ┆
151
+ ┆ │ ┆ ┌────────────────┐ ┆ │ ┆
152
+ ┆ ┌────────┴──┐ ┆ │ │ ┆ ┌────────┴──┐ ┆
153
+ ┆ │ │ ┆ │ Model │ ┆ │ │ ┆
154
+ ┆ Models Value Types ┆───►│ Transformation │───►┆ Models Value Types ┆
155
+ ┆ │ │ ┆◄───│ & │◄───┆ │ │ ┆
156
+ ┆ │ │ ┆ │ Mapping Rules │ ┆ │ │ ┆
157
+ ┆ │ ┌──────┴──┐ ┆ │ │ ┆ │ ┌──────┴──┐ ┆
158
+ ┆ │ │ │ ┆ └────────────────┘ ┆ │ │ │ ┆
159
+ ┆ │ String Integer ┆ ┆ │ String Integer ┆
160
+ ┆ │ Date Float ┆ ┆ │ Date Float ┆
161
+ ┆ │ Time Boolean ┆ ┆ │ Time Boolean ┆
162
+ ┆ │ ┆ ┆ │ ┆
163
+ ┆ └──────┐ ┆ ┆ └──────┐ ┆
164
+ ┆ │ ┆ ┆ │ ┆
165
+ ┆ Contains ┆ ┆ Contains ┆
166
+ ┆ more Models ┆ ┆ more Models ┆
167
+ ┆ (recursive) ┆ ┆ (recursive) ┆
168
+ ┆ ┆ ┆ ┆
169
+ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
170
+ ----
171
+
172
+ .The `Value` class, transformation, and serialization formats
173
+ [source]
174
+ ----
175
+ ╔═══════════════════════╗ ╔═══════════════════════╗
176
+ ║LutaML Value Class FOO ║ ║ Serialization Value ║
177
+ ╚═══════════════════════╝ ╚═══════════════════════╝
178
+ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
179
+ ┆ ┌───────────────┐ ┆ ┆ ┌───────────────┐ ┆
180
+ ┆ │ Value │ ┆ ┌──────────────────┐ ┆ │ XML Value │ ┆
181
+ ┆ └───────────────┘ ┆──►│ Value Serializer │──►┆ └───────────────┘ ┆
182
+ ┆ ┌───────────────┐ ┆ └──────────────────┘ ┆ ┌───────────────┐ ┆
183
+ ┆ │Primitive Types│ ┆ ┆ │XML Value Types│ ┆
184
+ ┆ └───────────────┘ ┆ ┆ └───────────────┘ ┆
185
+ ┆ ┌───┘ ┆ ┆ ┌───┘ ┆
186
+ ┆ ├─ string ┆ ┆ ├─ xs:string ┆
187
+ ┆ ├─ integer ┆ ┆ ├─ xs:integer ┆
188
+ ┆ ├─ float ┆ ┆ ├─ xs:decimal ┆
189
+ ┆ ├─ boolean ┆ ┆ ├─ xs:boolean ┆
190
+ ┆ ├─ date ┆ ┆ ├─ xs:date ┆
191
+ ┆ ├─ time_without_date ┆ ┆ ├─ xs:time ┆
192
+ ┆ ├─ date_time ┆ ┆ ├─ xs:dateTime ┆
193
+ ┆ ├─ time ┆ ┆ ├─ xs:decimal ┆
194
+ ┆ ├─ decimal ┆ ┆ ├─ xs:anyType ┆
195
+ ┆ └─ hash ┆ ┆ └─ (complex element) ┆
196
+ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
197
+
198
+
199
+ ┌───────────────────┐
200
+ │ Value Transformer │
201
+ └───────────────────┘
202
+
203
+
204
+ ╔═══════════════════════╗
205
+ ║LutaML Value Class BAR ║
206
+ ╚═══════════════════════╝
207
+ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
208
+ ┆ ┌───────────────┐ ┆
209
+ ┆ │ Value │ ┆
210
+ ┆ └───────────────┘ ┆
211
+ ┆ ┌───────────────┐ ┆
212
+ ┆ │Primitive Types│ ┆
213
+ ┆ └───────────────┘ ┆
214
+ ┆ ┌───┘ ┆
215
+ ┆ ├─ string ┆
216
+ ┆ ├─ integer ┆
217
+ ┆ ├─ float ┆
218
+ ┆ ├─ boolean ┆
219
+ ┆ ├─ date ┆
220
+ ┆ ├─ time_without_date ┆
221
+ ┆ ├─ date_time ┆
222
+ ┆ ├─ time ┆
223
+ ┆ ├─ decimal ┆
224
+ ┆ └─ hash ┆
225
+ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
226
+ ----
227
+
134
228
  .Example of LutaML Model instance transformed into a serialization model and serialized to JSON
135
229
  ====
136
230
  [source]
137
231
  ----
138
- Studio (Core Model) JSON Model Serialized JSON
139
- │ │ │
140
- ▼ ▼ ▼
141
- name: "Studio 1" ┌─► { "name": "...", {
142
- address: │ "address": { "name": "Studio 1",
143
- ├── street: "..." "street": "...", "address": {
144
- └── city: "..." │ "city": "..." ──► "street": "...",
145
- kilns: }, "city": "..."
146
- ├── count: 3 "kilnsCount": ..., },
147
- └── temp: 1200 │ "kilnsTemp": ... "kilnsCount": 3,
148
- └─► } "kilnsTemp": 1200
149
- }
232
+ ╔═════════════════════╗ ╔═════════════════════╗ ╔═════════════════════╗
233
+ ║ Studio (Core Model) ║ ║ JSON Model ║ ║ Serialized JSON ║
234
+ ╚═════════════════════╝ ╚═════════════════════╝ ╚═════════════════════╝
235
+
236
+ name: "Studio 1" ┌─► { ┌─► {
237
+ address: "name": "...", "name": "Studio 1",
238
+ ├── street: "..." │ "address": { │ "address": {
239
+ └── city: "..." "street": "...","street": "...",
240
+ kilns: ──┤ "city": "..." ──┤ "city": "..."
241
+ ├── count: 3 │ }, │ },
242
+ └── temp: 1200 │ "kilnsCount": ..., │ "kilnsCount": 3,
243
+ │ "kilnsTemp": ... │ "kilnsTemp": 1200
244
+ └─► } └─► }
150
245
  ----
151
246
  ====
152
247
 
@@ -548,6 +643,149 @@ end
548
643
  ====
549
644
 
550
645
 
646
+ === Sequence within XmlMapping
647
+
648
+ The sequence option enforces that the defined components must appear in a specified order.
649
+
650
+ NOTE: `sequence` only works within XML and only supports `map_element` mappings.
651
+
652
+ .Using the `sequence` keyword to define a set of elements in desired order.
653
+ [example]
654
+ ====
655
+ [source,ruby]
656
+ ----
657
+ class Kiln < Lutaml::Model::Serializable
658
+ attribute :id, :string
659
+ attribute :name, :string
660
+ attribute :type, :string
661
+ attribute :color, :string
662
+
663
+ xml do
664
+ sequence do
665
+ map_element :id, to: :id
666
+ map_element :name, to: :name
667
+ map_element :type, to: :type
668
+ map_element :color, to: :color
669
+ end
670
+ end
671
+ end
672
+
673
+ class KilnCollection < Lutaml::Model::Serializable
674
+ attribute :kiln, Kiln, collection: 1..2
675
+
676
+ xml do
677
+ root "collection"
678
+ map_element "kiln", to: :kiln
679
+ end
680
+ end
681
+ ----
682
+
683
+ [source,ruby]
684
+ ----
685
+ > parsed = Kiln.from_xml("<Kiln> <id>1</id> <name>Nick</name> <type>Hard</type> <color>Black</color> </Kiln>")
686
+ > # parsed.not_to raise_error
687
+ ----
688
+ ====
689
+
690
+
691
+ === Reusable Classes with Import
692
+
693
+ Lutaml lets you create reusable element and attribute collections using `no_root`. These can be imported into other models using:
694
+
695
+ - `import_model`: imports both attributes and mappings
696
+ - `import_model_attributes`: imports only attributes
697
+ - `import_model_mappings`: imports only mappings
698
+
699
+ NOTE: This feature works with XML. Import order determines how elements and attributes are overwritten.
700
+
701
+ [example]
702
+ ====
703
+ [source,ruby]
704
+ ----
705
+ class GroupOfItems < Lutaml::Model::Serializable
706
+ attribute :name, :string
707
+ attribute :type, :string
708
+ attribute :code, :string
709
+
710
+ xml do
711
+ no_root
712
+ sequence do
713
+ map_element "name", to: :name
714
+ map_element "type", to: :type, namespace: "http://www.example.com", prefix: "ex1"
715
+ end
716
+ map_attribute "code", to: :code
717
+ end
718
+ end
719
+
720
+ class ComplexType < Lutaml::Model::Serializable
721
+ attribute :tag, AttributeValueType
722
+ attribute :content, :string
723
+ attribute :group, :string
724
+ import_model_attributes GroupOfItems
725
+
726
+ xml do
727
+ root "GroupOfItems"
728
+ map_attribute "tag", to: :tag
729
+ map_content to: :content
730
+ map_element :group, to: :group
731
+ import_model_mappings GroupOfItems
732
+ end
733
+ end
734
+
735
+ class SimpleType < Lutaml::Model::Serializable
736
+ import_model GroupOfItems
737
+ end
738
+
739
+ class GenericType < Lutaml::Model::Serializable
740
+ import_model_mappings GroupOfItems
741
+ end
742
+ ----
743
+
744
+ [source,xml]
745
+ ----
746
+ <GroupOfItems xmlns:ex1="http://www.example.com">
747
+ <name>Name</name>
748
+ <ex1:type>Type</ex1:type>
749
+ </GroupOfItems>
750
+ ----
751
+
752
+ [source,ruby]
753
+ ----
754
+ > parsed = GroupOfItems.from_xml(xml)
755
+ > # Lutaml::Model::NoRootMappingError: "GroupOfItems has `no_root`, it allowed only for reusable models"
756
+ ----
757
+
758
+ NOTE: Models with `no_root` can only be parsed through **Parent Models**. Direct calling `from_xml` will raise `NoRootMappingError`.
759
+ And if `namespace` is defined with `no_root`, `NoRootNamespaceError` will raise.
760
+
761
+ ====
762
+
763
+
764
+ === Choice
765
+
766
+ The `choice` option ensures that elements from the specified range are included.
767
+
768
+ NOTE: Attribute-level definitions are supported. This can be used with both key_value and xml mappings.
769
+
770
+ .Using the `choice` option to define a set of attributes with a range.
771
+ [example]
772
+ ====
773
+ [source,ruby]
774
+ ----
775
+ class Studio < Lutaml::Model::Serializable
776
+ choice(min: 1, max: 3) do
777
+ choice(min: 1, max: 2) do
778
+ attribute :prefix, :string
779
+ attribute :forename, :string
780
+ end
781
+
782
+ attribute :completeName, :string
783
+ end
784
+ end
785
+ ----
786
+ ====
787
+
788
+
551
789
  === Attribute value validation
552
790
 
553
791
  ==== General
@@ -697,7 +935,7 @@ end
697
935
 
698
936
 
699
937
 
700
- === Attribute value default and rendering defaults
938
+ === Attribute value defaults
701
939
 
702
940
  Specify default values for attributes using the `default` option.
703
941
  The `default` option can be set to a value or a lambda that returns a value.
@@ -733,76 +971,6 @@ end
733
971
  The "default behavior" (pun intended) is to not render a default value if
734
972
  the current value is the same as the default value.
735
973
 
736
- In certain cases, it is necessary to render the default value even if the
737
- current value is the same as the default value. This can be achieved by setting
738
- the `render_default` option to `true`.
739
-
740
- Syntax:
741
-
742
- [source,ruby]
743
- ----
744
- attribute :name_of_attribute, Type, default: -> { value }, render_default: true
745
- ----
746
-
747
- .Using the `render_default` option to force encoding the default value
748
- [example]
749
- ====
750
- [source,ruby]
751
- ----
752
- class Glaze < Lutaml::Model::Serializable
753
- attribute :color, :string, default: -> { 'Clear' }
754
- attribute :opacity, :string, default: -> { 'Opaque' }
755
- attribute :temperature, :integer, default: -> { 1050 }
756
- attribute :firing_time, :integer, default: -> { 60 }
757
-
758
- xml do
759
- root "glaze"
760
- map_element 'color', to: :color
761
- map_element 'opacity', to: :opacity, render_default: true
762
- map_attribute 'temperature', to: :temperature
763
- map_attribute 'firingTime', to: :firing_time, render_default: true
764
- end
765
-
766
- json do
767
- map 'color', to: :color
768
- map 'opacity', to: :opacity, render_default: true
769
- map 'temperature', to: :temperature
770
- map 'firingTime', to: :firing_time, render_default: true
771
- end
772
- end
773
- ----
774
- ====
775
-
776
- .Attributes with `render_default: true` are rendered when the value is identical to the default
777
- [example]
778
- ====
779
- [source,ruby]
780
- ----
781
- > glaze_new = Glaze.new
782
- > puts glaze_new.to_xml
783
- # <glaze firingTime="60">
784
- # <opacity>Opaque</opacity>
785
- # </glaze>
786
- > puts glaze_new.to_json
787
- # {"firingTime":60,"opacity":"Opaque"}
788
- ----
789
- ====
790
-
791
- .Attributes with `render_default: true` with non-default values are rendered
792
- [example]
793
- ====
794
- [source,ruby]
795
- ----
796
- > glaze = Glaze.new(color: 'Celadon', opacity: 'Semitransparent', temperature: 1300, firing_time: 90)
797
- > puts glaze.to_xml
798
- # <glaze color="Celadon" temperature="1300" firingTime="90">
799
- # <opacity>Semitransparent</opacity>
800
- # </glaze>
801
- > puts glaze.to_json
802
- # {"color":"Celadon","temperature":1300,"firingTime":90,"opacity":"Semitransparent"}
803
- ----
804
- ====
805
-
806
974
 
807
975
 
808
976
  === Attribute as raw string
@@ -863,7 +1031,7 @@ defining serialization and deserialization mappings.
863
1031
  Serialization model mappings are defined under the `xml`, `json`, `yaml`, and
864
1032
  `toml` blocks.
865
1033
 
866
- .Using the `xml`, `json`, `yaml`, and `toml` blocks to define serialization mappings
1034
+ .Using the `xml`, `json`, `yaml`, `toml` and `key_value` blocks to define serialization mappings
867
1035
  [source,ruby]
868
1036
  ----
869
1037
  class Example < Lutaml::Model::Serializable
@@ -882,6 +1050,10 @@ class Example < Lutaml::Model::Serializable
882
1050
  toml do
883
1051
  # ...
884
1052
  end
1053
+
1054
+ key_value do
1055
+ # ...
1056
+ end
885
1057
  end
886
1058
  ----
887
1059
 
@@ -926,10 +1098,8 @@ end
926
1098
  ----
927
1099
  ====
928
1100
 
929
-
930
- ==== Mapping all content (XML only)
931
-
932
- WARNING: This feature is only applicable to XML (for now).
1101
+ [[xml-map-all]]
1102
+ ==== Mapping all XML content
933
1103
 
934
1104
  The `map_all` tag in XML mapping captures and maps all content within an XML
935
1105
  element into a single attribute in the target Ruby object.
@@ -937,6 +1107,12 @@ element into a single attribute in the target Ruby object.
937
1107
  The use case for `map_all` is to tell Lutaml::Model to not parse the content of
938
1108
  the XML element at all, and instead handle it as an XML string.
939
1109
 
1110
+ NOTE: The corresponding method for key-value formats is at <<key-value-map-all>>.
1111
+
1112
+ WARNING: Notice that usage of mapping all will lead to incompatibility between
1113
+ serialization formats, i.e. the raw string content will not be portable as
1114
+ objects are across different formats.
1115
+
940
1116
  This is useful in the case where the content of an XML element is not to be
941
1117
  handled by a Lutaml::Model::Serializable object.
942
1118
 
@@ -949,8 +1125,9 @@ This includes:
949
1125
  * attributes
950
1126
  * text nodes
951
1127
 
952
- The `map_all` tag is **exclusive** and cannot be combined with other mappings (`map_element`, `map_content`) except for `map_attribute` for the same element, ensuring
953
- it captures the entire inner XML content.
1128
+ The `map_all` tag is **exclusive** and cannot be combined with other mappings
1129
+ (`map_element`, `map_content`) except for `map_attribute` for the same element,
1130
+ ensuring it captures the entire inner XML content.
954
1131
 
955
1132
  NOTE: An error is raised if `map_all` is defined alongside any other mapping in
956
1133
  the same XML mapping context.
@@ -1284,72 +1461,15 @@ The following class will parse the XML snippet below:
1284
1461
 
1285
1462
  [source,ruby]
1286
1463
  ----
1287
- class Example < Lutaml::Model::Serializable
1288
- attribute :name, :string
1289
- attribute :description, :string
1290
- attribute :value, :integer
1291
-
1292
- xml do
1293
- root 'example'
1294
- map_element 'name', to: :name
1295
- map_attribute 'value', to: :value
1296
- map_content to: :description
1297
- end
1298
- end
1299
- ----
1300
-
1301
- [source,xml]
1302
- ----
1303
- <example value="12"><name>John Doe</name> is my moniker.</example>
1304
- ----
1305
-
1306
- [source,ruby]
1307
- ----
1308
- > Example.from_xml(xml)
1309
- > #<Example:0x0000000104ac7240 @name="John Doe", @description=" is my moniker.", @value=12>
1310
- > Example.new(name: "John Doe", description: " is my moniker.", value: 12).to_xml
1311
- > #<example value="12"><name>John Doe</name> is my moniker.</example>
1312
- ----
1313
- ====
1314
-
1315
-
1316
- ==== Encoding Options in XmlAdapter
1317
-
1318
- XmlAdapter supports the encoding in the following ways:
1319
-
1320
- . When encoding is not passed in to_xml:
1321
- ** Default encoding is UTF-8.
1322
-
1323
- . When encoding is explicitly passed nil:
1324
- ** Encoding will be nil, show the HexCode(Nokogiri) or ASCII-8bit(Ox).
1325
-
1326
- . When encoding is passed with some option:
1327
- ** Encoding option will be selected as passed.
1328
-
1329
-
1330
- Syntax:
1331
-
1332
- [source,ruby]
1333
- ----
1334
- Example.new(description: " ∑ is my ∏ moniker µ.").to_xml
1335
- Example.new(description: " ∑ is my ∏ moniker µ.").to_xml(encoding: nil)
1336
- Example.new(description: " ∑ is my ∏ moniker µ.").to_xml(encoding: "ASCII")
1337
- ----
1338
-
1339
- [example]
1340
- ====
1341
- The following class will parse the XML snippet below:
1342
-
1343
- [source,ruby]
1344
- ----
1345
- class Example < Lutaml::Model::Serializable
1464
+ class Ceramic < Lutaml::Model::Serializable
1346
1465
  attribute :name, :string
1347
1466
  attribute :description, :string
1348
- attribute :value, :integer
1467
+ attribute :temperature, :integer
1349
1468
 
1350
1469
  xml do
1351
- root 'example'
1470
+ root 'ceramic'
1352
1471
  map_element 'name', to: :name
1472
+ map_attribute 'temperature', to: :temperature
1353
1473
  map_content to: :description
1354
1474
  end
1355
1475
  end
@@ -1357,21 +1477,15 @@ end
1357
1477
 
1358
1478
  [source,xml]
1359
1479
  ----
1360
- <example><name>John &#x0026; Doe</name> &#x2211; is my &#x220F; moniker &#xB5;.</example>
1480
+ <ceramic temperature="1200"><name>Porcelain Vase</name> with celadon glaze.</ceramic>
1361
1481
  ----
1362
1482
 
1363
1483
  [source,ruby]
1364
1484
  ----
1365
- > Example.from_xml(xml)
1366
- > #<Example:0x0000000104ac7240 @name="John & Doe", @description=" is my ∏ moniker µ.">
1367
- > Example.new(name: "John & Doe", description: " is my moniker µ.").to_xml
1368
- > #<example><name>John &amp; Doe</name> is my ∏ moniker µ.</example>
1369
-
1370
- > Example.new(name: "John & Doe", description: " ∑ is my ∏ moniker µ.").to_xml(encoding: nil)
1371
- > #<example><name>John &amp; Doe</name> &#x2211; is my &#x220F; moniker &#xB5;.</example>
1372
-
1373
- > Example.new(name: "John & Doe", description: " ∑ is my ∏ moniker µ.").to_xml(encoding: "ASCII")
1374
- > #<example><name>John &amp; Doe</name> &#8721; is my &#8719; moniker &#181;.</example>
1485
+ > Ceramic.from_xml(xml)
1486
+ > #<Ceramic:0x0000000104ac7240 @name="Porcelain Vase", @description=" with celadon glaze.", @temperature=1200>
1487
+ > Ceramic.new(name: "Porcelain Vase", description: " with celadon glaze.", temperature: 1200).to_xml
1488
+ > #<ceramic temperature="1200"><name>Porcelain Vase</name> with celadon glaze.</ceramic>
1375
1489
  ----
1376
1490
  ====
1377
1491
 
@@ -1672,6 +1786,100 @@ end
1672
1786
 
1673
1787
  // TODO: How to create mixed content from `#new`?
1674
1788
 
1789
+
1790
+ [[ordered-content]]
1791
+ ==== Ordered content
1792
+
1793
+ `ordered: true` maintains the order of **XML Elements**, while `mixed: true` preserves the order of **XML Elements and Content**.
1794
+
1795
+ NOTE: When both options are used, `mixed: true` takes precedence.
1796
+
1797
+ To specify ordered content, the `ordered: true` option needs to be set at the
1798
+ `xml` block's `root` method.
1799
+
1800
+ Syntax:
1801
+
1802
+ [source,ruby]
1803
+ ----
1804
+ xml do
1805
+ root 'xml_element_name', ordered: true
1806
+ end
1807
+ ----
1808
+
1809
+ .Applying `ordered` to treat root as ordered content
1810
+ [example]
1811
+ ====
1812
+
1813
+ [source,ruby]
1814
+ ----
1815
+ class RootOrderedContent < Lutaml::Model::Serializable
1816
+ attribute :bold, :string
1817
+ attribute :italic, :string
1818
+ attribute :underline, :string
1819
+
1820
+ xml do
1821
+ root "RootOrderedContent", ordered: true
1822
+ map_element :bold, to: :bold
1823
+ map_element :italic, to: :italic
1824
+ map_element :underline, to: :underline
1825
+ end
1826
+ end
1827
+ ----
1828
+
1829
+ [source,xml]
1830
+ ----
1831
+ <RootOrderedContent>
1832
+ <underline>Moon</underline>
1833
+ <italic>384,400 km</italic>
1834
+ <bold>bell</bold>
1835
+ </RootOrderedContent>
1836
+ ----
1837
+
1838
+ [source,ruby]
1839
+ ----
1840
+ > instance = RootOrderedContent.from_xml(xml)
1841
+ > #<RootOrderedContent:0x0000000104ac7240 @bold="bell", @italic="384,400 km", @underline="Moon">
1842
+ > instance.to_xml
1843
+ > #<RootOrderedContent><underline>Moon</underline><italic>384,400 km</italic><bold>bell</bold></RootOrderedContent>
1844
+ ----
1845
+
1846
+ **Without Ordered True:**
1847
+
1848
+ [source,ruby]
1849
+ ----
1850
+ class RootOrderedContent < Lutaml::Model::Serializable
1851
+ attribute :bold, :string
1852
+ attribute :italic, :string
1853
+ attribute :underline, :string
1854
+
1855
+ xml do
1856
+ root "RootOrderedContent"
1857
+ map_element :bold, to: :bold
1858
+ map_element :italic, to: :italic
1859
+ map_element :underline, to: :underline
1860
+ end
1861
+ end
1862
+ ----
1863
+
1864
+ [source,xml]
1865
+ ----
1866
+ <RootOrderedContent>
1867
+ <underline>Moon</underline>
1868
+ <italic>384,400 km</italic>
1869
+ <bold>bell</bold>
1870
+ </RootOrderedContent>
1871
+ ----
1872
+
1873
+ [source,ruby]
1874
+ ----
1875
+ > instance = RootOrderedContent.from_xml(xml)
1876
+ > #<RootOrderedContent:0x0000000104ac7240 @bold="bell", @italic="384,400 km", @underline="Moon">
1877
+ > instance.to_xml
1878
+ > #<RootOrderedContent>\n <bold>bell</bold>\n <italic>384,400 km</italic>\n <underline>Moon</underline>\n</RootOrderedContent>
1879
+ ----
1880
+ ====
1881
+
1882
+
1675
1883
  [[xml-schema-location]]
1676
1884
  ==== Automatic support of `xsi:schemaLocation`
1677
1885
 
@@ -1793,6 +2001,345 @@ https://www.w3.org/TR/xmlschema-1/#xsi_schemaLocation[W3C XML standard].
1793
2001
 
1794
2002
 
1795
2003
 
2004
+ ==== Character encoding
2005
+
2006
+ ===== General
2007
+
2008
+ Lutaml::Model XML adapters use a default encoding of `UTF-8` for both input and
2009
+ output.
2010
+
2011
+ Serialization data to be parsed (deserialization) and serialization data to be
2012
+ exported (serialization) may be in a different character encoding than the
2013
+ default encoding used by the Lutaml::Model XML adapter. This mismatch may lead
2014
+ to incorrect data reading or incompatibilities when exporting data.
2015
+
2016
+ The possible values for setting character encoding to are:
2017
+
2018
+ * A valid encoding value, e.g. `UTF-8`, `Shift_JIS`, `ASCII`;
2019
+
2020
+ * `nil` to use the default encoding of the adapter. The behavior differs based
2021
+ on the adapter used.
2022
+
2023
+ ** Nokogiri: `UTF-8`. The encoding is set to the default encoding of the Nokogiri library,
2024
+ which is `UTF-8`.
2025
+
2026
+ ** Oga: `UTF-8`. The encoding is set to the default encoding of the Oga library, which
2027
+ uses `UTF-8`.
2028
+
2029
+ ** Ox: `ASCII-8bit`. The encoding is set to the default encoding of the Ox library, which uses
2030
+ `ASCII-8bit`.
2031
+
2032
+ When the `encoding` option is not set, the default encoding of `UTF-8` is
2033
+ used.
2034
+
2035
+
2036
+ ===== Serialization character encoding (exporting)
2037
+
2038
+ ====== General
2039
+
2040
+ There are two ways to set the character encoding of the XML document during
2041
+ serialization:
2042
+
2043
+ Instance setting::
2044
+ Setting the instance-level `encoding` option by setting
2045
+ `ModelClassInstance.encoding('...')`. This setting only affects serialization.
2046
+
2047
+ Per-export setting::
2048
+ Setting the `encoding` option when calling for serialization action using the
2049
+ `ModelClassInstance.to_xml(..., encoding: ...)` method.
2050
+
2051
+ [[encoding-instance-setting]]
2052
+ ====== Instance setting
2053
+
2054
+ The `encoding` value of an instance sets the character encoding of the XML
2055
+ document during serialization.
2056
+
2057
+ Syntax:
2058
+
2059
+ [source,ruby]
2060
+ ----
2061
+ ModelClassInstance.encoding = {encoding_value}
2062
+ ----
2063
+
2064
+ Where,
2065
+
2066
+ `ModelClassInstance`:: An instance of the class that inherits from
2067
+ Lutaml::Model::Serializable.
2068
+ `{encoding_value}`:: The encoding of the output data.
2069
+
2070
+ .Character encoding set to instance is reflected in its serialization output
2071
+ [example]
2072
+ ====
2073
+ [source,ruby]
2074
+ ----
2075
+ class JapaneseCeramic < Lutaml::Model::Serializable
2076
+ attribute :glaze_type, :string
2077
+ attribute :description, :string
2078
+
2079
+ xml do
2080
+ root 'JapaneseCeramic'
2081
+ map_attribute 'glazeType', to: :glaze_type
2082
+ map_element 'description' to: :description
2083
+ end
2084
+ end
2085
+ ----
2086
+
2087
+ [source,ruby]
2088
+ ----
2089
+ # Create a new instance with UTF-8 data
2090
+ > instance = JapaneseCeramic.new(glaze_type: "志野釉", description: "東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)")
2091
+ #=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
2092
+
2093
+ # Set character encoding to Shift_JIS
2094
+ > instance.encoding = "Shift_JIS"
2095
+ #=> "Shift_JIS"
2096
+
2097
+ # Serialize the instance
2098
+ > serialization_output = instance.to_xml
2099
+ #=> #<JapaneseCeramic><glazeType>\x{5FD8}\x{91CE}\x{91C9}</glazeType><description>\x{6771}\x{4EAC}\x{56FD}\x{7ACB}\x{535A}\x{7269}\x{9928}\x{30B3}\x{30EC}\x{30AF}\x{30B7}\x{30E7}\x{30F3}\x{306E}\x{7BC0}\x{8336}\x{7897}\x{300C}\x{6A4B}\x{672C}\x{300D}\x{FF08}\x{6853}\x{5C71}\x{6642}\x{4EE3}\x{FF09}</description></JapaneseCeramic>
2100
+
2101
+ # Check character encoding of output
2102
+ > serialization_output.encoding
2103
+ #=> "Shift_JIS"
2104
+ ----
2105
+ ====
2106
+
2107
+
2108
+ ====== Per-export setting
2109
+
2110
+ The `encoding` option is used in the `ModelClass#to_xml(..., encoding: ...)`
2111
+ call to set the character encoding of the XML document during serialization.
2112
+
2113
+ The per-export encoding setting supersedes the instance-level encoding setting.
2114
+
2115
+ Syntax:
2116
+
2117
+ [source,ruby]
2118
+ ----
2119
+ ModelClassInstance.to_xml(encoding: {encoding_value})
2120
+ ----
2121
+
2122
+ Where,
2123
+
2124
+ `ModelClassInstance`:: An instance of the class that inherits from
2125
+ Lutaml::Model::Serializable.
2126
+ `{encoding_value}`:: The encoding of the output data.
2127
+
2128
+
2129
+ [example]
2130
+ ====
2131
+ The following class will parse the XML snippet below:
2132
+
2133
+ [source,ruby]
2134
+ ----
2135
+ class Ceramic < Lutaml::Model::Serializable
2136
+ attribute :potter, :string
2137
+ attribute :description, :string
2138
+ attribute :temperature, :integer
2139
+
2140
+ xml do
2141
+ root 'ceramic'
2142
+ map_element 'potter', to: :potter
2143
+ map_content to: :description
2144
+ end
2145
+ end
2146
+ ----
2147
+
2148
+ [source,xml]
2149
+ ----
2150
+ <ceramic><potter>John &#x0026; Jane</potter> A &#x2211; series of &#x220F; porcelain &#xB5; vases.</ceramic>
2151
+ ----
2152
+
2153
+ [source,ruby]
2154
+ ----
2155
+ # Object with attributes
2156
+ > ceramic_instance = Ceramic.new(potter: "John & Jane", description: " A ∑ series of ∏ porcelain µ vases.")
2157
+ > #<Ceramic:0x0000000104ac7240 @potter="John & Jane", @description=" A ∑ series of ∏ porcelain µ vases.">
2158
+
2159
+ # Parsing the XML snippet with the default encoding of UTF-8
2160
+ > ceramic_parsed = Ceramic.from_xml(xml)
2161
+ > #<Ceramic:0x0000000104ac7242 @potter="John & Jane", @description=" A ∑ series of ∏ porcelain µ vases.">
2162
+
2163
+ # Object with attributes is equal to the parsed object
2164
+ > ceramic_parsed == ceramic_instance
2165
+ > # true
2166
+
2167
+ # Using the default encoding of UTF-8
2168
+ > ceramic_instance.to_xml
2169
+ > #<ceramic><potter>John &amp; Jane</potter> A ∑ series of ∏ porcelain µ vases.</ceramic>
2170
+
2171
+ # Using the default encoding of the adapter, which is UTF-8 in this case
2172
+ > ceramic_instance.to_xml(encoding: nil)
2173
+ > #<ceramic><potter>John &amp; Jane</potter> A &#x2211; series of &#x220F; porcelain &#xB5; vases.</ceramic>
2174
+
2175
+ # Using ASCII encoding
2176
+ > ceramic_instance.to_xml(encoding: "ASCII")
2177
+ > #<ceramic><potter>John &amp; Jane</potter> A &#8721; series of &#8719; porcelain &#181; vases.</ceramic>
2178
+ ----
2179
+ ====
2180
+
2181
+
2182
+ .Character encoding set at `to_xml` overrides instance encoding
2183
+ [example]
2184
+ ====
2185
+ [source,ruby]
2186
+ ----
2187
+ class JapaneseCeramic < Lutaml::Model::Serializable
2188
+ attribute :glaze_type, :string
2189
+ attribute :description, :string
2190
+
2191
+ xml do
2192
+ root 'JapaneseCeramic'
2193
+ map_attribute 'glazeType', to: :glaze_type
2194
+ map_element 'description' to: :description
2195
+ end
2196
+ end
2197
+ ----
2198
+
2199
+ [source,ruby]
2200
+ ----
2201
+ # Create a new instance with UTF-8 data
2202
+ > instance = JapaneseCeramic.new(glaze_type: "志野釉", description: "東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)")
2203
+ #=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
2204
+
2205
+ # Set character encoding to Shift_JIS
2206
+ > instance.encoding = "Shift_JIS"
2207
+ #=> "Shift_JIS"
2208
+
2209
+ # Serialize the instance
2210
+ > serialization_output = instance.to_xml(encoding: "UTF-8")
2211
+ #=> #<JapaneseCeramic><glazeType>志野釉</glazeType><description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description></JapaneseCeramic>
2212
+
2213
+ # Check character encoding of output
2214
+ > serialization_output.encoding
2215
+ #=> "UTF-8"
2216
+ ----
2217
+ ====
2218
+
2219
+
2220
+ ===== Deserialization character encoding (parsing)
2221
+
2222
+ The character encoding of the XML document being parsed is specified using the
2223
+ `encoding` option when the `ModelClass.from_{format}(...)` is called.
2224
+
2225
+ Syntax:
2226
+
2227
+ [source,ruby]
2228
+ ----
2229
+ ModelClass.from_{format}(string_in_format, encoding: {encoding_value})
2230
+ ----
2231
+
2232
+ Where,
2233
+
2234
+ `ModelClass`:: The class that inherits from Lutaml::Model::Serializable.
2235
+ `{format}`:: The format of the input data, e.g. `xml`, `json`, `yaml`, `toml`.
2236
+ `string_in_format`:: The input data in the specified format.
2237
+ `{encoding_value}`:: The encoding of the input data.
2238
+
2239
+
2240
+ .Setting the `encoding` option during parsing data not encoded in the default encoding (UTF-8)
2241
+ [example]
2242
+ ====
2243
+ Using the definition of `JapaneseCeramic` at <<encoding-instance-setting>>.
2244
+
2245
+ This XML snippet is in Shift-JIS.
2246
+
2247
+ [source,xml]
2248
+ ----
2249
+ <JapaneseCeramic>
2250
+ <glazeType>\x{5FD8}\x{91CE}\x{91C9}</glazeType>
2251
+ <description>\x{6771}\x{4EAC}\x{56FD}\x{7ACB}\x{535A}\x{7269}\x{9928}\x{30B3}\x{30EC}\x{30AF}\x{30B7}\x{30E7}\x{30F3}\x{306E}\x{7BC0}\x{8336}\x{7897}\x{300C}\x{6A4B}\x{672C}\x{300D}\x{FF08}\x{6853}\x{5C71}\x{6642}\x{4EE3}\x{FF09}</description>
2252
+ </JapaneseCeramic>
2253
+ ----
2254
+
2255
+ [source,ruby]
2256
+ ----
2257
+ # Parse the XML snippet with the encoding of Shift_JIS
2258
+ > instance = JapaneseCeramic.from_xml(xml, encoding: "Shift_JIS")
2259
+ #=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
2260
+
2261
+ # Check character encoding of the instance
2262
+ > instance.encoding
2263
+ #=> "Shift_JIS"
2264
+
2265
+ # Serialize the instance using UTF-8
2266
+ > serialization_output = instance.to_xml(encoding: "UTF-8")
2267
+ #=> #<JapaneseCeramic><glazeType>志野釉</glazeType><description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description></JapaneseCeramic>
2268
+ > serialization_output.encoding
2269
+ #=> "UTF-8"
2270
+ ----
2271
+ ====
2272
+
2273
+ .When the `encoding` option is not set, the default encoding of the adapter is used
2274
+ [example]
2275
+ ====
2276
+ Using the definition of `JapaneseCeramic` at <<encoding-instance-setting>>.
2277
+
2278
+ This XML snippet is in UTF-8.
2279
+
2280
+ [source,xml]
2281
+ ----
2282
+ <JapaneseCeramic>
2283
+ <glazeType>志野釉</glazeType>
2284
+ <description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description>
2285
+ </JapaneseCeramic>
2286
+ ----
2287
+
2288
+ In adapters that use a default encoding of `UTF-8`, the content is parsed
2289
+ properly.
2290
+
2291
+ [source,ruby]
2292
+ ----
2293
+ > instance = JapaneseCeramic.from_xml(xml, encoding: nil)
2294
+ #=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
2295
+ > instance.encoding
2296
+ #=> "UTF-8"
2297
+ > serialization_output = instance.to_xml
2298
+ #=> #<JapaneseCeramic><glazeType>志野釉</glazeType><description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description></JapaneseCeramic>
2299
+ > serialization_output.encoding
2300
+ #=> "UTF-8"
2301
+ ----
2302
+
2303
+ In adapters that use a default encoding of `ASCII-8bit`, the content becomes
2304
+ malformed.
2305
+
2306
+ [source,ruby]
2307
+ ----
2308
+ > instance = JapaneseCeramic.from_xml(xml, encoding: nil)
2309
+ #=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="�菑�", @description="�東京国立博物館コレクションの篠茶碗�橋本�桃山時代�">
2310
+ > instance.encoding
2311
+ #=> "ASCII-8bit"
2312
+ > serialization_output = instance.to_xml
2313
+ #=> #<JapaneseCeramic><glazeType>�菑�</glazeType><description>�東京国立博物館コレクションの篠茶碗�橋本�桃山時代�</description></JapaneseCeramic>
2314
+ > serialization_output.encoding
2315
+ #=> "ASCII-8bit"
2316
+ ----
2317
+ ====
2318
+
2319
+
2320
+ .Using an invalid encoding to deserialize causes data corruption
2321
+ [example]
2322
+ ====
2323
+ Using the definition of `JapaneseCeramic` at <<encoding-instance-setting>>.
2324
+
2325
+ This XML snippet is in UTF-8.
2326
+
2327
+ [source,xml]
2328
+ ----
2329
+ <JapaneseCeramic>
2330
+ <glazeType>志野釉</glazeType>
2331
+ <description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description>
2332
+ </JapaneseCeramic>
2333
+ ----
2334
+
2335
+ [source,ruby]
2336
+ ----
2337
+ > JapaneseCeramic.from_xml(xml, encoding: "Shift_JIS")
2338
+ #=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="�菑���p���P", @description="�東京国立博物館コレクションの篠茶碗�橋本�桃山時代�">
2339
+ ----
2340
+ ====
2341
+
2342
+
1796
2343
  === Key value data models
1797
2344
 
1798
2345
  ==== General
@@ -1917,21 +2464,117 @@ end
1917
2464
  [source,json]
1918
2465
  ----
1919
2466
  {
1920
- "name": "John Doe",
1921
- "value": 28
2467
+ "name": "John Doe",
2468
+ "value": 28
2469
+ }
2470
+ ----
2471
+
2472
+ [source,ruby]
2473
+ ----
2474
+ > Example.from_json(json)
2475
+ > #<Example:0x0000000104ac7240 @name="John Doe", @value=28>
2476
+ > Example.new(name: "John Doe", value: 28).to_json
2477
+ > #{"name"=>"John Doe", "value"=>28}
2478
+ ----
2479
+ ====
2480
+
2481
+ [[key-value-map-all]]
2482
+ ==== Mapping all key-value content
2483
+
2484
+ The `map_all` tag captures and maps all content within a serialization format
2485
+ into a single attribute in the target Ruby object.
2486
+
2487
+ The use case for `map_all` is to tell Lutaml::Model to not parse the content at
2488
+ all, and instead handle it as a raw string.
2489
+
2490
+ NOTE: The corresponding method for XML is at <<xml-map-all>>.
2491
+
2492
+ WARNING: Notice that usage of mapping all will lead to incompatibility between
2493
+ serialization formats, i.e. the raw string content will not be portable as
2494
+ objects are across different formats.
2495
+
2496
+ This is useful when the content needs to be handled as-is without parsing into
2497
+ individual attributes.
2498
+
2499
+ The `map_all` tag is **exclusive** and cannot be combined with other mappings,
2500
+ ensuring it captures the entire content.
2501
+
2502
+ NOTE: An error is raised if `map_all` is defined alongside any other mapping in
2503
+ the same mapping context.
2504
+
2505
+ Syntax:
2506
+
2507
+ [source,ruby]
2508
+ ----
2509
+ json | yaml | toml | key_value do
2510
+ map_all to: :name_of_attribute
2511
+ end
2512
+ ----
2513
+
2514
+ .Using `map_all` to capture all content across different formats
2515
+ [example]
2516
+ ====
2517
+ [source,ruby]
2518
+ ----
2519
+ class Document < Lutaml::Model::Serializable
2520
+ attribute :content, :string
2521
+
2522
+ json do
2523
+ map_all to: :content
2524
+ end
2525
+
2526
+ yaml do
2527
+ map_all to: :content
2528
+ end
2529
+
2530
+ toml do
2531
+ map_all to: :content
2532
+ end
2533
+ end
2534
+ ----
2535
+
2536
+ For JSON:
2537
+ [source,json]
2538
+ ----
2539
+ {
2540
+ "sections": [
2541
+ { "title": "Introduction", "text": "Chapter 1" },
2542
+ { "title": "Conclusion", "text": "Final chapter" }
2543
+ ],
2544
+ "metadata": {
2545
+ "author": "John Doe",
2546
+ "date": "2024-01-15"
2547
+ }
1922
2548
  }
1923
2549
  ----
1924
2550
 
2551
+ For YAML:
2552
+ [source,yaml]
2553
+ ----
2554
+ sections:
2555
+ - title: Introduction
2556
+ text: Chapter 1
2557
+ - title: Conclusion
2558
+ text: Final chapter
2559
+ metadata:
2560
+ author: John Doe
2561
+ date: 2024-01-15
2562
+ ----
2563
+
2564
+ The content is preserved exactly as provided:
2565
+
1925
2566
  [source,ruby]
1926
2567
  ----
1927
- > Example.from_json(json)
1928
- > #<Example:0x0000000104ac7240 @name="John Doe", @value=28>
1929
- > Example.new(name: "John Doe", value: 28).to_json
1930
- > #{"name"=>"John Doe", "value"=>28}
2568
+ > doc = Document.from_json(json_content)
2569
+ > puts doc.content
2570
+ > # "{\"sections\":[{\"title\":\"Introduction\",\"text\":\"Chapter 1\"},{\"title\":\"Conclusion\",\"text\":\"Final chapter\"}],\"metadata\":{\"author\":\"John Doe\",\"date\":\"2024-01-15\"}}"
2571
+
2572
+ > doc = Document.from_yaml(yaml_content)
2573
+ > puts doc.content
2574
+ > # "sections:\n - title: Introduction\n text: Chapter 1\n - title: Conclusion\n text: Final chapter\nmetadata:\n author: John Doe\n date: 2024-01-15\n"
1931
2575
  ----
1932
2576
  ====
1933
2577
 
1934
-
1935
2578
  ==== Nested attribute mappings
1936
2579
 
1937
2580
  The `map` method can also be used to map nested key-value data models
@@ -2970,6 +3613,91 @@ end
2970
3613
  ====
2971
3614
 
2972
3615
 
3616
+ === Rendering default values (forced rendering of default values)
3617
+
3618
+ By default, attributes with default values are not rendered if the current value
3619
+ is the same as the default value.
3620
+
3621
+ In certain cases, it is necessary to render the default value even if the
3622
+ current value is the same as the default value. This is achieved by setting the
3623
+ `render_default` option to `true`.
3624
+
3625
+ Syntax:
3626
+
3627
+ [source,ruby]
3628
+ ----
3629
+ attribute :name_of_attribute, Type, default: -> { value }
3630
+
3631
+ xml do
3632
+ map_element 'name_of_attribute', to: :name_of_attribute, render_default: true
3633
+ map_attribute 'name_of_attribute', to: :name_of_attribute, render_default: true
3634
+ end
3635
+
3636
+ json | yaml | toml | key_value do
3637
+ map 'name_of_attribute', to: :name_of_attribute, render_default: true
3638
+ end
3639
+ ----
3640
+
3641
+ .Using the `render_default` option to force encoding the default value
3642
+ [example]
3643
+ ====
3644
+ [source,ruby]
3645
+ ----
3646
+ class Glaze < Lutaml::Model::Serializable
3647
+ attribute :color, :string, default: -> { 'Clear' }
3648
+ attribute :opacity, :string, default: -> { 'Opaque' }
3649
+ attribute :temperature, :integer, default: -> { 1050 }
3650
+ attribute :firing_time, :integer, default: -> { 60 }
3651
+
3652
+ xml do
3653
+ root "glaze"
3654
+ map_element 'color', to: :color
3655
+ map_element 'opacity', to: :opacity, render_default: true
3656
+ map_attribute 'temperature', to: :temperature
3657
+ map_attribute 'firingTime', to: :firing_time, render_default: true
3658
+ end
3659
+
3660
+ json do
3661
+ map 'color', to: :color
3662
+ map 'opacity', to: :opacity, render_default: true
3663
+ map 'temperature', to: :temperature
3664
+ map 'firingTime', to: :firing_time, render_default: true
3665
+ end
3666
+ end
3667
+ ----
3668
+ ====
3669
+
3670
+ .Attributes with `render_default: true` are rendered when the value is identical to the default
3671
+ [example]
3672
+ ====
3673
+ [source,ruby]
3674
+ ----
3675
+ > glaze_new = Glaze.new
3676
+ > puts glaze_new.to_xml
3677
+ # <glaze firingTime="60">
3678
+ # <opacity>Opaque</opacity>
3679
+ # </glaze>
3680
+ > puts glaze_new.to_json
3681
+ # {"firingTime":60,"opacity":"Opaque"}
3682
+ ----
3683
+ ====
3684
+
3685
+ .Attributes with `render_default: true` with non-default values are rendered
3686
+ [example]
3687
+ ====
3688
+ [source,ruby]
3689
+ ----
3690
+ > glaze = Glaze.new(color: 'Celadon', opacity: 'Semitransparent', temperature: 1300, firing_time: 90)
3691
+ > puts glaze.to_xml
3692
+ # <glaze color="Celadon" temperature="1300" firingTime="90">
3693
+ # <opacity>Semitransparent</opacity>
3694
+ # </glaze>
3695
+ > puts glaze.to_json
3696
+ # {"color":"Celadon","temperature":1300,"firingTime":90,"opacity":"Semitransparent"}
3697
+ ----
3698
+ ====
3699
+
3700
+
2973
3701
 
2974
3702
  === Advanced attribute mapping
2975
3703
 
@@ -3149,6 +3877,63 @@ end
3149
3877
  NOTE: The corresponding keyword used by Shale is `receiver:` instead of
3150
3878
  `delegate:`.
3151
3879
 
3880
+ === Value Transformations
3881
+
3882
+ The `transform` option allows defining import/export transformations at both attribute and mapping levels:
3883
+
3884
+ [source,ruby]
3885
+ ----
3886
+ class Person < Lutaml::Model::Serializable
3887
+ # Attribute-level transformation
3888
+ attribute :name, :string, transform: {
3889
+ export: ->(value) { value.upcase },
3890
+ import: ->(value) { value.downcase }
3891
+ }
3892
+
3893
+ # Mapping-level transformation in JSON format
3894
+ json do
3895
+ map "fullName", to: :name, transform: {
3896
+ export: ->(value) { "Dr. #{value}" },
3897
+ import: ->(value) { value.gsub("Dr. ", "") }
3898
+ }
3899
+ end
3900
+
3901
+ # Mapping-level transformation in XML format
3902
+ xml do
3903
+ map "full-name", to: :name, transform: {
3904
+ export: ->(value) { "Dr. #{value}" },
3905
+ import: ->(value) { value.gsub("Dr. ", "") }
3906
+ }
3907
+ end
3908
+ end
3909
+ ----
3910
+
3911
+ The transformation precedence is:
3912
+
3913
+ 1. Mapping-level transformation if defined
3914
+ 2. Attribute-level transformation if no mapping transformation exists
3915
+
3916
+ This allows flexible value transformations without needing format-specific custom methods:
3917
+
3918
+ [source,ruby]
3919
+ ----
3920
+ person = Person.new(name: "john")
3921
+
3922
+ # Uses mapping transformation
3923
+ person.to_json # => {"fullName": "Dr. john"}
3924
+
3925
+ Person.from_json({ "fullName" => "Dr. john"}.to_json).name # => john
3926
+
3927
+ # Uses attribute transformation when no mapping exists
3928
+ person.to_yaml # => name: "JOHN"
3929
+ ----
3930
+
3931
+ The `transform` option supports:
3932
+
3933
+ * `export`: Transform value during serialization
3934
+ * `import`: Transform value during deserialization
3935
+ * Collections with array transformations
3936
+ * Chaining of attribute and mapping transformations
3152
3937
 
3153
3938
  ==== Attribute serialization with custom methods
3154
3939
 
@@ -3188,15 +3973,22 @@ The following class will parse the XML snippet below:
3188
3973
 
3189
3974
  [source,ruby]
3190
3975
  ----
3976
+ class Metadata < Lutaml::Model::Serializable
3977
+ attribute :category, :string
3978
+ attribute :identifier, :string
3979
+ end
3980
+
3191
3981
  class CustomCeramic < Lutaml::Model::Serializable
3192
3982
  attribute :name, :string
3193
3983
  attribute :size, :integer
3194
3984
  attribute :description, :string
3985
+ attribute :metadata, Metadata
3195
3986
 
3196
3987
  xml do
3197
3988
  map_element "Name", to: :name, with: { to: :name_to_xml, from: :name_from_xml }
3198
3989
  map_attribute "Size", to: :size, with: { to: :size_to_xml, from: :size_from_xml }
3199
3990
  map_content with: { to: :description_to_xml, from: :description_from_xml }
3991
+ map_element :metadata, to: :metadata, with: { to: :metadata_to_xml, from: :metadata_from_xml }
3200
3992
  end
3201
3993
 
3202
3994
  def name_to_xml(model, parent, doc)
@@ -3224,6 +4016,26 @@ class CustomCeramic < Lutaml::Model::Serializable
3224
4016
  def description_from_xml(model, value)
3225
4017
  model.description = value.join.strip.sub(/^XML Description: /, "")
3226
4018
  end
4019
+
4020
+ def metadata_to_xml(model, parent, doc)
4021
+ metadata_el = doc.create_element("metadata")
4022
+ category_el = doc.create_element("category")
4023
+ identifier_el = doc.create_element("identifier")
4024
+
4025
+ doc.add_text(category_el, model.metadata.category)
4026
+ doc.add_text(identifier_el, model.metadata.identifier)
4027
+
4028
+ doc.add_element(metadata_el, category_el)
4029
+ doc.add_element(metadata_el, identifier_el)
4030
+ doc.add_element(parent, metadata_el)
4031
+ end
4032
+
4033
+ def metadata_from_xml(model, value)
4034
+ model.metadata ||= Metadata.new
4035
+
4036
+ model.metadata.category = value["elements"]["category"].text
4037
+ model.metadata.identifier = value["elements"]["identifier"].text
4038
+ end
3227
4039
  end
3228
4040
  ----
3229
4041
 
@@ -3232,6 +4044,10 @@ end
3232
4044
  <CustomCeramic Size="15">
3233
4045
  <Name>XML Masterpiece: Vase</Name>
3234
4046
  XML Description: A beautiful ceramic vase
4047
+ <metadata>
4048
+ <category>Metadata</category>
4049
+ <identifier>123</identifier>
4050
+ </metadata>
3235
4051
  </CustomCeramic>
3236
4052
  ----
3237
4053
 
@@ -3243,13 +4059,180 @@ end
3243
4059
  @name="Masterpiece: Vase",
3244
4060
  @ordered=nil,
3245
4061
  @size=12,
3246
- @description="A beautiful ceramic vase">
3247
- > puts CustomCeramic.new(name: "Vase", size: 12, description: "A beautiful vase").to_xml
4062
+ @description="A beautiful ceramic vase",
4063
+ @metadata=#<Metadata:0x0000000105ad52e0 @category="Metadata", @identifier="123">>
4064
+ > puts CustomCeramic.new(name: "Vase", size: 12, description: "A beautiful vase", metadata: Metadata.new(category: "Glaze", identifier: 15)).to_xml
3248
4065
  # <CustomCeramic Size="15">
3249
4066
  # <Name>XML Masterpiece: Vase</Name>
4067
+ # <metadata>
4068
+ # <category>Glaze</category>
4069
+ # <identifier>15</identifier>
4070
+ # </metadata>
3250
4071
  # XML Description: A beautiful vase
3251
4072
  # </CustomCeramic>
3252
4073
  ----
4074
+
4075
+ [source,ruby]
4076
+ ----
4077
+ def custom_method_from_xml(model, value)
4078
+ instance = value.node # Lutaml::Model::XmlAdapter::AdapterElement
4079
+ # OR
4080
+ instance = value.node.adapter_node # Adapter::Element
4081
+
4082
+ xml = instance.to_xml
4083
+ end
4084
+ ----
4085
+
4086
+ When building a model from XML in **custom methods**, if the `value` parameter is a `mapping_hash`, then it allows access to the parsed XML structure through `value.node` which can be converted to an XML string using `to_xml`.
4087
+
4088
+ NOTE: For `NokogiriAdapter`, we can also call `to_xml` on `value.node.adapter_node`.
4089
+
4090
+ [source,ruby]
4091
+ ----
4092
+ > value
4093
+ > # {"text"=>["\n ", "\n ", "\n "], "elements"=>{"category"=>{"text"=>"Metadata"}}}
4094
+ > value.to_xml
4095
+ > # undefined_method `to_xml`
4096
+
4097
+ > value.node
4098
+
4099
+ # Nokogiri Adapter Node
4100
+
4101
+ #<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656ed8
4102
+ # @attributes={},
4103
+ # @children=
4104
+ # [#<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656cd0 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">,
4105
+ # #<Lutaml::Model::XmlAdapter::NokogiriElement:0x00000001076569b0
4106
+ # @attributes={},
4107
+ # @children=
4108
+ # [#<Lutaml::Model::XmlAdapter::NokogiriElement:0x00000001076567f8 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
4109
+ # @default_namespace=nil,
4110
+ # @name="category",
4111
+ # @namespace_prefix=nil,
4112
+ # @text="Metadata">,
4113
+ # #<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656028 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">],
4114
+ # @default_namespace=nil,
4115
+ # @name="metadata",
4116
+ # @namespace_prefix=nil,
4117
+ # @text="\n Metadata\n ">
4118
+
4119
+ # Ox Adapter Node
4120
+
4121
+ #<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584f78
4122
+ # @attributes={},
4123
+ # @children=
4124
+ # [#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584e60
4125
+ # @attributes={},
4126
+ # @children=[#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584d48 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
4127
+ # @default_namespace=nil,
4128
+ # @name="category",
4129
+ # @namespace_prefix=nil,
4130
+ # @text="Metadata">],
4131
+ # @default_namespace=nil,
4132
+ # @name="metadata",
4133
+ # @namespace_prefix=nil,
4134
+ # @text=nil>
4135
+
4136
+ # Oga Adapter Node
4137
+
4138
+ # <Lutaml::Model::XmlAdapter::Oga::Element:0x0000000107314158
4139
+ # @attributes={},
4140
+ # @children=
4141
+ # [#<Lutaml::Model::XmlAdapter::Oga::Element:0x0000000107314090 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">,
4142
+ # #<Lutaml::Model::XmlAdapter::Oga::Element:0x000000010730fe78
4143
+ # @attributes={},
4144
+ # @children=[#<Lutaml::Model::XmlAdapter::Oga::Element:0x000000010730fd88 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
4145
+ # @default_namespace=nil,
4146
+ # @name="category",
4147
+ # @namespace_prefix=nil,
4148
+ # @text="Metadata">,
4149
+ # #<Lutaml::Model::XmlAdapter::Oga::Element:0x000000010730f8d8 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">],
4150
+ # @default_namespace=nil,
4151
+ # @name="metadata",
4152
+ # @namespace_prefix=nil,
4153
+ # @text="\n Metadata\n ">
4154
+
4155
+ > value.node.to_xml
4156
+ > #<metadata><category>Metadata</category></metadata>
4157
+ ----
4158
+ ====
4159
+
4160
+ ==== Separate Serialization Model With Custom Methods
4161
+
4162
+ [example]
4163
+ ====
4164
+ The following class will parse the XML snippet below:
4165
+
4166
+ [source,ruby]
4167
+ ----
4168
+ class CustomModelChild
4169
+ attr_accessor :street, :city
4170
+ end
4171
+
4172
+ class CustomModelChildMapper < Lutaml::Model::Serializable
4173
+ model CustomModelChild
4174
+
4175
+ attribute :street, Lutaml::Model::Type::String
4176
+ attribute :city, Lutaml::Model::Type::String
4177
+
4178
+ xml do
4179
+ map_element :street, to: :street
4180
+ map_element :city, to: :city
4181
+ end
4182
+ end
4183
+
4184
+ class CustomModelParentMapper < Lutaml::Model::Serializable
4185
+ attribute :first_name, Lutaml::Model::Type::String
4186
+ attribute :child_mapper, CustomModelChildMapper
4187
+
4188
+ xml do
4189
+ map_element :first_name, to: :first_name
4190
+ map_element :CustomModelChild,
4191
+ with: { to: :child_to_xml, from: :child_from_xml }
4192
+ end
4193
+
4194
+ def child_to_xml(model, parent, doc)
4195
+ child_el = doc.create_element("CustomModelChild")
4196
+ street_el = doc.create_element("street")
4197
+ city_el = doc.create_element("city")
4198
+
4199
+ doc.add_text(street_el, model.child_mapper.street)
4200
+ doc.add_text(city_el, model.child_mapper.city)
4201
+
4202
+ doc.add_element(child_el, street_el)
4203
+ doc.add_element(child_el, city_el)
4204
+ doc.add_element(parent, child_el)
4205
+ end
4206
+
4207
+ def child_from_xml(model, value)
4208
+ model.child_mapper ||= CustomModelChild.new
4209
+
4210
+ model.child_mapper.street = value["elements"]["street"].text
4211
+ model.child_mapper.city = value["elements"]["city"].text
4212
+ end
4213
+ end
4214
+ ----
4215
+
4216
+ [source,xml]
4217
+ ----
4218
+ <CustomModelParent>
4219
+ <first_name>John</first_name>
4220
+ <CustomModelChild>
4221
+ <street>Oxford Street</street>
4222
+ <city>London</city>
4223
+ </CustomModelChild>
4224
+ </CustomModelParent>
4225
+ ----
4226
+
4227
+ [source,ruby]
4228
+ ----
4229
+ > instance = CustomModelParentMapper.from_xml(xml)
4230
+ > #<CustomModelParent:0x0000000107c9ca68 @child_mapper=#<CustomModelChild:0x0000000107c95218 @city="London", @street="Oxford Street">, @first_name="John">
4231
+ > CustomModelParentMapper.to_xml(instance)
4232
+ > #<CustomModelParent><first_name>John</first_name><CustomModelChild><street>Oxford Street</street><city>London</city></CustomModelChild></CustomModelParent>
4233
+ ----
4234
+
4235
+ NOTE: For **custom models**, `to_xml` is called on the **mapper class**, not on **model instance**.
3253
4236
  ====
3254
4237
 
3255
4238
 
@@ -3310,6 +4293,195 @@ end
3310
4293
  ====
3311
4294
 
3312
4295
 
4296
+ == Importing data models
4297
+
4298
+ === General
4299
+
4300
+ Lutaml::Model provides a way to import data models defined from various formats
4301
+ into the LutaML data modeling system.
4302
+
4303
+ Data model languages supported are:
4304
+
4305
+ * XSD (https://w3.org/TR/xmlschema-1/[XML Schema Definition Language])
4306
+ // * RNC (https://relaxng.org/compact-tutorial-20030326.html[RELAX NG Compact Syntax])
4307
+ // * RNG (https://relaxng.org/relaxng-compact.html[RELAX NG XML Syntax])
4308
+ // * JSON Schema (https://json-schema.org/understanding-json-schema/[JSON Schema])
4309
+ // * YAML Schema (https://yaml.org/spec/1.2/spec.html[YAML])
4310
+ // * LutaML
4311
+
4312
+
4313
+ The following figure illustrates the process of importing an XML Schema model to
4314
+ create LutaML core models. Once the LutaML core models are created, they can be
4315
+ used to parse and generate XML documents according to the imported XML Schema
4316
+ model.
4317
+
4318
+ Today, the LutaML core models are written into Ruby files, which can be used to
4319
+ parse and generate XML documents according to the imported XML Schema.
4320
+ This is to be changed so that the LutaML core models are directly loaded and
4321
+ interpreted.
4322
+
4323
+ .Importing an XML Schema model to create LutaML core models
4324
+ [source]
4325
+ ----
4326
+ ╔════════════════════════════╗ ╔═══════════════════════╗
4327
+ ║ Serialization Models ║ ║ Core Model ║
4328
+ ╚════════════════════════════╝ ╚═══════════════════════╝
4329
+
4330
+ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
4331
+ ┆ XML Schema (XSD/RNG/RNC) ┆ ┆ Model ┆
4332
+ ┆ │ ┆ ┌────────────────┐ ┆ │ ┆
4333
+ ┆ ┌──────┴──────┐ ┆ │ │ ┆ ┌────────┴──┐ ┆
4334
+ ┆ │ │ ┆ │ Model │ ┆ │ │ ┆
4335
+ ┆ Models Value Types ┆──►│ Importing │──►┆ Models Value Types ┆
4336
+ ┆ │ │ ┆ │ │ ┆ │ │ ┆
4337
+ ┆ │ │ ┆ └────────────────┘ ┆ │ │ ┆
4338
+ ┆ ┌────┴────┐ ┌─┴─┐ ┆ │ ┆ │ ┌──────┴──┐ ┆
4339
+ ┆ │ │ │ │ ┆ │ ┆ │ │ │ ┆
4340
+ ┆ Element Value xs:string ┆ │ ┆ │ String Integer ┆
4341
+ ┆ Attribute Type xs:date ┆ │ ┆ │ Date Float ┆
4342
+ ┆ Union Complex xs:boolean ┆ │ ┆ │ Time Boolean ┆
4343
+ ┆ Sequence Choice xs:anyURI ┆ │ ┆ │ ┆
4344
+ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯ │ ┆ └──────┐ ┆
4345
+ │ ┆ │ ┆
4346
+ │ ┆ Contains ┆
4347
+ │ ┆ more Models ┆
4348
+ │ ┆ (recursive) ┆
4349
+ │ ┆ ┆
4350
+ │ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
4351
+ │ ┌────────────────┐
4352
+ │ │ │
4353
+ │ │ Model │
4354
+ └──────────► │ Transformation │
4355
+ │ & │
4356
+ │ Mapping Rules │
4357
+ │ │
4358
+ └────────────────┘
4359
+ ----
4360
+
4361
+
4362
+ [[xml-schema-to-model-files]]
4363
+ === XML Schema (XSD)
4364
+
4365
+ W3C XSD is a schema language designed to define the structure of XML documents,
4366
+ alongside other XML schema languages like DTD, RELAX NG, and Schematron.
4367
+
4368
+ Lutaml::Model supports the import of XSD schema files to define information
4369
+ models that can be used to parse and generate XML documents.
4370
+
4371
+ Specifically, the `Lutaml::Model::Schema#from_xml` method loads XML Schema files
4372
+ (XSD, `*.xsd`) and generates Ruby files (`*.rb`) that inherit from
4373
+ `Lutaml::Model::Serializable` that are saved to disk.
4374
+
4375
+ Syntax:
4376
+
4377
+ [source,ruby]
4378
+ ----
4379
+ Lutaml::Model::Schema.from_xml(
4380
+ xsd_schema, <1>
4381
+ options: options <2>
4382
+ )
4383
+ ----
4384
+ <1> The `xsd_schema` is the XML Schema string to be converted to model files.
4385
+ <2> The `options` hash is an optional argument.
4386
+
4387
+ `options`:: Optional hash containing potentially the following key-values.
4388
+
4389
+ `output_dir`::: The directory where the model files will be saved. If not
4390
+ provided, a default directory named `lutaml_models_<timestamp>` is created.
4391
+ +
4392
+ [example]
4393
+ `"path/to/directory"`
4394
+
4395
+ `create_files`::: A `boolean` argument (`false` by default) to create files directly in the specified directory as defined by the `output_dir` option.
4396
+ +
4397
+ [example]
4398
+ `create_files: (true | false)`
4399
+
4400
+ `load_classes`::: A `boolean` argument (`false` by default) to load generated classes before returning them.
4401
+ +
4402
+ [example]
4403
+ `load_classes: (true | false)`
4404
+
4405
+ `namespace`::: The namespace of the schema. This will be added in the
4406
+ `Lutaml::Model::Serializable` file's `xml do` block.
4407
+ +
4408
+ [example]
4409
+ `http://example.com/namespace`
4410
+
4411
+ `prefix`::: The prefix of the namespace provided in the `namespace` option.
4412
+ +
4413
+ [example]
4414
+ `example-prefix`
4415
+
4416
+ `location`::: The URL or path of the directory containing all the files of the
4417
+ schema. For more information, refer to the
4418
+ link:https://www.w3.org/TR/xmlschema-1/#include[XML Schema specification].
4419
+ +
4420
+ [example]
4421
+ `"http://example.com/example.xsd"`
4422
+ +
4423
+ [example]
4424
+ `"path/to/schema/directory"`
4425
+
4426
+ NOTE: If both `create_files` and `load_classes` are provided, the `create_files` argument will take priority and generate files without loading them!
4427
+
4428
+ The generated LutaML models consists of two different kind of Ruby classes
4429
+ depending on the XSD schema:
4430
+
4431
+ XSD "SimpleTypes":: converted into classes that inherit from
4432
+ `Lutaml::Model::Type::Value`, which define the data types with restrictions and
4433
+ other validations of these values.
4434
+
4435
+ XSD "ComplexTypes":: converted into classes that inherit from
4436
+ `Lutaml::Model::Serializable` that model according to the defined structure.
4437
+
4438
+ Lutaml::Model uses the https://github.com/lutaml/lutaml-xsd[`lutaml-xsd` gem] to
4439
+ automatically resolve the `include` and `import` elements, enabling
4440
+ *Lutaml-Model* to generate the corresponding model files.
4441
+
4442
+ This auto-resolving feature allows seamless integration of these files into your
4443
+ models without the need for manual resolution of includes and imports.
4444
+
4445
+ [example]
4446
+ .Using `Lutaml::Model::Schema#from_xml` to convert an XML Schema to model files
4447
+ ====
4448
+ [source,ruby]
4449
+ ----
4450
+ xsd_schema = <<~XSD
4451
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4452
+ /* your schema here */
4453
+ </xs:schema>
4454
+ XSD
4455
+ options = {
4456
+ # These are all optional:
4457
+ output_dir: 'path/to/directory',
4458
+ namespace: 'http://example.com/namespace',
4459
+ prefix: "example-prefix",
4460
+ location: "http://example.com/example.xsd",
4461
+ # or
4462
+ # location: "path/to/schema/directory"
4463
+ create_files: true, # Default: false
4464
+ # OR
4465
+ load_classes: true, # Default: false
4466
+ }
4467
+
4468
+ # generates the files in the output_dir | default_dir
4469
+ Lutaml::Model::Schema.from_xml(xsd_schema, options: options)
4470
+ ----
4471
+ ====
4472
+
4473
+ You could also directly load the generated Ruby files into your application by
4474
+ requiring them.
4475
+
4476
+ [example]
4477
+ .Using the generated Ruby files in your application
4478
+ ====
4479
+ [source,ruby]
4480
+ ----
4481
+ Lutaml::Model::Schema.from_xml(xsd_schema, options: {output_dir: 'path/to/directory'})
4482
+ require_relative 'path/to/directory/*.rb'
4483
+ ----
4484
+ ====
3313
4485
 
3314
4486
 
3315
4487
  == Validation
@@ -3331,6 +4503,7 @@ Lutaml::Model supports the following validation methods:
3331
4503
 
3332
4504
  * `collection`:: Validates collection size range.
3333
4505
  * `values`:: Validates the value of an attribute from a set of fixed values.
4506
+ * `choice` :: Validates that attribute specified within defined range
3334
4507
 
3335
4508
  [example]
3336
4509
  ====
@@ -3344,6 +4517,17 @@ class Klin < Lutaml::Model::Serializable
3344
4517
  attribute :name, :string
3345
4518
  attribute :degree_settings, :integer, collection: (1..)
3346
4519
  attribute :description, :string, values: %w[one two three]
4520
+ attribute :id, :integer
4521
+ attribute :age, :integer
4522
+
4523
+ choice(min: 1, max: 1) do
4524
+ choice(min: 1, max: 2) do
4525
+ attribute :prefix, :string
4526
+ attribute :forename, :string
4527
+ end
4528
+
4529
+ attribute :nick_name, :string
4530
+ end
3347
4531
 
3348
4532
  xml do
3349
4533
  map_element 'name', to: :name
@@ -3351,26 +4535,30 @@ class Klin < Lutaml::Model::Serializable
3351
4535
  end
3352
4536
  end
3353
4537
 
3354
- klin = Klin.new(name: "Klin", degree_settings: [100, 200, 300], description: "one")
4538
+ klin = Klin.new(name: "Klin", degree_settings: [100, 200, 300], description: "one", prefix: "Ben")
3355
4539
  klin.validate
3356
4540
  # => []
3357
4541
 
3358
- klin = Klin.new(name: "Klin", degree_settings: [], description: "four")
4542
+ klin = Klin.new(name: "Klin", degree_settings: [], description: "four", prefix: "Ben", nick_name: "Smith")
3359
4543
  klin.validate
3360
4544
  # => [
3361
4545
  # #<Lutaml::Model::CollectionSizeError: degree_settings must have at least 1 element>,
3362
- # #<Lutaml::Model::ValueError: description must be one of [one, two, three]>
4546
+ # #<Lutaml::Model::ValueError: description must be one of [one, two, three]>,
4547
+ # #<Lutaml::Model::ChoiceUpperBoundError: Attribute count exceeds the upper bound>
3363
4548
  # ]
3364
4549
 
3365
4550
  e = klin.validate!
3366
4551
  # => Lutaml::Model::ValidationError: [
3367
4552
  # degree_settings must have at least 1 element,
3368
- # description must be one of [one, two, three]
4553
+ # description must be one of [one, two, three],
4554
+ # Attribute count exceeds the upper bound
3369
4555
  # ]
3370
4556
  e.errors
3371
4557
  # => [
3372
4558
  # #<Lutaml::Model::CollectionSizeError: degree_settings must have at least 1 element>,
3373
- # #<Lutaml::Model::ValueError: description must be one of [one, two, three]>
4559
+ # #<Lutaml::Model::ValueError: description must be one of [one, two, three]>,
4560
+ # #<Lutaml::Model::ChoiceUpperBoundError: Attribute count exceeds the upper bound>
4561
+ # #<Lutaml::Model::ChoiceLowerBoundError: Attribute count is less than lower bound>
3374
4562
  # ]
3375
4563
  ----
3376
4564
  ====
@@ -3407,6 +4595,30 @@ klin.validate
3407
4595
  ====
3408
4596
 
3409
4597
 
4598
+ == Liquid Compatability
4599
+
4600
+ `to_liquid` can be used to convert a class that inherit from *Lutaml::Model::Serializable* to `LiquidDrop` to be safely used in liquid templates. The returned drop provides all the attributes defined in the class as methods.
4601
+
4602
+ [example]
4603
+ ====
4604
+ [source,ruby]
4605
+ ----
4606
+ class Person < Lutaml::Model::Serializable
4607
+ attribute :name, :string
4608
+ attribute :age, integer
4609
+ end
4610
+
4611
+ person = Person.new({ name: "John", age: 22 })
4612
+ person_drop = person.to_liquid
4613
+ # Person::PersonDrop
4614
+
4615
+ puts person_drop.name
4616
+ # "John"
4617
+ puts person_drop.age
4618
+ # 22
4619
+ ----
4620
+ ====
4621
+
3410
4622
  == Adapters
3411
4623
 
3412
4624
  === General
@@ -3701,6 +4913,15 @@ attribute for every element.
3701
4913
  | Requires manual specification on every XML element that uses it.
3702
4914
  |
3703
4915
 
4916
+
4917
+ | Compiling XML Schema to *Lutaml::Model::Serializable* classes
4918
+ | Yes. Using <<xml-schema-to-model-files, `Lutaml::Model::Schema#from_xml`>>
4919
+
4920
+ 1. *ComplexTypes* are compiled to *Lutaml::Model::Serializable* classes containing the attributes.
4921
+ 2. *SimpleTypes* are compiled to *Lutaml::Model::Type::Value* classes to support XML Schema level validations.
4922
+ | Yes, Provides only an array of the classes and doesn't support `simple types` with restrictions and/or other validations.
4923
+ |
4924
+
3704
4925
  4+h| Attribute features
3705
4926
 
3706
4927
  | Attribute delegation