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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad19014fa289dad72ab50f6908d425ed7708b4f7046432ac05426b65087fc713
4
- data.tar.gz: 4f2d95722e22e73b3691e6eec7903d3967ec1d9cf9ea2b641b02b77cc59911ef
3
+ metadata.gz: 15f9489eb4db353b481f160703e848e35b730cef179d32874941fc537ed962d3
4
+ data.tar.gz: f0067a5ffc4d66c8312d9cc478d9bd7d1771d9b639111c3158956b2d98e4468e
5
5
  SHA512:
6
- metadata.gz: 575ac30c297c4d8059bccb27b42b1d1e942ffef3352072f1ebdbff55039073cffb228a2e6873c55afd940bc78aaac1830c5375df0b37ac1b4eb498cea3a73fe2
7
- data.tar.gz: 4d1c80a2a3fd04e5c461aec33ba2248547ae5f6c877990f457d23160c272a4f67e17ef4ab05ad450265f5c2aa8c5c637deead3c27b20a2ea1978ab4815d6d397
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.
@@ -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
- unless method_defined?(attr_reader_method, false)
268
- define_method(attr_reader_method) { attr }
269
- end
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
- current = instance_variable_get(:"@#{attribute_name}")
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(:"@#{attribute_name}", [])
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
- value = value.gsub(/[\t\n\r]/, " ") unless mode == :preserve
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)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.38"
5
+ VERSION = "0.8.39"
6
6
  end
7
7
  end
@@ -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 has no compare_by_identity; class identity is
22
- # the key semantic here.
23
- RULE_RECORDS = Concurrent::Map.new # rubocop:disable Lint/HashCompareByIdentity
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] || # rubocop:disable Lint/HashCompareByIdentity
728
- (RULE_RECORDS[model_class.object_id] = Concurrent::Map.new) # rubocop:disable Lint/HashCompareByIdentity,Layout/MultilineAssignmentLayout
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
- # Check if attribute name is a placeholder (e.g., :__content__)
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.38
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-18 00:00:00.000000000 Z
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