lutaml-model 0.4.0 → 0.5.1
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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +36 -20
- data/README.adoc +1003 -192
- data/lib/lutaml/model/attribute.rb +6 -2
- data/lib/lutaml/model/error/collection_true_missing_error.rb +16 -0
- data/lib/lutaml/model/error/multiple_mappings_error.rb +6 -0
- data/lib/lutaml/model/error.rb +2 -0
- data/lib/lutaml/model/key_value_mapping.rb +25 -4
- data/lib/lutaml/model/key_value_mapping_rule.rb +16 -3
- data/lib/lutaml/model/loggable.rb +15 -0
- data/lib/lutaml/model/mapping_rule.rb +14 -2
- data/lib/lutaml/model/serialize.rb +114 -64
- data/lib/lutaml/model/type/decimal.rb +5 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model/xml_adapter/builder/nokogiri.rb +1 -0
- data/lib/lutaml/model/xml_adapter/builder/oga.rb +180 -0
- data/lib/lutaml/model/xml_adapter/builder/ox.rb +1 -0
- data/lib/lutaml/model/xml_adapter/oga/document.rb +20 -0
- data/lib/lutaml/model/xml_adapter/oga/element.rb +117 -0
- data/lib/lutaml/model/xml_adapter/oga_adapter.rb +77 -44
- data/lib/lutaml/model/xml_adapter/xml_document.rb +14 -12
- data/lib/lutaml/model/xml_mapping.rb +3 -0
- data/lib/lutaml/model/xml_mapping_rule.rb +13 -4
- data/lib/lutaml/model.rb +1 -0
- data/spec/address_spec.rb +1 -0
- data/spec/fixtures/sample_model.rb +7 -0
- data/spec/lutaml/model/custom_model_spec.rb +47 -1
- data/spec/lutaml/model/included_spec.rb +192 -0
- data/spec/lutaml/model/mixed_content_spec.rb +48 -32
- data/spec/lutaml/model/multiple_mapping_spec.rb +329 -0
- data/spec/lutaml/model/ordered_content_spec.rb +1 -1
- data/spec/lutaml/model/render_nil_spec.rb +3 -0
- data/spec/lutaml/model/root_mappings_spec.rb +297 -0
- data/spec/lutaml/model/serializable_spec.rb +42 -7
- data/spec/lutaml/model/type/boolean_spec.rb +62 -0
- data/spec/lutaml/model/with_child_mapping_spec.rb +182 -0
- data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +11 -11
- data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +67 -1
- data/spec/lutaml/model/xml_adapter_spec.rb +2 -2
- data/spec/lutaml/model/xml_mapping_spec.rb +32 -9
- data/spec/sample_model_spec.rb +114 -0
- metadata +12 -2
data/README.adoc
CHANGED
@@ -1977,6 +1977,843 @@ end
|
|
1977
1977
|
----
|
1978
1978
|
====
|
1979
1979
|
|
1980
|
+
|
1981
|
+
|
1982
|
+
==== Collection with keyed elements (keyed collection)
|
1983
|
+
|
1984
|
+
===== General
|
1985
|
+
|
1986
|
+
NOTE: This feature is for key-value data model serialization and deserialization
|
1987
|
+
only.
|
1988
|
+
|
1989
|
+
The `map` method with the `root_mappings` option is used for key-value data that
|
1990
|
+
is keyed using an attribute value.
|
1991
|
+
|
1992
|
+
In other words, the key of a key-value pair in a collection is actually the
|
1993
|
+
value of an attribute that belongs to the value.
|
1994
|
+
|
1995
|
+
Simply put, the following two data structures are considered to have the same
|
1996
|
+
data:
|
1997
|
+
|
1998
|
+
[[collection-keyed-by-value]]
|
1999
|
+
.A YAML collection as a keyed object, each key with value of the `id` attribute
|
2000
|
+
[source,yaml]
|
2001
|
+
----
|
2002
|
+
---
|
2003
|
+
vase1:
|
2004
|
+
name: Imperial Vase
|
2005
|
+
bowl2:
|
2006
|
+
name: 18th Century Bowl
|
2007
|
+
----
|
2008
|
+
|
2009
|
+
[[collection-unkeyed-by-value]]
|
2010
|
+
.A YAML collection as an array, the `id` attribute value located inside each element
|
2011
|
+
[source,yaml]
|
2012
|
+
----
|
2013
|
+
---
|
2014
|
+
- id: vase1
|
2015
|
+
name: Imperial Vase
|
2016
|
+
- id: bowl2
|
2017
|
+
name: 18th Century Bowl
|
2018
|
+
----
|
2019
|
+
|
2020
|
+
There are key difference between these two data structures:
|
2021
|
+
|
2022
|
+
* The <<collection-keyed-by-value,keyed object>> (first data structure) ensures
|
2023
|
+
uniqueness of the `id` attribute value across the collection, while the
|
2024
|
+
<<collection-unkeyed-by-value,array>> (second data structure) does not.
|
2025
|
+
|
2026
|
+
* The value of the `id` attribute in the first data structure *exists outside*
|
2027
|
+
of the formal structure of the data object, instead, it *only exists at the
|
2028
|
+
collection level*. On the other hand, the value *exists inside* the structure of
|
2029
|
+
the data object in the second data structure.
|
2030
|
+
|
2031
|
+
The `map` method with the `root_mappings` option, in practice, parses the first
|
2032
|
+
data structure in the same way that you would access / manipulate the second
|
2033
|
+
data structure, while retaining the serialization semantics of using an
|
2034
|
+
attribute as key.
|
2035
|
+
|
2036
|
+
As a result, usage of lutaml-model across both types of collections are
|
2037
|
+
identical (except when serialized).
|
2038
|
+
|
2039
|
+
|
2040
|
+
Syntax:
|
2041
|
+
|
2042
|
+
[source,ruby]
|
2043
|
+
----
|
2044
|
+
class SomeKeyedCollection < Lutaml::Model::Serializable
|
2045
|
+
attribute :name_of_attribute, AttributeValueType, collection: true
|
2046
|
+
|
2047
|
+
json | yaml | toml | key_value do
|
2048
|
+
map to: :name_of_attribute, <1>
|
2049
|
+
root_mappings: { <2>
|
2050
|
+
# `:key` is a reserved keyword
|
2051
|
+
value_type_attribute_name_for_key: :key, <3>
|
2052
|
+
# `:value` is a reserved keyword (and optional)
|
2053
|
+
value_type_attribute_name_for_value: :value, <4>
|
2054
|
+
# `[path name]` represents the path to access the value in the
|
2055
|
+
# serialization data model to be assigned to
|
2056
|
+
# `AttributeValueType.value_type_attribute_name_for_custom_type`
|
2057
|
+
value_type_attribute_name_for_custom_type: [path name] <5>
|
2058
|
+
}
|
2059
|
+
end
|
2060
|
+
end
|
2061
|
+
|
2062
|
+
class AttributeValueType < Lutaml::Model::Serializable
|
2063
|
+
attribute :value_type_attribute_name_for_key, :string
|
2064
|
+
attribute :value_type_attribute_name_for_value, :string
|
2065
|
+
attribute :value_type_attribute_name_for_custom_type, CustomType
|
2066
|
+
end
|
2067
|
+
----
|
2068
|
+
<1> The `map` option indicates that this class represents the root of the
|
2069
|
+
serialization object being passed in. The `name_of_attribute` is the name
|
2070
|
+
of the attribute that will hold the collection data. (Mandatory)
|
2071
|
+
<2> The `root_mappings` keyword specifies what the collection key represents and
|
2072
|
+
and value for model. (Mandatory)
|
2073
|
+
<3> The `key` keyword specifies the attribute name of the individual collection
|
2074
|
+
object type that represents its key used in the collection. (Mandatory)
|
2075
|
+
<4> The `value` keyword specifies the attribute name of the individual collection
|
2076
|
+
object type that represents its data used in the collection. (Optional, if
|
2077
|
+
not specified, the entire object is used as the value.)
|
2078
|
+
<5> The `value_type_attribute_name_for_custom_type` is the name of the attribute
|
2079
|
+
inside the individual collection object (`AttributeValueType`) that will hold
|
2080
|
+
the value accessible in the serialization data model fetched at `[path name]`.
|
2081
|
+
|
2082
|
+
The mapping syntax here is similar to that of <<attribute-extraction>> except
|
2083
|
+
that the `:key` and `:value` keywords are allowed in addition to `{path}`.
|
2084
|
+
|
2085
|
+
|
2086
|
+
There are 3 cases when working with a keyed collection:
|
2087
|
+
|
2088
|
+
. Case 1: Only move the "key" into the collection object.
|
2089
|
+
|
2090
|
+
. Case 2: Move the "key" into the collection object, override all other
|
2091
|
+
mappings. Maps `:key` and another attribute, then we override all the other
|
2092
|
+
mappings (clean slate)
|
2093
|
+
|
2094
|
+
. Case 3: Move the "key" into the collection object to an attribute, map the
|
2095
|
+
entire "value" to another attribute of the collection object.
|
2096
|
+
|
2097
|
+
|
2098
|
+
===== Case 1: Only move the "key" into the collection object
|
2099
|
+
|
2100
|
+
In this case, the "key" of the keyed collection is moved into the collection
|
2101
|
+
object, and all other mappings are left as they are.
|
2102
|
+
|
2103
|
+
When the "key" is moved into the collection object, the following happens:
|
2104
|
+
|
2105
|
+
* The "key" of the keyed collection maps to a particular attribute of the
|
2106
|
+
collection's instance object.
|
2107
|
+
* The "value" of the keyed collection (with its various content) maps to the
|
2108
|
+
collection's instance object following the collection's instance object type's
|
2109
|
+
default mappings.
|
2110
|
+
|
2111
|
+
The `root_mappings` option **should only contain one mapping**, and the mapping
|
2112
|
+
must lead **to the `:key` keyword**.
|
2113
|
+
|
2114
|
+
Syntax:
|
2115
|
+
|
2116
|
+
[source,ruby]
|
2117
|
+
----
|
2118
|
+
class SomeKeyedCollection < Lutaml::Model::Serializable
|
2119
|
+
attribute :name_of_attribute, AttributeValueType, collection: true
|
2120
|
+
|
2121
|
+
json | yaml | toml | key_value do
|
2122
|
+
map to: :name_of_attribute,
|
2123
|
+
root_mappings: {
|
2124
|
+
value_type_attribute_name_for_key: :key, <1>
|
2125
|
+
}
|
2126
|
+
end
|
2127
|
+
end
|
2128
|
+
|
2129
|
+
class AttributeValueType < Lutaml::Model::Serializable
|
2130
|
+
attribute :value_type_attribute_name_for_key, :string
|
2131
|
+
attribute :value_type_attribute_name_for_value, :string
|
2132
|
+
attribute :value_type_attribute_name_for_custom_type, CustomType
|
2133
|
+
end
|
2134
|
+
----
|
2135
|
+
<1> The `:key` keyword specifies that the "key" of the keyed collection maps
|
2136
|
+
to the `value_type_attribute_name_for_key` attribute of the collection's
|
2137
|
+
instance object (i.e. `AttributeValueType`).
|
2138
|
+
|
2139
|
+
|
2140
|
+
|
2141
|
+
.Using `map` with `root_mappings` (only `key`) to map a keyed collection into individual models
|
2142
|
+
[example]
|
2143
|
+
====
|
2144
|
+
Given this data:
|
2145
|
+
|
2146
|
+
[source,yaml]
|
2147
|
+
----
|
2148
|
+
---
|
2149
|
+
vase1:
|
2150
|
+
name: Imperial Vase
|
2151
|
+
bowl2:
|
2152
|
+
name: 18th Century Bowl
|
2153
|
+
----
|
2154
|
+
|
2155
|
+
A model can be defined for this YAML as follows:
|
2156
|
+
|
2157
|
+
[source,ruby]
|
2158
|
+
----
|
2159
|
+
# This is a normal Lutaml::Model class
|
2160
|
+
class Ceramic < Lutaml::Model::Serializable
|
2161
|
+
attribute :ceramic_id, :string
|
2162
|
+
attribute :ceramic_name, :string
|
2163
|
+
|
2164
|
+
key_value do
|
2165
|
+
map 'id', to: :ceramic_id
|
2166
|
+
map 'name', to: :ceramic_name
|
2167
|
+
end
|
2168
|
+
end
|
2169
|
+
|
2170
|
+
# This is Lutaml::Model class that represents the collection of Ceramic objects
|
2171
|
+
class CeramicCollection < Lutaml::Model::Serializable
|
2172
|
+
attribute :ceramics, Ceramic, collection: true
|
2173
|
+
|
2174
|
+
key_value do
|
2175
|
+
map to: :ceramics, # All data goes to the `ceramics` attribute
|
2176
|
+
root_mappings: {
|
2177
|
+
# The key of an object in this collection is mapped to the ceramic_id
|
2178
|
+
# attribute of the Ceramic object.
|
2179
|
+
ceramic_id: :key # "key" is a reserved keyword
|
2180
|
+
}
|
2181
|
+
end
|
2182
|
+
end
|
2183
|
+
----
|
2184
|
+
|
2185
|
+
[source,ruby]
|
2186
|
+
----
|
2187
|
+
# Parsing the YAML collection with dynamic data keys
|
2188
|
+
> ceramic_collection = CeramicCollection.from_yaml(yaml)
|
2189
|
+
> #<CeramicCollection:0x0000000104ac7240
|
2190
|
+
@ceramics=
|
2191
|
+
[#<Ceramic:0x0000000104ac6e30 @ceramic_id="vase1", @ceramic_name="Imperial Vase">,
|
2192
|
+
#<Ceramic:0x0000000104ac58f0 @ceramic_id="bowl2", @ceramic_name="18th Century Bowl">]
|
2193
|
+
|
2194
|
+
# NOTE: When an individual Ceramic object is serialized, the `id` attribute is
|
2195
|
+
# the original key in the incoming YAML data, and because there were no mappings defined along with the `:key`, everyting is mapped to the `Ceramic` object using the mappings defined in the `Ceramic` class.
|
2196
|
+
> first_ceramic = ceramic_collection.ceramics.first
|
2197
|
+
> puts first_ceramic.to_yaml
|
2198
|
+
=>
|
2199
|
+
# ---
|
2200
|
+
# id: vase1
|
2201
|
+
# name: Imperial Vase
|
2202
|
+
|
2203
|
+
# NOTE: When in a collection, the `ceramic_id` attribute is used to key the data,
|
2204
|
+
# and it disappears from the individual object.
|
2205
|
+
> puts ceramic_collection.to_yaml
|
2206
|
+
=>
|
2207
|
+
# ---
|
2208
|
+
# vase1:
|
2209
|
+
# name: Imperial Vase
|
2210
|
+
# bowl2:
|
2211
|
+
# name: 18th Century Bowl
|
2212
|
+
|
2213
|
+
# NOTE: When the collection is serialized, the `ceramic_id` attribute is used to
|
2214
|
+
# key the data. This is defined through the `map` with `root_mappings` method in
|
2215
|
+
# CeramicCollection.
|
2216
|
+
> new_collection = CeramicCollection.new(ceramics: [
|
2217
|
+
Ceramic.new(ceramic_id: "vase1", ceramic_name: "Imperial Vase"),
|
2218
|
+
Ceramic.new(ceramic_id: "bowl2", ceramic_name: "18th Century Bowl")
|
2219
|
+
])
|
2220
|
+
> puts new_collection.to_yaml
|
2221
|
+
=>
|
2222
|
+
# ---
|
2223
|
+
# vase1:
|
2224
|
+
# name: Imperial Vase
|
2225
|
+
# bowl2:
|
2226
|
+
# name: 18th Century Bowl
|
2227
|
+
----
|
2228
|
+
====
|
2229
|
+
|
2230
|
+
|
2231
|
+
|
2232
|
+
===== Case 2: Mapping the `key` and complex ``value``s
|
2233
|
+
|
2234
|
+
In this use case, the "key" of the keyed collection is moved into the collection
|
2235
|
+
object, and all other mappings are overridden.
|
2236
|
+
|
2237
|
+
When more than one mapping rule exists in the `root_mappings` option, the
|
2238
|
+
`root_mappings` option will override all other mappings in the collection object.
|
2239
|
+
|
2240
|
+
When the "key" is moved into the collection object, the following happens:
|
2241
|
+
|
2242
|
+
* The "key" of the keyed collection maps to a particular attribute of the
|
2243
|
+
collection's instance object.
|
2244
|
+
|
2245
|
+
* The data of the "value" of the keyed collection have their own mappings
|
2246
|
+
overridden by the new mapping rules of the `root_mappings` option.
|
2247
|
+
|
2248
|
+
The `root_mappings` option **can contain more than one mapping**, with one of
|
2249
|
+
the mapping rules leading **to the `:key` keyword**.
|
2250
|
+
|
2251
|
+
|
2252
|
+
Syntax:
|
2253
|
+
|
2254
|
+
[source,ruby]
|
2255
|
+
----
|
2256
|
+
class SomeKeyedCollection < Lutaml::Model::Serializable
|
2257
|
+
attribute :name_of_attribute, AttributeValueType, collection: true
|
2258
|
+
|
2259
|
+
json | yaml | toml | key_value do
|
2260
|
+
map to: :name_of_attribute,
|
2261
|
+
root_mappings: {
|
2262
|
+
value_type_attribute_name_for_key: :key, <1>
|
2263
|
+
value_type_attribute_name_for_value_data_1: "serialization_format_name_1", <2>
|
2264
|
+
value_type_attribute_name_for_value_data_2: "serialization_format_name_2",
|
2265
|
+
value_type_attribute_name_for_value_data_3: ["path name", ...] <3>
|
2266
|
+
# ...
|
2267
|
+
}
|
2268
|
+
end
|
2269
|
+
end
|
2270
|
+
|
2271
|
+
class AttributeValueType < Lutaml::Model::Serializable
|
2272
|
+
attribute :value_type_attribute_name_for_key, :string
|
2273
|
+
attribute :value_type_attribute_name_for_value_data_1, :string
|
2274
|
+
attribute :value_type_attribute_name_for_value_data_2, SomeType
|
2275
|
+
attribute :value_type_attribute_name_for_value_data_3, MoreType
|
2276
|
+
# ...
|
2277
|
+
end
|
2278
|
+
----
|
2279
|
+
<1> The `:key` keyword specifies that the "key" of the keyed collection maps
|
2280
|
+
to the `value_type_attribute_name_for_key` attribute of the collection's
|
2281
|
+
instance object (i.e. `AttributeValueType`).
|
2282
|
+
<2> The `serialization_format_name_1` target specifies that the
|
2283
|
+
`serialization_format_name_2` key of the keyed collection value maps to the
|
2284
|
+
`value_type_attribute_name_for_value_data_1` attribute of the collection's
|
2285
|
+
instance object.
|
2286
|
+
<3> The `[path name]` target specifies to fetch from `[path name]` in the
|
2287
|
+
serialization data model to be assigned to the
|
2288
|
+
`value_type_attribute_name_for_value_data_3` attribute of the collection's
|
2289
|
+
instance object.
|
2290
|
+
|
2291
|
+
When the `root_mappings` mapping contains more than one mapping rule that is not
|
2292
|
+
to `:key` or `:value`, the `root_mappings` mapping will override all other
|
2293
|
+
mappings in the collection object. This means that unmapped attributes in
|
2294
|
+
`root_mappings` will not be incorporated in the collection instance objects.
|
2295
|
+
|
2296
|
+
.Using `map` with `root_mappings` (`key` and complex `value`) to map a keyed collection into individual models
|
2297
|
+
[example]
|
2298
|
+
====
|
2299
|
+
|
2300
|
+
[source,yaml]
|
2301
|
+
----
|
2302
|
+
"vase1":
|
2303
|
+
type: "vase"
|
2304
|
+
details:
|
2305
|
+
name: "Imperial Vase"
|
2306
|
+
insignia: "Tang Tianbao"
|
2307
|
+
urn:
|
2308
|
+
primary: "urn:ceramic:vase:vase1"
|
2309
|
+
"bowl2":
|
2310
|
+
type: "bowl"
|
2311
|
+
details:
|
2312
|
+
name: "18th Century Bowl"
|
2313
|
+
insignia: "Ming Wanli"
|
2314
|
+
urn:
|
2315
|
+
primary: "urn:ceramic:bowl:bowl2"
|
2316
|
+
----
|
2317
|
+
|
2318
|
+
A model can be defined for this YAML as follows:
|
2319
|
+
|
2320
|
+
[source,ruby]
|
2321
|
+
----
|
2322
|
+
# This is a normal Lutaml::Model class
|
2323
|
+
class CeramicDetails < Lutaml::Model::Serializable
|
2324
|
+
attribute :name, :string
|
2325
|
+
attribute :insignia, :string
|
2326
|
+
|
2327
|
+
key_value do
|
2328
|
+
map 'name', to: :name
|
2329
|
+
map 'insignia', to: :insignia
|
2330
|
+
end
|
2331
|
+
end
|
2332
|
+
|
2333
|
+
# This is a normal Lutaml::Model class
|
2334
|
+
class Ceramic < Lutaml::Model::Serializable
|
2335
|
+
attribute :ceramic_id, :string
|
2336
|
+
attribute :ceramic_type, :string
|
2337
|
+
attribute :ceramic_details, CeramicDetails
|
2338
|
+
attribute :ceramic_urn, :string
|
2339
|
+
|
2340
|
+
key_value do
|
2341
|
+
map 'id', to: :ceramic_id
|
2342
|
+
map 'type', to: :ceramic_type
|
2343
|
+
map 'details', to: :ceramic_details
|
2344
|
+
map 'urn', to: :ceramic_urn
|
2345
|
+
end
|
2346
|
+
end
|
2347
|
+
|
2348
|
+
# This is Lutaml::Model class that represents the collection of Ceramic objects
|
2349
|
+
class CeramicCollection < Lutaml::Model::Serializable
|
2350
|
+
attribute :ceramics, Ceramic, collection: true
|
2351
|
+
|
2352
|
+
key_value do
|
2353
|
+
map to: :ceramics, # All data goes to the `ceramics` attribute
|
2354
|
+
root_mappings: {
|
2355
|
+
# The key of an object in this collection is mapped to the ceramic_id
|
2356
|
+
# attribute of the Ceramic object.
|
2357
|
+
# (e.g. `vase1`, `bowl2`)
|
2358
|
+
ceramic_id: :key,
|
2359
|
+
ceramic_type: :type,
|
2360
|
+
ceramic_details: "details",
|
2361
|
+
ceramic_urn: ["urn", "primary"]
|
2362
|
+
}
|
2363
|
+
end
|
2364
|
+
end
|
2365
|
+
----
|
2366
|
+
|
2367
|
+
The output becomes:
|
2368
|
+
|
2369
|
+
[source,ruby]
|
2370
|
+
----
|
2371
|
+
> ceramics_collection = CeramicCollection.from_yaml(yaml)
|
2372
|
+
=> #<CeramicCollection:0x0000000107a2cf30
|
2373
|
+
@ceramics=
|
2374
|
+
[#<Ceramic:0x0000000107a2cf30
|
2375
|
+
@ceramic_id="vase1",
|
2376
|
+
@ceramic_type="vase",
|
2377
|
+
@ceramic_details=
|
2378
|
+
#<CeramicDetails:0x0000000107a2cf30
|
2379
|
+
@name="Imperial Vase",
|
2380
|
+
@insignia="Tang Tianbao">,
|
2381
|
+
@ceramic_urn="urn:ceramic:vase:vase1">,
|
2382
|
+
#<Ceramic:0x0000000107a2cf30
|
2383
|
+
@ceramic_id="bowl2",
|
2384
|
+
@ceramic_type="bowl",
|
2385
|
+
@ceramic_details=
|
2386
|
+
#<CeramicDetails:0x0000000107a2cf30
|
2387
|
+
@name="18th Century Bowl",
|
2388
|
+
@insignia="Ming Wanli">
|
2389
|
+
@ceramic_urn="urn:ceramic:bowl:bowl2">]
|
2390
|
+
|
2391
|
+
> first_ceramic = ceramics_collection.ceramics.first
|
2392
|
+
> puts first_ceramic.to_yaml
|
2393
|
+
=>
|
2394
|
+
# ---
|
2395
|
+
# id: vase1
|
2396
|
+
# type: vase
|
2397
|
+
# details:
|
2398
|
+
# name: Imperial Vase
|
2399
|
+
# insignia: Tang Tianbao
|
2400
|
+
# urn: urn:ceramic:vase:vase1
|
2401
|
+
|
2402
|
+
> new_collection = CeramicCollection.new(ceramics: [
|
2403
|
+
Ceramic.new(ceramic_id: "vase1",
|
2404
|
+
ceramic_type: "vase",
|
2405
|
+
ceramic_urn: "urn:ceramic:vase:vase1",
|
2406
|
+
ceramic_details: CeramicDetails.new(
|
2407
|
+
name: "Imperial Vase", insignia: "Tang Tianbao")
|
2408
|
+
),
|
2409
|
+
Ceramic.new(ceramic_id: "bowl2",
|
2410
|
+
ceramic_type: "bowl",
|
2411
|
+
ceramic_urn: "urn:ceramic:vase:bowl2",
|
2412
|
+
ceramic_details: CeramicDetails.new(
|
2413
|
+
name: "18th Century Bowl", insignia: "Ming Wanli")
|
2414
|
+
)
|
2415
|
+
])
|
2416
|
+
> new_collection.to_yaml
|
2417
|
+
>
|
2418
|
+
# ---
|
2419
|
+
# vase1:
|
2420
|
+
# type: vase
|
2421
|
+
# details:
|
2422
|
+
# name: Imperial Vase
|
2423
|
+
# insignia: Tang Tianbao
|
2424
|
+
# urn:
|
2425
|
+
# primary: urn:ceramic:vase:vase1
|
2426
|
+
# bowl2:
|
2427
|
+
# type: bowl
|
2428
|
+
# details:
|
2429
|
+
# name: 18th Century Bowl
|
2430
|
+
# insignia: Ming Wanli
|
2431
|
+
# urn:
|
2432
|
+
# primary: urn:ceramic:bowl:bowl2
|
2433
|
+
----
|
2434
|
+
====
|
2435
|
+
|
2436
|
+
|
2437
|
+
===== Case 3: Mapping the `key` and delegating `value` to an inner object
|
2438
|
+
|
2439
|
+
In this use case, the "key" of the keyed collection is moved into the collection
|
2440
|
+
object to an attribute, and the entire "value" of the keyed collection is mapped
|
2441
|
+
to another attribute of the collection object.
|
2442
|
+
|
2443
|
+
When the "key" is moved into the collection object, the following happens:
|
2444
|
+
|
2445
|
+
* The "key" of the keyed collection maps to a particular attribute of the
|
2446
|
+
collection's instance object.
|
2447
|
+
|
2448
|
+
* The data of the "value" of the keyed collection will be entirely mapped into
|
2449
|
+
an attribute of the collection's instance object.
|
2450
|
+
|
2451
|
+
* The original mapping of the "value" attribute of the collection's instance
|
2452
|
+
object is retained.
|
2453
|
+
|
2454
|
+
The `root_mappings` option **should only contain two mappings**, and the mappings
|
2455
|
+
must lead **to both the `:key` and `:value` keywords**.
|
2456
|
+
|
2457
|
+
|
2458
|
+
Syntax:
|
2459
|
+
|
2460
|
+
[source,ruby]
|
2461
|
+
----
|
2462
|
+
class SomeKeyedCollection < Lutaml::Model::Serializable
|
2463
|
+
attribute :name_of_attribute, AttributeValueType, collection: true
|
2464
|
+
|
2465
|
+
json | yaml | toml | key_value do
|
2466
|
+
map to: :name_of_attribute,
|
2467
|
+
root_mappings: {
|
2468
|
+
value_type_attribute_name_for_key: :key, <1>
|
2469
|
+
value_type_attribute_name_for_value: :value <2>
|
2470
|
+
}
|
2471
|
+
end
|
2472
|
+
end
|
2473
|
+
|
2474
|
+
class AttributeValueType < Lutaml::Model::Serializable
|
2475
|
+
attribute :value_type_attribute_name_for_key, :string
|
2476
|
+
attribute :value_type_attribute_name_for_value, SomeObject
|
2477
|
+
end
|
2478
|
+
----
|
2479
|
+
<1> The `:key` keyword specifies that the "key" of the keyed collection maps
|
2480
|
+
to the `value_type_attribute_name_for_key` attribute of the collection's
|
2481
|
+
instance object (i.e. `AttributeValueType`).
|
2482
|
+
<2> The `:value` keyword specifies that the entire "value" of the keyed
|
2483
|
+
collection maps to the `value_type_attribute_name_for_value` attribute of the
|
2484
|
+
collection's instance object (i.e. `SomeObject`).
|
2485
|
+
|
2486
|
+
When the `root_mappings` mapping contains more than one mapping rule, the
|
2487
|
+
`root_mappings` mapping will override all other mappings in the collection
|
2488
|
+
object. This means that unmapped attributes in `root_mappings` will not be
|
2489
|
+
incorporated in the collection instance objects.
|
2490
|
+
|
2491
|
+
|
2492
|
+
|
2493
|
+
.Using `map` with `root_mappings` (`key` and `value`) to map a keyed collection into individual models
|
2494
|
+
[example]
|
2495
|
+
====
|
2496
|
+
Given this data:
|
2497
|
+
|
2498
|
+
[source,yaml]
|
2499
|
+
----
|
2500
|
+
---
|
2501
|
+
vase1:
|
2502
|
+
name: Imperial Vase
|
2503
|
+
insignia: "Tang Tianbao"
|
2504
|
+
bowl2:
|
2505
|
+
name: 18th Century Bowl
|
2506
|
+
insignia: "Ming Wanli"
|
2507
|
+
----
|
2508
|
+
|
2509
|
+
A model can be defined for this YAML as follows:
|
2510
|
+
|
2511
|
+
[source,ruby]
|
2512
|
+
----
|
2513
|
+
# This is a normal Lutaml::Model class
|
2514
|
+
class CeramicDetails < Lutaml::Model::Serializable
|
2515
|
+
attribute :name, :string
|
2516
|
+
attribute :insignia, :string
|
2517
|
+
|
2518
|
+
key_value do
|
2519
|
+
map 'name', to: :name
|
2520
|
+
map 'insignia', to: :insignia
|
2521
|
+
end
|
2522
|
+
end
|
2523
|
+
|
2524
|
+
# This is a normal Lutaml::Model class
|
2525
|
+
class Ceramic < Lutaml::Model::Serializable
|
2526
|
+
attribute :ceramic_id, :string
|
2527
|
+
attribute :ceramic_details, CeramicDetails
|
2528
|
+
|
2529
|
+
key_value do
|
2530
|
+
map 'id', to: :ceramic_id
|
2531
|
+
map 'details', to: :ceramic_details
|
2532
|
+
end
|
2533
|
+
end
|
2534
|
+
|
2535
|
+
# This is Lutaml::Model class that represents the collection of Ceramic objects
|
2536
|
+
class CeramicCollection < Lutaml::Model::Serializable
|
2537
|
+
attribute :ceramics, Ceramic, collection: true
|
2538
|
+
|
2539
|
+
key_value do
|
2540
|
+
map to: :ceramics, # All data goes to the `ceramics` attribute
|
2541
|
+
root_mappings: {
|
2542
|
+
# The key of an object in this collection is mapped to the ceramic_id
|
2543
|
+
# attribute of the Ceramic object.
|
2544
|
+
# (e.g. `vase1`, `bowl2`)
|
2545
|
+
ceramic_id: :key,
|
2546
|
+
# The value of an object in this collection is mapped to the
|
2547
|
+
# ceramic_details attribute of the Ceramic object.
|
2548
|
+
# (e.g. `name: 18th Century Bowl`, `insignia: "Ming Wanli"`
|
2549
|
+
ceramic_details: :value
|
2550
|
+
}
|
2551
|
+
end
|
2552
|
+
end
|
2553
|
+
----
|
2554
|
+
|
2555
|
+
[source,ruby]
|
2556
|
+
----
|
2557
|
+
# Parsing the YAML collection with dynamic data keys
|
2558
|
+
> ceramic_collection = CeramicCollection.from_yaml(yaml)
|
2559
|
+
> #<CeramicCollection:0x0000000104ac7240
|
2560
|
+
@ceramics=
|
2561
|
+
[#<Ceramic:0x0000000104ac6e30
|
2562
|
+
@ceramic_id="vase1",
|
2563
|
+
@ceramic_details=
|
2564
|
+
#<CeramicDetails:0x0000000104ac6e30
|
2565
|
+
@name="Imperial Vase",
|
2566
|
+
@insignia="Tang Tianbao">,
|
2567
|
+
#<Ceramic:0x0000000104ac58f0
|
2568
|
+
@ceramic_id="bowl2",
|
2569
|
+
@ceramic_details=
|
2570
|
+
#<CeramicDetails:0x0000000104ac58f0
|
2571
|
+
@name="18th Century Bowl",
|
2572
|
+
@insignia="Ming Wanli">]
|
2573
|
+
|
2574
|
+
# NOTE: When an individual Ceramic object is serialized, the `id` attribute is
|
2575
|
+
# the original key in the incoming YAML data.
|
2576
|
+
> first_ceramic = ceramic_collection.ceramics.first
|
2577
|
+
> puts first_ceramic.to_yaml
|
2578
|
+
=>
|
2579
|
+
# ---
|
2580
|
+
# id: vase1
|
2581
|
+
# details:
|
2582
|
+
# name: Imperial Vase
|
2583
|
+
# insignia: Tang Tianbao
|
2584
|
+
|
2585
|
+
# NOTE: When in a collection, the `ceramic_id` attribute is used to key the data,
|
2586
|
+
# and it disappears from the individual object.
|
2587
|
+
> puts ceramic_collection.to_yaml
|
2588
|
+
=>
|
2589
|
+
# ---
|
2590
|
+
# vase1:
|
2591
|
+
# name: Imperial Vase
|
2592
|
+
# insignia: Tang Tianbao
|
2593
|
+
# bowl2:
|
2594
|
+
# name: 18th Century Bowl
|
2595
|
+
# insignia: Ming Wanli
|
2596
|
+
|
2597
|
+
# NOTE: When the collection is serialized, the `ceramic_id` attribute is used to
|
2598
|
+
# key the data. This is defined through the `map` with `root_mappings` method in
|
2599
|
+
# CeramicCollection.
|
2600
|
+
> new_collection = CeramicCollection.new(ceramics: [
|
2601
|
+
Ceramic.new(ceramic_id: "vase1",
|
2602
|
+
ceramic_details: CeramicDetails.new(
|
2603
|
+
name: "Imperial Vase", insignia: "Tang Tianbao")
|
2604
|
+
),
|
2605
|
+
Ceramic.new(ceramic_id: "bowl2",
|
2606
|
+
ceramic_details: CeramicDetails.new(
|
2607
|
+
name: "18th Century Bowl", insignia: "Ming Wanli")
|
2608
|
+
)
|
2609
|
+
])
|
2610
|
+
> puts new_collection.to_yaml
|
2611
|
+
=>
|
2612
|
+
# ---
|
2613
|
+
# vase1:
|
2614
|
+
# name: Imperial Vase
|
2615
|
+
# insignia: Tang Tianbao
|
2616
|
+
# bowl2:
|
2617
|
+
# name: 18th Century Bowl
|
2618
|
+
# insignia: Ming Wanli
|
2619
|
+
----
|
2620
|
+
====
|
2621
|
+
|
2622
|
+
|
2623
|
+
[[attribute-extraction]]
|
2624
|
+
==== Attribute extraction
|
2625
|
+
|
2626
|
+
NOTE: This feature is for key-value data model serialization only.
|
2627
|
+
|
2628
|
+
The `child_mappings` option is used to extract results from a key-value
|
2629
|
+
serialization data model (JSON, YAML, TOML) into a `Lutaml::Model::Serializable`
|
2630
|
+
object (collection or not).
|
2631
|
+
|
2632
|
+
The values are extracted from the key-value data model using the list of keys
|
2633
|
+
provided.
|
2634
|
+
|
2635
|
+
Syntax:
|
2636
|
+
|
2637
|
+
[source,ruby]
|
2638
|
+
----
|
2639
|
+
class SomeObject < Lutaml::Model::Serializable
|
2640
|
+
attribute :name_of_attribute, AttributeValueType, collection: true
|
2641
|
+
|
2642
|
+
json | yaml | toml | key_value do
|
2643
|
+
map 'key_value_model_attribute_name', to: :name_of_attribute,
|
2644
|
+
child_mappings: {
|
2645
|
+
value_type_attribute_name_1: <1>
|
2646
|
+
{path_to_value_1}, <2>
|
2647
|
+
value_type_attribute_name_2:
|
2648
|
+
{path_to_value_2},
|
2649
|
+
# ...
|
2650
|
+
}
|
2651
|
+
end
|
2652
|
+
end
|
2653
|
+
----
|
2654
|
+
<1> The `value_type_attribute_name_1` is the attribute name in the
|
2655
|
+
`AttributeValueType` model. The value of this attribute will be assigned the key
|
2656
|
+
of the hash in the key-value data model.
|
2657
|
+
|
2658
|
+
<2> The `path_to_value_1` is an array of keys that represent the path to the
|
2659
|
+
value in the key-value serialization data model. The keys are used to extract the value from
|
2660
|
+
the key-value serialization data model and assign it to the attribute in the
|
2661
|
+
`AttributeValueType` model.
|
2662
|
+
+
|
2663
|
+
The `path_to_value` is in a nested array format with each value a symbol or a
|
2664
|
+
string, where each symbol represents a key to traverse down. The last key in the
|
2665
|
+
path is the value to be extracted.
|
2666
|
+
|
2667
|
+
.Determining the path to value in a key-value data model
|
2668
|
+
[example]
|
2669
|
+
====
|
2670
|
+
The following JSON contains 2 keys in schema named `engine` and `gearbox`.
|
2671
|
+
|
2672
|
+
[source,json]
|
2673
|
+
----
|
2674
|
+
{
|
2675
|
+
"components": {
|
2676
|
+
"engine": {
|
2677
|
+
"manufacturer": "Ford",
|
2678
|
+
"model": "V8"
|
2679
|
+
},
|
2680
|
+
"gearbox": {
|
2681
|
+
"manufacturer": "Toyota",
|
2682
|
+
"model": "4-speed"
|
2683
|
+
}
|
2684
|
+
}
|
2685
|
+
}
|
2686
|
+
----
|
2687
|
+
|
2688
|
+
The path to value for the `engine` schema is `[:components, :engine]` and for
|
2689
|
+
the `gearbox` schema is `[:components, :gearbox]`.
|
2690
|
+
====
|
2691
|
+
|
2692
|
+
In `path_to_value`, the `:key` and `:value` are reserved instructions used to
|
2693
|
+
assign the key or value of the serialization data respectively as the value to
|
2694
|
+
the attribute.
|
2695
|
+
|
2696
|
+
[example]
|
2697
|
+
====
|
2698
|
+
In the following JSON content, the `path_to_value` for the object keys named
|
2699
|
+
`engine` and `gearbox` will utilize the `:key` keyword to assign the key of the
|
2700
|
+
object as the value of a designated attribute.
|
2701
|
+
|
2702
|
+
[source,json]
|
2703
|
+
----
|
2704
|
+
{
|
2705
|
+
"components": {
|
2706
|
+
"engine": { /*...*/ },
|
2707
|
+
"gearbox": { /*...*/ }
|
2708
|
+
}
|
2709
|
+
}
|
2710
|
+
----
|
2711
|
+
====
|
2712
|
+
|
2713
|
+
If a specified value path is not found, the corresponding attribute in the model
|
2714
|
+
will be assigned a `nil` value.
|
2715
|
+
|
2716
|
+
.Attribute values set to `nil` when the `path_to_value` is not found
|
2717
|
+
[example]
|
2718
|
+
====
|
2719
|
+
In the following JSON content, the `path_to_value` of `[:extras, :sunroof]` and
|
2720
|
+
`[:extras, :drinks_cooler]` at the object `"gearbox"` would be set to `nil`.
|
2721
|
+
|
2722
|
+
[source,json]
|
2723
|
+
----
|
2724
|
+
{
|
2725
|
+
"components": {
|
2726
|
+
"engine": {
|
2727
|
+
"manufacturer": "Ford",
|
2728
|
+
"extras": {
|
2729
|
+
"sunroof": true,
|
2730
|
+
"drinks_cooler": true
|
2731
|
+
}
|
2732
|
+
},
|
2733
|
+
"gearbox": {
|
2734
|
+
"manufacturer": "Toyota"
|
2735
|
+
}
|
2736
|
+
}
|
2737
|
+
}
|
2738
|
+
----
|
2739
|
+
====
|
2740
|
+
|
2741
|
+
|
2742
|
+
.Using the `child_mappings` option to extract values from a key-value data model
|
2743
|
+
[example]
|
2744
|
+
====
|
2745
|
+
The following JSON contains 2 keys in schema named `foo` and `bar`.
|
2746
|
+
|
2747
|
+
[source,json]
|
2748
|
+
----
|
2749
|
+
{
|
2750
|
+
"schemas": {
|
2751
|
+
"foo": { <1>
|
2752
|
+
"path": { <2>
|
2753
|
+
"link": "link one",
|
2754
|
+
"name": "one"
|
2755
|
+
}
|
2756
|
+
},
|
2757
|
+
"bar": { <1>
|
2758
|
+
"path": { <2>
|
2759
|
+
"link": "link two",
|
2760
|
+
"name": "two"
|
2761
|
+
}
|
2762
|
+
}
|
2763
|
+
}
|
2764
|
+
}
|
2765
|
+
----
|
2766
|
+
<1> The keys `foo` and `bar` are to be mapped to the `id` attribute.
|
2767
|
+
<2> The nested `path.link` and `path.name` keys are used as the `link` and
|
2768
|
+
`name` attributes, respectively.
|
2769
|
+
|
2770
|
+
A model can be defined for this JSON as follows:
|
2771
|
+
|
2772
|
+
[source,ruby]
|
2773
|
+
----
|
2774
|
+
class Schema < Lutaml::Model::Serializable
|
2775
|
+
attribute :id, :string
|
2776
|
+
attribute :link, :string
|
2777
|
+
attribute :name, :string
|
2778
|
+
end
|
2779
|
+
|
2780
|
+
class ChildMappingClass < Lutaml::Model::Serializable
|
2781
|
+
attribute :schemas, Schema, collection: true
|
2782
|
+
|
2783
|
+
json do
|
2784
|
+
map "schemas", to: :schemas,
|
2785
|
+
child_mappings: {
|
2786
|
+
id: :key,
|
2787
|
+
link: %i[path link],
|
2788
|
+
name: %i[path name],
|
2789
|
+
}
|
2790
|
+
end
|
2791
|
+
end
|
2792
|
+
----
|
2793
|
+
|
2794
|
+
The output becomes:
|
2795
|
+
|
2796
|
+
[source,ruby]
|
2797
|
+
----
|
2798
|
+
> ChildMappingClass.from_json(json)
|
2799
|
+
> #<ChildMappingClass:0x0000000104ac7240
|
2800
|
+
@schemas=
|
2801
|
+
[#<Schema:0x0000000104ac6e30 @id="foo", @link="link one", @name="one">,
|
2802
|
+
#<Schema:0x0000000104ac58f0 @id="bar", @link="link two", @name="two">]>
|
2803
|
+
> ChildMappingClass.new(schemas: [Schema.new(id: "foo", link: "link one", name: "one"), Schema.new(id: "bar", link: "link two", name: "two")]).to_json
|
2804
|
+
> #{"schemas"=>{"foo"=>{"path"=>{"link"=>"link one", "name"=>"one"}}, {"bar"=>{"path"=>{"link"=>"link two", "name"=>"two"}}}}}
|
2805
|
+
----
|
2806
|
+
|
2807
|
+
In this example:
|
2808
|
+
|
2809
|
+
* The `key` of each schema (`foo` and `bar`) is mapped to the `id` attribute.
|
2810
|
+
|
2811
|
+
* The nested `path.link` and `path.name` keys are mapped to the `link` and
|
2812
|
+
`name` attributes, respectively.
|
2813
|
+
====
|
2814
|
+
|
2815
|
+
|
2816
|
+
|
1980
2817
|
[[separate-serialization-model]]
|
1981
2818
|
=== Separate serialization model
|
1982
2819
|
|
@@ -2131,6 +2968,121 @@ end
|
|
2131
2968
|
|
2132
2969
|
=== Advanced attribute mapping
|
2133
2970
|
|
2971
|
+
==== Multiple mappings to single attribute
|
2972
|
+
|
2973
|
+
The mapping methods support multiple names mapping to a single attribute using
|
2974
|
+
an array of names.
|
2975
|
+
|
2976
|
+
Syntax:
|
2977
|
+
|
2978
|
+
[source,ruby]
|
2979
|
+
----
|
2980
|
+
json | yaml | toml | key_value do
|
2981
|
+
map ["name1", "name2"], to: :attribute_name
|
2982
|
+
end
|
2983
|
+
|
2984
|
+
xml do
|
2985
|
+
map_element ["name1", "name2"], to: :attribute_name
|
2986
|
+
map_attribute ["attr1", "attr2"], to: :attribute_name
|
2987
|
+
end
|
2988
|
+
----
|
2989
|
+
|
2990
|
+
When serializing, the first element in the array of mapped names is always used
|
2991
|
+
as the output name.
|
2992
|
+
|
2993
|
+
|
2994
|
+
.Using multiple names to map to a single attribute
|
2995
|
+
[example]
|
2996
|
+
====
|
2997
|
+
[source,ruby]
|
2998
|
+
----
|
2999
|
+
class CustomModel < Lutaml::Model::Serializable
|
3000
|
+
attribute :full_name, Lutaml::Model::Type::String
|
3001
|
+
attribute :color, Lutaml::Model::Type::String
|
3002
|
+
attribute :id, Lutaml::Model::Type::String
|
3003
|
+
|
3004
|
+
json do
|
3005
|
+
map ["name", "custom_name"], with: { to: :name_to_json, from: :name_from_json }
|
3006
|
+
map ["color", "shade"], with: { to: :color_to_json, from: :color_from_json }
|
3007
|
+
end
|
3008
|
+
|
3009
|
+
xml do
|
3010
|
+
root "CustomModel"
|
3011
|
+
map_element ["name", "custom-name"], with: { to: :name_to_xml, from: :name_from_xml }
|
3012
|
+
map_element ["color", "shade"], with: { to: :color_to_xml, from: :color_from_xml }
|
3013
|
+
map_attribute ["id", "identifier"], to: :id
|
3014
|
+
end
|
3015
|
+
|
3016
|
+
# Custom methods for JSON
|
3017
|
+
def name_to_json(model, doc)
|
3018
|
+
doc["name"] = "JSON Model: #{model.full_name}"
|
3019
|
+
end
|
3020
|
+
|
3021
|
+
def name_from_json(model, value)
|
3022
|
+
model.full_name = value&.sub(/^JSON Model: /, "")
|
3023
|
+
end
|
3024
|
+
|
3025
|
+
def color_to_json(model, doc)
|
3026
|
+
doc["color"] = model.color.upcase
|
3027
|
+
end
|
3028
|
+
|
3029
|
+
def color_from_json(model, value)
|
3030
|
+
model.color = value&.downcase
|
3031
|
+
end
|
3032
|
+
|
3033
|
+
# Custom methods for XML
|
3034
|
+
def name_to_xml(model, parent, doc)
|
3035
|
+
el = doc.create_element("name")
|
3036
|
+
doc.add_text(el, "XML Model: #{model.full_name}")
|
3037
|
+
doc.add_element(parent, el)
|
3038
|
+
end
|
3039
|
+
|
3040
|
+
def name_from_xml(model, value)
|
3041
|
+
model.full_name = value.sub(/^XML Model: /, "")
|
3042
|
+
end
|
3043
|
+
|
3044
|
+
def color_to_xml(model, parent, doc)
|
3045
|
+
el = doc.create_element("color")
|
3046
|
+
doc.add_text(el, model.color.upcase)
|
3047
|
+
doc.add_element(parent, el)
|
3048
|
+
end
|
3049
|
+
|
3050
|
+
def color_from_xml(model, value)
|
3051
|
+
model.color = value.downcase
|
3052
|
+
end
|
3053
|
+
end
|
3054
|
+
----
|
3055
|
+
|
3056
|
+
For JSON:
|
3057
|
+
[source,json]
|
3058
|
+
----
|
3059
|
+
{
|
3060
|
+
"custom_name": "JSON Model: Vase",
|
3061
|
+
"shade": "BLUE",
|
3062
|
+
"identifier": "123"
|
3063
|
+
}
|
3064
|
+
----
|
3065
|
+
|
3066
|
+
For XML:
|
3067
|
+
[source,xml]
|
3068
|
+
----
|
3069
|
+
<CustomModel id="123">
|
3070
|
+
<name>XML Model: Vase</name>
|
3071
|
+
<color>BLUE</color>
|
3072
|
+
</CustomModel>
|
3073
|
+
----
|
3074
|
+
|
3075
|
+
[source,ruby]
|
3076
|
+
----
|
3077
|
+
> model = CustomModel.from_json(json)
|
3078
|
+
> model.full_name
|
3079
|
+
> # "Vase"
|
3080
|
+
> model.color
|
3081
|
+
> # "blue"
|
3082
|
+
----
|
3083
|
+
====
|
3084
|
+
|
3085
|
+
|
2134
3086
|
==== Attribute mapping delegation
|
2135
3087
|
|
2136
3088
|
Delegate attribute mappings to nested objects using the `delegate` option.
|
@@ -2353,190 +3305,6 @@ end
|
|
2353
3305
|
====
|
2354
3306
|
|
2355
3307
|
|
2356
|
-
[[attribute-extraction]]
|
2357
|
-
==== Attribute extraction (for key-value data models only)
|
2358
|
-
|
2359
|
-
NOTE: This feature is for key-value data model serialization only.
|
2360
|
-
|
2361
|
-
The `child_mappings` option is used to extract results from a key-value data
|
2362
|
-
model (JSON, YAML, TOML) into a `Lutaml::Model` collection.
|
2363
|
-
|
2364
|
-
The values are extracted from the key-value data model using the list of keys
|
2365
|
-
provided.
|
2366
|
-
|
2367
|
-
Syntax:
|
2368
|
-
|
2369
|
-
[source,ruby]
|
2370
|
-
----
|
2371
|
-
json | yaml | toml do
|
2372
|
-
map 'key_value_model_attribute_name', to: :name_of_attribute,
|
2373
|
-
child_mappings: {
|
2374
|
-
key_attribute_name_1: <1>
|
2375
|
-
{path_to_value_1}, <2>
|
2376
|
-
key_attribute_name_2:
|
2377
|
-
{path_to_value_2},
|
2378
|
-
# ...
|
2379
|
-
}
|
2380
|
-
end
|
2381
|
-
----
|
2382
|
-
<1> The `key_attribute_name_1` is the attribute name in the model. The value of
|
2383
|
-
this attribute will be assigned the key of the hash in the key-value data model.
|
2384
|
-
|
2385
|
-
<2> The `path_to_value_1` is an array of keys that represent the path to the
|
2386
|
-
value in the key-value data model. The keys are used to extract the value from
|
2387
|
-
the key-value data model and assign it to the attribute in the model.
|
2388
|
-
|
2389
|
-
The `path_to_value` is in a nested array format with each value a symbol, where
|
2390
|
-
each symbol represents a key to traverse down. The last key in the path is the
|
2391
|
-
value to be extracted.
|
2392
|
-
|
2393
|
-
.Determining the path to value in a key-value data model
|
2394
|
-
[example]
|
2395
|
-
====
|
2396
|
-
The following JSON contains 2 keys in schema named `engine` and `gearbox`.
|
2397
|
-
|
2398
|
-
[source,json]
|
2399
|
-
----
|
2400
|
-
{
|
2401
|
-
"components": {
|
2402
|
-
"engine": {
|
2403
|
-
"manufacturer": "Ford",
|
2404
|
-
"model": "V8"
|
2405
|
-
},
|
2406
|
-
"gearbox": {
|
2407
|
-
"manufacturer": "Toyota",
|
2408
|
-
"model": "4-speed"
|
2409
|
-
}
|
2410
|
-
}
|
2411
|
-
}
|
2412
|
-
----
|
2413
|
-
|
2414
|
-
The path to value for the `engine` schema is `[:components, :engine]` and for
|
2415
|
-
the `gearbox` schema is `[:components, :gearbox]`.
|
2416
|
-
====
|
2417
|
-
|
2418
|
-
In `path_to_value`, the `:key` and `:value` are reserved instructions used to
|
2419
|
-
assign the key or value of the serialization data respectively as the value to
|
2420
|
-
the attribute.
|
2421
|
-
|
2422
|
-
[example]
|
2423
|
-
====
|
2424
|
-
In the following JSON content, the `path_to_value` for the object keys named
|
2425
|
-
`engine` and `gearbox` will utilize the `:key` keyword to assign the key of the
|
2426
|
-
object as the value of a designated attribute.
|
2427
|
-
|
2428
|
-
[source,json]
|
2429
|
-
----
|
2430
|
-
{
|
2431
|
-
"components": {
|
2432
|
-
"engine": { /*...*/ },
|
2433
|
-
"gearbox": { /*...*/ }
|
2434
|
-
}
|
2435
|
-
}
|
2436
|
-
----
|
2437
|
-
====
|
2438
|
-
|
2439
|
-
If a specified value path is not found, the corresponding attribute in the model
|
2440
|
-
will be assigned a `nil` value.
|
2441
|
-
|
2442
|
-
.Attribute values set to `nil` when the `path_to_value` is not found
|
2443
|
-
[example]
|
2444
|
-
====
|
2445
|
-
In the following JSON content, the `path_to_value` of `[:extras, :sunroof]` and
|
2446
|
-
`[:extras, :drinks_cooler]` at the object `"gearbox"` would be set to `nil`.
|
2447
|
-
|
2448
|
-
[source,json]
|
2449
|
-
----
|
2450
|
-
{
|
2451
|
-
"components": {
|
2452
|
-
"engine": {
|
2453
|
-
"manufacturer": "Ford",
|
2454
|
-
"extras": {
|
2455
|
-
"sunroof": true,
|
2456
|
-
"drinks_cooler": true
|
2457
|
-
}
|
2458
|
-
},
|
2459
|
-
"gearbox": {
|
2460
|
-
"manufacturer": "Toyota"
|
2461
|
-
}
|
2462
|
-
}
|
2463
|
-
}
|
2464
|
-
----
|
2465
|
-
====
|
2466
|
-
|
2467
|
-
|
2468
|
-
.Using the `child_mappings` option to extract values from a key-value data model
|
2469
|
-
[example]
|
2470
|
-
====
|
2471
|
-
The following JSON contains 2 keys in schema named `foo` and `bar`.
|
2472
|
-
|
2473
|
-
[source,json]
|
2474
|
-
----
|
2475
|
-
{
|
2476
|
-
"schemas": {
|
2477
|
-
"foo": { <1>
|
2478
|
-
"path": { <2>
|
2479
|
-
"link": "link one",
|
2480
|
-
"name": "one"
|
2481
|
-
}
|
2482
|
-
},
|
2483
|
-
"bar": { <1>
|
2484
|
-
"path": { <2>
|
2485
|
-
"link": "link two",
|
2486
|
-
"name": "two"
|
2487
|
-
}
|
2488
|
-
}
|
2489
|
-
}
|
2490
|
-
}
|
2491
|
-
----
|
2492
|
-
<1> The keys `foo` and `bar` are to be mapped to the `id` attribute.
|
2493
|
-
<2> The nested `path.link` and `path.name` keys are used as the `link` and
|
2494
|
-
`name` attributes, respectively.
|
2495
|
-
|
2496
|
-
A model can be defined for this JSON as follows:
|
2497
|
-
|
2498
|
-
[source,ruby]
|
2499
|
-
----
|
2500
|
-
class Schema < Lutaml::Model::Serializable
|
2501
|
-
attribute :id, :string
|
2502
|
-
attribute :link, :string
|
2503
|
-
attribute :name, :string
|
2504
|
-
end
|
2505
|
-
|
2506
|
-
class ChildMappingClass < Lutaml::Model::Serializable
|
2507
|
-
attribute :schemas, Schema, collection: true
|
2508
|
-
|
2509
|
-
json do
|
2510
|
-
map "schemas", to: :schemas,
|
2511
|
-
child_mappings: {
|
2512
|
-
id: :key,
|
2513
|
-
link: %i[path link],
|
2514
|
-
name: %i[path name],
|
2515
|
-
}
|
2516
|
-
end
|
2517
|
-
end
|
2518
|
-
----
|
2519
|
-
|
2520
|
-
The output becomes:
|
2521
|
-
|
2522
|
-
[source,ruby]
|
2523
|
-
----
|
2524
|
-
> ChildMappingClass.from_json(json)
|
2525
|
-
> #<ChildMappingClass:0x0000000104ac7240
|
2526
|
-
@schemas=
|
2527
|
-
[#<Schema:0x0000000104ac6e30 @id="foo", @link="link one", @name="one">,
|
2528
|
-
#<Schema:0x0000000104ac58f0 @id="bar", @link="link two", @name="two">]>
|
2529
|
-
> ChildMappingClass.new(schemas: [Schema.new(id: "foo", link: "link one", name: "one"), Schema.new(id: "bar", link: "link two", name: "two")]).to_json
|
2530
|
-
> #{"schemas"=>{"foo"=>{"path"=>{"link"=>"link one", "name"=>"one"}}, {"bar"=>{"path"=>{"link"=>"link two", "name"=>"two"}}}}
|
2531
|
-
----
|
2532
|
-
|
2533
|
-
In this example:
|
2534
|
-
|
2535
|
-
* The `key` of each schema (`foo` and `bar`) is mapped to the `id` attribute.
|
2536
|
-
|
2537
|
-
* The nested `path.link` and `path.name` keys are mapped to the `link` and
|
2538
|
-
`name` attributes, respectively.
|
2539
|
-
====
|
2540
3308
|
|
2541
3309
|
|
2542
3310
|
== Validation
|
@@ -2641,6 +3409,13 @@ klin.validate
|
|
2641
3409
|
Lutaml::Model uses an adapter pattern to support multiple libraries for each
|
2642
3410
|
serialization format.
|
2643
3411
|
|
3412
|
+
Lutaml::Model supports the following serialization formats:
|
3413
|
+
|
3414
|
+
* XML (https://www.w3.org/TR/xmlschema-1/[W3C XML Schema (Second Edition)], XML 1.0)
|
3415
|
+
* YAML (https://yaml.org/[YAML version 1.2])
|
3416
|
+
* JSON (https://www.ecma-international.org/publications-and-standards/standards/ecma-404/[ECMA-404 The JSON Data Interchange Standard], unofficial link: https://www.json.org[JSON])
|
3417
|
+
* TOML (https://toml.io/en[TOML version 1.0])
|
3418
|
+
|
2644
3419
|
You will need to specify the configuration for the adapter you want to use. The
|
2645
3420
|
easiest way is to copy and paste the following configuration into your code.
|
2646
3421
|
|
@@ -2684,9 +3459,25 @@ NOTE: By default `yaml_adapter_type` and `json_adapter_type` are set to
|
|
2684
3459
|
|
2685
3460
|
Lutaml::Model supports the following XML adapters:
|
2686
3461
|
|
2687
|
-
|
2688
|
-
|
2689
|
-
|
3462
|
+
Nokogiri::
|
3463
|
+
(default)
|
3464
|
+
Popular `libxml` based XML parser for Ruby.
|
3465
|
+
Requires native extensions (i.e. compiled C code).
|
3466
|
+
Requires the `nokogiri` gem.
|
3467
|
+
|
3468
|
+
Oga::
|
3469
|
+
(optional)
|
3470
|
+
Pure Ruby XML parser.
|
3471
|
+
Does not require native extensions and is suitable for
|
3472
|
+
https://opalrb.com[Opal] (Ruby on JavaScript).
|
3473
|
+
Requires the `oga` gem.
|
3474
|
+
|
3475
|
+
Ox::
|
3476
|
+
(optional)
|
3477
|
+
Fast XML parser and object serializer for Ruby, implemented partially in C.
|
3478
|
+
Requires native extensions (i.e. compiled C code).
|
3479
|
+
Requires the `ox` gem.
|
3480
|
+
|
2690
3481
|
|
2691
3482
|
.Using the Nokogiri XML adapter
|
2692
3483
|
[source,ruby]
|
@@ -2726,7 +3517,10 @@ end
|
|
2726
3517
|
|
2727
3518
|
Lutaml::Model supports only one YAML adapter.
|
2728
3519
|
|
2729
|
-
|
3520
|
+
YAML::
|
3521
|
+
(default)
|
3522
|
+
The Psych YAML parser and emitter for Ruby.
|
3523
|
+
Included in the Ruby standard library.
|
2730
3524
|
|
2731
3525
|
.Using the YAML adapter
|
2732
3526
|
[source,ruby]
|
@@ -2745,8 +3539,15 @@ end
|
|
2745
3539
|
|
2746
3540
|
Lutaml::Model supports the following JSON adapters:
|
2747
3541
|
|
2748
|
-
|
2749
|
-
|
3542
|
+
JSON::
|
3543
|
+
(default)
|
3544
|
+
The standard JSON library for Ruby.
|
3545
|
+
Included in the Ruby standard library.
|
3546
|
+
|
3547
|
+
MultiJson::
|
3548
|
+
(optional)
|
3549
|
+
A gem that provides a common interface to multiple JSON libraries.
|
3550
|
+
Requires the `multi_json` gem.
|
2750
3551
|
|
2751
3552
|
.Using the JSON adapter
|
2752
3553
|
[source,ruby]
|
@@ -2774,8 +3575,18 @@ end
|
|
2774
3575
|
|
2775
3576
|
Lutaml::Model supports the following TOML adapters:
|
2776
3577
|
|
2777
|
-
|
2778
|
-
|
3578
|
+
Toml-rb::
|
3579
|
+
(default)
|
3580
|
+
A TOML parser and serializer for Ruby that is compatible with the TOML v1.0.0
|
3581
|
+
specification.
|
3582
|
+
Requires the `toml-rb` gem.
|
3583
|
+
|
3584
|
+
Tomlib::
|
3585
|
+
(optional)
|
3586
|
+
Toml-rb fork that is compatible with the TOML v1.0.0 specification, but with
|
3587
|
+
additional features.
|
3588
|
+
Requires the `tomlib` gem.
|
3589
|
+
|
2779
3590
|
|
2780
3591
|
.Using the Toml-rb adapter
|
2781
3592
|
[source,ruby]
|