lutaml-model 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.adoc CHANGED
@@ -69,16 +69,18 @@ Or install it yourself as:
69
69
  gem install lutaml-model
70
70
  ----
71
71
 
72
- == Defining a data model class
72
+ == Data model class
73
73
 
74
- === General
74
+ === Definition
75
+
76
+ ==== General
75
77
 
76
78
  There are two ways to define a data model in Lutaml::Model:
77
79
 
78
80
  * Inheriting from the `Lutaml::Model::Serializable` class
79
81
  * Including the `Lutaml::Model::Serialize` module
80
82
 
81
- === Definition through inheritance
83
+ ==== Definition through inheritance
82
84
 
83
85
  The simplest way to define a model is to create a class that inherits from
84
86
  `Lutaml::Model::Serializable`.
@@ -90,13 +92,13 @@ The `attribute` class method is used to define attributes.
90
92
  require 'lutaml/model'
91
93
 
92
94
  class Kiln < Lutaml::Model::Serializable
93
- attribute :brand, Lutaml::Model::Type::String
94
- attribute :capacity, Lutaml::Model::Type::Integer
95
- attribute :temperature, Lutaml::Model::Type::Integer
95
+ attribute :brand, :string
96
+ attribute :capacity, :integer
97
+ attribute :temperature, :integer
96
98
  end
97
99
  ----
98
100
 
99
- === Definition through inclusion
101
+ ==== Definition through inclusion
100
102
 
101
103
  If the model class already has a super class that it inherits from, the model
102
104
  can be extended using the `Lutaml::Model::Serialize` module.
@@ -108,12 +110,38 @@ require 'lutaml/model'
108
110
  class Kiln < SomeSuperClass
109
111
  include Lutaml::Model::Serialize
110
112
 
111
- attribute :brand, Lutaml::Model::Type::String
112
- attribute :capacity, Lutaml::Model::Type::Integer
113
- attribute :temperature, Lutaml::Model::Type::Integer
113
+ attribute :brand, :string
114
+ attribute :capacity, :integer
115
+ attribute :temperature, :integer
114
116
  end
115
117
  ----
116
118
 
119
+
120
+ === Comparison
121
+
122
+ A `Serialize` / `Serializable` object can be compared with another object of the
123
+ same class using the `==` operator. This is implemented through the
124
+ `ComparableModel` module.
125
+
126
+ Two objects are considered equal if they have the same class and all their
127
+ attributes are equal. This behavior differs from the typical Ruby behavior,
128
+ where two objects are considered equal only if they have the same object ID.
129
+
130
+ NOTE: Two `Serialize` objects will have the same `hash` value if they have the
131
+ same class and all their attributes are equal.
132
+
133
+ [source,ruby]
134
+ ----
135
+ > a = Kiln.new(brand: 'Kiln 1', capacity: 100, temperature: 1050)
136
+ > b = Kiln.new(brand: 'Kiln 1', capacity: 100, temperature: 1050)
137
+ > a == b
138
+ > # true
139
+ > a.hash == b.hash
140
+ > # true
141
+ ----
142
+
143
+
144
+
117
145
  == Defining attributes
118
146
 
119
147
  === Supported attribute value types
@@ -160,7 +188,7 @@ class Studio < Lutaml::Model::Serializable
160
188
  # The following are equivalent
161
189
  attribute :location, :string
162
190
  attribute :potter, "String"
163
- attribute :kiln, Lutaml::Model::Type::String
191
+ attribute :kiln, :string
164
192
  end
165
193
  ----
166
194
 
@@ -196,8 +224,8 @@ attribute :name_of_attribute, Type, collection: true
196
224
  [source,ruby]
197
225
  ----
198
226
  class Studio < Lutaml::Model::Serializable
199
- attribute :location, Lutaml::Model::Type::String
200
- attribute :potters, Lutaml::Model::Type::String, collection: true
227
+ attribute :location, :string
228
+ attribute :potters, :string, collection: true
201
229
  end
202
230
  ----
203
231
 
@@ -224,25 +252,85 @@ Syntax:
224
252
  attribute :name_of_attribute, Type, values: [value1, value2, ...]
225
253
  ----
226
254
 
227
- .Using the `values` directive to define acceptable values for an attribute
255
+ The values set inside the `values:` option can be of any type, but they must
256
+ match the type of the attribute. The values are compared using the `==` operator,
257
+ so the type must implement the `==` method.
258
+
259
+ .Using the `values` directive to define acceptable values for an attribute (basic types)
260
+ [example]
261
+ ====
262
+ [source,ruby]
263
+ ----
264
+ class GlazeTechnique < Lutaml::Model::Serializable
265
+ attribute :name, :string, values: ["Celadon", "Raku", "Majolica"]
266
+ end
267
+ ----
268
+
269
+ [source,ruby]
270
+ ----
271
+ > GlazeTechnique.new(name: "Celadon").name
272
+ > # "Celadon"
273
+ > GlazeTechnique.new(name: "Raku").name
274
+ > # "Raku"
275
+ > GlazeTechnique.new(name: "Majolica").name
276
+ > # "Majolica"
277
+ > GlazeTechnique.new(name: "Earthenware").name
278
+ > # Lutaml::Model::InvalidValueError: Invalid value for attribute 'name'
279
+ ----
280
+ ====
281
+
282
+ The values can be Serialize objects, which are compared using the `==`
283
+ and the `hash` methods through the Lutaml::Model::ComparableModel module.
284
+
285
+
286
+ .Using the `values` directive to define acceptable values for an attribute (Serializable objects)
228
287
  [example]
229
288
  ====
230
289
  [source,ruby]
231
290
  ----
232
291
  class Ceramic < Lutaml::Model::Serializable
233
- attribute :type, Lutaml::Model::Type::String,
234
- values: ['Porcelain', 'Earthenware', 'Stoneware']
292
+ attribute :type, :string
293
+ attribute :firing_temperature, :integer
294
+ end
295
+
296
+ class CeramicCollection < Lutaml::Model::Serializable
297
+ attribute :featured_piece,
298
+ Ceramic,
299
+ values: [
300
+ Ceramic.new(type: "Porcelain", firing_temperature: 1300),
301
+ Ceramic.new(type: "Stoneware", firing_temperature: 1200),
302
+ Ceramic.new(type: "Earthenware", firing_temperature: 1000),
303
+ ]
235
304
  end
236
305
  ----
237
306
 
238
307
  [source,ruby]
239
308
  ----
240
- > Ceramic.new(type: 'Porcelain').type
241
- > # "Porcelain"
242
- > Ceramic.new(type: 'Earthenware').type
243
- > # "Earthenware"
244
- > Ceramic.new(type: 'Bone China').type
245
- > # Lutaml::Model::InvalidValueError: Invalid value for attribute 'type'
309
+ > CeramicCollection.new(featured_piece: Ceramic.new(type: "Porcelain", firing_temperature: 1300)).featured_piece
310
+ > # Ceramic:0x0000000104ac7240 @type="Porcelain", @firing_temperature=1300
311
+ > CeramicCollection.new(featured_piece: Ceramic.new(type: "Bone China", firing_temperature: 1300)).featured_piece
312
+ > # Lutaml::Model::InvalidValueError: Invalid value for attribute 'featured_piece'
313
+ ----
314
+ ====
315
+
316
+ Serialize provides a `validate` method that checks if all its attributes have
317
+ valid values. This is necessary for the case when a value is valid at the
318
+ component level, but not accepted at the aggregation level.
319
+
320
+ If a change has been made at the component level (a nested attribute has
321
+ changed), the aggregation level needs to call the `validate` method to verify
322
+ acceptance of the newly updated component.
323
+
324
+ .Using the `validate` method to check if all attributes have valid values
325
+ [example]
326
+ ====
327
+ [source,ruby]
328
+ ----
329
+ > collection = CeramicCollection.new(featured_piece: Ceramic.new(type: "Porcelain", firing_temperature: 1300))
330
+ > collection.featured_piece.firing_temperature = 1400
331
+ > # No error raised in changed nested attribute
332
+ > collection.validate
333
+ > # Lutaml::Model::InvalidValueError: Invalid value for attribute 'featured_piece'
246
334
  ----
247
335
  ====
248
336
 
@@ -266,8 +354,8 @@ attribute :name_of_attribute, Type, default: -> { value }
266
354
  [source,ruby]
267
355
  ----
268
356
  class Glaze < Lutaml::Model::Serializable
269
- attribute :color, Lutaml::Model::Type::String, default: -> { 'Clear' }
270
- attribute :temperature, Lutaml::Model::Type::Integer, default: -> { 1050 }
357
+ attribute :color, :string, default: -> { 'Clear' }
358
+ attribute :temperature, :integer, default: -> { 1050 }
271
359
  end
272
360
  ----
273
361
 
@@ -378,7 +466,7 @@ end
378
466
  [source,ruby]
379
467
  ----
380
468
  class Example < Lutaml::Model::Serializable
381
- attribute :name, Lutaml::Model::Type::String
469
+ attribute :name, :string
382
470
 
383
471
  xml do
384
472
  root 'example'
@@ -401,6 +489,42 @@ end
401
489
  ----
402
490
  ====
403
491
 
492
+ If an element is mapped to a model object with the XML `root` tag name set, the
493
+ mapped tag name will be used as the root name, overriding the root name.
494
+
495
+ .The mapped tag name is used as the root name
496
+ [example]
497
+ ====
498
+ [source,ruby]
499
+ ----
500
+ class RecordDate < Lutaml::Model::Serializable
501
+ attribute :content, :string
502
+
503
+ xml do
504
+ root "recordDate"
505
+ map_content to: :content
506
+ end
507
+ end
508
+
509
+ class OriginInfo < Lutaml::Model::Serializable
510
+ attribute :date_issued, RecordDate, collection: true
511
+
512
+ xml do
513
+ root "originInfo"
514
+ map_element "dateIssued", to: :date_issued
515
+ end
516
+ end
517
+ ----
518
+
519
+ [source,ruby]
520
+ ----
521
+ > RecordDate.new(date: "2021-01-01").to_xml
522
+ > #<recordDate>2021-01-01</recordDate>
523
+ > OriginInfo.new(date_issued: [RecordDate.new(date: "2021-01-01")]).to_xml
524
+ > #<originInfo><dateIssued>2021-01-01</dateIssued></originInfo>
525
+ ----
526
+ ====
527
+
404
528
  ==== Mapping attributes
405
529
 
406
530
  The `map_attribute` method maps an XML attribute to a data model attribute.
@@ -422,7 +546,7 @@ The following class will parse the XML snippet below:
422
546
  [source,ruby]
423
547
  ----
424
548
  class Example < Lutaml::Model::Serializable
425
- attribute :value, Lutaml::Model::Type::Integer
549
+ attribute :value, :integer
426
550
 
427
551
  xml do
428
552
  root 'example'
@@ -470,7 +594,7 @@ The following class will parse the XML snippet below:
470
594
  [source,ruby]
471
595
  ----
472
596
  class Example < Lutaml::Model::Serializable
473
- attribute :description, Lutaml::Model::Type::String
597
+ attribute :description, :string
474
598
 
475
599
  xml do
476
600
  root 'example'
@@ -504,9 +628,9 @@ The following class will parse the XML snippet below:
504
628
  [source,ruby]
505
629
  ----
506
630
  class Example < Lutaml::Model::Serializable
507
- attribute :name, Lutaml::Model::Type::String
508
- attribute :description, Lutaml::Model::Type::String
509
- attribute :value, Lutaml::Model::Type::Integer
631
+ attribute :name, :string
632
+ attribute :description, :string
633
+ attribute :value, :integer
510
634
 
511
635
  xml do
512
636
  root 'example'
@@ -554,8 +678,8 @@ end
554
678
  [source,ruby]
555
679
  ----
556
680
  class Ceramic < Lutaml::Model::Serializable
557
- attribute :type, Lutaml::Model::Type::String
558
- attribute :glaze, Lutaml::Model::Type::String
681
+ attribute :type, :string
682
+ attribute :glaze, :string
559
683
 
560
684
  xml do
561
685
  root 'Ceramic'
@@ -608,8 +732,8 @@ In this example, `glz` will be used for `Glaze` if it is added inside the
608
732
  [source,ruby]
609
733
  ----
610
734
  class Glaze < Lutaml::Model::Serializable
611
- attribute :color, Lutaml::Model::Type::String
612
- attribute :temperature, Lutaml::Model::Type::Integer
735
+ attribute :color, :string
736
+ attribute :temperature, :integer
613
737
 
614
738
  xml do
615
739
  root 'Glaze'
@@ -621,7 +745,7 @@ class Glaze < Lutaml::Model::Serializable
621
745
  end
622
746
 
623
747
  class Ceramic < Lutaml::Model::Serializable
624
- attribute :type, Lutaml::Model::Type::String
748
+ attribute :type, :string
625
749
  attribute :glaze, Glaze
626
750
 
627
751
  xml do
@@ -675,9 +799,9 @@ In this example, the `Type` element will inherit the namespace from the root.
675
799
  [source,ruby]
676
800
  ----
677
801
  class Ceramic < Lutaml::Model::Serializable
678
- attribute :type, Lutaml::Model::Type::String
679
- attribute :glaze, Lutaml::Model::Type::String
680
- attribute :color, Lutaml::Model::Type::String
802
+ attribute :type, :string
803
+ attribute :glaze, :string
804
+ attribute :color, :string
681
805
 
682
806
  xml do
683
807
  root 'Ceramic'
@@ -755,8 +879,8 @@ end
755
879
  [source,ruby]
756
880
  ----
757
881
  class Paragraph < Lutaml::Model::Serializable
758
- attribute :bold, Lutaml::Model::Type::String
759
- attribute :italic, Lutaml::Model::Type::String
882
+ attribute :bold, :string
883
+ attribute :italic, :string
760
884
 
761
885
  xml do
762
886
  root 'p', mixed: true
@@ -799,8 +923,8 @@ end
799
923
  [source,ruby]
800
924
  ----
801
925
  class Paragraph < Lutaml::Model::Serializable
802
- attribute :bold, Lutaml::Model::Type::String
803
- attribute :italic, Lutaml::Model::Type::String
926
+ attribute :bold, :string
927
+ attribute :italic, :string
804
928
 
805
929
  xml do
806
930
  root 'p'
@@ -859,8 +983,8 @@ end
859
983
  [source,ruby]
860
984
  ----
861
985
  class Example < Lutaml::Model::Serializable
862
- attribute :name, Lutaml::Model::Type::String
863
- attribute :value, Lutaml::Model::Type::Integer
986
+ attribute :name, :string
987
+ attribute :value, :integer
864
988
 
865
989
  json do
866
990
  map 'name', to: :name
@@ -907,8 +1031,8 @@ by referring to a Lutaml::Model class as an attribute class.
907
1031
  [source,ruby]
908
1032
  ----
909
1033
  class Glaze < Lutaml::Model::Serializable
910
- attribute :color, Lutaml::Model::Type::String
911
- attribute :temperature, Lutaml::Model::Type::Integer
1034
+ attribute :color, :string
1035
+ attribute :temperature, :integer
912
1036
 
913
1037
  json do
914
1038
  map 'color', to: :color
@@ -917,7 +1041,7 @@ class Glaze < Lutaml::Model::Serializable
917
1041
  end
918
1042
 
919
1043
  class Ceramic < Lutaml::Model::Serializable
920
- attribute :type, Lutaml::Model::Type::String
1044
+ attribute :type, :string
921
1045
  attribute :glaze, Glaze
922
1046
 
923
1047
  json do
@@ -947,6 +1071,158 @@ end
947
1071
  ----
948
1072
  ====
949
1073
 
1074
+
1075
+ === Separate serialization model
1076
+
1077
+ The `Serialize` module can be used to define only serialization mappings for a
1078
+ separately defined model (a Ruby class).
1079
+
1080
+ Syntax:
1081
+
1082
+ [source,ruby]
1083
+ ----
1084
+ class Foo < Lutaml::Model::Serializable
1085
+ model {DataModelClass}
1086
+
1087
+ # ...
1088
+ end
1089
+ ----
1090
+
1091
+ [example]
1092
+ .Using the `model` method to define serialization mappings for a separate model
1093
+ ====
1094
+ [source,ruby]
1095
+ ----
1096
+ class Ceramic
1097
+ attr_accessor :type, :glaze
1098
+
1099
+ def name
1100
+ "#{type} with #{glaze}"
1101
+ end
1102
+ end
1103
+
1104
+ class CeramicSerialization < Lutaml::Model::Serializable
1105
+ model Ceramic
1106
+
1107
+ xml do
1108
+ map_element 'type', to: :type
1109
+ map_element 'glaze', to: :glaze
1110
+ end
1111
+ end
1112
+ ----
1113
+
1114
+ [source,ruby]
1115
+ ----
1116
+ > Ceramic.new(type: "Porcelain", glaze: "Clear").name
1117
+ > # "Porcelain with Clear"
1118
+ > CeramicSerialization.from_xml(xml)
1119
+ > #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze="Clear">
1120
+ > Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
1121
+ > #<Ceramic><type>Porcelain</type><glaze>Clear</glaze></Ceramic>
1122
+ ----
1123
+ ====
1124
+
1125
+
1126
+ === Rendering empty attributes and collections
1127
+
1128
+ By default, empty attributes and collections are not rendered in the output.
1129
+
1130
+ To render empty attributes and collections, use the `render_nil` option.
1131
+
1132
+ Syntax:
1133
+
1134
+ [source,ruby]
1135
+ ----
1136
+ xml do
1137
+ map_element 'key_value_model_attribute_name', to: :name_of_attribute, render_nil: true
1138
+ end
1139
+ ----
1140
+
1141
+ [source,ruby]
1142
+ ----
1143
+ json | yaml | toml do
1144
+ map 'key_value_model_attribute_name', to: :name_of_attribute, render_nil: true
1145
+ end
1146
+ ----
1147
+
1148
+ .Using the `render_nil` option to render empty attributes
1149
+ [example]
1150
+ ====
1151
+ [source,ruby]
1152
+ ----
1153
+ class Ceramic < Lutaml::Model::Serializable
1154
+ attribute :type, :string
1155
+ attribute :glaze, :string
1156
+
1157
+ xml do
1158
+ map_element 'type', to: :type, render_nil: true
1159
+ map_element 'glaze', to: :glaze
1160
+ end
1161
+
1162
+ json do
1163
+ map 'type', to: :type, render_nil: true
1164
+ map 'glaze', to: :glaze
1165
+ end
1166
+ end
1167
+ ----
1168
+
1169
+ [source,ruby]
1170
+ ----
1171
+ > Ceramic.new.to_json
1172
+ > # { 'type': null }
1173
+ > Ceramic.new(type: "Porcelain", glaze: "Clear").to_json
1174
+ > # { 'type': 'Porcelain', 'glaze': 'Clear' }
1175
+ ----
1176
+
1177
+ [source,ruby]
1178
+ ----
1179
+ > Ceramic.new.to_xml
1180
+ > # <Ceramic><type></type></Ceramic>
1181
+ > Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
1182
+ > # <Ceramic><type>Porcelain</type><glaze>Clear</glaze></Ceramic>
1183
+ ----
1184
+ ====
1185
+
1186
+ .Using the `render_nil` option to render empty attribute collections
1187
+ [example]
1188
+ ====
1189
+ [source,ruby]
1190
+ ----
1191
+ class Ceramic < Lutaml::Model::Serializable
1192
+ attribute :type, :string
1193
+ attribute :glazes, :string, collection: true
1194
+
1195
+ xml do
1196
+ map_element 'type', to: :type, render_nil: true
1197
+ map_element 'glazes', to: :glazes, render_nil: true
1198
+ end
1199
+
1200
+ json do
1201
+ map 'type', to: :type, render_nil: true
1202
+ map 'glazes', to: :glazes, render_nil: true
1203
+ end
1204
+ end
1205
+ ----
1206
+
1207
+ [source,ruby]
1208
+ ----
1209
+ > Ceramic.new.to_json
1210
+ > # { 'type': null, 'glazes': [] }
1211
+ > Ceramic.new(type: "Porcelain", glazes: ["Clear"]).to_json
1212
+ > # { 'type': 'Porcelain', 'glazes': ['Clear'] }
1213
+ ----
1214
+
1215
+ [source,ruby]
1216
+ ----
1217
+ > Ceramic.new.to_xml
1218
+ > # <Ceramic><type></type><glazes></glazes></Ceramic>
1219
+ > Ceramic.new(type: "Porcelain", glazes: ["Clear"]).to_xml
1220
+ > # <Ceramic><type>Porcelain</type><glazes>Clear</glazes></Ceramic>
1221
+ ----
1222
+ ====
1223
+
1224
+
1225
+
950
1226
  === Advanced attribute mapping
951
1227
 
952
1228
  ==== Attribute mapping delegation
@@ -970,8 +1246,8 @@ The following class will parse the JSON snippet below:
970
1246
  [source,ruby]
971
1247
  ----
972
1248
  class Glaze < Lutaml::Model::Serializable
973
- attribute :color, Lutaml::Model::Type::String
974
- attribute :temperature, Lutaml::Model::Type::Integer
1249
+ attribute :color, :string
1250
+ attribute :temperature, :integer
975
1251
 
976
1252
  json do
977
1253
  map 'color', to: :color
@@ -980,7 +1256,7 @@ class Glaze < Lutaml::Model::Serializable
980
1256
  end
981
1257
 
982
1258
  class Ceramic < Lutaml::Model::Serializable
983
- attribute :type, Lutaml::Model::Type::String
1259
+ attribute :type, :string
984
1260
  attribute :glaze, Glaze
985
1261
 
986
1262
  json do
@@ -1033,8 +1309,8 @@ The following class will parse the JSON snippet below:
1033
1309
  [source,ruby]
1034
1310
  ----
1035
1311
  class CustomCeramic < Lutaml::Model::Serializable
1036
- attribute :name, Lutaml::Model::Type::String
1037
- attribute :size, Lutaml::Model::Type::Integer
1312
+ attribute :name, :string
1313
+ attribute :size, :integer
1038
1314
 
1039
1315
  json do
1040
1316
  map 'name', to: :name, with: { to: :name_to_json, from: :name_from_json }
@@ -1214,9 +1490,9 @@ A model can be defined for this JSON as follows:
1214
1490
  [source,ruby]
1215
1491
  ----
1216
1492
  class Schema < Lutaml::Model::Serializable
1217
- attribute :id, Lutaml::Model::Type::String
1218
- attribute :link, Lutaml::Model::Type::String
1219
- attribute :name, Lutaml::Model::Type::String
1493
+ attribute :id, :string
1494
+ attribute :link, :string
1495
+ attribute :name, :string
1220
1496
  end
1221
1497
 
1222
1498
  class ChildMappingClass < Lutaml::Model::Serializable
data/exe/lutaml-model ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require_relative "../lib/lutaml/model/cli"
6
+
7
+ Lutaml::Model::Cli.start(ARGV)