lutaml-model 0.8.125 → 0.8.127
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/README.adoc +52 -0
- data/docs/_guides/keyvalue-serialization.adoc +75 -0
- data/lib/compat/opal/lutaml_model_boot.rb +15 -0
- data/lib/lutaml/key_value/transform.rb +126 -26
- data/lib/lutaml/model/error/unknown_mapping_error.rb +18 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +115 -2
- data/lib/lutaml/model/serialize/transformation_builder.rb +4 -3
- data/lib/lutaml/model/transform.rb +29 -4
- data/lib/lutaml/model/transformation_registry.rb +7 -5
- data/lib/lutaml/model/type/date_time_with_timezone.rb +24 -0
- data/lib/lutaml/model/type/date_with_timezone.rb +24 -0
- data/lib/lutaml/model/type/day_time_duration.rb +31 -0
- data/lib/lutaml/model/type/email_address.rb +22 -0
- data/lib/lutaml/model/type/hostname.rb +24 -0
- data/lib/lutaml/model/type/ip_v4_address.rb +23 -0
- data/lib/lutaml/model/type/ip_v6_address.rb +42 -0
- data/lib/lutaml/model/type/metaschema_pattern.rb +36 -0
- data/lib/lutaml/model/type/non_negative_integer.rb +24 -0
- data/lib/lutaml/model/type/positive_integer.rb +24 -0
- data/lib/lutaml/model/type/token.rb +23 -0
- data/lib/lutaml/model/type/uri_reference.rb +23 -0
- data/lib/lutaml/model/type/uuid.rb +23 -0
- data/lib/lutaml/model/type/year_month_duration.rb +25 -0
- data/lib/lutaml/model/type.rb +27 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +2 -0
- data/lib/lutaml/xml/adapter/xml_serializer.rb +8 -6
- data/lib/lutaml/xml/model_transform.rb +8 -4
- data/lib/lutaml/xml/schema/xsd_schema.rb +30 -9
- data/lib/lutaml/xml/transformation.rb +3 -2
- data/spec/lutaml/key_value/child_map_plan_spec.rb +169 -0
- data/spec/lutaml/model/metaschema_types_spec.rb +224 -0
- data/spec/lutaml/model/serialization_profiles_spec.rb +276 -0
- metadata +20 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e11de7c2a53b5c720a4d8066af0a1b0bfef2107e6c853bfb441d478b32c0d794
|
|
4
|
+
data.tar.gz: 6f8e67b8df0ab6ee2635b3b2c83a7dc86332e7883d951df45f8422f303b1ca61
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 447cbac13172eab2c4fcfa38f03f6777a164dfd31108e9aa3ce46ca8eed631863c9bd584f88a83dae005d80836c6be5ee4ff046676a77492a8c52f509b2cb4e8
|
|
7
|
+
data.tar.gz: 7b13afe6b4d08b89b4e79143599ac90da0fb6536583f8e315e829f66a6579e6fd22ed8aa87acc46359e06741eada456f7e5ee97db897df5e172fbcb4c43705db
|
data/README.adoc
CHANGED
|
@@ -830,6 +830,19 @@ Every type has a corresponding Ruby class and a serialization format type.
|
|
|
830
830
|
| `:qname` | `Lutaml::Model::Type::QName` | `xs:QName` | `string` | `string` | `"prefix:localName"`
|
|
831
831
|
| `:base64_binary` | `String` | `xs:base64Binary` | `string` | `string` | `"SGVsbG8gV29ybGQ="`
|
|
832
832
|
| `:hex_binary` | `String` | `xs:hexBinary` | `string` | `string` | `"48656c6c6f"`
|
|
833
|
+
| `:email_address` | `String` | `xs:string` | `string` | `string` | `"user@example.com"`
|
|
834
|
+
| `:hostname` | `String` | `xs:string` | `string` | `string` | `"example.com"`
|
|
835
|
+
| `:ip_v4_address` | `String` | `xs:string` | `string` | `string` | `"192.168.0.1"`
|
|
836
|
+
| `:ip_v6_address` | `String` | `xs:string` | `string` | `string` | `"2001:db8::2:1"`
|
|
837
|
+
| `:token` | `String` | `xs:string` | `string` | `string` | `"label_3.v2"`
|
|
838
|
+
| `:uri_reference` | `String` | `xs:anyURI` | `string` | `string` | `"path/to/resource"`
|
|
839
|
+
| `:uuid` | `String` | `xs:string` | `string` | `string` | `"a3bb189e-8bf9-4888-9912-ace4e6543002"`
|
|
840
|
+
| `:non_negative_integer` | `Integer` | `xs:nonNegativeInteger` | `number` | `integer` | `7`
|
|
841
|
+
| `:positive_integer` | `Integer` | `xs:positiveInteger` | `number` | `integer` | `7`
|
|
842
|
+
| `:date_with_timezone` | `Date`/`DateTime` | `xs:date` | `string` | `string` | `"2019-12-02-08:00"`
|
|
843
|
+
| `:date_time_with_timezone` | `DateTime` | `xs:dateTime` | `string` | `string` | `"2019-12-02T16:39:57-08:00"`
|
|
844
|
+
| `:day_time_duration` | `Lutaml::Model::Type::Duration` | `xs:dayTimeDuration` | `string` | `string` | `"P1DT12H45M"`
|
|
845
|
+
| `:year_month_duration` | `Lutaml::Model::Type::Duration` | `xs:yearMonthDuration` | `string` | `string` | `"P1Y6M"`
|
|
833
846
|
| `:hash` | `Hash` | complex element | object | map | `{key: "value"}`
|
|
834
847
|
| (nil value) | `nil` | `xs:anyType` | `null` | `null` | `null`
|
|
835
848
|
| `{ ref: [Model, :attr] }` | `Reference` | `xs:string` | `string` | `string` | `"model-id"`
|
|
@@ -840,6 +853,45 @@ Every type has a corresponding Ruby class and a serialization format type.
|
|
|
840
853
|
|===
|
|
841
854
|
|
|
842
855
|
|
|
856
|
+
=== NIST Metaschema types
|
|
857
|
+
|
|
858
|
+
The NIST Metaschema simple data types are supported as first-class value
|
|
859
|
+
types (https://pages.nist.gov/metaschema/specification/datatypes/). Each
|
|
860
|
+
type carries the normative metaschema-datatypes pattern and answers
|
|
861
|
+
`valid?` for a value; casting follows the lenient base-type behavior.
|
|
862
|
+
|
|
863
|
+
.Source types
|
|
864
|
+
[cols="3,4,2"]
|
|
865
|
+
|===
|
|
866
|
+
| Type code | Metaschema datatype | Restriction
|
|
867
|
+
|
|
868
|
+
| `:email_address` | `email-address` | contains an at-sign
|
|
869
|
+
| `:hostname` | `hostname` | non-empty, trimmed
|
|
870
|
+
| `:ip_v4_address` | `ip-v4-address` | dotted-quad IPv4
|
|
871
|
+
| `:ip_v6_address` | `ip-v6-address` | RFC3513 IPv6 (incl. zones, embedded IPv4)
|
|
872
|
+
| `:token` | `token` | NMTOKEN-like letter/number/punctuation run
|
|
873
|
+
| `:uri_reference` | `uri-reference` | relative or absolute URI reference
|
|
874
|
+
| `:uuid` | `uuid` | RFC4122 UUID, version 4 or 5
|
|
875
|
+
| `:non_negative_integer` | `non-negative-integer` | integer >= 0
|
|
876
|
+
| `:positive_integer` | `positive-integer` | integer >= 1
|
|
877
|
+
| `:date_with_timezone` | `date-with-timezone` | date with `Z` or offset
|
|
878
|
+
| `:date_time_with_timezone` | `date-time-with-timezone` | date-time with `Z` or offset
|
|
879
|
+
| `:day_time_duration` | `day-time-duration` | duration without years/months
|
|
880
|
+
| `:year_month_duration` | `year-month-duration` | duration without time components
|
|
881
|
+
|===
|
|
882
|
+
|
|
883
|
+
.Usage
|
|
884
|
+
[source,ruby]
|
|
885
|
+
----
|
|
886
|
+
class Asset < Lutaml::Model::Serializable
|
|
887
|
+
attribute :id, :uuid
|
|
888
|
+
attribute :owner, :email_address
|
|
889
|
+
attribute :host, :hostname
|
|
890
|
+
attribute :address, :ip_v4_address
|
|
891
|
+
attribute :renewal, :year_month_duration
|
|
892
|
+
end
|
|
893
|
+
----
|
|
894
|
+
|
|
843
895
|
=== Decimal type
|
|
844
896
|
|
|
845
897
|
The Decimal type uses Ruby's `BigDecimal` for arbitrary-precision decimal
|
|
@@ -1188,3 +1188,78 @@ In this example:
|
|
|
1188
1188
|
`name` attributes, respectively.
|
|
1189
1189
|
====
|
|
1190
1190
|
|
|
1191
|
+
|
|
1192
|
+
== Serialization mapping profiles
|
|
1193
|
+
|
|
1194
|
+
A model can declare multiple serialization mappings for the same format —
|
|
1195
|
+
*serialization mapping profiles* (also called projections or lenses). Each
|
|
1196
|
+
profile is a complete mapping: what it maps is the projection, and
|
|
1197
|
+
anything it does not map stays outside it. The base mapping (declared
|
|
1198
|
+
without a name) remains the default.
|
|
1199
|
+
|
|
1200
|
+
=== Declaring a profile
|
|
1201
|
+
|
|
1202
|
+
[source,ruby]
|
|
1203
|
+
----
|
|
1204
|
+
class Person < Lutaml::Model::Serializable
|
|
1205
|
+
attribute :name, :string
|
|
1206
|
+
attribute :email, :string
|
|
1207
|
+
attribute :internal_id, :string
|
|
1208
|
+
|
|
1209
|
+
json do
|
|
1210
|
+
map "name", to: :name
|
|
1211
|
+
map "email", to: :email
|
|
1212
|
+
map "internal_id", to: :internal_id
|
|
1213
|
+
end
|
|
1214
|
+
|
|
1215
|
+
json :compact do
|
|
1216
|
+
map "name", to: :name
|
|
1217
|
+
end
|
|
1218
|
+
end
|
|
1219
|
+
----
|
|
1220
|
+
|
|
1221
|
+
=== Selecting a profile
|
|
1222
|
+
|
|
1223
|
+
Profiles are selected per call with the `mapping:` option, for both
|
|
1224
|
+
serialization and parsing. An unknown profile name raises
|
|
1225
|
+
`Lutaml::Model::UnknownMappingError`.
|
|
1226
|
+
|
|
1227
|
+
[source,ruby]
|
|
1228
|
+
----
|
|
1229
|
+
person.to_json # => {"name"=>"Ed", ...}
|
|
1230
|
+
person.to_json(mapping: :compact) # => {"name"=>"Ed"}
|
|
1231
|
+
Person.from_json(json, mapping: :compact)
|
|
1232
|
+
----
|
|
1233
|
+
|
|
1234
|
+
`only:` and `except:` compose with the selected profile. Child models
|
|
1235
|
+
always serialize through their own mappings; a parent's profile does not
|
|
1236
|
+
propagate to them.
|
|
1237
|
+
|
|
1238
|
+
=== Runtime-constructed mappings
|
|
1239
|
+
|
|
1240
|
+
A mapping object can be built at runtime and wired to the model, or used
|
|
1241
|
+
directly for a single call. This supports projections that are not known
|
|
1242
|
+
at class-definition time.
|
|
1243
|
+
|
|
1244
|
+
[source,ruby]
|
|
1245
|
+
----
|
|
1246
|
+
mapping = Lutaml::Model::Config.mappings_class_for(:yaml).new
|
|
1247
|
+
mapping.map "full_name", to: :name
|
|
1248
|
+
|
|
1249
|
+
Person.mapping :public, mapping # register as a named profile
|
|
1250
|
+
person.to_yaml(mapping: :public) # by name
|
|
1251
|
+
|
|
1252
|
+
person.to_yaml(mapping: mapping) # ephemeral, unwired single use
|
|
1253
|
+
----
|
|
1254
|
+
|
|
1255
|
+
`Person.mapping :name, nil, format: :yaml` removes a registered profile.
|
|
1256
|
+
`Person.serialization_mappings(:yaml)` returns the registered profiles of
|
|
1257
|
+
a format. Re-declaring or re-registering a name replaces the previous
|
|
1258
|
+
mapping.
|
|
1259
|
+
|
|
1260
|
+
XML profiles follow the same shape: `xml :registry do ... end` declares a
|
|
1261
|
+
profile with its own root element, and `to_xml(mapping: :registry)` /
|
|
1262
|
+
`from_xml(data, mapping: :registry)` select it. XSD generation honors
|
|
1263
|
+
the selection too: `Lutaml::Model::Schema.to_xsd(Record, mapping:
|
|
1264
|
+
:registry)` generates the schema of that projection, leaving unmapped
|
|
1265
|
+
attributes outside it.
|
|
@@ -281,6 +281,7 @@ require "lutaml/model/error/undefined_attribute_error"
|
|
|
281
281
|
require "lutaml/model/error/union_schema_unsupported_error"
|
|
282
282
|
require "lutaml/model/error/unknown_adapter_type_error"
|
|
283
283
|
require "lutaml/model/error/unknown_discriminator_error"
|
|
284
|
+
require "lutaml/model/error/unknown_mapping_error"
|
|
284
285
|
require "lutaml/model/error/unknown_sequence_mapping_error"
|
|
285
286
|
require "lutaml/model/error/unknown_type_error"
|
|
286
287
|
require "lutaml/model/error/unresolvable_type_error"
|
|
@@ -333,21 +334,35 @@ require "lutaml/model/type/base64_binary"
|
|
|
333
334
|
require "lutaml/model/type/boolean"
|
|
334
335
|
require "lutaml/model/type/date"
|
|
335
336
|
require "lutaml/model/type/date_time"
|
|
337
|
+
require "lutaml/model/type/date_time_with_timezone"
|
|
338
|
+
require "lutaml/model/type/date_with_timezone"
|
|
339
|
+
require "lutaml/model/type/day_time_duration"
|
|
336
340
|
require "lutaml/model/type/decimal"
|
|
337
341
|
require "lutaml/model/type/duration"
|
|
342
|
+
require "lutaml/model/type/email_address"
|
|
338
343
|
require "lutaml/model/type/float"
|
|
339
344
|
require "lutaml/model/type/hash"
|
|
340
345
|
require "lutaml/model/type/hex_binary"
|
|
346
|
+
require "lutaml/model/type/hostname"
|
|
341
347
|
require "lutaml/model/type/integer"
|
|
348
|
+
require "lutaml/model/type/ip_v4_address"
|
|
349
|
+
require "lutaml/model/type/ip_v6_address"
|
|
350
|
+
require "lutaml/model/type/metaschema_pattern"
|
|
351
|
+
require "lutaml/model/type/non_negative_integer"
|
|
352
|
+
require "lutaml/model/type/positive_integer"
|
|
342
353
|
require "lutaml/model/type/qname"
|
|
343
354
|
require "lutaml/model/type/reference"
|
|
344
355
|
require "lutaml/model/type/string"
|
|
345
356
|
require "lutaml/model/type/symbol"
|
|
346
357
|
require "lutaml/model/type/time"
|
|
347
358
|
require "lutaml/model/type/time_without_date"
|
|
359
|
+
require "lutaml/model/type/token"
|
|
348
360
|
require "lutaml/model/type/uninitialized_class_guard"
|
|
349
361
|
require "lutaml/model/type/uri"
|
|
362
|
+
require "lutaml/model/type/uri_reference"
|
|
363
|
+
require "lutaml/model/type/uuid"
|
|
350
364
|
require "lutaml/model/type/value"
|
|
365
|
+
require "lutaml/model/type/year_month_duration"
|
|
351
366
|
require "lutaml/rdf/namespaces/dcterms_namespace"
|
|
352
367
|
require "lutaml/rdf/namespaces/owl_namespace"
|
|
353
368
|
require "lutaml/rdf/namespaces/rdf_namespace"
|
|
@@ -97,7 +97,9 @@ module Lutaml
|
|
|
97
97
|
# NEW ARCHITECTURE: Use KeyValue::Transformation if available
|
|
98
98
|
# This provides symmetric OOP architecture with XmlDataModel
|
|
99
99
|
if context.is_a?(Class) && context.include?(Lutaml::Model::Serialize)
|
|
100
|
-
transformation = context.transformation_for(
|
|
100
|
+
transformation = context.transformation_for(
|
|
101
|
+
format, lutaml_register, mapping: @lutaml_mapping_override
|
|
102
|
+
)
|
|
101
103
|
|
|
102
104
|
# transformation_for returns nil for cyclic dependencies or :building sentinel
|
|
103
105
|
# Fall back to legacy approach in these cases
|
|
@@ -475,12 +477,14 @@ options)
|
|
|
475
477
|
generate_remaining_mappings_for_value(child_mappings, value, format)
|
|
476
478
|
|
|
477
479
|
attr_type = attr.type(lutaml_register)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
+
rules = attr_type.mappings_for(format)
|
|
481
|
+
pairs = child_mappings.map do |attr_name, path|
|
|
482
|
+
[rules.find_by_to!(attr_name), path]
|
|
483
|
+
end
|
|
480
484
|
|
|
485
|
+
value.each do |child_obj|
|
|
481
486
|
hash.merge!(
|
|
482
|
-
extract_hash_for_child_mapping(
|
|
483
|
-
format),
|
|
487
|
+
extract_hash_for_child_mapping(pairs, child_obj, format),
|
|
484
488
|
)
|
|
485
489
|
end
|
|
486
490
|
|
|
@@ -508,14 +512,12 @@ options)
|
|
|
508
512
|
mappings.find_by_to(name)&.name.to_s
|
|
509
513
|
end
|
|
510
514
|
|
|
511
|
-
def extract_hash_for_child_mapping(
|
|
512
|
-
format)
|
|
515
|
+
def extract_hash_for_child_mapping(pairs, child_obj, format)
|
|
513
516
|
key = nil
|
|
514
517
|
value = {}
|
|
515
518
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
attr_value = normalize_attribute_value(child_obj, rule.from, format)
|
|
519
|
+
pairs.each do |rule, path|
|
|
520
|
+
attr_value = normalize_attribute_value(child_obj, rule, format)
|
|
519
521
|
|
|
520
522
|
next unless rule&.render?(attr_value, nil)
|
|
521
523
|
next key = attr_value if path == :key
|
|
@@ -527,10 +529,16 @@ format)
|
|
|
527
529
|
{ key => value }
|
|
528
530
|
end
|
|
529
531
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
532
|
+
# The child's wire-form value for one attribute, produced through
|
|
533
|
+
# the same pipeline the child's own serializer uses for a rendered
|
|
534
|
+
# value (rule transform, then attribute serialization) — without
|
|
535
|
+
# the serialize-parse string round-trip this read previously paid
|
|
536
|
+
# per child per attribute.
|
|
537
|
+
def normalize_attribute_value(child_obj, rule, format)
|
|
538
|
+
child_attr = child_obj.class.attributes(lutaml_register)[rule.to]
|
|
539
|
+
value = child_obj.public_send(rule.to)
|
|
540
|
+
value = rule.transform_value(child_attr, value, :to, format)
|
|
541
|
+
child_attr.serialize(value, format, lutaml_register)
|
|
534
542
|
end
|
|
535
543
|
|
|
536
544
|
def extract_hash_value_for_child_mapping(path, value, map_value)
|
|
@@ -764,8 +772,14 @@ partition = nil)
|
|
|
764
772
|
# crashed translate_mappings).
|
|
765
773
|
return if value.nil? && kind == :hash_map
|
|
766
774
|
|
|
767
|
-
|
|
768
|
-
|
|
775
|
+
child_plan = plan.child_map
|
|
776
|
+
value = if child_plan
|
|
777
|
+
translate_child_map_compiled(value, child_plan, attr,
|
|
778
|
+
format, instance)
|
|
779
|
+
else
|
|
780
|
+
translate_mappings(value, rule.hash_mappings, attr, format,
|
|
781
|
+
instance)
|
|
782
|
+
end
|
|
769
783
|
unless kind == :hash_map
|
|
770
784
|
value = cast_value(value, attr, format, rule,
|
|
771
785
|
instance)
|
|
@@ -798,12 +812,86 @@ partition = nil)
|
|
|
798
812
|
# (custom methods, transforms, value maps, delegates, hash mappings,
|
|
799
813
|
# unions, polymorphism) return nil and keep the interpretive path.
|
|
800
814
|
WalkPlan = ::Struct.new(:kind, :attr, :valid, :wire, :klass,
|
|
801
|
-
:collection)
|
|
815
|
+
:collection, :child_map)
|
|
816
|
+
|
|
817
|
+
# Compiled child-map translation (#986b): the per-node registry
|
|
818
|
+
# lookups (attribute type, child mapping rules, per-attribute wire
|
|
819
|
+
# names, path string casts) collapse to per-rule constants. nil
|
|
820
|
+
# child_map keeps the interpretive translate path (including its
|
|
821
|
+
# declaration-error timing).
|
|
822
|
+
ChildMapPlan = ::Struct.new(:child_mappings, :wire_names, :paths,
|
|
823
|
+
:keys_only, :child_rules, :attr_type)
|
|
802
824
|
|
|
803
825
|
# Memoized (per transform) wire name for plain rules — the from
|
|
804
826
|
# spelling never changes after definition.
|
|
805
827
|
KvSerializePlan = ::Struct.new(:wire)
|
|
806
828
|
|
|
829
|
+
def build_child_map_plan(format, rule, attr)
|
|
830
|
+
child_mappings = rule.hash_mappings
|
|
831
|
+
return nil unless child_mappings.is_a?(::Hash)
|
|
832
|
+
|
|
833
|
+
attr_type = attr.type(lutaml_register)
|
|
834
|
+
return nil unless attr_type.is_a?(Class) &&
|
|
835
|
+
attr_type.include?(Lutaml::Model::Serialize)
|
|
836
|
+
|
|
837
|
+
child_rules = attr_type.mappings_for(format, lutaml_register)
|
|
838
|
+
wire_names = {}
|
|
839
|
+
paths = {}
|
|
840
|
+
child_mappings.each do |attr_name, path|
|
|
841
|
+
attr_rule = child_rules.find_by_to!(attr_name)
|
|
842
|
+
wire_names[attr_name] = attr_rule.from.to_s
|
|
843
|
+
paths[attr_name] = compile_child_map_path(path)
|
|
844
|
+
end
|
|
845
|
+
ChildMapPlan.new(child_mappings, wire_names, paths,
|
|
846
|
+
child_mappings.values == [:key], child_rules,
|
|
847
|
+
attr_type)
|
|
848
|
+
rescue Lutaml::Model::NoMappingFoundError
|
|
849
|
+
nil
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
def compile_child_map_path(path)
|
|
853
|
+
case path
|
|
854
|
+
when :key then :key
|
|
855
|
+
when :value then :value
|
|
856
|
+
else Array(path).map(&:to_s).freeze
|
|
857
|
+
end
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
# Compiled translate_mappings (#986b). Same shape as the
|
|
861
|
+
# interpretive pair: keys_only is decided once per rule VALUE (the
|
|
862
|
+
# original tests the whole document hash, not each child).
|
|
863
|
+
def translate_child_map_compiled(value, child_plan, attr, format,
|
|
864
|
+
instance)
|
|
865
|
+
child_options = {
|
|
866
|
+
mappings: child_plan.child_rules,
|
|
867
|
+
lutaml_parent: instance,
|
|
868
|
+
lutaml_root: instance.lutaml_root || instance,
|
|
869
|
+
}
|
|
870
|
+
keys_only = child_plan.keys_only && value.values.all?(::Hash)
|
|
871
|
+
wire_names = child_plan.wire_names
|
|
872
|
+
paths = child_plan.paths
|
|
873
|
+
child_mappings = child_plan.child_mappings
|
|
874
|
+
|
|
875
|
+
value.map do |key, child_value|
|
|
876
|
+
child_hash = {}
|
|
877
|
+
child_mappings.each_key do |attr_name|
|
|
878
|
+
child_hash[wire_names[attr_name]] =
|
|
879
|
+
extract_compiled_child_value(paths[attr_name], key, child_value)
|
|
880
|
+
end
|
|
881
|
+
child_hash.merge!(child_value) if keys_only
|
|
882
|
+
map_child_data(child_hash, attr, format, instance, child_plan,
|
|
883
|
+
child_options)
|
|
884
|
+
end
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
def extract_compiled_child_value(compiled_path, key, value)
|
|
888
|
+
case compiled_path
|
|
889
|
+
when :key then key
|
|
890
|
+
when :value then value
|
|
891
|
+
else value.dig(*compiled_path)
|
|
892
|
+
end
|
|
893
|
+
end
|
|
894
|
+
|
|
807
895
|
def kv_serialize_plan(rule, attr, _instance)
|
|
808
896
|
return nil if rule.delegate || rule.raw_mapping? || rule.root_mapping? ||
|
|
809
897
|
rule.hash_mappings || rule.child_mappings ||
|
|
@@ -824,7 +912,8 @@ partition = nil)
|
|
|
824
912
|
generation = Lutaml::Model::GlobalContext.context_generation
|
|
825
913
|
if @lutaml_walk_plans.nil? || @lutaml_walk_generation != generation ||
|
|
826
914
|
@lutaml_walk_format != format
|
|
827
|
-
key = [model_class, lutaml_register, format, generation
|
|
915
|
+
key = [model_class, lutaml_register, format, generation,
|
|
916
|
+
@lutaml_mapping_override]
|
|
828
917
|
@lutaml_walk_plans =
|
|
829
918
|
(KV_WALK_PLANS[key] ||= build_walk_plans(format))
|
|
830
919
|
@lutaml_walk_generation = generation
|
|
@@ -861,6 +950,11 @@ partition = nil)
|
|
|
861
950
|
plan ? :scalar : :interpretive
|
|
862
951
|
end
|
|
863
952
|
|
|
953
|
+
if kind == :hash_map && attr
|
|
954
|
+
child = build_child_map_plan(format, rule, attr)
|
|
955
|
+
return WalkPlan.new(:hash_map, attr, valid, nil, nil, false, child)
|
|
956
|
+
end
|
|
957
|
+
|
|
864
958
|
plan || WalkPlan.new(kind, attr, valid, nil, nil, false)
|
|
865
959
|
end
|
|
866
960
|
|
|
@@ -979,14 +1073,20 @@ instance)
|
|
|
979
1073
|
child_mappings.values == [:key] && hash.values.all?(::Hash)
|
|
980
1074
|
end
|
|
981
1075
|
|
|
982
|
-
def map_child_data(child_hash, attr, format, instance
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1076
|
+
def map_child_data(child_hash, attr, format, instance,
|
|
1077
|
+
child_plan = nil, child_options = nil)
|
|
1078
|
+
if child_plan
|
|
1079
|
+
self.class.data_to_model(child_plan.attr_type, child_hash, format,
|
|
1080
|
+
child_options)
|
|
1081
|
+
else
|
|
1082
|
+
attr_type = attr.type(lutaml_register)
|
|
1083
|
+
options = {
|
|
1084
|
+
mappings: attr_type.mappings_for(format, lutaml_register),
|
|
1085
|
+
lutaml_parent: instance,
|
|
1086
|
+
lutaml_root: instance.lutaml_root || instance,
|
|
1087
|
+
}
|
|
1088
|
+
self.class.data_to_model(attr_type, child_hash, format, options)
|
|
1089
|
+
end
|
|
990
1090
|
end
|
|
991
1091
|
|
|
992
1092
|
def rule_value_extractor_class
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
# Raised when a `mapping:` selection names a serialization mapping
|
|
6
|
+
# profile the model does not declare (#176).
|
|
7
|
+
class UnknownMappingError < Error
|
|
8
|
+
def initialize(name, format)
|
|
9
|
+
dsl = format == :hash ? :hsh : format
|
|
10
|
+
super(
|
|
11
|
+
"unknown #{format} mapping profile #{name.inspect} — declare it " \
|
|
12
|
+
"with `#{dsl} :#{name} do ... end` or wire it with " \
|
|
13
|
+
"mapping :#{name}, mapping",
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -11,9 +11,16 @@ module Lutaml
|
|
|
11
11
|
# Process mapping DSL for a format
|
|
12
12
|
#
|
|
13
13
|
# @param format [Symbol] The format (:xml, :json, etc.)
|
|
14
|
-
# @param args [Array]
|
|
14
|
+
# @param args [Array] Optional profile name (Symbol/String) naming a
|
|
15
|
+
# serialization mapping (#176); other arguments keep their
|
|
16
|
+
# historical no-op behavior
|
|
15
17
|
# @param block [Proc] The DSL block to evaluate
|
|
16
|
-
def process_mapping(format, *
|
|
18
|
+
def process_mapping(format, *args, &block)
|
|
19
|
+
name = args.first
|
|
20
|
+
if name.is_a?(::Symbol) || name.is_a?(::String)
|
|
21
|
+
return declare_serialization_mapping(format, name.to_sym, &block)
|
|
22
|
+
end
|
|
23
|
+
|
|
17
24
|
klass = ::Lutaml::Model::Config.mappings_class_for(format)
|
|
18
25
|
existing = mappings[format]
|
|
19
26
|
mappings[format] = if existing.nil? || !existing.is_a?(klass)
|
|
@@ -30,6 +37,68 @@ module Lutaml
|
|
|
30
37
|
post_process_mapping(format)
|
|
31
38
|
end
|
|
32
39
|
|
|
40
|
+
# Declare a named serialization mapping (#176): a complete mapping
|
|
41
|
+
# for one format, selectable per call via `mapping:`. Re-declaring
|
|
42
|
+
# the same name replaces.
|
|
43
|
+
def declare_serialization_mapping(format, name, &block)
|
|
44
|
+
klass = ::Lutaml::Model::Config.mappings_class_for(format)
|
|
45
|
+
mapping = klass.new
|
|
46
|
+
mapping.instance_eval(&block)
|
|
47
|
+
mapping.finalize(self) if mapping.is_a?(Lutaml::Xml::Mapping)
|
|
48
|
+
post_process_mapping(format)
|
|
49
|
+
lutaml_named_mappings[format][name] = mapping
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Wire a runtime-constructed serialization mapping object under a
|
|
53
|
+
# name (#176). The format is inferred from the mapping's class;
|
|
54
|
+
# pass `format:` when the class does not identify one. `nil`
|
|
55
|
+
# mapping removes the name (requires `format:`).
|
|
56
|
+
def mapping(name, mapping_object, format: nil)
|
|
57
|
+
if mapping_object.nil?
|
|
58
|
+
raise ::ArgumentError, "removal requires format:" unless format
|
|
59
|
+
|
|
60
|
+
lutaml_named_mappings[format].delete(name)
|
|
61
|
+
return
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
format ||= format_of_mapping_object(mapping_object)
|
|
65
|
+
klass = ::Lutaml::Model::Config.mappings_class_for(format)
|
|
66
|
+
unless mapping_object.is_a?(klass)
|
|
67
|
+
raise ::ArgumentError,
|
|
68
|
+
"mapping is a #{mapping_object.class} but the #{format} " \
|
|
69
|
+
"mapping class is #{klass}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
lutaml_named_mappings[format][name] = mapping_object
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Named serialization mappings declared for a format (#176).
|
|
76
|
+
#
|
|
77
|
+
# @param format [Symbol] The format
|
|
78
|
+
# @return [Hash] name -> mapping (a shallow copy)
|
|
79
|
+
def serialization_mappings(format)
|
|
80
|
+
named = lutaml_named_mappings[format]
|
|
81
|
+
named ? named.dup : {}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def lutaml_named_mappings
|
|
85
|
+
@lutaml_named_mappings ||= ::Hash.new { |h, k| h[k] = {} }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Resolve the `mapping:` selection option (#176) into the
|
|
89
|
+
# transform seam option the transforms already read. A user-level
|
|
90
|
+
# `mappings:` takes precedence; a nil `mapping:` selects the base
|
|
91
|
+
# mapping. Mutates and returns options.
|
|
92
|
+
def resolve_serialization_mapping_option(format, options)
|
|
93
|
+
return options unless options.is_a?(::Hash) && options.key?(:mapping)
|
|
94
|
+
|
|
95
|
+
selected = options.delete(:mapping)
|
|
96
|
+
return options if selected.nil? || options.key?(:mappings)
|
|
97
|
+
|
|
98
|
+
options[:mappings] = serialization_mapping_for(format, selected)
|
|
99
|
+
options
|
|
100
|
+
end
|
|
101
|
+
|
|
33
102
|
# Hook for format-specific post-processing after mapping DSL evaluation.
|
|
34
103
|
# XML overrides this to call check_sort_configs!.
|
|
35
104
|
#
|
|
@@ -45,6 +114,8 @@ module Lutaml
|
|
|
45
114
|
# @param options [Hash] Additional options
|
|
46
115
|
# @return [Object] The deserialized model instance
|
|
47
116
|
def from(format, data, options = {})
|
|
117
|
+
resolve_serialization_mapping_option(format, options)
|
|
118
|
+
|
|
48
119
|
if format == :xml && Lutaml::Model::Config.instance.xml_plan_fast_path
|
|
49
120
|
fast = xml_plan_fast_path(data, options)
|
|
50
121
|
return fast if fast
|
|
@@ -223,6 +294,8 @@ module Lutaml
|
|
|
223
294
|
# @param options [Hash] Additional options
|
|
224
295
|
# @return [Object] The model instance
|
|
225
296
|
def of(format, doc, options = {})
|
|
297
|
+
resolve_serialization_mapping_option(format, options)
|
|
298
|
+
|
|
226
299
|
if doc.is_a?(Array) && !array_passthrough_format?(format)
|
|
227
300
|
return doc.map { |item| send(:"of_#{format}", item) }
|
|
228
301
|
end
|
|
@@ -280,6 +353,8 @@ module Lutaml
|
|
|
280
353
|
# is always preserved when available, regardless of this option.
|
|
281
354
|
# @return [String] The serialized output
|
|
282
355
|
def to(format, instance, options = {})
|
|
356
|
+
resolve_serialization_mapping_option(format, options)
|
|
357
|
+
|
|
283
358
|
# The rescue sits on the parse block, not the method, so cache
|
|
284
359
|
# store failures propagate instead of becoming InvalidFormatError.
|
|
285
360
|
with_conversion_cache(:to, format, instance, options) do
|
|
@@ -355,6 +430,8 @@ module Lutaml
|
|
|
355
430
|
# @param options [Hash] Additional options
|
|
356
431
|
# @return [Object] The format-specific data structure
|
|
357
432
|
def as(format, instance, options = {})
|
|
433
|
+
resolve_serialization_mapping_option(format, options)
|
|
434
|
+
|
|
358
435
|
if instance.is_a?(Array)
|
|
359
436
|
return instance.map { |item| public_send(:"as_#{format}", item) }
|
|
360
437
|
end
|
|
@@ -458,8 +535,44 @@ module Lutaml
|
|
|
458
535
|
end
|
|
459
536
|
end
|
|
460
537
|
|
|
538
|
+
# Resolve a mapping selection (#176): a registered profile name
|
|
539
|
+
# or a mapping instance of the format, validated. Schema
|
|
540
|
+
# generation and other mapping consumers share this resolution.
|
|
541
|
+
#
|
|
542
|
+
# @param format [Symbol] The format
|
|
543
|
+
# @param selected [Symbol, String, Mapping] The selection
|
|
544
|
+
# @return [Mapping] The resolved mapping
|
|
545
|
+
def serialization_mapping_for(format, selected)
|
|
546
|
+
if selected.is_a?(::Symbol) || selected.is_a?(::String)
|
|
547
|
+
name = selected.to_sym
|
|
548
|
+
named = lutaml_named_mappings[format]
|
|
549
|
+
return named[name] if named.key?(name)
|
|
550
|
+
|
|
551
|
+
raise Lutaml::Model::UnknownMappingError.new(name, format)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
klass = ::Lutaml::Model::Config.mappings_class_for(format)
|
|
555
|
+
return selected if selected.is_a?(klass)
|
|
556
|
+
|
|
557
|
+
raise ::ArgumentError,
|
|
558
|
+
"mapping must be a #{klass} or a registered profile " \
|
|
559
|
+
"name, got #{selected.class}"
|
|
560
|
+
end
|
|
561
|
+
|
|
461
562
|
private
|
|
462
563
|
|
|
564
|
+
def format_of_mapping_object(mapping_object)
|
|
565
|
+
format = ::Lutaml::Model::FormatRegistry.formats.find do |f|
|
|
566
|
+
::Lutaml::Model::FormatRegistry.mappings_class_for(f) ==
|
|
567
|
+
mapping_object.class
|
|
568
|
+
end
|
|
569
|
+
return format if format
|
|
570
|
+
|
|
571
|
+
raise ::ArgumentError,
|
|
572
|
+
"cannot infer the format of a #{mapping_object.class}; " \
|
|
573
|
+
"pass format:"
|
|
574
|
+
end
|
|
575
|
+
|
|
463
576
|
# Resolve adapter class for a format operation.
|
|
464
577
|
#
|
|
465
578
|
# @param format [Symbol] the format name
|
|
@@ -29,11 +29,12 @@ module Lutaml
|
|
|
29
29
|
# @param format [Symbol] The format (:xml, :json, :yaml, etc.)
|
|
30
30
|
# @param register [Symbol, Register, nil] The register for type resolution
|
|
31
31
|
# @return [Transformation, nil] The pre-compiled transformation, or nil if cycle detected
|
|
32
|
-
def transformation_for(format, register = nil)
|
|
32
|
+
def transformation_for(format, register = nil, mapping: nil)
|
|
33
33
|
resolved_register = Lutaml::Model::Register.resolve_for_child(self,
|
|
34
34
|
register)
|
|
35
|
-
TransformationRegistry.instance.get_or_build_transformation(
|
|
36
|
-
|
|
35
|
+
TransformationRegistry.instance.get_or_build_transformation(
|
|
36
|
+
self, format, resolved_register, mapping: mapping
|
|
37
|
+
)
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
# Build a new transformation instance for the format
|