lutaml-model 0.3.24 → 0.3.26

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.
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