lutaml-model 0.3.24 → 0.3.26

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +36 -17
  3. data/README.adoc +396 -30
  4. data/lib/lutaml/model/attribute.rb +52 -27
  5. data/lib/lutaml/model/error/pattern_not_matched_error.rb +17 -0
  6. data/lib/lutaml/model/error/type_error.rb +9 -0
  7. data/lib/lutaml/model/error/unknown_type_error.rb +9 -0
  8. data/lib/lutaml/model/error/validation_error.rb +0 -1
  9. data/lib/lutaml/model/error.rb +3 -0
  10. data/lib/lutaml/model/mapping_hash.rb +8 -0
  11. data/lib/lutaml/model/serialize.rb +10 -5
  12. data/lib/lutaml/model/type/boolean.rb +38 -0
  13. data/lib/lutaml/model/type/date.rb +35 -0
  14. data/lib/lutaml/model/type/date_time.rb +32 -4
  15. data/lib/lutaml/model/type/decimal.rb +42 -0
  16. data/lib/lutaml/model/type/float.rb +37 -0
  17. data/lib/lutaml/model/type/hash.rb +62 -0
  18. data/lib/lutaml/model/type/integer.rb +41 -0
  19. data/lib/lutaml/model/type/string.rb +49 -0
  20. data/lib/lutaml/model/type/time.rb +49 -0
  21. data/lib/lutaml/model/type/time_without_date.rb +37 -5
  22. data/lib/lutaml/model/type/value.rb +52 -0
  23. data/lib/lutaml/model/type.rb +50 -114
  24. data/lib/lutaml/model/validation.rb +2 -1
  25. data/lib/lutaml/model/version.rb +1 -1
  26. data/lib/lutaml/model/xml_adapter/builder/nokogiri.rb +12 -3
  27. data/lib/lutaml/model/xml_adapter/builder/ox.rb +7 -1
  28. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +3 -1
  29. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +12 -8
  30. data/lib/lutaml/model/xml_adapter/xml_document.rb +6 -8
  31. data/lib/lutaml/model/xml_mapping.rb +6 -2
  32. data/lib/lutaml/model/xml_mapping_rule.rb +7 -1
  33. data/lutaml-model.gemspec +1 -1
  34. data/spec/address_spec.rb +170 -0
  35. data/spec/fixtures/address.rb +33 -0
  36. data/spec/fixtures/person.rb +73 -0
  37. data/spec/fixtures/sample_model.rb +40 -0
  38. data/spec/fixtures/vase.rb +38 -0
  39. data/spec/fixtures/xml/special_char.xml +13 -0
  40. data/spec/lutaml/model/attribute_spec.rb +139 -0
  41. data/spec/lutaml/model/cdata_spec.rb +520 -0
  42. data/spec/lutaml/model/collection_spec.rb +299 -0
  43. data/spec/lutaml/model/comparable_model_spec.rb +106 -0
  44. data/spec/lutaml/model/custom_model_spec.rb +410 -0
  45. data/spec/lutaml/model/custom_serialization_spec.rb +170 -0
  46. data/spec/lutaml/model/defaults_spec.rb +221 -0
  47. data/spec/lutaml/model/delegation_spec.rb +340 -0
  48. data/spec/lutaml/model/inheritance_spec.rb +92 -0
  49. data/spec/lutaml/model/json_adapter_spec.rb +37 -0
  50. data/spec/lutaml/model/key_value_mapping_spec.rb +86 -0
  51. data/spec/lutaml/model/map_content_spec.rb +118 -0
  52. data/spec/lutaml/model/mixed_content_spec.rb +625 -0
  53. data/spec/lutaml/model/namespace_spec.rb +57 -0
  54. data/spec/lutaml/model/ordered_content_spec.rb +83 -0
  55. data/spec/lutaml/model/render_nil_spec.rb +138 -0
  56. data/spec/lutaml/model/schema/json_schema_spec.rb +79 -0
  57. data/spec/lutaml/model/schema/relaxng_schema_spec.rb +60 -0
  58. data/spec/lutaml/model/schema/xsd_schema_spec.rb +55 -0
  59. data/spec/lutaml/model/schema/yaml_schema_spec.rb +47 -0
  60. data/spec/lutaml/model/serializable_spec.rb +297 -0
  61. data/spec/lutaml/model/serializable_validation_spec.rb +90 -0
  62. data/spec/lutaml/model/simple_model_spec.rb +314 -0
  63. data/spec/lutaml/model/toml_adapter_spec.rb +39 -0
  64. data/spec/lutaml/model/type/boolean_spec.rb +54 -0
  65. data/spec/lutaml/model/type/date_spec.rb +118 -0
  66. data/spec/lutaml/model/type/date_time_spec.rb +127 -0
  67. data/spec/lutaml/model/type/decimal_spec.rb +125 -0
  68. data/spec/lutaml/model/type/float_spec.rb +191 -0
  69. data/spec/lutaml/model/type/hash_spec.rb +63 -0
  70. data/spec/lutaml/model/type/integer_spec.rb +145 -0
  71. data/spec/lutaml/model/type/string_spec.rb +150 -0
  72. data/spec/lutaml/model/type/time_spec.rb +142 -0
  73. data/spec/lutaml/model/type/time_without_date_spec.rb +125 -0
  74. data/spec/lutaml/model/type_spec.rb +276 -0
  75. data/spec/lutaml/model/utils_spec.rb +79 -0
  76. data/spec/lutaml/model/validation_spec.rb +83 -0
  77. data/spec/lutaml/model/with_child_mapping_spec.rb +174 -0
  78. data/spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb +56 -0
  79. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +56 -0
  80. data/spec/lutaml/model/xml_adapter/ox_adapter_spec.rb +61 -0
  81. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +251 -0
  82. data/spec/lutaml/model/xml_adapter_spec.rb +178 -0
  83. data/spec/lutaml/model/xml_mapping_spec.rb +863 -0
  84. data/spec/lutaml/model/yaml_adapter_spec.rb +30 -0
  85. data/spec/lutaml/model_spec.rb +1 -0
  86. data/spec/person_spec.rb +161 -0
  87. data/spec/spec_helper.rb +33 -0
  88. metadata +68 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 18b88c11cf49c7a08199273885c202ea29ebc3aa9f470d1bffc177282b1d588b
4
- data.tar.gz: 75b4d90ea27a62c55ca6643a80358ab80a0d7636b4c6511ecafa526e0855f294
3
+ metadata.gz: 1dc709174fab4f4df2214f08b536e0b1aded39f0c9f48a18eb5a4a53da3fc4b6
4
+ data.tar.gz: 9eea83160b8210f84d4b2f9baf2ab577ca91d2a737eaf98b175d52385e6fb5d3
5
5
  SHA512:
6
- metadata.gz: 16d82eb3101b2e22f5d2e9197d3fdea94c787675d489a4a3792e7548f4275c6c926a87cf6a6242ee4672526ba1f0d3eb26c8fd629341240d4a93ab6d614d522f
7
- data.tar.gz: 1369a5abd8dd616e28b4a8c5b7db3369634bf37c701c0cca76b20182df922e18e120c754bb486745f7d47df0bff983bdf6cb2e3651713e08aebe5c1eeb87297f
6
+ metadata.gz: 1e75326adeb9b8804f1bcc99104707edc3656e870d16c8af22e07e32ead9f4d78cd87eb372ebb834f2a5d942d58819e0fd3800e23a8099cf951360765223cd25
7
+ data.tar.gz: ecc145fb2d9250cf855243c7b9cf468919f2530eed03039bd6c7a521fa534327a1a67c5348237b3d5959e20fa0fc2b2cadc87e57139f451610cd2d7d695f251b
data/.rubocop_todo.yml CHANGED
@@ -1,19 +1,19 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2024-11-08 07:37:57 UTC using RuboCop version 1.66.1.
3
+ # on 2024-11-11 10:52:30 UTC using RuboCop version 1.68.0.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
- # Offense count: 127
9
+ # Offense count: 132
10
10
  # This cop supports safe autocorrection (--autocorrect).
11
11
  # Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
12
12
  # URISchemes: http, https
13
13
  Layout/LineLength:
14
14
  Enabled: false
15
15
 
16
- # Offense count: 11
16
+ # Offense count: 12
17
17
  # Configuration parameters: AllowedMethods.
18
18
  # AllowedMethods: enums
19
19
  Lint/ConstantDefinitionInBlock:
@@ -22,15 +22,17 @@ Lint/ConstantDefinitionInBlock:
22
22
  - 'spec/lutaml/model/schema/relaxng_schema_spec.rb'
23
23
  - 'spec/lutaml/model/schema/xsd_schema_spec.rb'
24
24
  - 'spec/lutaml/model/schema/yaml_schema_spec.rb'
25
+ - 'spec/lutaml/model/type_spec.rb'
25
26
  - 'spec/lutaml/model/validation_spec.rb'
26
27
  - 'spec/lutaml/model/xml_adapter/xml_namespace_spec.rb'
27
28
 
28
- # Offense count: 1
29
+ # Offense count: 2
29
30
  Lint/DuplicateMethods:
30
31
  Exclude:
31
32
  - 'lib/lutaml/model/attribute.rb'
33
+ - 'lib/lutaml/model/type/float.rb'
32
34
 
33
- # Offense count: 38
35
+ # Offense count: 36
34
36
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
35
37
  Metrics/AbcSize:
36
38
  Exclude:
@@ -40,7 +42,6 @@ Metrics/AbcSize:
40
42
  - 'lib/lutaml/model/schema/relaxng_schema.rb'
41
43
  - 'lib/lutaml/model/schema/xsd_schema.rb'
42
44
  - 'lib/lutaml/model/serialize.rb'
43
- - 'lib/lutaml/model/type.rb'
44
45
  - 'lib/lutaml/model/xml_adapter/nokogiri_adapter.rb'
45
46
  - 'lib/lutaml/model/xml_adapter/ox_adapter.rb'
46
47
  - 'lib/lutaml/model/xml_adapter/xml_document.rb'
@@ -52,19 +53,19 @@ Metrics/AbcSize:
52
53
  Metrics/BlockLength:
53
54
  Max: 47
54
55
 
55
- # Offense count: 27
56
+ # Offense count: 26
56
57
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
57
58
  Metrics/CyclomaticComplexity:
58
59
  Exclude:
59
60
  - 'lib/lutaml/model/attribute.rb'
60
61
  - 'lib/lutaml/model/comparable_model.rb'
61
62
  - 'lib/lutaml/model/serialize.rb'
62
- - 'lib/lutaml/model/type.rb'
63
+ - 'lib/lutaml/model/type/integer.rb'
63
64
  - 'lib/lutaml/model/xml_adapter/nokogiri_adapter.rb'
64
65
  - 'lib/lutaml/model/xml_adapter/ox_adapter.rb'
65
66
  - 'lib/lutaml/model/xml_adapter/xml_document.rb'
66
67
 
67
- # Offense count: 50
68
+ # Offense count: 53
68
69
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
69
70
  Metrics/MethodLength:
70
71
  Max: 46
@@ -72,15 +73,16 @@ Metrics/MethodLength:
72
73
  # Offense count: 7
73
74
  # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
74
75
  Metrics/ParameterLists:
75
- Max: 13
76
+ Max: 14
76
77
 
77
- # Offense count: 22
78
+ # Offense count: 23
78
79
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
79
80
  Metrics/PerceivedComplexity:
80
81
  Exclude:
81
82
  - 'lib/lutaml/model/attribute.rb'
82
83
  - 'lib/lutaml/model/comparable_model.rb'
83
84
  - 'lib/lutaml/model/serialize.rb'
85
+ - 'lib/lutaml/model/type/integer.rb'
84
86
  - 'lib/lutaml/model/xml_adapter/nokogiri_adapter.rb'
85
87
  - 'lib/lutaml/model/xml_adapter/ox_adapter.rb'
86
88
  - 'lib/lutaml/model/xml_adapter/xml_document.rb'
@@ -95,7 +97,7 @@ RSpec/ContextWording:
95
97
  - 'spec/lutaml/model/xml_adapter/xml_namespace_spec.rb'
96
98
  - 'spec/lutaml/model/xml_mapping_spec.rb'
97
99
 
98
- # Offense count: 110
100
+ # Offense count: 117
99
101
  # Configuration parameters: CountAsOne.
100
102
  RSpec/ExampleLength:
101
103
  Max: 54
@@ -106,13 +108,14 @@ RSpec/IndexedLet:
106
108
  Exclude:
107
109
  - 'spec/address_spec.rb'
108
110
 
109
- # Offense count: 19
111
+ # Offense count: 20
110
112
  RSpec/LeakyConstantDeclaration:
111
113
  Exclude:
112
114
  - 'spec/lutaml/model/schema/json_schema_spec.rb'
113
115
  - 'spec/lutaml/model/schema/relaxng_schema_spec.rb'
114
116
  - 'spec/lutaml/model/schema/xsd_schema_spec.rb'
115
117
  - 'spec/lutaml/model/schema/yaml_schema_spec.rb'
118
+ - 'spec/lutaml/model/type_spec.rb'
116
119
  - 'spec/lutaml/model/validation_spec.rb'
117
120
  - 'spec/lutaml/model/xml_adapter/xml_namespace_spec.rb'
118
121
 
@@ -124,7 +127,7 @@ RSpec/MultipleDescribes:
124
127
  - 'spec/lutaml/model/xml_adapter/xml_namespace_spec.rb'
125
128
  - 'spec/lutaml/model/xml_adapter_spec.rb'
126
129
 
127
- # Offense count: 123
130
+ # Offense count: 149
128
131
  RSpec/MultipleExpectations:
129
132
  Max: 14
130
133
 
@@ -133,24 +136,34 @@ RSpec/MultipleExpectations:
133
136
  RSpec/MultipleMemoizedHelpers:
134
137
  Max: 9
135
138
 
136
- # Offense count: 4
139
+ # Offense count: 9
137
140
  # Configuration parameters: AllowedGroups.
138
141
  RSpec/NestedGroups:
139
142
  Max: 4
140
143
 
141
- # Offense count: 7
144
+ # Offense count: 13
142
145
  RSpec/PendingWithoutReason:
143
146
  Exclude:
144
147
  - 'spec/lutaml/model/mixed_content_spec.rb'
148
+ - 'spec/lutaml/model/type/date_time_spec.rb'
149
+ - 'spec/lutaml/model/type/integer_spec.rb'
150
+ - 'spec/lutaml/model/type/time_spec.rb'
151
+ - 'spec/lutaml/model/type/time_without_date_spec.rb'
145
152
  - 'spec/lutaml/model/validation_spec.rb'
146
153
  - 'spec/lutaml/model/xml_adapter/oga_adapter_spec.rb'
147
154
  - 'spec/lutaml/model/xml_adapter/xml_namespace_spec.rb'
148
155
  - 'spec/lutaml/model/xml_adapter_spec.rb'
149
156
 
150
- # Offense count: 1
157
+ # Offense count: 2
151
158
  RSpec/RemoveConst:
152
159
  Exclude:
153
160
  - 'spec/lutaml/model/type/decimal_spec.rb'
161
+ - 'spec/lutaml/model/type_spec.rb'
162
+
163
+ # Offense count: 2
164
+ RSpec/RepeatedExampleGroupBody:
165
+ Exclude:
166
+ - 'spec/lutaml/model/type/time_without_date_spec.rb'
154
167
 
155
168
  # Offense count: 2
156
169
  RSpec/RepeatedExampleGroupDescription:
@@ -167,6 +180,12 @@ RSpec/SpecFilePathFormat:
167
180
  - 'spec/lutaml/model/defaults_spec.rb'
168
181
  - 'spec/lutaml/model/type/decimal_spec.rb'
169
182
 
183
+ # Offense count: 1
184
+ # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
185
+ RSpec/VerifiedDoubles:
186
+ Exclude:
187
+ - 'spec/lutaml/model/type/hash_spec.rb'
188
+
170
189
  # Offense count: 1
171
190
  Security/CompoundHash:
172
191
  Exclude:
data/README.adoc CHANGED
@@ -22,6 +22,17 @@ NOTE: Instructions on how to migrate from Shale to Lutaml::Model are provided in
22
22
  <<migrate-from-shale>>.
23
23
 
24
24
 
25
+ == Features
26
+
27
+ * Define models with attributes and types
28
+ * Serialize and deserialize models to/from JSON, XML, YAML, and TOML
29
+ * Support for multiple serialization libraries (e.g., `toml-rb`, `tomlib`)
30
+ * Configurable adapters for different serialization formats
31
+ * Support for collections and default values
32
+ * Custom serialization/deserialization methods
33
+ * XML namespaces and mappings
34
+
35
+
25
36
  == Data modeling in a nutshell
26
37
 
27
38
  Data modeling is the process of creating a data model for the data to be stored
@@ -32,16 +43,110 @@ Lutaml::Model simplifies data modeling in Ruby by allowing you to define models
32
43
  with attributes and serialize/deserialize them to/from various serialization
33
44
  formats seamlessly.
34
45
 
46
+ The Lutaml::Model data modelling approach is as follows:
47
+
48
+ .Modeling relationships of a LutaML Model
49
+ [source]
50
+ ----
51
+ Lutaml Model
52
+
53
+ Has many attributes
54
+
55
+
56
+ Attribute
57
+
58
+ Has type of
59
+
60
+ ┌──────────┴──────────┐
61
+ │ │
62
+ Model Value (Leaf)
63
+ │ │
64
+ Has many attributes Contains one basic value
65
+ │ │
66
+ ┌───────┴─────┐ ┌──────┴──────┐
67
+ │ │ │ │
68
+ Model Value (Leaf) String Integer
69
+ │ Date Boolean
70
+ │ Time Float
71
+ Has many attributes ... ...
72
+
73
+
74
+ (Recursive pattern continues...)
75
+ ----
76
+
77
+ .Example of LutaML Model instance with assigned values
78
+ ====
79
+ [source]
80
+ ----
81
+ Studio (Model)
82
+ ├── name (Value: String) = "Pottery Studio"
83
+ ├── address (Model)
84
+ │ ├── street (Value: String) = "123 Clay St"
85
+ │ ├── city (Value: String) = "Ceramics City"
86
+ │ └── postcode (Value: String) = "12345"
87
+ ├── established (Value: Date) = 2020-01-01
88
+ └── kilns (Model)
89
+ ├── count (Value: Integer) = 3
90
+ └── temperature (Value: Float) = 1200.0
91
+ ----
92
+ ====
93
+
94
+
95
+ .Modeling relationships of a LutaML Model to serialization models
96
+ [source]
97
+ ----
98
+ Core Model Serialization Model
99
+ ========== ===================
100
+ │ (mapping)
101
+ │ │
102
+ ▼ ▼
103
+ Model XML Model
104
+ │ │
105
+ ┌─────┴─────┐ ┌──────┴──────┐
106
+ │ │ │ │
107
+ Models Value Types ────┬──► Models Value Types
108
+ │ │ │ │ │
109
+ │ │ │ │ │
110
+ │ ┌──────┴──┐ │ ┌────┴────┐ ┌─┴─┐
111
+ │ │ │ │ │ │ │ │
112
+ │ String Integer │ Element Value xs:string
113
+ │ Date Float │ Attribute Type xs:date
114
+ │ Time Boolean │ xs:boolean
115
+ │ │ xs:anyURI
116
+ └──────┐ │
117
+ │ │ JSON Model
118
+ Contains │ │
119
+ more Models │ ┌──────┴──────┐
120
+ (recursive) │ │ │
121
+ └──► Models Value Types
122
+ │ │
123
+ │ │
124
+ ┌────┴────┐ ┌─┴─┐
125
+ │ │ │ │
126
+ object array number string
127
+ value boolean null
128
+ ----
129
+
130
+ .Example of LutaML Model instance transformed into a serialization model and serialized to JSON
131
+ ====
132
+ [source]
133
+ ----
134
+ Studio (Core Model) JSON Model Serialized JSON
135
+ │ │ │
136
+ ▼ ▼ ▼
137
+ name: "Studio 1" ┌─► { "name": "...", {
138
+ address: │ "address": { "name": "Studio 1",
139
+ ├── street: "..." │ "street": "...", "address": {
140
+ └── city: "..." │ "city": "..." ──► "street": "...",
141
+ kilns: │ }, "city": "..."
142
+ ├── count: 3 │ "kilnsCount": ..., },
143
+ └── temp: 1200 │ "kilnsTemp": ... "kilnsCount": 3,
144
+ └─► } "kilnsTemp": 1200
145
+ }
146
+ ----
147
+ ====
35
148
 
36
- == Features
37
149
 
38
- * Define models with attributes and types
39
- * Serialize and deserialize models to/from JSON, XML, YAML, and TOML
40
- * Support for multiple serialization libraries (e.g., `toml-rb`, `tomlib`)
41
- * Configurable adapters for different serialization formats
42
- * Support for collections and default values
43
- * Custom serialization/deserialization methods
44
- * XML namespaces and mappings
45
150
 
46
151
  == Installation
47
152
 
@@ -150,6 +255,8 @@ same class and all their attributes are equal.
150
255
  Lutaml::Model supports the following attribute types, they can be
151
256
  referred by a string, a symbol, or their class constant.
152
257
 
258
+ Every type has a corresponding Ruby class and a serialization format type.
259
+
153
260
  Syntax:
154
261
 
155
262
  [source,ruby]
@@ -159,20 +266,20 @@ attribute :name_of_attribute, {symbol | string | class}
159
266
 
160
267
  .Mapping between Lutaml::Model::Type classes, Ruby equivalents and serialization format types
161
268
  |===
162
- | Lutaml::Model::Type | Ruby Class | XML | JSON | YAML | Example Value
163
-
164
- | `:string` | `String` | `xs:string` | `string` | `string` | `"text"`
165
- | `:integer` | `Integer` | `xs:integer` | `number` | `integer` | `42`
166
- | `:float` | `Float` | `xs:decimal` | `number` | `float` | `3.14`
167
- | `:boolean` | `TrueClass`/`FalseClass` | `xs:boolean` | `boolean` | `boolean` | `true/false`
168
- | `:date` | `Date` | `xs:date` | `string` (ISO8601) | `date` | `"2024-01-01"`
169
- | `:time_without_date` | `Time` | `xs:time` | `string` (ISO8601) | `time` | `"12:34:56"`
170
- | `:date_time` | `DateTime` | `xs:dateTime` | `string` (ISO8601) | `timestamp` | `"2024-01-01T00:00:00Z"`
171
- | `:time` | `Time` | `xs:dateTime` | `string` (ISO8601) | `timestamp` | `"2024-01-01T00:00:00Z"`
172
- | `:decimal` (optional) | `BigDecimal` | `xs:decimal` | `number` | `float` | `123.45`
173
- | `:hash` | `Hash` | complex element | `object` | `map` | `{key: "value"}`
174
- | `class` | Custom Class | complex element | `object` | `map` | `CustomObject`
175
- | `collection: true` | `Array` of Type | repeated elements | `array` | `sequence` | `[obj1, obj2]`
269
+ | Lutaml::Model::Type | Ruby class | XML | JSON | YAML | Example value
270
+
271
+ | `:string` | `String` | `xs:string` | `string` | `string` | `"text"`
272
+ | `:integer` | `Integer` | `xs:integer` | `number` | `integer` | `42`
273
+ | `:float` | `Float` | `xs:decimal` | `number` | `float` | `3.14`
274
+ | `:boolean` | `TrueClass`/`FalseClass` | `xs:boolean` | `boolean` | `boolean` | `true`, `false`
275
+ | `:date` | `Date` | `xs:date` | `string` | `string` | `2024-01-01` (JSON/YAML `"2024-01-01"`)
276
+ | `:time_without_date` | `Time` | `xs:time` | `string` | `string` | `"12:34:56"`
277
+ | `:date_time` | `DateTime` | `xs:dateTime` | `string` | `string` | `"2024-01-01T12:00:00+00:00"`
278
+ | `:time` | `Time` | `xs:dateTime` | `string` | `string` | `"2024-01-01T12:00:00+00:00"`
279
+ | `:decimal` (optional) | `BigDecimal` | `xs:decimal` | `number` | `float` | `123.45`
280
+ | `:hash` | `Hash` | complex element | object | map | `{key: "value"}`
281
+ // | `class` | Custom class | complex element | object | map | `CustomObject`
282
+ // | `collection: true` | `Array` of type | repeated elements | array | sequence | `[obj1, obj2]`
176
283
  // | `any`
177
284
 
178
285
  |===
@@ -204,14 +311,17 @@ end
204
311
  ----
205
312
  ====
206
313
 
207
- ==== (optional) Decimal type
314
+ ==== Decimal type
315
+
316
+ The Decimal type is an optional type that is disabled by default.
208
317
 
209
- The `BigDecimal` class is no longer part of the standard Ruby library from Ruby
210
- 3.4 onwards, hence the `Decimal` type is only enabled when the `bigdecimal`
211
- library is loaded.
318
+ NOTE: The reason why the Decimal type is disalbed by default is that the
319
+ `BigDecimal` class became optional to the standard Ruby library from Ruby 3.4
320
+ onwards. The `Decimal` type is only enabled when the `bigdecimal` library is
321
+ loaded.
212
322
 
213
- This means that the following code needs to be run before using (and parsing)
214
- the `Decimal` type:
323
+ The following code needs to be run before using (and parsing) the Decimal
324
+ type:
215
325
 
216
326
  [source,ruby]
217
327
  ----
@@ -222,6 +332,142 @@ If the `bigdecimal` library is not loaded, usage of the `Decimal` type will
222
332
  raise a `Lutaml::Model::TypeNotSupportedError`.
223
333
 
224
334
 
335
+ ==== Custom type
336
+
337
+ A custom class can be used as an attribute type. The custom class must inherit
338
+ from `Lutaml::Model::Type::Value` or a class that inherits from it.
339
+
340
+ A class inheriting from the `Value` class carries the attribute `value` which
341
+ stores the one-and-only "true" value that is independent of serialization
342
+ formats.
343
+
344
+ The minimum requirement for a custom class is to implement the following
345
+ methods:
346
+
347
+ `self.cast(value)`:: Assignment of an external value to the `Value` class to be
348
+ set as `value`. Casts the value to the custom type.
349
+
350
+ `self.serialize(value)`:: Serializes the custom type to an object (e.g. a
351
+ string). Takes the internal `value` and converts it into an output suitable for
352
+ serialization.
353
+
354
+
355
+ .Using a custom value type to normalize a postcode with minimal methods
356
+ [example]
357
+ ====
358
+ [source,ruby]
359
+ ----
360
+ class FiveDigitPostCode < Lutaml::Model::Type::String
361
+ def self.cast(value)
362
+ value = value.to_s if value.is_a?(Integer)
363
+
364
+ unless value.is_a?(::String)
365
+ raise Lutaml::Model::InvalidValueError, "Invalid value for type 'FiveDigitPostCode'"
366
+ end
367
+
368
+ # Pad zeros to the left
369
+ value.rjust(5, '0')
370
+ end
371
+
372
+ def self.serialize(value)
373
+ value
374
+ end
375
+ end
376
+
377
+ class Studio < Lutaml::Model::Serializable
378
+ attribute :postcode, FiveDigitPostCode
379
+ end
380
+ ----
381
+ ====
382
+
383
+
384
+ ==== Serialization of custom types
385
+
386
+ The serialization of custom types can be made to differ per serialization format
387
+ by defining methods in the class definitions. This requires additional methods
388
+ than the minimum required for a custom class (i.e. `self.cast(value)` and
389
+ `self.serialize(value)`).
390
+
391
+ This is useful in the case when different serialization formats of the same
392
+ model expect differentiated value representations.
393
+
394
+ The methods that can be overridden are named:
395
+
396
+ `self.from_{format}(serialized_string)`:: Deserializes a string of the
397
+ serialization format and returns the object to be assigned to the `Value` class'
398
+ `value`.
399
+
400
+ `to_{format}`:: Serializes the object to a string of the serialization format.
401
+
402
+ The `{format}` part of the method name is the serialization format in lowercase
403
+ (e.g. `json`, `xml`, `yaml`, `toml`).
404
+
405
+ .Using custom serialization methods to handle a high-precision date-time type
406
+ [example]
407
+ ====
408
+ Suppose in XML we handle a high-precision date-time type that requires custom
409
+ serialization methods, but other formats such as JSON do not support this type.
410
+
411
+ For instance, in the normal DateTime class, the serialized string is
412
+ `2012-04-07T01:51:37+02:00`, and the high-precision format is
413
+ `2012-04-07T01:51:37.112+02:00`.
414
+
415
+ We create `HighPrecisionDateTime` class is a custom class that inherits
416
+ from `Lutaml::Model::Type::DateTime`.
417
+
418
+ [source,ruby]
419
+ ----
420
+ class HighPrecisionDateTime < Lutaml::Model::Type::DateTime
421
+ # Inherit the `self.cast(value)` and `self.serialize(value)` methods
422
+ # from Lutaml::Model::Type::DateTime
423
+
424
+ # The format looks like this `2012-04-07T01:51:37.112+02:00`
425
+ def self.from_xml(xml_string)
426
+ ::DateTime.parse(xml_string)
427
+ end
428
+
429
+ # The %L adds milliseconds to the time
430
+ def to_xml
431
+ value.strftime('%Y-%m-%dT%H:%M:%S.%L%:z')
432
+ end
433
+ end
434
+
435
+ class Ceramic < Lutaml::Model::Serializable
436
+ attribute :kiln_firing_time, HighPrecisionDateTime
437
+ xml do
438
+ root 'ceramic'
439
+ map_element 'kilnFiringTime', to: :kiln_firing_time
440
+ # ...
441
+ end
442
+ end
443
+ ----
444
+
445
+ An XML snippet with the high-precision date-time type:
446
+
447
+ [source,xml]
448
+ ----
449
+ <ceramic>
450
+ <kilnFiringTime>2012-04-07T01:51:37.112+02:00</kilnFiringTime>
451
+ <!-- ... -->
452
+ </ceramic>
453
+ ----
454
+
455
+ When loading the XML snippet, the `HighPrecisionDateTime` class will be used to
456
+ parse the high-precision date-time string.
457
+
458
+ However, when serializing to JSON, the value will have the high-precision
459
+ part lost due to the inability of JSON to handle high-precision date-time.
460
+
461
+ [source,ruby]
462
+ ----
463
+ > c = Ceramic.from_xml(xml)
464
+ > #<Ceramic:0x0000000104ac7240 @kiln_firing_time=#<HighPrecisionDateTime:0x0000000104ac7240 @value=2012-04-07 01:51:37.112000000 +0200>>
465
+ > c.to_json
466
+ > # {"kilnFiringTime":"2012-04-07T01:51:37+02:00"}
467
+ ----
468
+ ====
469
+
470
+
225
471
  === Attribute as a collection
226
472
 
227
473
  Define attributes as collections (arrays or hashes) to store multiple values
@@ -297,8 +543,15 @@ end
297
543
  ====
298
544
 
299
545
 
546
+ === Attribute value validation
547
+
548
+ ==== General
549
+
550
+ There are several mechanisms to validate attribute values in Lutaml::Model.
551
+
552
+
300
553
  [[attribute-enumeration]]
301
- === Attribute as an enumeration
554
+ ==== Values of an enumeration
302
555
 
303
556
  An attribute can be defined as an enumeration by using the `values` directive.
304
557
 
@@ -395,6 +648,44 @@ acceptance of the newly updated component.
395
648
  ====
396
649
 
397
650
 
651
+ ==== String values restricted to patterns
652
+
653
+ An attribute that accepts a string value accepts value validation using regular
654
+ expressions.
655
+
656
+ Syntax:
657
+
658
+ [source,ruby]
659
+ ----
660
+ attribute :name_of_attribute, :string, pattern: /regex/
661
+ ----
662
+
663
+ .Using the `pattern` option to restrict the value of an attribute
664
+ [example]
665
+ ====
666
+ In this example, the `color` attribute takes hex color values such as `#ccddee`.
667
+
668
+ A regular expression can be used to validate values assigned to the attribute.
669
+ In this case, it is `/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/`.
670
+
671
+ [source,ruby]
672
+ ----
673
+ class Glaze < Lutaml::Model::Serializable
674
+ attribute :color, :string, pattern: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/
675
+ end
676
+ ----
677
+
678
+ [source,ruby]
679
+ ----
680
+ > Glaze.new(color: '#ff0000').color
681
+ > # "#ff0000"
682
+ > Glaze.new(color: '#ff000').color
683
+ > # Lutaml::Model::InvalidValueError: Invalid value for attribute 'color'
684
+ ----
685
+ ====
686
+
687
+
688
+
398
689
  === Attribute value default and rendering defaults
399
690
 
400
691
  Specify default values for attributes using the `default` option.
@@ -525,7 +816,8 @@ class Person < Lutaml::Model::Serializable
525
816
  end
526
817
  ----
527
818
 
528
- For the following xml
819
+ For the following XML snippet:
820
+
529
821
  [source,xml]
530
822
  ----
531
823
  <Person>
@@ -898,6 +1190,80 @@ end
898
1190
  ====
899
1191
 
900
1192
 
1193
+ ==== CDATA nodes
1194
+
1195
+ CDATA is an XML feature that allows the inclusion of text that may contain
1196
+ characters that are unescaped in XML.
1197
+
1198
+ While CDATA is not preferred in XML, it is sometimes necessary to handle CDATA
1199
+ nodes for both input and output.
1200
+
1201
+ NOTE: The W3C XML Recommendation explicitly encourages escaping characters over
1202
+ usage of CDATA.
1203
+
1204
+ Lutaml::Model supports the handling of CDATA nodes in XML in the following
1205
+ behavior:
1206
+
1207
+ . When an attribute contains a CDATA node with no text:
1208
+ ** On reading: The node (CDATA or text) is read as its value.
1209
+ ** On writing: The value is written as its native type.
1210
+
1211
+ . When an XML mapping sets `cdata: true` on `map_element` or `map_content`:
1212
+ ** On reading: The node (CDATA or text) is read as its value.
1213
+ ** On writing: The value is written as a CDATA node.
1214
+
1215
+ . When an XML mapping sets `cdata: false` on `map_element` or `map_content`:
1216
+ ** On reading: The node (CDATA or text) is read as its value.
1217
+ ** On writing: The value is written as a text node (string).
1218
+
1219
+
1220
+ Syntax:
1221
+
1222
+ [source,ruby]
1223
+ ----
1224
+ xml do
1225
+ map_content to: :name_of_attribute, cdata: (true | false)
1226
+ map_element :name, to: :name, cdata: (true | false)
1227
+ end
1228
+ ----
1229
+
1230
+ .Using `cdata` to map CDATA content
1231
+ [example]
1232
+ ====
1233
+ The following class will parse the XML snippet below:
1234
+
1235
+ [source,ruby]
1236
+ ----
1237
+ class Example < Lutaml::Model::Serializable
1238
+ attribute :name, :string
1239
+ attribute :description, :string
1240
+ attribute :title, :string
1241
+ attribute :note, :string
1242
+
1243
+ xml do
1244
+ root 'example'
1245
+ map_element :name, to: :name, cdata: true
1246
+ map_content to: :description, cdata: true
1247
+ map_element :title, to: :title, cdata: false
1248
+ map_element :note, to: :note, cdata: false
1249
+ end
1250
+ end
1251
+ ----
1252
+
1253
+ [source,xml]
1254
+ ----
1255
+ <example><name><![CDATA[John]]></name><![CDATA[here is the description]]><title><![CDATA[Lutaml]]></title><note>Careful</note></example>
1256
+ ----
1257
+
1258
+ [source,ruby]
1259
+ ----
1260
+ > Example.from_xml(xml)
1261
+ > #<Example:0x0000000104ac7240 @name="John" @description="here is the description" @title="Lutaml" @note="Careful">
1262
+ > Example.new(name: "John", description: "here is the description", title: "Lutaml", note: "Careful").to_xml
1263
+ > #<example><name><![CDATA[John]]></name><![CDATA[here is the description]]><title>Lutaml</title><note>Careful</note></example>
1264
+ ----
1265
+ ====
1266
+
901
1267
 
902
1268
  ==== Example for mapping
903
1269