lutaml-model 0.8.38 → 0.8.39
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/docs/_guides/custom-methods.adoc +71 -0
- data/docs/_guides/index.adoc +1 -0
- data/lib/lutaml/key_value/transform.rb +0 -1
- data/lib/lutaml/model/compiled_rule.rb +9 -0
- data/lib/lutaml/model/mapping/mapping_rule.rb +12 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +6 -3
- data/lib/lutaml/model/serialize.rb +12 -2
- data/lib/lutaml/model/type/string.rb +4 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/model_transform.rb +12 -5
- data/lib/lutaml/xml/transformation/rule_applier.rb +1 -7
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 15f9489eb4db353b481f160703e848e35b730cef179d32874941fc537ed962d3
|
|
4
|
+
data.tar.gz: f0067a5ffc4d66c8312d9cc478d9bd7d1771d9b639111c3158956b2d98e4468e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f1372cbf492e141015524382a3ac98cdc5f484441d70c593b7ad67b9021bfa959732700679534bb3690f86c064e3a1a82269833c5c73794fe87af24f21d239b
|
|
7
|
+
data.tar.gz: '048d412becdba7a8a99070740663d0d0a33e88d02018708818ce816d1ba4669a1f26efbf08d0c67390818c5efede0e362ef8f1785536c4233a2fa6dadd07da61'
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
= Custom serialization methods
|
|
2
|
+
|
|
3
|
+
:toc:
|
|
4
|
+
|
|
5
|
+
Lutaml::Model maps serialization constructs (`map_element`, `map_attribute`, `map_content`, key-value `map`) to *model attributes*. When the wire shape and the model shape disagree, custom methods bridge the gap at the mapping site instead of hand-rolled `to_h`/`from_h` code.
|
|
6
|
+
|
|
7
|
+
== `with:` — per-rule custom methods
|
|
8
|
+
|
|
9
|
+
Declare both directions on the rule. The methods live on the model (instance methods); the names are symbols.
|
|
10
|
+
|
|
11
|
+
[source,ruby]
|
|
12
|
+
----
|
|
13
|
+
class Requirement
|
|
14
|
+
include Lutaml::Model::Serialize
|
|
15
|
+
|
|
16
|
+
attribute :guidance, :string
|
|
17
|
+
attribute :purpose, :string
|
|
18
|
+
|
|
19
|
+
xml do
|
|
20
|
+
element "requirement"
|
|
21
|
+
map_element "component", to: :guidance,
|
|
22
|
+
with: { from: :component_from_guidance, to: :component_to_guidance }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def component_from_guidance(model, value, ctx = nil)
|
|
26
|
+
# `from` receives (model, value[, context]); assign the model attribute
|
|
27
|
+
model.guidance = value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def component_to_guidance(model, parent, doc = nil, ctx = nil)
|
|
31
|
+
# `to` receives (model, parent[, doc[, context]]); write into parent/doc
|
|
32
|
+
parent.add_element("component", model.guidance, type: "guidance")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
----
|
|
36
|
+
|
|
37
|
+
Notes:
|
|
38
|
+
|
|
39
|
+
* The `from` method *assigns* the attribute (it receives the whole model); the `to` method *writes* the output document.
|
|
40
|
+
* The optional trailing `context` parameter receives the options hash passed to `Model.from_xml(data, context: {...})` / `model.to_xml(context: {...})` — it is forwarded when the method declares it (lutaml-model#550).
|
|
41
|
+
* Callables work in key-value mappings: `with: { from: ->(value) { ... } }` maps and assigns the return value; an exact-arity-2 callable receives the context as its second argument.
|
|
42
|
+
|
|
43
|
+
== `transform:` — value-level transforms
|
|
44
|
+
|
|
45
|
+
`transform:` converts values (not documents). A Hash form (`transform: { from: ->(v) {...}, to: ->(v) {...} }`) wraps the value in both directions without touching assignment; a `ValueTransformer` subclass receives `from(value, format)` / `to(value, format)` and is shared by every rule that references it.
|
|
46
|
+
|
|
47
|
+
== Type-level serialization
|
|
48
|
+
|
|
49
|
+
A *type class* (a `Lutaml::Model::Type::Value` subclass) can carry its own format behavior, so reuse lives on the type rather than the parent mapping:
|
|
50
|
+
|
|
51
|
+
[source,ruby]
|
|
52
|
+
----
|
|
53
|
+
class XmiNsType < Lutaml::Model::Type::String
|
|
54
|
+
xml do
|
|
55
|
+
namespace XmiNs # bind the type's own namespace
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
----
|
|
59
|
+
|
|
60
|
+
`Lutaml::Model::Type::Value.register_format_type_serializer(format, type_class, to:, from:)` registers per-format serialize/deserialize procs for a type class; resolution walks the hierarchy.
|
|
61
|
+
|
|
62
|
+
== `of_*` and the conversion entry points
|
|
63
|
+
|
|
64
|
+
`Model.of_<format>(parsed_document, options)` is the documented seam between adapter parsing and mapping application: the adapter (`from_<format>`) parses the wire data into a document object, and `of_<format>` (class method) applies the mappings to build instances. It is public API for tooling that already holds a parsed document — e.g. re-applying mappings to a reparsed tree — and is what `from_<format>` delegates to.
|
|
65
|
+
|
|
66
|
+
The per-instance counterparts are `to_<format>` (serialize through the mappings) and `as_<format>` (render without register-specific side effects). Prefer the generated `from_*/to_*` methods; reach for `of_*` only when you interpose between parse and mapping.
|
|
67
|
+
|
|
68
|
+
== Common recipes
|
|
69
|
+
|
|
70
|
+
* One wire element, several attributes dispatched by an attribute value (`component/@type`): one `with:` pair per attribute whose `from` inspects the dispatch attribute and whose `to` emits only for its type. (Polymorphic *class* dispatch has first-class support via `polymorphic:`.)
|
|
71
|
+
* Key remapping, nesting shifts, delimiter splitting: prefer `transform:`, `child_mappings`, and `root`/`prefix` options before custom methods — the mapping stays declarative and reviewable.
|
data/docs/_guides/index.adoc
CHANGED
|
@@ -19,6 +19,7 @@ Task-oriented guides for accomplishing specific goals with Lutaml::Model.
|
|
|
19
19
|
|
|
20
20
|
== Key-value serialization
|
|
21
21
|
|
|
22
|
+
* link:../custom-methods[Custom Serialization Methods] - with:/transform:, type-level serialization, of_* entry points
|
|
22
23
|
* link:../keyvalue-serialization[Key-Value Serialization] - JSON/YAML/TOML/Hash
|
|
23
24
|
* link:../collection-serialization[Collection Serialization] - JSONL and YAML Stream
|
|
24
25
|
|
|
@@ -286,7 +286,6 @@ format)
|
|
|
286
286
|
# only nil (non-existent) skips it (lutaml-model#746).
|
|
287
287
|
return if value.nil?
|
|
288
288
|
|
|
289
|
-
warn "PROBE-264 keys=#{options.keys.inspect} ctx=#{options[:context].inspect}"
|
|
290
289
|
return rule.deserialize(instance, value, attributes, model_class,
|
|
291
290
|
options[:context])
|
|
292
291
|
end
|
|
@@ -208,6 +208,15 @@ module Lutaml
|
|
|
208
208
|
# @param args [Array] Arguments (ignored for options)
|
|
209
209
|
# @param block [Proc] Block (ignored for options)
|
|
210
210
|
# @return [Object] The option value or nil
|
|
211
|
+
# Static per rule (see MappingRule#custom_method_only?). Compiled
|
|
212
|
+
# rules are frozen, so the answer is recomputed with a single
|
|
213
|
+
# to_s instead of the historical two.
|
|
214
|
+
def custom_method_only?
|
|
215
|
+
name = attribute_name.to_s
|
|
216
|
+
(name.start_with?("__") && name.end_with?("__")) ||
|
|
217
|
+
(!custom_methods.empty? && attribute_type.nil?)
|
|
218
|
+
end
|
|
219
|
+
|
|
211
220
|
def method_missing(method_name, *args, &block)
|
|
212
221
|
# Check if this is an option key
|
|
213
222
|
if options.key?(method_name)
|
|
@@ -307,6 +307,18 @@ context = nil)
|
|
|
307
307
|
end
|
|
308
308
|
end
|
|
309
309
|
|
|
310
|
+
# Static per rule: placeholder `__name__` targets and inferred
|
|
311
|
+
# custom-method targets without an attribute type never change —
|
|
312
|
+
# the applier asked this per element and paid two to_s strings
|
|
313
|
+
# each time (240k allocations on the ISO-13849 serialize profile).
|
|
314
|
+
# Rules may be frozen, so no per-instance memo: one to_s instead
|
|
315
|
+
# of two is the frozen-safe diet.
|
|
316
|
+
def custom_method_only?
|
|
317
|
+
name = attribute_name.to_s
|
|
318
|
+
(name.start_with?("__") && name.end_with?("__")) ||
|
|
319
|
+
(has_custom_methods? && attribute_type.nil?)
|
|
320
|
+
end
|
|
321
|
+
|
|
310
322
|
def has_custom_method_for_serialization?
|
|
311
323
|
!custom_methods.empty? && custom_methods[:to]
|
|
312
324
|
end
|
|
@@ -264,9 +264,12 @@ module Lutaml
|
|
|
264
264
|
# class_eval'd bodies cannot close over locals; a hidden
|
|
265
265
|
# define_method accessor holds the Attribute handle.
|
|
266
266
|
attr_reader_method = :"__attribute_definition_#{name}"
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
267
|
+
reader_defined = if Lutaml::Model.opal?
|
|
268
|
+
method_defined?(attr_reader_method)
|
|
269
|
+
else
|
|
270
|
+
method_defined?(attr_reader_method, false)
|
|
271
|
+
end
|
|
272
|
+
define_method(attr_reader_method) { attr } unless reader_defined
|
|
270
273
|
|
|
271
274
|
if attr.collection?
|
|
272
275
|
# class_eval interpolates, e.g.:
|
|
@@ -33,6 +33,11 @@ module Lutaml
|
|
|
33
33
|
# The getter materializes a real Array on first access.
|
|
34
34
|
LAZY_EMPTY_COLLECTION = [].freeze
|
|
35
35
|
|
|
36
|
+
# Ivar symbols for lazy collections, memoized per attribute name:
|
|
37
|
+
# the "@name" interpolation allocated twice per collection read
|
|
38
|
+
# (491k on the ISO-13849 serialize profile).
|
|
39
|
+
LAZY_IVAR = {}.compare_by_identity
|
|
40
|
+
|
|
36
41
|
# Sentinel distinguishing "getter called with no argument" from
|
|
37
42
|
# builder-syntax `g.attr(value)` where value may be nil. Compiled
|
|
38
43
|
# getters default to it instead of a `*args` splat.
|
|
@@ -406,11 +411,16 @@ module Lutaml
|
|
|
406
411
|
# @param attribute_name [Symbol] the collection attribute
|
|
407
412
|
# @return [Object] the stored collection
|
|
408
413
|
def materialize_lazy_collection(attribute_name)
|
|
409
|
-
|
|
414
|
+
ivar = LAZY_IVAR[attribute_name]
|
|
415
|
+
unless ivar
|
|
416
|
+
ivar = :"@#{attribute_name}"
|
|
417
|
+
LAZY_IVAR[attribute_name] = ivar
|
|
418
|
+
end
|
|
419
|
+
current = instance_variable_get(ivar)
|
|
410
420
|
return current unless current.equal?(LAZY_EMPTY_COLLECTION)
|
|
411
421
|
return current if frozen?
|
|
412
422
|
|
|
413
|
-
instance_variable_set(
|
|
423
|
+
instance_variable_set(ivar, [])
|
|
414
424
|
end
|
|
415
425
|
|
|
416
426
|
# Hand back a reference collection the caller can push onto.
|
|
@@ -19,7 +19,10 @@ module Lutaml
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
value = value.to_s
|
|
22
|
-
|
|
22
|
+
# tr, not gsub: the regexp engine is measurable on hot text
|
|
23
|
+
# paths; :collapse keeps squeeze+strip (two C passes, no
|
|
24
|
+
# intermediate regexp state).
|
|
25
|
+
value = value.tr("\t\n\r", " ") unless mode == :preserve
|
|
23
26
|
value = value.squeeze(" ").strip if mode == :collapse
|
|
24
27
|
|
|
25
28
|
unless options.equal?(EMPTY_OPTIONS)
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -18,9 +18,13 @@ module Lutaml
|
|
|
18
18
|
# rules — attribute, derived/valid flags, and #765 group-skip —
|
|
19
19
|
# keyed on the mapping's finalize version so late changes rebuild.
|
|
20
20
|
# The loop body used to re-derive these per parsed instance.
|
|
21
|
-
# Concurrent::Map
|
|
22
|
-
#
|
|
23
|
-
RULE_RECORDS =
|
|
21
|
+
# Concurrent::Map under threaded MRI, plain Hash under Opal
|
|
22
|
+
# (Concurrent is unavailable there); class identity keys either.
|
|
23
|
+
RULE_RECORDS = if Lutaml::Model.opal?
|
|
24
|
+
{}
|
|
25
|
+
else
|
|
26
|
+
Concurrent::Map.new # rubocop:disable Lint/HashCompareByIdentity
|
|
27
|
+
end
|
|
24
28
|
|
|
25
29
|
# Namespaced rule name -> [local_name, rule_uri]. Pure string
|
|
26
30
|
# splitting, deterministic per spelling, shared across parses.
|
|
@@ -724,8 +728,11 @@ _effective_register)
|
|
|
724
728
|
|
|
725
729
|
mapping = model_class.mappings_for(:xml, register)
|
|
726
730
|
version = mapping&.rule_records_version.to_i
|
|
727
|
-
per_class = RULE_RECORDS[model_class.object_id]
|
|
728
|
-
|
|
731
|
+
per_class = RULE_RECORDS[model_class.object_id] # rubocop:disable Lint/HashCompareByIdentity
|
|
732
|
+
unless per_class
|
|
733
|
+
per_class = Lutaml::Model.opal? ? {} : Concurrent::Map.new
|
|
734
|
+
RULE_RECORDS[model_class.object_id] = per_class # rubocop:disable Lint/HashCompareByIdentity
|
|
735
|
+
end
|
|
729
736
|
cached = per_class[register]
|
|
730
737
|
return cached.records if cached && cached.version == version
|
|
731
738
|
|
|
@@ -227,13 +227,7 @@ register_id)
|
|
|
227
227
|
# @param rule [CompiledRule] The rule
|
|
228
228
|
# @return [Boolean] true if custom method only
|
|
229
229
|
def custom_method_only?(rule)
|
|
230
|
-
|
|
231
|
-
return true if rule.attribute_name.to_s.start_with?("__") &&
|
|
232
|
-
rule.attribute_name.to_s.end_with?("__")
|
|
233
|
-
|
|
234
|
-
# Also check if rule has custom methods but attribute_type is nil
|
|
235
|
-
# This handles cases where we inferred an attribute name for custom methods
|
|
236
|
-
rule.has_custom_methods? && rule.attribute_type.nil?
|
|
230
|
+
rule.custom_method_only?
|
|
237
231
|
end
|
|
238
232
|
|
|
239
233
|
# Extract value for a rule
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.39
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -202,6 +202,7 @@ files:
|
|
|
202
202
|
- docs/_guides/character-encoding.adoc
|
|
203
203
|
- docs/_guides/collection-serialization.adoc
|
|
204
204
|
- docs/_guides/creating-xsd.adoc
|
|
205
|
+
- docs/_guides/custom-methods.adoc
|
|
205
206
|
- docs/_guides/document-validation.adoc
|
|
206
207
|
- docs/_guides/index.adoc
|
|
207
208
|
- docs/_guides/jsonld-serialization.adoc
|