lutaml-model 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.adoc CHANGED
@@ -1,4 +1,4 @@
1
- = LutaML Ruby modeller: `Lutaml::Model`
1
+ = LutaML Ruby modeller
2
2
 
3
3
  https://github.com/lutaml/lutaml-model[image:https://img.shields.io/github/stars/lutaml/lutaml-model.svg?style=social[GitHub Stars]]
4
4
  https://github.com/lutaml/lutaml-model[image:https://img.shields.io/github/forks/lutaml/lutaml-model.svg?style=social[GitHub Forks]]
@@ -6,39 +6,41 @@ image:https://img.shields.io/github/license/lutaml/lutaml-model.svg[License]
6
6
  image:https://img.shields.io/github/actions/workflow/status/lutaml/lutaml-model/test.yml?branch=main[Build Status]
7
7
  image:https://img.shields.io/gem/v/lutaml-model.svg[RubyGems Version]
8
8
 
9
- == What is Lutaml::Model
9
+ == Purpose
10
10
 
11
11
  Lutaml::Model is a lightweight library for serializing and deserializing Ruby
12
12
  objects to and from various formats such as JSON, XML, YAML, and TOML. It uses
13
13
  an adapter pattern to support multiple libraries for each format, providing
14
14
  flexibility and extensibility for your data modeling needs.
15
15
 
16
- The name "LutaML" comes from the Latin word "Lutum," which means clay, and "ML"
16
+ The name "LutaML" comes from the Latin word for clay, "Lutum", and "ML"
17
17
  for Markup Language. Just as clay can be molded and modeled into beautiful and
18
18
  practical end products, the Lutaml::Model gem is used for data modeling,
19
19
  allowing you to shape and structure your data into useful forms.
20
20
 
21
21
 
22
+ NOTE: Lutaml::Model is designed to be compatible with the
23
+ https://www.shalerb.org[Shale] data modeling API. Shale is an amazing Ruby data
24
+ modeller. Lutaml::Model is meant to address needs that are not currently
25
+ addressed by Shale.
22
26
 
23
- NOTE: Lutaml::Model is designed to be compatible with the Shale data modeling
24
- API. Shale is an amazing Ruby data modeller. Lutaml::Model is meant to address
25
- needs that are not currently addresed by Shale.
26
27
 
27
- == Introduction to Data Modeling
28
+ == Data modeling in a nutshell
28
29
 
29
30
  Data modeling is the process of creating a data model for the data to be stored
30
31
  in a database or used in an application. It helps in defining the structure,
31
32
  relationships, and constraints of the data, making it easier to manage and use.
32
33
 
33
34
  Lutaml::Model simplifies data modeling in Ruby by allowing you to define models
34
- with attributes and serialize/deserialize them to/from various formats
35
- seamlessly.
35
+ with attributes and serialize/deserialize them to/from various serialization
36
+ formats seamlessly.
37
+
36
38
 
37
39
  == Features
38
40
 
39
41
  * Define models with attributes and types
40
42
  * Serialize and deserialize models to/from JSON, XML, YAML, and TOML
41
- * Support for multiple libraries (e.g., `toml-rb`, `tomlib`)
43
+ * Support for multiple serialization libraries (e.g., `toml-rb`, `tomlib`)
42
44
  * Configurable adapters for different serialization formats
43
45
  * Support for collections and default values
44
46
  * Custom serialization/deserialization methods
@@ -67,9 +69,21 @@ Or install it yourself as:
67
69
  gem install lutaml-model
68
70
  ----
69
71
 
70
- == Writing a Data Model in Ruby
72
+ == Defining a data model class
73
+
74
+ === General
75
+
76
+ There are two ways to define a data model in Lutaml::Model:
77
+
78
+ * Inheriting from the `Lutaml::Model::Serializable` class
79
+ * Including the `Lutaml::Model::Serialize` module
71
80
 
72
- To define a model, inherit from `Lutaml::Model::Serializable` and use the `attribute` class method to define attributes.
81
+ === Definition through inheritance
82
+
83
+ The simplest way to define a model is to create a class that inherits from
84
+ `Lutaml::Model::Serializable`.
85
+
86
+ The `attribute` class method is used to define attributes.
73
87
 
74
88
  [source,ruby]
75
89
  ----
@@ -82,203 +96,399 @@ class Kiln < Lutaml::Model::Serializable
82
96
  end
83
97
  ----
84
98
 
85
- == Translating a Data Model into Different Serialization Models
99
+ === Definition through inclusion
100
+
101
+ If the model class already has a super class that it inherits from, the model
102
+ can be extended using the `Lutaml::Model::Serialize` module.
103
+
104
+ [source,ruby]
105
+ ----
106
+ require 'lutaml/model'
107
+
108
+ class Kiln < SomeSuperClass
109
+ include Lutaml::Model::Serialize
110
+
111
+ attribute :brand, Lutaml::Model::Type::String
112
+ attribute :capacity, Lutaml::Model::Type::Integer
113
+ attribute :temperature, Lutaml::Model::Type::Integer
114
+ end
115
+ ----
116
+
117
+ == Defining attributes
86
118
 
87
- Lutaml::Model allows you to translate a data model into various serialization
88
- formats including XML, JSON, YAML, and TOML.
119
+ === Attribute as a collection
89
120
 
90
- === XML: Element, Attribute, Namespaces
121
+ Define attributes as collections (arrays or hashes) to store multiple values
122
+ using the `collection` option.
91
123
 
92
- Define XML mappings using `map_element`, `map_attribute`, and `map_content`.
124
+ Syntax:
93
125
 
94
126
  [source,ruby]
95
127
  ----
96
- class Example < Lutaml::Model::Serializable
97
- attribute :name, Lutaml::Model::Type::String
98
- attribute :value, Lutaml::Model::Type::Integer
128
+ attribute :name_of_attribute, Type, collection: true
129
+ ----
99
130
 
100
- xml do
101
- root 'example'
102
- map_element 'name', to: :name
103
- map_attribute 'value', to: :value
104
- end
131
+ .Using the `collection` option to define a collection attribute
132
+ [example]
133
+ ====
134
+ [source,ruby]
135
+ ----
136
+ class Studio < Lutaml::Model::Serializable
137
+ attribute :location, Lutaml::Model::Type::String
138
+ attribute :potters, Lutaml::Model::Type::String, collection: true
139
+ end
140
+ ----
141
+
142
+ [source,ruby]
143
+ ----
144
+ > Studio.new.potters
145
+ > # []
146
+ > Studio.new(potters: ['John Doe', 'Jane Doe']).potters
147
+ > # ['John Doe', 'Jane Doe']
148
+ ----
149
+ ====
150
+
151
+ === Attribute as an enumeration
152
+
153
+ An attribute can be defined as an enumeration by using the `values` directive.
154
+
155
+ The `values` directive is used to define acceptable values in an attribute. If
156
+ any other value is given, a `Lutaml::Model::InvalidValueError` will be raised.
157
+
158
+ Syntax:
159
+
160
+ [source,ruby]
161
+ ----
162
+ attribute :name_of_attribute, Type, values: [value1, value2, ...]
163
+ ----
164
+
165
+ .Using the `values` directive to define acceptable values for an attribute
166
+ [example]
167
+ ====
168
+ [source,ruby]
169
+ ----
170
+ class Ceramic < Lutaml::Model::Serializable
171
+ attribute :type, Lutaml::Model::Type::String,
172
+ values: ['Porcelain', 'Earthenware', 'Stoneware']
173
+ end
174
+ ----
175
+
176
+ [source,ruby]
177
+ ----
178
+ > Ceramic.new(type: 'Porcelain').type
179
+ > # "Porcelain"
180
+ > Ceramic.new(type: 'Earthenware').type
181
+ > # "Earthenware"
182
+ > Ceramic.new(type: 'Bone China').type
183
+ > # Lutaml::Model::InvalidValueError: Invalid value for attribute 'type'
184
+ ----
185
+ ====
186
+
187
+
188
+ === Attribute value default
189
+
190
+ Specify default values for attributes using the `default` option.
191
+ The `default` option can be set to a value or a lambda that returns a value.
192
+
193
+ Syntax:
194
+
195
+ [source,ruby]
196
+ ----
197
+ attribute :name_of_attribute, Type, default: -> { value }
198
+ ----
199
+
200
+
201
+ .Using the `default` option to set a default value for an attribute
202
+ [example]
203
+ ====
204
+ [source,ruby]
205
+ ----
206
+ class Glaze < Lutaml::Model::Serializable
207
+ attribute :color, Lutaml::Model::Type::String, default: -> { 'Clear' }
208
+ attribute :temperature, Lutaml::Model::Type::Integer, default: -> { 1050 }
105
209
  end
106
210
  ----
107
211
 
108
- NOTE: If root is not given then the class name will be used as the root.
212
+ [source,ruby]
213
+ ----
214
+ > Glaze.new.color
215
+ > # "Clear"
216
+ > Glaze.new.temperature
217
+ > # 1050
218
+ ----
219
+ ====
220
+
221
+ == Serialization model mappings
222
+
223
+ === General
109
224
 
110
- === Key Value Data Models: JSON, YAML, TOML
225
+ Lutaml::Model allows you to translate a data model into serialization models of
226
+ various serialization formats including XML, JSON, YAML, and TOML.
111
227
 
112
- Define key-value data models like JSON, YAML, and TOML using the `map` method.
228
+ Depending on the serialization format, different methods are supported for
229
+ defining serialization and deserialization mappings.
113
230
 
231
+ Serialization model mappings are defined under the `xml`, `json`, `yaml`, and
232
+ `toml` blocks.
233
+
234
+ .Using the `xml`, `json`, `yaml`, and `toml` blocks to define serialization mappings
114
235
  [source,ruby]
115
236
  ----
116
237
  class Example < Lutaml::Model::Serializable
117
- attribute :name, Lutaml::Model::Type::String
118
- attribute :value, Lutaml::Model::Type::Integer
238
+ xml do
239
+ # ...
240
+ end
119
241
 
120
242
  json do
121
- map 'name', to: :name
122
- map 'value', to: :value
243
+ # ...
123
244
  end
124
245
 
125
246
  yaml do
126
- map 'name', to: :name
127
- map 'value', to: :value
247
+ # ...
128
248
  end
129
249
 
130
250
  toml do
131
- map 'name', to: :name
132
- map 'value', to: :value
251
+ # ...
133
252
  end
134
253
  end
135
254
  ----
136
255
 
137
- == Develop Serialization and Deserialization Mappings
256
+ === XML
257
+
258
+ ==== Setting root element name
138
259
 
139
- Lutaml::Model supports various methods for defining serialization and deserialization mappings.
260
+ The `root` method sets the root element tag name of the XML document.
140
261
 
141
- === XML (`map_element`, `map_attribute`, `map_content`)
262
+ If `root` is not given, then the snake-cased class name will be used as the
263
+ root.
142
264
 
143
- Use `map_element` to map XML elements, `map_attribute` to map XML attributes, and `map_content` to map text content within an XML element.
265
+ [example]
266
+ Sets the tag name for `<example>` in XML `<example>...</example>`.
267
+
268
+ Syntax:
144
269
 
145
270
  [source,ruby]
146
271
  ----
147
- class Example < Lutaml::Model::Serializable
148
- attribute :name, Lutaml::Model::Type::String
149
- attribute :description, Lutaml::Model::Type::String
272
+ xml do
273
+ root 'xml_element_name'
274
+ end
275
+ ----
150
276
 
277
+ .Setting the root element name to `example`
278
+ [example]
279
+ ====
280
+ [source,ruby]
281
+ ----
282
+ class Example < Lutaml::Model::Serializable
151
283
  xml do
152
284
  root 'example'
153
- map_element 'name', to: :name
154
- map_content to: :description
155
285
  end
156
286
  end
157
287
  ----
158
288
 
159
- === JSON (`map` method)
289
+ [source,ruby]
290
+ ----
291
+ > Example.new.to_xml
292
+ > #<example></example>
293
+ ----
294
+ ====
295
+
296
+ ==== Mapping elements
297
+
298
+ The `map_element` method maps an XML element to a data model attribute.
299
+
300
+ [example]
301
+ To handle the `<name>` tag in `<example><name>John Doe</name></example>`.
302
+ The value will be set to `John Doe`.
303
+
304
+ Syntax:
160
305
 
161
- Use the `map` method to define JSON mappings.
306
+ [source,ruby]
307
+ ----
308
+ xml do
309
+ map_element 'xml_element_name', to: :name_of_attribute
310
+ end
311
+ ----
162
312
 
313
+ .Mapping the `name` tag to the `name` attribute
314
+ [example]
315
+ ====
163
316
  [source,ruby]
164
317
  ----
165
318
  class Example < Lutaml::Model::Serializable
166
319
  attribute :name, Lutaml::Model::Type::String
167
- attribute :value, Lutaml::Model::Type::Integer
168
320
 
169
- json do
170
- map 'name', to: :name
171
- map 'value', to: :value
321
+ xml do
322
+ root 'example'
323
+ map_element 'name', to: :name
172
324
  end
173
325
  end
174
326
  ----
175
327
 
176
- === YAML
177
-
178
- Use the `map` method to define YAML mappings.
328
+ [source,xml]
329
+ ----
330
+ <example><name>John Doe</name></example>
331
+ ----
179
332
 
180
333
  [source,ruby]
181
334
  ----
182
- class Example < Lutaml::Model::Serializable
183
- attribute :name, Lutaml::Model::Type::String
184
- attribute :value, Lutaml::Model::Type::Integer
335
+ > Example.from_xml(xml)
336
+ > #<Example:0x0000000104ac7240 @name="John Doe">
337
+ > Example.new(name: "John Doe").to_xml
338
+ > #<example><name>John Doe</name></example>
339
+ ----
340
+ ====
185
341
 
186
- yaml do
187
- map 'name', to: :name
188
- map 'value', to: :value
189
- end
342
+ ==== Mapping attributes
343
+
344
+ The `map_attribute` method maps an XML attribute to a data model attribute.
345
+
346
+ Syntax:
347
+
348
+ [source,ruby]
349
+ ----
350
+ xml do
351
+ map_attribute 'xml_attribute_name', to: :name_of_attribute
190
352
  end
191
353
  ----
192
354
 
193
- === TOML
194
-
195
- Use the `map` method to define TOML mappings.
355
+ .Using `map_attribute` to map the `value` attribute
356
+ [example]
357
+ ====
358
+ The following class will parse the XML snippet below:
196
359
 
197
360
  [source,ruby]
198
361
  ----
199
362
  class Example < Lutaml::Model::Serializable
200
- attribute :name, Lutaml::Model::Type::String
201
363
  attribute :value, Lutaml::Model::Type::Integer
202
364
 
203
- toml do
204
- map 'name', to: :name
205
- map 'value', to: :value
365
+ xml do
366
+ root 'example'
367
+ map_attribute 'value', to: :value
206
368
  end
207
369
  end
208
370
  ----
209
371
 
210
- == Attribute Collections Using the `collection` Option
211
-
212
- You can define attributes as collections (arrays or hashes) to store multiple values.
372
+ [source,xml]
373
+ ----
374
+ <example value=12><name>John Doe</name></example>
375
+ ----
213
376
 
214
377
  [source,ruby]
215
378
  ----
216
- class Studio < Lutaml::Model::Serializable
217
- attribute :location, Lutaml::Model::Type::String
218
- attribute :potters, Lutaml::Model::Type::String, collection: true
219
- end
379
+ > Example.from_xml(xml)
380
+ > #<Example:0x0000000104ac7240 @value=12>
381
+ > Example.new(value: 12).to_xml
382
+ > #<example value="12"></example>
220
383
  ----
384
+ ====
221
385
 
222
- == Attribute Defaults Using the `default` Option
223
386
 
224
- Specify default values for attributes using the `default` option.
387
+ ==== Mapping content
388
+
389
+ Content represents the text inside an XML element, inclusive of whitespace.
390
+
391
+ The `map_content` method maps an XML element's content to a data model
392
+ attribute.
393
+
394
+ Syntax:
225
395
 
226
396
  [source,ruby]
227
397
  ----
228
- class Glaze < Lutaml::Model::Serializable
229
- attribute :color, Lutaml::Model::Type::String, default: -> { 'Clear' }
230
- attribute :temperature, Lutaml::Model::Type::Integer, default: -> { 1050 }
398
+ xml do
399
+ map_content to: :name_of_attribute
231
400
  end
232
401
  ----
233
402
 
234
- == Attribute Delegation Using the `delegate` Option
235
-
236
- Delegate attribute mappings to nested objects using the `delegate` option.
403
+ .Using `map_content` to map content of the `description` tag
404
+ [example]
405
+ ====
406
+ The following class will parse the XML snippet below:
237
407
 
238
408
  [source,ruby]
239
409
  ----
240
- class Ceramic < Lutaml::Model::Serializable
241
- attribute :type, Lutaml::Model::Type::String
242
- attribute :glaze, Glaze
410
+ class Example < Lutaml::Model::Serializable
411
+ attribute :description, Lutaml::Model::Type::String
243
412
 
244
- json do
245
- map 'type', to: :type
246
- map 'color', to: :color, delegate: :glaze
413
+ xml do
414
+ root 'example'
415
+ map_content to: :description
247
416
  end
248
417
  end
249
418
  ----
250
419
 
251
- == Attribute Serialization with Custom `from` and `to` Methods
420
+ [source,xml]
421
+ ----
422
+ <example>John Doe is my moniker.</example>
423
+ ----
424
+
425
+ [source,ruby]
426
+ ----
427
+ > Example.from_xml(xml)
428
+ > #<Example:0x0000000104ac7240 @description="John Doe is my moniker.">
429
+ > Example.new(description: "John Doe is my moniker.").to_xml
430
+ > #<example>John Doe is my moniker.</example>
431
+ ----
432
+ ====
433
+
434
+
435
+
436
+ ==== Example for mapping
252
437
 
253
- Define custom methods for specific attribute mappings using the `with:` key for each serialization mapping block.
438
+ [example]
439
+ ====
440
+ The following class will parse the XML snippet below:
254
441
 
255
442
  [source,ruby]
256
443
  ----
257
- class CustomCeramic < Lutaml::Model::Serializable
444
+ class Example < Lutaml::Model::Serializable
258
445
  attribute :name, Lutaml::Model::Type::String
259
- attribute :size, Lutaml::Model::Type::Integer
446
+ attribute :description, Lutaml::Model::Type::String
447
+ attribute :value, Lutaml::Model::Type::Integer
260
448
 
261
- json do
262
- map 'name', to: :name, with: { to: :name_to_json, from: :name_from_json }
263
- map 'size', to: :size
449
+ xml do
450
+ root 'example'
451
+ map_element 'name', to: :name
452
+ map_attribute 'value', to: :value
453
+ map_content to: :description
264
454
  end
455
+ end
456
+ ----
265
457
 
266
- def name_to_json(model, value)
267
- "Masterpiece: #{value}"
268
- end
458
+ [source,xml]
459
+ ----
460
+ <example value=12><name>John Doe</name> is my moniker.</example>
461
+ ----
269
462
 
270
- def name_from_json(model, doc)
271
- doc['name'].sub('Masterpiece: ', '')
272
- end
273
- end
463
+ [source,ruby]
274
464
  ----
465
+ > Example.from_xml(xml)
466
+ > #<Example:0x0000000104ac7240 @name="John Doe", @description=" is my moniker.", @value=12>
467
+ > Example.new(name: "John Doe", description: " is my moniker.", value: 12).to_xml
468
+ > #<example value="12"><name>John Doe</name> is my moniker.</example>
469
+ ----
470
+ ====
471
+
472
+
473
+ ==== Namespaces
474
+
475
+ ===== Namespace at root
275
476
 
276
- == Using XML Namespaces
477
+ The `namespace` method in the `xml` block sets the namespace for the root
478
+ element.
277
479
 
278
- Define XML namespaces for your models to handle namespaced XML elements.
480
+ Syntax:
279
481
 
280
- === XML Namespace on Element
482
+ [source,ruby]
483
+ ----
484
+ xml do
485
+ namespace 'http://example.com/namespace'
486
+ end
487
+ ----
281
488
 
489
+ .Using the `namespace` method to set the namespace for the root element
490
+ [example]
491
+ ====
282
492
  [source,ruby]
283
493
  ----
284
494
  class Ceramic < Lutaml::Model::Serializable
@@ -294,9 +504,44 @@ class Ceramic < Lutaml::Model::Serializable
294
504
  end
295
505
  ----
296
506
 
297
- === XML Namespace on Attribute
507
+ [source,xml]
508
+ ----
509
+ <Ceramic xmlns='http://example.com/ceramic'><Type>Porcelain</Type><Glaze>Clear</Glaze></Ceramic>
510
+ ----
511
+
512
+ [source,ruby]
513
+ ----
514
+ > Ceramic.from_xml(xml_file)
515
+ > #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze="Clear">
516
+ > Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
517
+ > #<Ceramic xmlns="http://example.com/ceramic"><Type>Porcelain</Type><Glaze>Clear</Glaze></Ceramic>
518
+ ----
519
+ ====
520
+
521
+ ===== Namespace on attribute
522
+
523
+ If the namespace is defined on an XML attribute, then that will be given
524
+ priority over the one defined in the class.
525
+
526
+ Syntax:
527
+
528
+ [source,ruby]
529
+ ----
530
+ xml do
531
+ map_element 'xml_element_name', to: :name_of_attribute,
532
+ namespace: 'http://example.com/namespace',
533
+ prefix: 'prefix'
534
+ end
535
+ ----
536
+
537
+ `namespace`:: The XML namespace used by this element
538
+ `prefix`:: The XML namespace prefix used by this element (optional)
298
539
 
299
- If the namespace is defined on an attribute then that will be given priority over the one defined in the class. In the example below `glz` will be used for `Glaze` if it is added inside the `Ceramic` class, and `glaze` will be used otherwise.
540
+ .Using the `namespace` option to set the namespace for an element
541
+ [example]
542
+ ====
543
+ In this example, `glz` will be used for `Glaze` if it is added inside the
544
+ `Ceramic` class, and `glaze` will be used otherwise.
300
545
 
301
546
  [source,ruby]
302
547
  ----
@@ -326,41 +571,133 @@ class Ceramic < Lutaml::Model::Serializable
326
571
  end
327
572
  ----
328
573
 
329
- === XML Namespace with `inherit` Option
574
+ [source,xml]
575
+ ----
576
+ <Ceramic xmlns='http://example.com/ceramic'>
577
+ <Type>Porcelain</Type>
578
+ <glz:Glaze xmlns='http://example.com/glaze'>
579
+ <color>Clear</color>
580
+ <temperature>1050</temperature>
581
+ </glz:Glaze>
582
+ </Ceramic>
583
+ ----
584
+
585
+ [source,ruby]
586
+ ----
587
+ > Ceramic.from_xml(xml_file)
588
+ > #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze=#<Glaze:0x0000000104ac7240 @color="Clear", @temperature=1050>>
589
+ > Ceramic.new(type: "Porcelain", glaze: Glaze.new(color: "Clear", temperature: 1050)).to_xml
590
+ > #<Ceramic xmlns="http://example.com/ceramic"><Type>Porcelain</Type><glz:Glaze xmlns="http://example.com/glaze"><color>Clear</color><temperature>1050</temperature></glz:Glaze></Ceramic>
591
+ ----
592
+ ====
593
+
594
+ ===== Namespace with `inherit` option
595
+
596
+ The `inherit` option is used at the element level to inherit the namespace from
597
+ the root element.
598
+
599
+ Syntax:
600
+
601
+ [source,ruby]
602
+ ----
603
+ xml do
604
+ map_element 'xml_element_name', to: :name_of_attribute, namespace: :inherit
605
+ end
606
+ ----
607
+
608
+ .Using the `inherit` option to inherit the namespace from the root element
609
+ [example]
610
+ ====
611
+ In this example, the `Type` element will inherit the namespace from the root.
330
612
 
331
613
  [source,ruby]
332
614
  ----
333
615
  class Ceramic < Lutaml::Model::Serializable
334
616
  attribute :type, Lutaml::Model::Type::String
335
617
  attribute :glaze, Lutaml::Model::Type::String
618
+ attribute :color, Lutaml::Model::Type::String
336
619
 
337
620
  xml do
338
621
  root 'Ceramic'
339
622
  namespace 'http://example.com/ceramic', prefix: 'cera'
340
623
  map_element 'Type', to: :type, namespace: :inherit
341
624
  map_element 'Glaze', to: :glaze
625
+ map_attribute 'color', to: :color, namespace: 'http://example.com/color', prefix: 'clr'
342
626
  end
343
627
  end
344
628
  ----
345
629
 
346
- == Using XML `mixed` option
630
+ [source,xml]
631
+ ----
632
+ <Ceramic
633
+ xmlns:cera='http://example.com/ceramic'
634
+ xmlns:clr='http://example.com/color'
635
+ clr:color="navy-blue">
636
+ <cera:Type>Porcelain</cera:Type>
637
+ <Glaze>Clear</Glaze>
638
+ </Ceramic>
639
+ ----
640
+
641
+ [source,ruby]
642
+ ----
643
+ > Ceramic.from_xml(xml_file)
644
+ > #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze="Clear", @color="navy-blue">
645
+ > Ceramic.new(type: "Porcelain", glaze: "Clear", color: "navy-blue").to_xml
646
+ > #<Ceramic xmlns:cera="http://example.com/ceramic"
647
+ # xmlns:clr='http://example.com/color'
648
+ # clr:color="navy-blue">
649
+ # <cera:Type>Porcelain</cera:Type>
650
+ # <Glaze>Clear</Glaze>
651
+ # </Ceramic>
652
+ ----
653
+ ====
347
654
 
348
- In XML there can be some tags that containg content mixed with other tags for example `<description><p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p></description>`
349
655
 
350
- To map this to Lutaml::Model we can use the mixed option when defining the model or when referencing it.
656
+ ==== Mixed content
351
657
 
352
- === Adding mixed option when defining a Model
658
+ ===== General
353
659
 
354
- This will always treat the content of `<p>` tag as mixed content.
660
+ In XML there can be tags that contain content mixed with other tags and where
661
+ whitespace is significant, such as to represent rich text.
355
662
 
356
- [source,ruby]
663
+ [example]
664
+ ====
665
+ [source,xml]
357
666
  ----
358
- class Paragraph < Lutaml::Model::Serializable
359
- attribute :bold, Lutaml::Model::Type::String
360
- attribute :italic, Lutaml::Model::Type::String
667
+ <description><p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p></description>
668
+ ----
669
+ ====
361
670
 
362
- xml do
363
- root 'p', mixed: true
671
+ To map this to Lutaml::Model we can use the `mixed` option in either way:
672
+
673
+ * when defining the model;
674
+ * when referencing the model.
675
+
676
+
677
+ ===== Specifying the `mixed` option at `root`
678
+
679
+ This will always treat the content of the element itself as mixed content.
680
+
681
+ Syntax:
682
+
683
+ [source,ruby]
684
+ ----
685
+ xml do
686
+ root 'xml_element_name', mixed: true
687
+ end
688
+ ----
689
+
690
+ .Applying `mixed` to treat root as mixed content
691
+ [example]
692
+ ====
693
+ [source,ruby]
694
+ ----
695
+ class Paragraph < Lutaml::Model::Serializable
696
+ attribute :bold, Lutaml::Model::Type::String
697
+ attribute :italic, Lutaml::Model::Type::String
698
+
699
+ xml do
700
+ root 'p', mixed: true
364
701
 
365
702
  map_element 'bold', to: :bold
366
703
  map_element 'i', to: :italic
@@ -368,10 +705,35 @@ class Paragraph < Lutaml::Model::Serializable
368
705
  end
369
706
  ----
370
707
 
371
- === Adding mixed option when referencing a Model
708
+ [source,ruby]
709
+ ----
710
+ > Paragraph.from_xml("<p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p>")
711
+ > #<Paragraph:0x0000000104ac7240 @bold="John Doe", @italic="28">
712
+ > Paragraph.new(bold: "John Doe", italic: "28").to_xml
713
+ > #<p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p>
714
+ ----
715
+ ====
716
+
717
+ TODO: How to create mixed content from `#new`?
372
718
 
373
- This will only treat the content of `<p>` tag as mixed content if the `mixed: true` is added when referencing it.
719
+
720
+ ===== Specifying the `mixed` option when referencing a model
721
+
722
+ This will only treat the content of the referenced model as mixed content if the
723
+ `mixed: true` is added when referencing it.
724
+
725
+ Syntax:
374
726
 
727
+ [source,ruby]
728
+ ----
729
+ xml do
730
+ map_element 'xml_element_name', to: :name_of_attribute, mixed: true
731
+ end
732
+ ----
733
+
734
+ .Applying `mixed` to treat an inner element as mixed content
735
+ [example]
736
+ ====
375
737
  [source,ruby]
376
738
  ----
377
739
  class Paragraph < Lutaml::Model::Serializable
@@ -397,55 +759,550 @@ class Description < Lutaml::Model::Serializable
397
759
  end
398
760
  ----
399
761
 
762
+ [source,ruby]
763
+ ----
764
+ > Description.from_xml("<description><p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p></description>")
765
+ > #<Description:0x0000000104ac7240 @paragraph=#<Paragraph:0x0000000104ac7240 @bold="John Doe", @italic="28">>
766
+ > Description.new(paragraph: Paragraph.new(bold: "John Doe", italic: "28")).to_xml
767
+ > #<description><p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p></description>
768
+ ----
769
+ ====
770
+
771
+
772
+ === Key value data models
773
+
774
+ ==== General
775
+
776
+ Key-value data models like JSON, YAML, and TOML all share a similar structure
777
+ where data is stored as key-value pairs.
778
+
779
+ Lutaml::Model works with these formats in a similar way.
780
+
781
+ ==== Mapping
782
+
783
+ The `map` method is used to define key-value mappings.
784
+
785
+ Syntax:
786
+
787
+ [source,ruby]
788
+ ----
789
+ json | yaml | toml do
790
+ map 'key_value_model_attribute_name', to: :name_of_attribute
791
+ end
792
+ ----
793
+
794
+ .Using the `map` method to define key-value mappings
795
+ [example]
796
+ ====
797
+ [source,ruby]
798
+ ----
799
+ class Example < Lutaml::Model::Serializable
800
+ attribute :name, Lutaml::Model::Type::String
801
+ attribute :value, Lutaml::Model::Type::Integer
802
+
803
+ json do
804
+ map 'name', to: :name
805
+ map 'value', to: :value
806
+ end
807
+
808
+ yaml do
809
+ map 'name', to: :name
810
+ map 'value', to: :value
811
+ end
812
+
813
+ toml do
814
+ map 'name', to: :name
815
+ map 'value', to: :value
816
+ end
817
+ end
818
+ ----
819
+
820
+ [source,json]
821
+ ----
822
+ {
823
+ "name": "John Doe",
824
+ "value": 28
825
+ }
826
+ ----
827
+
828
+ [source,ruby]
829
+ ----
830
+ > Example.from_json(json)
831
+ > #<Example:0x0000000104ac7240 @name="John Doe", @value=28>
832
+ > Example.new(name: "John Doe", value: 28).to_json
833
+ > #{"name"=>"John Doe", "value"=>28}
834
+ ----
835
+ ====
836
+
837
+
838
+ ==== Nested attribute mappings
839
+
840
+ The `map` method can also be used to map nested key-value data models
841
+ by referring to a Lutaml::Model class as an attribute class.
842
+
843
+ [example]
844
+ ====
845
+ [source,ruby]
846
+ ----
847
+ class Glaze < Lutaml::Model::Serializable
848
+ attribute :color, Lutaml::Model::Type::String
849
+ attribute :temperature, Lutaml::Model::Type::Integer
850
+
851
+ json do
852
+ map 'color', to: :color
853
+ map 'temperature', to: :temperature
854
+ end
855
+ end
856
+
857
+ class Ceramic < Lutaml::Model::Serializable
858
+ attribute :type, Lutaml::Model::Type::String
859
+ attribute :glaze, Glaze
860
+
861
+ json do
862
+ map 'type', to: :type
863
+ map 'glaze', to: :glaze
864
+ end
865
+ end
866
+ ----
867
+
868
+ [source,json]
869
+ ----
870
+ {
871
+ "type": "Porcelain",
872
+ "glaze": {
873
+ "color": "Clear",
874
+ "temperature": 1050
875
+ }
876
+ }
877
+ ----
878
+
879
+ [source,ruby]
880
+ ----
881
+ > Ceramic.from_json(json)
882
+ > #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze=#<Glaze:0x0000000104ac7240 @color="Clear", @temperature=1050>>
883
+ > Ceramic.new(type: "Porcelain", glaze: Glaze.new(color: "Clear", temperature: 1050)).to_json
884
+ > #{"type"=>"Porcelain", "glaze"=>{"color"=>"Clear", "temperature"=>1050}}
885
+ ----
886
+ ====
887
+
888
+ === Advanced attribute mapping
889
+
890
+ ==== Attribute mapping delegation
891
+
892
+ Delegate attribute mappings to nested objects using the `delegate` option.
893
+
894
+ Syntax:
895
+
896
+ [source,ruby]
897
+ ----
898
+ xml | json | yaml | toml do
899
+ map 'key_value_model_attribute_name', to: :name_of_attribute, delegate: :model_to_delegate_to
900
+ end
901
+ ----
902
+
903
+ .Using the `delegate` option to map attributes to nested objects
904
+ [example]
905
+ ====
906
+ The following class will parse the JSON snippet below:
907
+
908
+ [source,ruby]
909
+ ----
910
+ class Glaze < Lutaml::Model::Serializable
911
+ attribute :color, Lutaml::Model::Type::String
912
+ attribute :temperature, Lutaml::Model::Type::Integer
913
+
914
+ json do
915
+ map 'color', to: :color
916
+ map 'temperature', to: :temperature
917
+ end
918
+ end
919
+
920
+ class Ceramic < Lutaml::Model::Serializable
921
+ attribute :type, Lutaml::Model::Type::String
922
+ attribute :glaze, Glaze
923
+
924
+ json do
925
+ map 'type', to: :type
926
+ map 'color', to: :color, delegate: :glaze
927
+ end
928
+ end
929
+ ----
930
+
931
+ [source,json]
932
+ ----
933
+ {
934
+ "type": "Porcelain",
935
+ "color": "Clear"
936
+ }
937
+ ----
938
+
939
+ [source,ruby]
940
+ ----
941
+ > Ceramic.from_json(json)
942
+ > #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze=#<Glaze:0x0000000104ac7240 @color="Clear", @temperature=nil>>
943
+ > Ceramic.new(type: "Porcelain", glaze: Glaze.new(color: "Clear")).to_json
944
+ > #{"type"=>"Porcelain", "color"=>"Clear"}
945
+ ----
946
+ ====
947
+
948
+
949
+ ==== Attribute serialization with custom methods
950
+
951
+ Define custom methods for specific attribute mappings using the `with:` key for
952
+ each serialization mapping block for `from` and `to`.
953
+
954
+ Syntax:
955
+
956
+ [source,ruby]
957
+ ----
958
+ xml | json | yaml | toml do
959
+ map 'key_value_model_attribute_name', to: :name_of_attribute, with: {
960
+ to: :method_name_to_serialize,
961
+ from: :method_name_to_deserialize
962
+ }
963
+ end
964
+ ----
965
+
966
+ .Using the `with:` key to define custom serialization methods
967
+ [example]
968
+ ====
969
+ The following class will parse the JSON snippet below:
970
+
971
+ [source,ruby]
972
+ ----
973
+ class CustomCeramic < Lutaml::Model::Serializable
974
+ attribute :name, Lutaml::Model::Type::String
975
+ attribute :size, Lutaml::Model::Type::Integer
976
+
977
+ json do
978
+ map 'name', to: :name, with: { to: :name_to_json, from: :name_from_json }
979
+ map 'size', to: :size
980
+ end
981
+
982
+ def name_to_json(model, value)
983
+ "Masterpiece: #{value}"
984
+ end
985
+
986
+ def name_from_json(model, doc)
987
+ doc['name'].sub(/^Masterpiece: /, '')
988
+ end
989
+ end
990
+ ----
991
+
992
+ [source,json]
993
+ ----
994
+ {
995
+ "name": "Masterpiece: Vase",
996
+ "size": 12
997
+ }
998
+ ----
999
+
1000
+ [source,ruby]
1001
+ ----
1002
+ > CustomCeramic.from_json(json)
1003
+ > #<CustomCeramic:0x0000000104ac7240 @name="Vase", @size=12>
1004
+ > CustomCeramic.new(name: "Vase", size: 12).to_json
1005
+ > #{"name"=>"Masterpiece: Vase", "size"=>12}
1006
+ ----
1007
+ ====
1008
+
1009
+
1010
+
1011
+ ==== Attribute extraction
1012
+
1013
+ NOTE: This feature is for key-value data model serialization only.
1014
+
1015
+ The `child_mappings` option is used to extract results from a key-value data
1016
+ model (JSON, YAML, TOML) into a `Lutaml::Model` collection.
1017
+
1018
+ The values are extracted from the key-value data model using the list of keys
1019
+ provided.
1020
+
1021
+ Syntax:
1022
+
1023
+ [source,ruby]
1024
+ ----
1025
+ json | yaml | toml do
1026
+ map 'key_value_model_attribute_name', to: :name_of_attribute,
1027
+ child_mappings: {
1028
+ key_attribute_name_1: <1>
1029
+ {path_to_value_1}, <2>
1030
+ key_attribute_name_2:
1031
+ {path_to_value_2},
1032
+ # ...
1033
+ }
1034
+ end
1035
+ ----
1036
+ <1> The `key_attribute_name_1` is the attribute name in the model. The value of
1037
+ this attribute will be assigned the key of the hash in the key-value data model.
1038
+
1039
+ <2> The `path_to_value_1` is an array of keys that represent the path to the
1040
+ value in the key-value data model. The keys are used to extract the value from
1041
+ the key-value data model and assign it to the attribute in the model.
1042
+
1043
+ The `path_to_value` is in a nested array format with each value a symbol, where
1044
+ each symbol represents a key to traverse down. The last key in the path is the
1045
+ value to be extracted.
1046
+
1047
+ .Determining the path to value in a key-value data model
1048
+ [example]
1049
+ ====
1050
+ The following JSON contains 2 keys in schema named `engine` and `gearbox`.
1051
+
1052
+ [source,json]
1053
+ ----
1054
+ {
1055
+ "components": {
1056
+ "engine": {
1057
+ "manufacturer": "Ford",
1058
+ "model": "V8"
1059
+ },
1060
+ "gearbox": {
1061
+ "manufacturer": "Toyota",
1062
+ "model": "4-speed"
1063
+ }
1064
+ }
1065
+ }
1066
+ ----
1067
+
1068
+ The path to value for the `engine` schema is `[:components, :engine]` and for
1069
+ the `gearbox` schema is `[:components, :gearbox]`.
1070
+ ====
1071
+
1072
+ In `path_to_value`, the `:key` and `:value` are reserved instructions used to
1073
+ assign the key or value of the serialization data respectively as the value to
1074
+ the attribute.
1075
+
1076
+ [example]
1077
+ ====
1078
+ In the following JSON content, the `path_to_value` for the object keys named
1079
+ `engine` and `gearbox` will utilize the `:key` keyword to assign the key of the
1080
+ object as the value of a designated attribute.
1081
+
1082
+ [source,json]
1083
+ ----
1084
+ {
1085
+ "components": {
1086
+ "engine": { /*...*/ },
1087
+ "gearbox": { /*...*/ }
1088
+ }
1089
+ }
1090
+ ----
1091
+ ====
1092
+
1093
+ If a specified value path is not found, the corresponding attribute in the model
1094
+ will be assigned a `nil` value.
1095
+
1096
+ .Attribute values set to `nil` when the `path_to_value` is not found
1097
+ [example]
1098
+ ====
1099
+ In the following JSON content, the `path_to_value` of `[:extras, :sunroof]` and
1100
+ `[:extras, :drinks_cooler]` at the object `"gearbox"` would be set to `nil`.
1101
+
1102
+ [source,json]
1103
+ ----
1104
+ {
1105
+ "components": {
1106
+ "engine": {
1107
+ "manufacturer": "Ford",
1108
+ "extras": {
1109
+ "sunroof": true,
1110
+ "drinks_cooler": true
1111
+ }
1112
+ },
1113
+ "gearbox": {
1114
+ "manufacturer": "Toyota"
1115
+ }
1116
+ }
1117
+ }
1118
+ ----
1119
+ ====
1120
+
1121
+
1122
+ .Using the `child_mappings` option to extract values from a key-value data model
1123
+ [example]
1124
+ ====
1125
+ The following JSON contains 2 keys in schema named `foo` and `bar`.
1126
+
1127
+ [source,json]
1128
+ ----
1129
+ {
1130
+ "schemas": {
1131
+ "foo": { <1>
1132
+ "path": { <2>
1133
+ "link": "link one",
1134
+ "name": "one"
1135
+ }
1136
+ },
1137
+ "bar": { <1>
1138
+ "path": { <2>
1139
+ "link": "link two",
1140
+ "name": "two"
1141
+ }
1142
+ }
1143
+ }
1144
+ }
1145
+ ----
1146
+ <1> The keys `foo` and `bar` are to be mapped to the `id` attribute.
1147
+ <2> The nested `path.link` and `path.name` keys are used as the `link` and
1148
+ `name` attributes, respectively.
1149
+
1150
+ A model can be defined for this JSON as follows:
1151
+
1152
+ [source,ruby]
1153
+ ----
1154
+ class Schema < Lutaml::Model::Serializable
1155
+ attribute :id, Lutaml::Model::Type::String
1156
+ attribute :link, Lutaml::Model::Type::String
1157
+ attribute :name, Lutaml::Model::Type::String
1158
+ end
1159
+
1160
+ class ChildMappingClass < Lutaml::Model::Serializable
1161
+ attribute :schemas, Schema, collection: true
1162
+
1163
+ json do
1164
+ map "schemas", to: :schemas,
1165
+ child_mappings: {
1166
+ id: :key,
1167
+ link: %i[path link],
1168
+ name: %i[path name],
1169
+ }
1170
+ end
1171
+ end
1172
+ ----
1173
+
1174
+ The output becomes:
1175
+
1176
+ [source,ruby]
1177
+ ----
1178
+ > ChildMappingClass.from_json(json)
1179
+ > #<ChildMappingClass:0x0000000104ac7240
1180
+ @schemas=
1181
+ [#<Schema:0x0000000104ac6e30 @id="foo", @link="link one", @name="one">,
1182
+ #<Schema:0x0000000104ac58f0 @id="bar", @link="link two", @name="two">]>
1183
+ > ChildMappingClass.new(schemas: [Schema.new(id: "foo", link: "link one", name: "one"), Schema.new(id: "bar", link: "link two", name: "two")]).to_json
1184
+ > #{"schemas"=>{"foo"=>{"path"=>{"link"=>"link one", "name"=>"one"}}, {"bar"=>{"path"=>{"link"=>"link two", "name"=>"two"}}}}
1185
+ ----
1186
+
1187
+ In this example:
1188
+
1189
+ * The `key` of each schema (`foo` and `bar`) is mapped to the `id` attribute.
1190
+
1191
+ * The nested `path.link` and `path.name` keys are mapped to the `link` and
1192
+ `name` attributes, respectively.
1193
+ ====
1194
+
1195
+
400
1196
  == Adapters
401
1197
 
402
- Lutaml::Model uses an adapter pattern to support multiple libraries for each serialization format.
1198
+ === General
1199
+
1200
+ Lutaml::Model uses an adapter pattern to support multiple libraries for each
1201
+ serialization format.
1202
+
1203
+ === XML
403
1204
 
404
- === XML: Nokogiri, Oga, Ox
1205
+ Lutaml::Model supports the following XML adapters:
405
1206
 
1207
+ * Nokogiri (default)
1208
+ * Oga (optional, plain Ruby suitable for Opal/JS)
1209
+ * Ox (optional)
1210
+
1211
+ .Using the Nokogiri XML adapter
406
1212
  [source,ruby]
407
1213
  ----
408
1214
  require 'lutaml/model'
409
- require 'lutaml/model/xml_adapter/nokogiri_adapter'
410
- require 'lutaml/model/xml_adapter/ox_adapter'
411
- require 'lutaml/model/xml_adapter/oga_adapter'
412
1215
 
413
1216
  Lutaml::Model::Config.configure do |config|
1217
+ require 'lutaml/model/xml_adapter/nokogiri_adapter'
414
1218
  config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
415
- # Or use OxAdapter or OgaAdapter
416
1219
  end
417
1220
  ----
418
1221
 
419
- === JSON: `JSON` and `MultiJson`
1222
+ .Using the Oga XML adapter
1223
+ [source,ruby]
1224
+ ----
1225
+ require 'lutaml/model'
1226
+
1227
+ Lutaml::Model::Config.configure do |config|
1228
+ require 'lutaml/model/xml_adapter/oga_adapter'
1229
+ config.xml_adapter = Lutaml::Model::XmlAdapter::OgaAdapter
1230
+ end
1231
+ ----
1232
+
1233
+ .Using the Ox XML adapter
1234
+ [source,ruby]
1235
+ ----
1236
+ require 'lutaml/model'
1237
+
1238
+ Lutaml::Model::Config.configure do |config|
1239
+ require 'lutaml/model/xml_adapter/ox_adapter'
1240
+ config.xml_adapter = Lutaml::Model::XmlAdapter::OxAdapter
1241
+ end
1242
+ ----
1243
+
1244
+
1245
+ === JSON
420
1246
 
1247
+ Lutaml::Model supports the following JSON adapters:
1248
+
1249
+ * JSON (default)
1250
+ * MultiJson (optional)
1251
+
1252
+ .Using the JSON adapter
421
1253
  [source,ruby]
422
1254
  ----
423
1255
  require 'lutaml/model'
424
- require 'lutaml/model/json_adapter/standard'
425
- require 'lutaml/model/json_adapter/multi_json'
426
1256
 
427
1257
  Lutaml::Model::Config.configure do |config|
1258
+ require 'lutaml/model/json_adapter/standard'
428
1259
  config.json_adapter = Lutaml::Model::JsonAdapter::StandardDocument
429
- # Or use MultiJsonDocument
430
1260
  end
431
1261
  ----
432
1262
 
433
- === TOML: `Tomlib` and `Toml-rb`
1263
+ .Using the MultiJson adapter
1264
+ [source,ruby]
1265
+ ----
1266
+ require 'lutaml/model'
1267
+
1268
+ Lutaml::Model::Config.configure do |config|
1269
+ require 'lutaml/model/json_adapter/multi_json'
1270
+ config.json_adapter = Lutaml::Model::JsonAdapter::MultiJsonDocument
1271
+ end
1272
+ ----
1273
+
1274
+ === TOML
1275
+
1276
+ Lutaml::Model supports the following TOML adapters:
1277
+
1278
+ * Toml-rb (default)
1279
+ * Tomlib (optional)
434
1280
 
1281
+ .Using the Toml-rb adapter
435
1282
  [source,ruby]
436
1283
  ----
437
1284
  require 'lutaml/model'
438
- require 'lutaml/model/toml_adapter/toml_rb_adapter'
439
- require 'lutaml/model/toml_adapter/tomlib_adapter'
440
1285
 
441
1286
  Lutaml::Model::Config.configure do |config|
1287
+ require 'lutaml/model/toml_adapter/toml_rb_adapter'
442
1288
  config.toml_adapter = Lutaml::Model::TomlAdapter::TomlRbDocument
443
- # Or use TomlibDocument
1289
+ end
1290
+ ----
1291
+
1292
+ .Using the Tomlib adapter
1293
+ [source,ruby]
1294
+ ----
1295
+ require 'lutaml/model'
1296
+
1297
+ Lutaml::Model::Config.configure do |config|
1298
+ config.toml_adapter = Lutaml::Model::TomlAdapter::TomlibDocument
1299
+ require 'lutaml/model/toml_adapter/tomlib_adapter'
444
1300
  end
445
1301
  ----
446
1302
 
447
1303
  == License and Copyright
448
1304
 
449
- This project is licensed under the BSD 2-clause License - see the LICENSE file for details.
1305
+ This project is licensed under the BSD 2-clause License.
1306
+ See the LICENSE file for details.
450
1307
 
451
- This project is maintained by Ribose.
1308
+ Copyright Ribose.