lutaml-model 0.8.38 → 0.8.40
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 +7 -3
- data/lib/lutaml/model/serialize.rb +12 -2
- data/lib/lutaml/model/store.rb +19 -0
- data/lib/lutaml/model/type/string.rb +4 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/adapter/plan_based_builder.rb +16 -3
- data/lib/lutaml/xml/adapter/xml_serializer.rb +13 -2
- data/lib/lutaml/xml/adapter_element.rb +7 -0
- data/lib/lutaml/xml/mapping.rb +8 -3
- data/lib/lutaml/xml/model_transform.rb +37 -10
- data/lib/lutaml/xml/transformation/rule_applier.rb +1 -7
- data/spec/lutaml/model/map_all_spec.rb +23 -19
- data/spec/lutaml/model/store_spec.rb +7 -1
- data/spec/lutaml/xml/mapping_spec.rb +6 -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: 597b465123fe390703bdb760a4f6e18e4069b55bea8998633e2a4c0c7f5fcb9c
|
|
4
|
+
data.tar.gz: e848597e1ad2cef05670d8dc8de6f7480bf028fde627021c5a61f554ad99a2f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 231f9601a9ce76ae516a6435b2c7185e15c2cc6680d0aefbad27ef328b25f67f0ed2635be8319eff6c3a64338f3bd2f9855ab1daa51058018345eb77cf3aa0d9
|
|
7
|
+
data.tar.gz: 88af6f65b2fadf91990f6f6f10a03a799007acda351d87a059f00325c85514291fbaccfd99580dd1daac3090bd61f4aa1ec26efffd058438c7e6abdd9a53cea4
|
|
@@ -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
|
|
@@ -129,6 +129,7 @@ module Lutaml
|
|
|
129
129
|
end
|
|
130
130
|
end
|
|
131
131
|
elsif attr.unresolved_type == Lutaml::Model::Type::Reference
|
|
132
|
+
Lutaml::Model::Store.reference_types_in_use!
|
|
132
133
|
define_reference_methods(name, register_id)
|
|
133
134
|
else
|
|
134
135
|
define_regular_attribute_methods(name, attr)
|
|
@@ -264,9 +265,12 @@ module Lutaml
|
|
|
264
265
|
# class_eval'd bodies cannot close over locals; a hidden
|
|
265
266
|
# define_method accessor holds the Attribute handle.
|
|
266
267
|
attr_reader_method = :"__attribute_definition_#{name}"
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
reader_defined = if Lutaml::Model.opal?
|
|
269
|
+
method_defined?(attr_reader_method)
|
|
270
|
+
else
|
|
271
|
+
method_defined?(attr_reader_method, false)
|
|
272
|
+
end
|
|
273
|
+
define_method(attr_reader_method) { attr } unless reader_defined
|
|
270
274
|
|
|
271
275
|
if attr.collection?
|
|
272
276
|
# 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.
|
data/lib/lutaml/model/store.rb
CHANGED
|
@@ -33,11 +33,30 @@ module Lutaml
|
|
|
33
33
|
@instance ||= new
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
# lutaml-model#808: Ruby 3.3's GC has a WeakMap-mark hazard under
|
|
37
|
+
# heavy insert churn (freed slot dereferenced at wmap mark time).
|
|
38
|
+
# Registration is only ever consumed by `ref:` resolution — when
|
|
39
|
+
# no Reference-typed attribute exists anywhere, nothing can
|
|
40
|
+
# resolve, so registration is skipped entirely and the WeakMap
|
|
41
|
+
# stays quiet. Set the first time an attribute with a Reference
|
|
42
|
+
# type is defined.
|
|
43
|
+
# Not memoized `||=` — the setter flips it to true for the
|
|
44
|
+
# process lifetime and reads must see that.
|
|
45
|
+
def reference_types_in_use?
|
|
46
|
+
@reference_types_in_use ? true : false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def reference_types_in_use!
|
|
50
|
+
@reference_types_in_use = true
|
|
51
|
+
end
|
|
52
|
+
|
|
36
53
|
def reset!
|
|
37
54
|
@instance = new
|
|
38
55
|
end
|
|
39
56
|
|
|
40
57
|
def register(object)
|
|
58
|
+
return unless reference_types_in_use?
|
|
59
|
+
|
|
41
60
|
instance.register(object)
|
|
42
61
|
end
|
|
43
62
|
|
|
@@ -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
|
@@ -299,13 +299,23 @@ module Lutaml
|
|
|
299
299
|
# Create element
|
|
300
300
|
xml.create_and_add_element(tag_name, attributes: attributes,
|
|
301
301
|
prefix: prefix) do |inner_xml|
|
|
302
|
-
# Handle raw content (map_all directive)
|
|
302
|
+
# Handle raw content (map_all directive). lutaml-model#223:
|
|
303
|
+
# with coexisting mappings the raw remainder is the TAIL —
|
|
304
|
+
# mapped children render first through the normal paths
|
|
305
|
+
# below, then the fragment is appended. Standalone map_all
|
|
306
|
+
# (raw is the whole content) keeps the historical
|
|
307
|
+
# raw-only rendering by fronting the fragment and skipping
|
|
308
|
+
# children only when there is nothing mapped.
|
|
303
309
|
has_raw_content = false
|
|
304
310
|
if element.is_a?(Lutaml::Xml::DataModel::XmlElement)
|
|
305
311
|
raw_content = element.raw_content
|
|
306
312
|
if raw_content && !raw_content.to_s.empty?
|
|
307
|
-
|
|
308
|
-
|
|
313
|
+
if element.children.empty?
|
|
314
|
+
inner_xml.add_xml_fragment(inner_xml, raw_content.to_s)
|
|
315
|
+
has_raw_content = true
|
|
316
|
+
else
|
|
317
|
+
(tail_raw_fragments ||= []) << raw_content.to_s
|
|
318
|
+
end
|
|
309
319
|
end
|
|
310
320
|
end
|
|
311
321
|
|
|
@@ -343,6 +353,9 @@ module Lutaml
|
|
|
343
353
|
end
|
|
344
354
|
end
|
|
345
355
|
end
|
|
356
|
+
(tail_raw_fragments || []).each do |fragment|
|
|
357
|
+
inner_xml.add_xml_fragment(inner_xml, fragment)
|
|
358
|
+
end
|
|
346
359
|
end
|
|
347
360
|
end
|
|
348
361
|
|
|
@@ -268,11 +268,20 @@ module Lutaml
|
|
|
268
268
|
attributes["xmlns"] = "" if needs_xmlns_blank
|
|
269
269
|
|
|
270
270
|
xml.create_and_add_element(qualified_name, attributes: attributes) do
|
|
271
|
+
tail_raw = nil
|
|
271
272
|
if xml_element.is_a?(Lutaml::Xml::DataModel::XmlElement)
|
|
272
273
|
raw_content = xml_element.raw_content
|
|
273
274
|
if raw_content && !raw_content.to_s.empty?
|
|
274
|
-
|
|
275
|
-
|
|
275
|
+
# lutaml-model#223: with coexisting map_all, the raw
|
|
276
|
+
# remainder is the tail — mapped children render first.
|
|
277
|
+
# Standalone map_all (no mapped children) keeps the
|
|
278
|
+
# historical raw-only rendering.
|
|
279
|
+
if xml_element.children.empty?
|
|
280
|
+
xml.add_xml_fragment(xml, raw_content.to_s)
|
|
281
|
+
return
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
tail_raw = raw_content.to_s
|
|
276
285
|
end
|
|
277
286
|
end
|
|
278
287
|
|
|
@@ -306,6 +315,8 @@ module Lutaml
|
|
|
306
315
|
end
|
|
307
316
|
end
|
|
308
317
|
|
|
318
|
+
xml.add_xml_fragment(xml, tail_raw) if tail_raw
|
|
319
|
+
|
|
309
320
|
if xml_element.text_content
|
|
310
321
|
if xml_element.cdata
|
|
311
322
|
xml.cdata(xml_element.text_content.to_s)
|
|
@@ -109,6 +109,13 @@ module Lutaml
|
|
|
109
109
|
children.map(&:to_xml).join
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
+
# The element including its own tags — the building block for
|
|
113
|
+
# scoped raw capture (#223): an unclaimed child contributes its
|
|
114
|
+
# whole subtree verbatim.
|
|
115
|
+
def inner_xml_or_self
|
|
116
|
+
to_xml
|
|
117
|
+
end
|
|
118
|
+
|
|
112
119
|
private
|
|
113
120
|
|
|
114
121
|
def build_element_xml(builder)
|
data/lib/lutaml/xml/mapping.rb
CHANGED
|
@@ -979,9 +979,14 @@ module Lutaml
|
|
|
979
979
|
"is allowed with #{TYPES[:all_content]}"
|
|
980
980
|
end
|
|
981
981
|
|
|
982
|
-
if
|
|
983
|
-
|
|
984
|
-
|
|
982
|
+
if type == TYPES[:all_content] &&
|
|
983
|
+
!(elements.empty? && content_mapping.nil?)
|
|
984
|
+
# lutaml-model#223: map_all may coexist with mapped siblings.
|
|
985
|
+
# The raw capture receives only the UNCLAIMED content — the
|
|
986
|
+
# verbatim subtree minus children matched by element rules.
|
|
987
|
+
# The historical exclusivity raised here; the coexistence
|
|
988
|
+
# contract is enforced by scoped capture on parse and
|
|
989
|
+
# remainder-after-mapped emission on serialize.
|
|
985
990
|
end
|
|
986
991
|
end
|
|
987
992
|
|
|
@@ -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.
|
|
@@ -51,14 +55,17 @@ module Lutaml
|
|
|
51
55
|
# ISO-13849 profile). Memoize one instance per (element, class)
|
|
52
56
|
# for the whole parse; the memo travels in the propagated
|
|
53
57
|
# deserialization options.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
# Nested identity maps keyed on the element object itself —
|
|
59
|
+
# flat object_id pairs could collide after GC id reuse on
|
|
60
|
+
# long parses (#808 audit).
|
|
61
|
+
memo = options[:__lutaml_child_memo] ||= {}.compare_by_identity
|
|
62
|
+
per_class = (memo[data] ||= {}.compare_by_identity)
|
|
63
|
+
if (existing = per_class[model_class])
|
|
57
64
|
return existing
|
|
58
65
|
end
|
|
59
66
|
|
|
60
67
|
instance = model_class.allocate_for_deserialization(child_register)
|
|
61
|
-
|
|
68
|
+
per_class[model_class] = instance
|
|
62
69
|
else
|
|
63
70
|
instance = model_class.new
|
|
64
71
|
register_accessor_methods_for(instance, child_register)
|
|
@@ -341,7 +348,7 @@ module Lutaml
|
|
|
341
348
|
end
|
|
342
349
|
|
|
343
350
|
value = if rule.raw_mapping?
|
|
344
|
-
doc
|
|
351
|
+
scoped_raw_inner_xml(doc, xml_mapping)
|
|
345
352
|
elsif rule.content_mapping?
|
|
346
353
|
rule.cdata ? doc.cdata : doc.text
|
|
347
354
|
elsif (group = grouped_plain_rules[rule_to]) &&
|
|
@@ -709,6 +716,23 @@ _effective_register)
|
|
|
709
716
|
# lutaml-model#154: decode HTML entities for mappings that opted in
|
|
710
717
|
# (`html_entities` in the xml block). Values stay untouched
|
|
711
718
|
# otherwise — XML keeps undefined entities literal by design.
|
|
719
|
+
# lutaml-model#223: with coexisting map_all, the raw capture is
|
|
720
|
+
# the verbatim subtree MINUS the children claimed by element
|
|
721
|
+
# rules (they hydrate their own attributes). Standalone map_all
|
|
722
|
+
# (no element rules) keeps the full inner_xml.
|
|
723
|
+
def scoped_raw_inner_xml(doc, xml_mapping)
|
|
724
|
+
claimed = []
|
|
725
|
+
xml_mapping&.elements&.each do |element_rule|
|
|
726
|
+
Array(element_rule.name).each { |n| claimed << n.to_s }
|
|
727
|
+
end
|
|
728
|
+
return doc.root.inner_xml if claimed.empty?
|
|
729
|
+
|
|
730
|
+
doc.root.element_children.reject do |child|
|
|
731
|
+
claimed.include?(child.unprefixed_name) ||
|
|
732
|
+
claimed.include?(child.name)
|
|
733
|
+
end.map(&:inner_xml_or_self).join
|
|
734
|
+
end
|
|
735
|
+
|
|
712
736
|
def decode_html_entities_value(value, enabled)
|
|
713
737
|
return value if value.nil? || !value.is_a?(String)
|
|
714
738
|
return value unless enabled
|
|
@@ -724,8 +748,11 @@ _effective_register)
|
|
|
724
748
|
|
|
725
749
|
mapping = model_class.mappings_for(:xml, register)
|
|
726
750
|
version = mapping&.rule_records_version.to_i
|
|
727
|
-
per_class = RULE_RECORDS[model_class.object_id]
|
|
728
|
-
|
|
751
|
+
per_class = RULE_RECORDS[model_class.object_id] # rubocop:disable Lint/HashCompareByIdentity
|
|
752
|
+
unless per_class
|
|
753
|
+
per_class = Lutaml::Model.opal? ? {} : Concurrent::Map.new
|
|
754
|
+
RULE_RECORDS[model_class.object_id] = per_class # rubocop:disable Lint/HashCompareByIdentity
|
|
755
|
+
end
|
|
729
756
|
cached = per_class[register]
|
|
730
757
|
return cached.records if cached && cached.version == version
|
|
731
758
|
|
|
@@ -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
|
|
@@ -185,28 +185,32 @@ module MapAllSpec
|
|
|
185
185
|
end
|
|
186
186
|
end
|
|
187
187
|
|
|
188
|
-
describe "
|
|
189
|
-
it "
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
describe "coexistence with other mappings (#223)" do
|
|
189
|
+
it "captures only unclaimed content alongside mapped elements" do
|
|
190
|
+
doc_class = Class.new(Lutaml::Model::Serializable) do
|
|
191
|
+
attribute :title, :string
|
|
192
|
+
attribute :rest, :string
|
|
193
|
+
|
|
194
|
+
xml do
|
|
195
|
+
element "eref"
|
|
192
196
|
map_element "title", to: :title
|
|
197
|
+
map_all to: :rest
|
|
193
198
|
end
|
|
194
|
-
end
|
|
195
|
-
StandardError,
|
|
196
|
-
"map_all is not allowed with other mappings",
|
|
197
|
-
)
|
|
198
|
-
end
|
|
199
|
+
end
|
|
199
200
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
201
|
+
source = %(<eref><title>T</title><p>para</p><note>N</note></eref>)
|
|
202
|
+
parsed = doc_class.from_xml(source)
|
|
203
|
+
expect(parsed.title).to eq("T")
|
|
204
|
+
expect(parsed.rest).to eq("<p>para</p><note>N</note>")
|
|
205
|
+
|
|
206
|
+
out = doc_class.to_xml(parsed)
|
|
207
|
+
expect(out).to include("<title>T</title>")
|
|
208
|
+
expect(out).to include("<p>para</p>")
|
|
209
|
+
expect(out).to include("<note>N</note>")
|
|
210
|
+
|
|
211
|
+
reparsed = doc_class.from_xml(out)
|
|
212
|
+
expect(reparsed.title).to eq("T")
|
|
213
|
+
expect(reparsed.rest).to eq("<p>para</p><note>N</note>")
|
|
210
214
|
end
|
|
211
215
|
end
|
|
212
216
|
end
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
|
|
5
5
|
RSpec.describe Lutaml::Model::Store do
|
|
6
|
+
before do
|
|
7
|
+
# Registration only runs when a Reference-typed attribute exists
|
|
8
|
+
# somewhere (#808); the internal specs opt in explicitly.
|
|
9
|
+
described_class.reference_types_in_use!
|
|
10
|
+
described_class.clear
|
|
11
|
+
end
|
|
12
|
+
|
|
6
13
|
let(:model_class) do
|
|
7
14
|
Class.new(Lutaml::Model::Serializable) do
|
|
8
15
|
attribute :id, :string
|
|
@@ -10,7 +17,6 @@ RSpec.describe Lutaml::Model::Store do
|
|
|
10
17
|
end
|
|
11
18
|
end
|
|
12
19
|
|
|
13
|
-
before { described_class.clear }
|
|
14
20
|
after { described_class.clear }
|
|
15
21
|
|
|
16
22
|
describe "#register and #resolve" do
|
|
@@ -974,7 +974,6 @@ RSpec.describe Lutaml::Xml::Mapping do
|
|
|
974
974
|
end
|
|
975
975
|
|
|
976
976
|
context "when mixed content is true and child is content_mapping" do
|
|
977
|
-
let(:map_all_error) { "map_all is not allowed with other mappings" }
|
|
978
977
|
let(:invalid_element) { "must be defined without namespace" }
|
|
979
978
|
let(:mfenced) do
|
|
980
979
|
<<~XML
|
|
@@ -994,7 +993,7 @@ RSpec.describe Lutaml::Xml::Mapping do
|
|
|
994
993
|
XML
|
|
995
994
|
end
|
|
996
995
|
|
|
997
|
-
it "
|
|
996
|
+
it "allows map_all alongside element mappings (#223)" do
|
|
998
997
|
original_mapping = XmlMappingSpec::MmlMath.mappings[:xml].deep_dup
|
|
999
998
|
|
|
1000
999
|
begin
|
|
@@ -1005,7 +1004,7 @@ RSpec.describe Lutaml::Xml::Mapping do
|
|
|
1005
1004
|
XmlMappingSpec::MmlMath.xml do
|
|
1006
1005
|
map_all to: :all_content
|
|
1007
1006
|
end
|
|
1008
|
-
end.
|
|
1007
|
+
end.not_to raise_error
|
|
1009
1008
|
ensure
|
|
1010
1009
|
XmlMappingSpec::MmlMath.instance_variable_set(:@mappings,
|
|
1011
1010
|
{ xml: original_mapping })
|
|
@@ -1013,16 +1012,16 @@ RSpec.describe Lutaml::Xml::Mapping do
|
|
|
1013
1012
|
end
|
|
1014
1013
|
end
|
|
1015
1014
|
|
|
1016
|
-
it "
|
|
1015
|
+
it "still validates its own arguments when combined (#223)" do
|
|
1017
1016
|
original_mapping = XmlMappingSpec::MmlMath.mappings[:xml].deep_dup
|
|
1018
1017
|
|
|
1019
1018
|
begin
|
|
1020
1019
|
expect do
|
|
1021
1020
|
XmlMappingSpec::MmlMath.xml do
|
|
1022
|
-
map_all to: :all_content
|
|
1023
|
-
namespace XmiNewNamespace
|
|
1021
|
+
map_all to: :all_content, namespace: "http://explicit.ns"
|
|
1024
1022
|
end
|
|
1025
|
-
end.to raise_error(
|
|
1023
|
+
end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError,
|
|
1024
|
+
/namespace is not allowed/)
|
|
1026
1025
|
ensure
|
|
1027
1026
|
XmlMappingSpec::MmlMath.instance_variable_set(:@mappings,
|
|
1028
1027
|
{ xml: original_mapping })
|
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.40
|
|
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
|