lutaml-model 0.3.3 → 0.3.5

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'
@@ -458,7 +546,7 @@ The following class will parse the XML snippet below:
458
546
  [source,ruby]
459
547
  ----
460
548
  class Example < Lutaml::Model::Serializable
461
- attribute :value, Lutaml::Model::Type::Integer
549
+ attribute :value, :integer
462
550
 
463
551
  xml do
464
552
  root 'example'
@@ -506,7 +594,7 @@ The following class will parse the XML snippet below:
506
594
  [source,ruby]
507
595
  ----
508
596
  class Example < Lutaml::Model::Serializable
509
- attribute :description, Lutaml::Model::Type::String
597
+ attribute :description, :string
510
598
 
511
599
  xml do
512
600
  root 'example'
@@ -540,9 +628,9 @@ The following class will parse the XML snippet below:
540
628
  [source,ruby]
541
629
  ----
542
630
  class Example < Lutaml::Model::Serializable
543
- attribute :name, Lutaml::Model::Type::String
544
- attribute :description, Lutaml::Model::Type::String
545
- attribute :value, Lutaml::Model::Type::Integer
631
+ attribute :name, :string
632
+ attribute :description, :string
633
+ attribute :value, :integer
546
634
 
547
635
  xml do
548
636
  root 'example'
@@ -590,8 +678,8 @@ end
590
678
  [source,ruby]
591
679
  ----
592
680
  class Ceramic < Lutaml::Model::Serializable
593
- attribute :type, Lutaml::Model::Type::String
594
- attribute :glaze, Lutaml::Model::Type::String
681
+ attribute :type, :string
682
+ attribute :glaze, :string
595
683
 
596
684
  xml do
597
685
  root 'Ceramic'
@@ -644,8 +732,8 @@ In this example, `glz` will be used for `Glaze` if it is added inside the
644
732
  [source,ruby]
645
733
  ----
646
734
  class Glaze < Lutaml::Model::Serializable
647
- attribute :color, Lutaml::Model::Type::String
648
- attribute :temperature, Lutaml::Model::Type::Integer
735
+ attribute :color, :string
736
+ attribute :temperature, :integer
649
737
 
650
738
  xml do
651
739
  root 'Glaze'
@@ -657,7 +745,7 @@ class Glaze < Lutaml::Model::Serializable
657
745
  end
658
746
 
659
747
  class Ceramic < Lutaml::Model::Serializable
660
- attribute :type, Lutaml::Model::Type::String
748
+ attribute :type, :string
661
749
  attribute :glaze, Glaze
662
750
 
663
751
  xml do
@@ -711,9 +799,9 @@ In this example, the `Type` element will inherit the namespace from the root.
711
799
  [source,ruby]
712
800
  ----
713
801
  class Ceramic < Lutaml::Model::Serializable
714
- attribute :type, Lutaml::Model::Type::String
715
- attribute :glaze, Lutaml::Model::Type::String
716
- attribute :color, Lutaml::Model::Type::String
802
+ attribute :type, :string
803
+ attribute :glaze, :string
804
+ attribute :color, :string
717
805
 
718
806
  xml do
719
807
  root 'Ceramic'
@@ -791,8 +879,8 @@ end
791
879
  [source,ruby]
792
880
  ----
793
881
  class Paragraph < Lutaml::Model::Serializable
794
- attribute :bold, Lutaml::Model::Type::String
795
- attribute :italic, Lutaml::Model::Type::String
882
+ attribute :bold, :string
883
+ attribute :italic, :string
796
884
 
797
885
  xml do
798
886
  root 'p', mixed: true
@@ -835,8 +923,8 @@ end
835
923
  [source,ruby]
836
924
  ----
837
925
  class Paragraph < Lutaml::Model::Serializable
838
- attribute :bold, Lutaml::Model::Type::String
839
- attribute :italic, Lutaml::Model::Type::String
926
+ attribute :bold, :string
927
+ attribute :italic, :string
840
928
 
841
929
  xml do
842
930
  root 'p'
@@ -895,8 +983,8 @@ end
895
983
  [source,ruby]
896
984
  ----
897
985
  class Example < Lutaml::Model::Serializable
898
- attribute :name, Lutaml::Model::Type::String
899
- attribute :value, Lutaml::Model::Type::Integer
986
+ attribute :name, :string
987
+ attribute :value, :integer
900
988
 
901
989
  json do
902
990
  map 'name', to: :name
@@ -943,8 +1031,8 @@ by referring to a Lutaml::Model class as an attribute class.
943
1031
  [source,ruby]
944
1032
  ----
945
1033
  class Glaze < Lutaml::Model::Serializable
946
- attribute :color, Lutaml::Model::Type::String
947
- attribute :temperature, Lutaml::Model::Type::Integer
1034
+ attribute :color, :string
1035
+ attribute :temperature, :integer
948
1036
 
949
1037
  json do
950
1038
  map 'color', to: :color
@@ -953,7 +1041,7 @@ class Glaze < Lutaml::Model::Serializable
953
1041
  end
954
1042
 
955
1043
  class Ceramic < Lutaml::Model::Serializable
956
- attribute :type, Lutaml::Model::Type::String
1044
+ attribute :type, :string
957
1045
  attribute :glaze, Glaze
958
1046
 
959
1047
  json do
@@ -983,6 +1071,158 @@ end
983
1071
  ----
984
1072
  ====
985
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
+
986
1226
  === Advanced attribute mapping
987
1227
 
988
1228
  ==== Attribute mapping delegation
@@ -1006,8 +1246,8 @@ The following class will parse the JSON snippet below:
1006
1246
  [source,ruby]
1007
1247
  ----
1008
1248
  class Glaze < Lutaml::Model::Serializable
1009
- attribute :color, Lutaml::Model::Type::String
1010
- attribute :temperature, Lutaml::Model::Type::Integer
1249
+ attribute :color, :string
1250
+ attribute :temperature, :integer
1011
1251
 
1012
1252
  json do
1013
1253
  map 'color', to: :color
@@ -1016,7 +1256,7 @@ class Glaze < Lutaml::Model::Serializable
1016
1256
  end
1017
1257
 
1018
1258
  class Ceramic < Lutaml::Model::Serializable
1019
- attribute :type, Lutaml::Model::Type::String
1259
+ attribute :type, :string
1020
1260
  attribute :glaze, Glaze
1021
1261
 
1022
1262
  json do
@@ -1051,10 +1291,30 @@ each serialization mapping block for `from` and `to`.
1051
1291
 
1052
1292
  Syntax:
1053
1293
 
1294
+ .XML serialization with custom methods
1054
1295
  [source,ruby]
1055
1296
  ----
1056
- xml | json | yaml | toml do
1057
- map 'key_value_model_attribute_name', to: :name_of_attribute, with: {
1297
+ xml do
1298
+ map_element 'element_name', to: :name_of_element, with: {
1299
+ to: :method_name_to_serialize,
1300
+ from: :method_name_to_deserialize
1301
+ }
1302
+ map_attribute 'attribute_name', to: :name_of_attribute, with: {
1303
+ to: :method_name_to_serialize,
1304
+ from: :method_name_to_deserialize
1305
+ }
1306
+ map_content, to: :name_of_content, with: {
1307
+ to: :method_name_to_serialize,
1308
+ from: :method_name_to_deserialize
1309
+ }
1310
+ end
1311
+ ----
1312
+
1313
+ .Key-value data model serialization with custom methods
1314
+ [source,ruby]
1315
+ ----
1316
+ json | yaml | toml do
1317
+ map 'attribute_name', to: :name_of_attribute, with: {
1058
1318
  to: :method_name_to_serialize,
1059
1319
  from: :method_name_to_deserialize
1060
1320
  }
@@ -1069,8 +1329,8 @@ The following class will parse the JSON snippet below:
1069
1329
  [source,ruby]
1070
1330
  ----
1071
1331
  class CustomCeramic < Lutaml::Model::Serializable
1072
- attribute :name, Lutaml::Model::Type::String
1073
- attribute :size, Lutaml::Model::Type::Integer
1332
+ attribute :name, :string
1333
+ attribute :size, :integer
1074
1334
 
1075
1335
  json do
1076
1336
  map 'name', to: :name, with: { to: :name_to_json, from: :name_from_json }
@@ -1250,9 +1510,9 @@ A model can be defined for this JSON as follows:
1250
1510
  [source,ruby]
1251
1511
  ----
1252
1512
  class Schema < Lutaml::Model::Serializable
1253
- attribute :id, Lutaml::Model::Type::String
1254
- attribute :link, Lutaml::Model::Type::String
1255
- attribute :name, Lutaml::Model::Type::String
1513
+ attribute :id, :string
1514
+ attribute :link, :string
1515
+ attribute :name, :string
1256
1516
  end
1257
1517
 
1258
1518
  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)