lutaml-model 0.2.1 → 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,201 +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
- === Key Value Data Models: JSON, YAML, TOML
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
224
+
225
+ Lutaml::Model allows you to translate a data model into serialization models of
226
+ various serialization formats including XML, JSON, YAML, and TOML.
227
+
228
+ Depending on the serialization format, different methods are supported for
229
+ defining serialization and deserialization mappings.
109
230
 
110
- Define key-value data models like JSON, YAML, and TOML using the `map` method.
231
+ Serialization model mappings are defined under the `xml`, `json`, `yaml`, and
232
+ `toml` blocks.
111
233
 
234
+ .Using the `xml`, `json`, `yaml`, and `toml` blocks to define serialization mappings
112
235
  [source,ruby]
113
236
  ----
114
237
  class Example < Lutaml::Model::Serializable
115
- attribute :name, Lutaml::Model::Type::String
116
- attribute :value, Lutaml::Model::Type::Integer
238
+ xml do
239
+ # ...
240
+ end
117
241
 
118
242
  json do
119
- map 'name', to: :name
120
- map 'value', to: :value
243
+ # ...
121
244
  end
122
245
 
123
246
  yaml do
124
- map 'name', to: :name
125
- map 'value', to: :value
247
+ # ...
126
248
  end
127
249
 
128
250
  toml do
129
- map 'name', to: :name
130
- map 'value', to: :value
251
+ # ...
131
252
  end
132
253
  end
133
254
  ----
134
255
 
135
- == Develop Serialization and Deserialization Mappings
256
+ === XML
257
+
258
+ ==== Setting root element name
259
+
260
+ The `root` method sets the root element tag name of the XML document.
136
261
 
137
- Lutaml::Model supports various methods for defining serialization and deserialization mappings.
262
+ If `root` is not given, then the snake-cased class name will be used as the
263
+ root.
138
264
 
139
- === XML (`map_element`, `map_attribute`, `map_content`)
265
+ [example]
266
+ Sets the tag name for `<example>` in XML `<example>...</example>`.
140
267
 
141
- Use `map_element` to map XML elements, `map_attribute` to map XML attributes, and `map_content` to map text content within an XML element.
268
+ Syntax:
142
269
 
143
270
  [source,ruby]
144
271
  ----
145
- class Example < Lutaml::Model::Serializable
146
- attribute :name, Lutaml::Model::Type::String
147
- attribute :description, Lutaml::Model::Type::String
272
+ xml do
273
+ root 'xml_element_name'
274
+ end
275
+ ----
148
276
 
277
+ .Setting the root element name to `example`
278
+ [example]
279
+ ====
280
+ [source,ruby]
281
+ ----
282
+ class Example < Lutaml::Model::Serializable
149
283
  xml do
150
284
  root 'example'
151
- map_element 'name', to: :name
152
- map_content to: :description
153
285
  end
154
286
  end
155
287
  ----
156
288
 
157
- === 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`.
158
303
 
159
- Use the `map` method to define JSON mappings.
304
+ Syntax:
305
+
306
+ [source,ruby]
307
+ ----
308
+ xml do
309
+ map_element 'xml_element_name', to: :name_of_attribute
310
+ end
311
+ ----
160
312
 
313
+ .Mapping the `name` tag to the `name` attribute
314
+ [example]
315
+ ====
161
316
  [source,ruby]
162
317
  ----
163
318
  class Example < Lutaml::Model::Serializable
164
319
  attribute :name, Lutaml::Model::Type::String
165
- attribute :value, Lutaml::Model::Type::Integer
166
320
 
167
- json do
168
- map 'name', to: :name
169
- map 'value', to: :value
321
+ xml do
322
+ root 'example'
323
+ map_element 'name', to: :name
170
324
  end
171
325
  end
172
326
  ----
173
327
 
174
- === YAML
175
-
176
- Use the `map` method to define YAML mappings.
328
+ [source,xml]
329
+ ----
330
+ <example><name>John Doe</name></example>
331
+ ----
177
332
 
178
333
  [source,ruby]
179
334
  ----
180
- class Example < Lutaml::Model::Serializable
181
- attribute :name, Lutaml::Model::Type::String
182
- 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
+ ====
183
341
 
184
- yaml do
185
- map 'name', to: :name
186
- map 'value', to: :value
187
- 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
188
352
  end
189
353
  ----
190
354
 
191
- === TOML
192
-
193
- 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:
194
359
 
195
360
  [source,ruby]
196
361
  ----
197
362
  class Example < Lutaml::Model::Serializable
198
- attribute :name, Lutaml::Model::Type::String
199
363
  attribute :value, Lutaml::Model::Type::Integer
200
364
 
201
- toml do
202
- map 'name', to: :name
203
- map 'value', to: :value
365
+ xml do
366
+ root 'example'
367
+ map_attribute 'value', to: :value
204
368
  end
205
369
  end
206
370
  ----
207
371
 
208
- == Attribute Collections Using the `collection` Option
209
-
210
- 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
+ ----
211
376
 
212
377
  [source,ruby]
213
378
  ----
214
- class Studio < Lutaml::Model::Serializable
215
- attribute :location, Lutaml::Model::Type::String
216
- attribute :potters, Lutaml::Model::Type::String, collection: true
217
- end
379
+ > Example.from_xml(xml)
380
+ > #<Example:0x0000000104ac7240 @value=12>
381
+ > Example.new(value: 12).to_xml
382
+ > #<example value="12"></example>
218
383
  ----
384
+ ====
219
385
 
220
- == Attribute Defaults Using the `default` Option
221
386
 
222
- 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:
223
395
 
224
396
  [source,ruby]
225
397
  ----
226
- class Glaze < Lutaml::Model::Serializable
227
- attribute :color, Lutaml::Model::Type::String, default: -> { 'Clear' }
228
- attribute :temperature, Lutaml::Model::Type::Integer, default: -> { 1050 }
398
+ xml do
399
+ map_content to: :name_of_attribute
229
400
  end
230
401
  ----
231
402
 
232
- == Attribute Delegation Using the `delegate` Option
233
-
234
- 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:
235
407
 
236
408
  [source,ruby]
237
409
  ----
238
- class Ceramic < Lutaml::Model::Serializable
239
- attribute :type, Lutaml::Model::Type::String
240
- attribute :glaze, Glaze
410
+ class Example < Lutaml::Model::Serializable
411
+ attribute :description, Lutaml::Model::Type::String
241
412
 
242
- json do
243
- map 'type', to: :type
244
- map 'color', to: :color, delegate: :glaze
413
+ xml do
414
+ root 'example'
415
+ map_content to: :description
245
416
  end
246
417
  end
247
418
  ----
248
419
 
249
- == 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
+
250
435
 
251
- Define custom methods for specific attribute mappings using the `with:` key for each serialization mapping block.
436
+ ==== Example for mapping
437
+
438
+ [example]
439
+ ====
440
+ The following class will parse the XML snippet below:
252
441
 
253
442
  [source,ruby]
254
443
  ----
255
- class CustomCeramic < Lutaml::Model::Serializable
444
+ class Example < Lutaml::Model::Serializable
256
445
  attribute :name, Lutaml::Model::Type::String
257
- attribute :size, Lutaml::Model::Type::Integer
446
+ attribute :description, Lutaml::Model::Type::String
447
+ attribute :value, Lutaml::Model::Type::Integer
258
448
 
259
- json do
260
- map 'name', to: :name, with: { to: :name_to_json, from: :name_from_json }
261
- 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
262
454
  end
455
+ end
456
+ ----
263
457
 
264
- def name_to_json(model, value)
265
- "Masterpiece: #{value}"
266
- end
458
+ [source,xml]
459
+ ----
460
+ <example value=12><name>John Doe</name> is my moniker.</example>
461
+ ----
267
462
 
268
- def name_from_json(model, doc)
269
- doc['name'].sub('Masterpiece: ', '')
270
- end
271
- end
463
+ [source,ruby]
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>
272
469
  ----
470
+ ====
471
+
472
+
473
+ ==== Namespaces
273
474
 
274
- == Using XML Namespaces
475
+ ===== Namespace at root
275
476
 
276
- Define XML namespaces for your models to handle namespaced XML elements.
477
+ The `namespace` method in the `xml` block sets the namespace for the root
478
+ element.
277
479
 
278
- === XML Namespace on Element
480
+ Syntax:
279
481
 
482
+ [source,ruby]
483
+ ----
484
+ xml do
485
+ namespace 'http://example.com/namespace'
486
+ end
487
+ ----
488
+
489
+ .Using the `namespace` method to set the namespace for the root element
490
+ [example]
491
+ ====
280
492
  [source,ruby]
281
493
  ----
282
494
  class Ceramic < Lutaml::Model::Serializable
@@ -292,89 +504,805 @@ class Ceramic < Lutaml::Model::Serializable
292
504
  end
293
505
  ----
294
506
 
295
- === 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)
539
+
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.
296
545
 
297
546
  [source,ruby]
298
547
  ----
548
+ class Glaze < Lutaml::Model::Serializable
549
+ attribute :color, Lutaml::Model::Type::String
550
+ attribute :temperature, Lutaml::Model::Type::Integer
551
+
552
+ xml do
553
+ root 'Glaze'
554
+ namespace 'http://example.com/old_glaze', 'glaze'
555
+
556
+ map_element 'color', to: :color
557
+ map_element 'temperature', to: :temperature
558
+ end
559
+ end
560
+
299
561
  class Ceramic < Lutaml::Model::Serializable
300
562
  attribute :type, Lutaml::Model::Type::String
301
- attribute :glaze, Lutaml::Model::Type::String
563
+ attribute :glaze, Glaze
302
564
 
303
565
  xml do
304
566
  root 'Ceramic'
305
567
  map_element 'Type', to: :type
306
- map_element 'Glaze', to: :glaze
568
+ map_element 'Glaze', to: :glaze, namespace: 'http://example.com/glaze', prefix: "glz"
307
569
  map_attribute 'xmlns', to: :namespace, namespace: 'http://example.com/ceramic'
308
570
  end
309
571
  end
310
572
  ----
311
573
 
312
- === 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.
313
612
 
314
613
  [source,ruby]
315
614
  ----
316
615
  class Ceramic < Lutaml::Model::Serializable
317
616
  attribute :type, Lutaml::Model::Type::String
318
617
  attribute :glaze, Lutaml::Model::Type::String
618
+ attribute :color, Lutaml::Model::Type::String
319
619
 
320
620
  xml do
321
621
  root 'Ceramic'
322
622
  namespace 'http://example.com/ceramic', prefix: 'cera'
323
623
  map_element 'Type', to: :type, namespace: :inherit
324
624
  map_element 'Glaze', to: :glaze
625
+ map_attribute 'color', to: :color, namespace: 'http://example.com/color', prefix: 'clr'
325
626
  end
326
627
  end
327
628
  ----
328
629
 
329
- == Adapters
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
+ ====
654
+
655
+
656
+ ==== Mixed content
657
+
658
+ ===== General
659
+
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.
662
+
663
+ [example]
664
+ ====
665
+ [source,xml]
666
+ ----
667
+ <description><p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p></description>
668
+ ----
669
+ ====
670
+
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
701
+
702
+ map_element 'bold', to: :bold
703
+ map_element 'i', to: :italic
704
+ end
705
+ end
706
+ ----
707
+
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`?
718
+
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:
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
+ ====
737
+ [source,ruby]
738
+ ----
739
+ class Paragraph < Lutaml::Model::Serializable
740
+ attribute :bold, Lutaml::Model::Type::String
741
+ attribute :italic, Lutaml::Model::Type::String
742
+
743
+ xml do
744
+ root 'p'
745
+
746
+ map_element 'bold', to: :bold
747
+ map_element 'i', to: :italic
748
+ end
749
+ end
750
+
751
+ class Description < Lutaml::Model::Serializable
752
+ attribute :paragraph, Paragraph
753
+
754
+ xml do
755
+ root 'description'
756
+
757
+ map_element 'p', to: :paragraph, mixed: true
758
+ end
759
+ end
760
+ ----
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
330
850
 
331
- Lutaml::Model uses an adapter pattern to support multiple libraries for each serialization format.
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
+ ----
332
867
 
333
- === XML: Nokogiri, Oga, Ox
868
+ [source,json]
869
+ ----
870
+ {
871
+ "type": "Porcelain",
872
+ "glaze": {
873
+ "color": "Clear",
874
+ "temperature": 1050
875
+ }
876
+ }
877
+ ----
334
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
+
1196
+ == Adapters
1197
+
1198
+ === General
1199
+
1200
+ Lutaml::Model uses an adapter pattern to support multiple libraries for each
1201
+ serialization format.
1202
+
1203
+ === XML
1204
+
1205
+ Lutaml::Model supports the following XML adapters:
1206
+
1207
+ * Nokogiri (default)
1208
+ * Oga (optional, plain Ruby suitable for Opal/JS)
1209
+ * Ox (optional)
1210
+
1211
+ .Using the Nokogiri XML adapter
335
1212
  [source,ruby]
336
1213
  ----
337
1214
  require 'lutaml/model'
338
- require 'lutaml/model/xml_adapter/nokogiri_adapter'
339
- require 'lutaml/model/xml_adapter/ox_adapter'
340
- require 'lutaml/model/xml_adapter/oga_adapter'
341
1215
 
342
1216
  Lutaml::Model::Config.configure do |config|
1217
+ require 'lutaml/model/xml_adapter/nokogiri_adapter'
343
1218
  config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
344
- # Or use OxAdapter or OgaAdapter
345
1219
  end
346
1220
  ----
347
1221
 
348
- === 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
1246
+
1247
+ Lutaml::Model supports the following JSON adapters:
1248
+
1249
+ * JSON (default)
1250
+ * MultiJson (optional)
349
1251
 
1252
+ .Using the JSON adapter
350
1253
  [source,ruby]
351
1254
  ----
352
1255
  require 'lutaml/model'
353
- require 'lutaml/model/json_adapter/standard'
354
- require 'lutaml/model/json_adapter/multi_json'
355
1256
 
356
1257
  Lutaml::Model::Config.configure do |config|
1258
+ require 'lutaml/model/json_adapter/standard'
357
1259
  config.json_adapter = Lutaml::Model::JsonAdapter::StandardDocument
358
- # Or use MultiJsonDocument
359
1260
  end
360
1261
  ----
361
1262
 
362
- === 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:
363
1277
 
1278
+ * Toml-rb (default)
1279
+ * Tomlib (optional)
1280
+
1281
+ .Using the Toml-rb adapter
364
1282
  [source,ruby]
365
1283
  ----
366
1284
  require 'lutaml/model'
367
- require 'lutaml/model/toml_adapter/toml_rb_adapter'
368
- require 'lutaml/model/toml_adapter/tomlib_adapter'
369
1285
 
370
1286
  Lutaml::Model::Config.configure do |config|
1287
+ require 'lutaml/model/toml_adapter/toml_rb_adapter'
371
1288
  config.toml_adapter = Lutaml::Model::TomlAdapter::TomlRbDocument
372
- # 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'
373
1300
  end
374
1301
  ----
375
1302
 
376
1303
  == License and Copyright
377
1304
 
378
- 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.
379
1307
 
380
- This project is maintained by Ribose.
1308
+ Copyright Ribose.