lutaml-model 0.8.37 → 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.
@@ -29,6 +29,7 @@ module Lutaml
29
29
  import_declaration_plan
30
30
  polymorphic
31
31
  collection
32
+ __lutaml_child_memo
32
33
  render_empty
33
34
  render_nil
34
35
  cdata
@@ -33,6 +33,16 @@ 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
+
41
+ # Sentinel distinguishing "getter called with no argument" from
42
+ # builder-syntax `g.attr(value)` where value may be nil. Compiled
43
+ # getters default to it instead of a `*args` splat.
44
+ NO_ARG = Object.new.freeze
45
+
36
46
  INTERNAL_ATTRIBUTES = %i[@using_default @lutaml_register @lutaml_parent @lutaml_root
37
47
  @register_records].freeze
38
48
 
@@ -73,6 +83,49 @@ module Lutaml
73
83
  end
74
84
  end
75
85
 
86
+ # lutaml-model#436: multi-document streams. `from_<f>_stream`
87
+ # parses each document separately; `discriminator:` receives each
88
+ # parsed document hash and returns the model class to build with,
89
+ # enabling polymorphic streams. `to_<f>_stream` joins instances
90
+ # into one stream document.
91
+ def self.register_stream_methods(format)
92
+ ClassMethods.define_method(:"from_#{format}_stream") do |data, options = {}|
93
+ docs = Lutaml::Model::Serialize.stream_documents(data, format, options)
94
+ discriminator = options[:discriminator]
95
+ docs.map do |doc|
96
+ target = discriminator ? discriminator.call(doc) : self
97
+ target.send(:"of_#{format}", doc, options)
98
+ end
99
+ end
100
+
101
+ ClassMethods.define_method(:"to_#{format}_stream") do |instances, options = {}|
102
+ instances.map do |instance|
103
+ instance.send(:"to_#{format}", options)
104
+ end.join("---\n")
105
+ end
106
+ end
107
+
108
+ def self.stream_documents(data, format, options)
109
+ require "yaml"
110
+ if YAML.respond_to?(:load_stream, true)
111
+ begin
112
+ docs = YAML.load_stream(data,
113
+ aliases: options.fetch(:aliases, true))
114
+ rescue ArgumentError
115
+ # Older Psych builds without the aliases keyword: aliases are
116
+ # on by default there.
117
+ docs = YAML.load_stream(data)
118
+ end
119
+ docs.is_a?(Array) ? docs.compact : [docs].compact
120
+ else
121
+ data.split(/^---\s*$/).filter_map do |chunk|
122
+ chunk.strip.empty? ? nil : YAML.safe_load(chunk)
123
+ end
124
+ end
125
+ rescue StandardError => e
126
+ raise Lutaml::Model::InvalidFormatError.new(format, e.message)
127
+ end
128
+
76
129
  def self.register_to_format_method(format)
77
130
  ClassMethods.define_method(:"to_#{format}") do |instance, options = {}|
78
131
  to(format, instance, options)
@@ -108,21 +161,12 @@ module Lutaml
108
161
  @using_default = nil
109
162
  @lutaml_register = register
110
163
 
111
- # Default register: seed through the per-class compiled method
112
- # (plain `@x = sentinel` writes). The previous per-instance
113
- # instance_variable_set loop was a measured hot spot — one send
114
- # and one symbol allocation per attribute per instance. Register-
115
- # specific attribute sets are rare and keep the generic walk.
116
- if register.nil? || register == :default
117
- klass = self.class
118
- klass.compile_state_defaults! unless klass.method_defined?(:__init_deserialized_state_defaults)
119
- __init_deserialized_state_defaults
120
- else
121
- self.class.attributes(register).each do |name, attr|
122
- instance_variable_set(:"@#{name}",
123
- attr.collection? ? LAZY_EMPTY_COLLECTION : Lutaml::Model::UninitializedClass.instance)
124
- end
125
- end
164
+ # Seed every attribute through the per-(class, register) compiled
165
+ # method: plain `@x = sentinel` writes, no per-call "@#{name}"
166
+ # string interpolation, no instance_variable_set. The previous
167
+ # generic walk interpolated a name string per attribute per
168
+ # instance — 581k allocations on the ISO-13849 profile.
169
+ public_send(self.class.compiled_state_defaults_name!(self.class.extract_register_id(register)))
126
170
  end
127
171
 
128
172
  # Complete deserialization initialization after allocation.
@@ -367,11 +411,16 @@ module Lutaml
367
411
  # @param attribute_name [Symbol] the collection attribute
368
412
  # @return [Object] the stored collection
369
413
  def materialize_lazy_collection(attribute_name)
370
- 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)
371
420
  return current unless current.equal?(LAZY_EMPTY_COLLECTION)
372
421
  return current if frozen?
373
422
 
374
- instance_variable_set(:"@#{attribute_name}", [])
423
+ instance_variable_set(ivar, [])
375
424
  end
376
425
 
377
426
  # Hand back a reference collection the caller can push onto.
@@ -2,8 +2,8 @@ module Lutaml
2
2
  module Model
3
3
  class Transformer
4
4
  class << self
5
- def call(value, rule, attribute, format: nil)
6
- new(rule, attribute, format).call(value)
5
+ def call(value, rule, attribute, format: nil, context: nil)
6
+ new(rule, attribute, format, context).call(value)
7
7
  end
8
8
 
9
9
  private
@@ -15,7 +15,7 @@ module Lutaml
15
15
  transform.is_a?(::Hash) ? transform[direction] : transform
16
16
  end
17
17
 
18
- def apply_static(value, sources, direction, format)
18
+ def apply_static(value, sources, direction, format, context = nil)
19
19
  methods = sources.filter_map do |obj|
20
20
  get_transform_static(obj, direction)
21
21
  end
@@ -34,16 +34,23 @@ module Lutaml
34
34
  tc.public_send(apply_direction, v, format)
35
35
  end
36
36
 
37
- methods.reduce(result) { |tv, m| m.call(tv) }
37
+ methods.reduce(result) do |tv, m|
38
+ if m.arity == 2
39
+ m.call(tv, context)
40
+ else
41
+ m.call(tv)
42
+ end
43
+ end
38
44
  end
39
45
  end
40
46
 
41
- attr_reader :rule, :attribute, :format
47
+ attr_reader :rule, :attribute, :format, :context
42
48
 
43
- def initialize(rule, attribute, format = nil)
49
+ def initialize(rule, attribute, format = nil, context = nil)
44
50
  @rule = rule
45
51
  @attribute = attribute
46
52
  @format = format
53
+ @context = context
47
54
  end
48
55
 
49
56
  def call(value)
@@ -61,7 +68,19 @@ module Lutaml
61
68
  end
62
69
 
63
70
  methods.reduce(result) do |transformed_value, method|
64
- method.call(transformed_value)
71
+ invoke_transform(method, transformed_value)
72
+ end
73
+ end
74
+
75
+ # lutaml-model#550: `with:` custom methods opt into the options
76
+ # passed to `from_*` / `to_*` by declaring a second parameter
77
+ # (exact arity 2); one-parameter methods keep the historical
78
+ # single-argument call.
79
+ def invoke_transform(method, value)
80
+ if method.arity == 2
81
+ method.call(value, context)
82
+ else
83
+ method.call(value)
65
84
  end
66
85
  end
67
86
 
@@ -87,8 +106,8 @@ module Lutaml
87
106
 
88
107
  class ImportTransformer < Transformer
89
108
  class << self
90
- def call(value, rule, attribute, format: nil)
91
- apply_static(value, [rule, attribute], :import, format)
109
+ def call(value, rule, attribute, format: nil, context: nil)
110
+ apply_static(value, [rule, attribute], :import, format, context)
92
111
  end
93
112
  end
94
113
 
@@ -103,8 +122,8 @@ module Lutaml
103
122
 
104
123
  class ExportTransformer < Transformer
105
124
  class << self
106
- def call(value, rule, attribute, format: nil)
107
- apply_static(value, [attribute, rule], :export, format)
125
+ def call(value, rule, attribute, format: nil, context: nil)
126
+ apply_static(value, [attribute, rule], :export, format, context)
108
127
  end
109
128
  end
110
129
 
@@ -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.37"
5
+ VERSION = "0.8.39"
6
6
  end
7
7
  end
@@ -21,6 +21,7 @@ end
21
21
 
22
22
  module Lutaml
23
23
  module Xml
24
+ autoload :HtmlEntities, "#{__dir__}/html_entities"
24
25
  autoload :PlanCompiler, "#{__dir__}/plan_compiler"
25
26
  autoload :PlanHydrator, "#{__dir__}/plan_hydrator"
26
27
  autoload :PlanOrder, "#{__dir__}/plan_order"
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lutaml-model#154: HTML 4.01 named entity table (plus apos) for opt-in
4
+ # decoding of HTML entities in XML text (MathML and friends rely on
5
+ # them although XML itself defines none beyond the predefined five).
6
+
7
+ module Lutaml
8
+ module Xml
9
+ module HtmlEntities
10
+ # The table is the data: a gem dependency or data file for a static
11
+ # W3C list would cost more than the frozen literal itself.
12
+ # The table is the data: a gem dependency or data file for a static
13
+ # W3C list would cost more than the frozen literal itself.
14
+ NAMED_TO_CODEPOINT = { # rubocop:disable Metrics/CollectionLiteralLength
15
+ "AElig" => 198,
16
+ "Aacute" => 193,
17
+ "Acirc" => 194,
18
+ "Agrave" => 192,
19
+ "Alpha" => 913,
20
+ "Aring" => 197,
21
+ "Atilde" => 195,
22
+ "Auml" => 196,
23
+ "Beta" => 914,
24
+ "Ccedil" => 199,
25
+ "Chi" => 935,
26
+ "Dagger" => 8225,
27
+ "Delta" => 916,
28
+ "ETH" => 208,
29
+ "Eacute" => 201,
30
+ "Ecirc" => 202,
31
+ "Egrave" => 200,
32
+ "Epsilon" => 917,
33
+ "Eta" => 919,
34
+ "Euml" => 203,
35
+ "Gamma" => 915,
36
+ "Iacute" => 205,
37
+ "Icirc" => 206,
38
+ "Igrave" => 204,
39
+ "Iota" => 921,
40
+ "Iuml" => 207,
41
+ "Kappa" => 922,
42
+ "Lambda" => 923,
43
+ "Mu" => 924,
44
+ "Ntilde" => 209,
45
+ "Nu" => 925,
46
+ "OElig" => 338,
47
+ "Oacute" => 211,
48
+ "Ocirc" => 212,
49
+ "Ograve" => 210,
50
+ "Omega" => 937,
51
+ "Omicron" => 927,
52
+ "Oslash" => 216,
53
+ "Otilde" => 213,
54
+ "Ouml" => 214,
55
+ "Phi" => 934,
56
+ "Pi" => 928,
57
+ "Prime" => 8243,
58
+ "Psi" => 936,
59
+ "Rho" => 929,
60
+ "Scaron" => 352,
61
+ "Sigma" => 931,
62
+ "THORN" => 222,
63
+ "Tau" => 932,
64
+ "Theta" => 920,
65
+ "Uacute" => 218,
66
+ "Ucirc" => 219,
67
+ "Ugrave" => 217,
68
+ "Upsilon" => 933,
69
+ "Uuml" => 220,
70
+ "Xi" => 926,
71
+ "Yacute" => 221,
72
+ "Yuml" => 376,
73
+ "Zeta" => 918,
74
+ "aacute" => 225,
75
+ "acirc" => 226,
76
+ "acute" => 180,
77
+ "aelig" => 230,
78
+ "agrave" => 224,
79
+ "alefsym" => 8501,
80
+ "alpha" => 945,
81
+ "amp" => 38,
82
+ "and" => 8743,
83
+ "ang" => 8736,
84
+ "apos" => 39,
85
+ "aring" => 229,
86
+ "asymp" => 8776,
87
+ "atilde" => 227,
88
+ "auml" => 228,
89
+ "bdquo" => 8222,
90
+ "beta" => 946,
91
+ "brvbar" => 166,
92
+ "bull" => 8226,
93
+ "cap" => 8745,
94
+ "ccedil" => 231,
95
+ "cedil" => 184,
96
+ "cent" => 162,
97
+ "chi" => 967,
98
+ "circ" => 710,
99
+ "clubs" => 9827,
100
+ "cong" => 8773,
101
+ "copy" => 169,
102
+ "crarr" => 8629,
103
+ "cup" => 8746,
104
+ "curren" => 164,
105
+ "dArr" => 8659,
106
+ "dagger" => 8224,
107
+ "darr" => 8595,
108
+ "deg" => 176,
109
+ "delta" => 948,
110
+ "diams" => 9830,
111
+ "divide" => 247,
112
+ "eacute" => 233,
113
+ "ecirc" => 234,
114
+ "egrave" => 232,
115
+ "empty" => 8709,
116
+ "emsp" => 8195,
117
+ "ensp" => 8194,
118
+ "epsilon" => 949,
119
+ "equiv" => 8801,
120
+ "eta" => 951,
121
+ "eth" => 240,
122
+ "euml" => 235,
123
+ "euro" => 8364,
124
+ "exist" => 8707,
125
+ "fnof" => 402,
126
+ "forall" => 8704,
127
+ "frac12" => 189,
128
+ "frac14" => 188,
129
+ "frac34" => 190,
130
+ "frasl" => 8260,
131
+ "gamma" => 947,
132
+ "ge" => 8805,
133
+ "gt" => 62,
134
+ "hArr" => 8660,
135
+ "harr" => 8596,
136
+ "hearts" => 9829,
137
+ "hellip" => 8230,
138
+ "iacute" => 237,
139
+ "icirc" => 238,
140
+ "iexcl" => 161,
141
+ "igrave" => 236,
142
+ "image" => 8465,
143
+ "infin" => 8734,
144
+ "int" => 8747,
145
+ "iota" => 953,
146
+ "iquest" => 191,
147
+ "isin" => 8712,
148
+ "iuml" => 239,
149
+ "kappa" => 954,
150
+ "lArr" => 8656,
151
+ "lambda" => 955,
152
+ "lang" => 9001,
153
+ "laquo" => 171,
154
+ "larr" => 8592,
155
+ "lceil" => 8968,
156
+ "ldquo" => 8220,
157
+ "le" => 8804,
158
+ "lfloor" => 8970,
159
+ "lowast" => 8727,
160
+ "loz" => 9674,
161
+ "lrm" => 8206,
162
+ "lsaquo" => 8249,
163
+ "lsquo" => 8216,
164
+ "lt" => 60,
165
+ "macr" => 175,
166
+ "mdash" => 8212,
167
+ "micro" => 181,
168
+ "middot" => 183,
169
+ "minus" => 8722,
170
+ "mu" => 956,
171
+ "nabla" => 8711,
172
+ "nbsp" => 160,
173
+ "ndash" => 8211,
174
+ "ne" => 8800,
175
+ "ni" => 8715,
176
+ "not" => 172,
177
+ "notin" => 8713,
178
+ "nsub" => 8836,
179
+ "ntilde" => 241,
180
+ "nu" => 957,
181
+ "oacute" => 243,
182
+ "ocirc" => 244,
183
+ "oelig" => 339,
184
+ "ograve" => 242,
185
+ "oline" => 8254,
186
+ "omega" => 969,
187
+ "omicron" => 959,
188
+ "oplus" => 8853,
189
+ "or" => 8744,
190
+ "ordf" => 170,
191
+ "ordm" => 186,
192
+ "oslash" => 248,
193
+ "otilde" => 245,
194
+ "otimes" => 8855,
195
+ "ouml" => 246,
196
+ "para" => 182,
197
+ "part" => 8706,
198
+ "permil" => 8240,
199
+ "perp" => 8869,
200
+ "phi" => 966,
201
+ "pi" => 960,
202
+ "piv" => 982,
203
+ "plusmn" => 177,
204
+ "pound" => 163,
205
+ "prime" => 8242,
206
+ "prod" => 8719,
207
+ "prop" => 8733,
208
+ "psi" => 968,
209
+ "quot" => 34,
210
+ "rArr" => 8658,
211
+ "radic" => 8730,
212
+ "rang" => 9002,
213
+ "raquo" => 187,
214
+ "rarr" => 8594,
215
+ "rceil" => 8969,
216
+ "rdquo" => 8221,
217
+ "real" => 8476,
218
+ "reg" => 174,
219
+ "rfloor" => 8971,
220
+ "rho" => 961,
221
+ "rlm" => 8207,
222
+ "rsaquo" => 8250,
223
+ "rsquo" => 8217,
224
+ "sbquo" => 8218,
225
+ "scaron" => 353,
226
+ "sdot" => 8901,
227
+ "sect" => 167,
228
+ "shy" => 173,
229
+ "sigma" => 963,
230
+ "sigmaf" => 962,
231
+ "sim" => 8764,
232
+ "spades" => 9824,
233
+ "sub" => 8834,
234
+ "sube" => 8838,
235
+ "sum" => 8721,
236
+ "sup" => 8835,
237
+ "sup1" => 185,
238
+ "sup2" => 178,
239
+ "sup3" => 179,
240
+ "supe" => 8839,
241
+ "szlig" => 223,
242
+ "tau" => 964,
243
+ "there4" => 8756,
244
+ "theta" => 952,
245
+ "thetasym" => 977,
246
+ "thinsp" => 8201,
247
+ "thorn" => 254,
248
+ "tilde" => 732,
249
+ "times" => 215,
250
+ "trade" => 8482,
251
+ "uArr" => 8657,
252
+ "uacute" => 250,
253
+ "uarr" => 8593,
254
+ "ucirc" => 251,
255
+ "ugrave" => 249,
256
+ "uml" => 168,
257
+ "upsih" => 978,
258
+ "upsilon" => 965,
259
+ "uuml" => 252,
260
+ "weierp" => 8472,
261
+ "xi" => 958,
262
+ "yacute" => 253,
263
+ "yen" => 165,
264
+ "yuml" => 255,
265
+ "zeta" => 950,
266
+ "zwj" => 8205,
267
+ "zwnj" => 8204,
268
+ }.freeze
269
+
270
+ NAME_PATTERN = /&(#x?[0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]*);/
271
+
272
+ module_function
273
+
274
+ def decode(value)
275
+ value.gsub(NAME_PATTERN) do |match|
276
+ body = match[1..-2]
277
+ case body[0]
278
+ when "x", "X"
279
+ body[1..].to_i(16).chr(Encoding::UTF_8)
280
+ when "#"
281
+ body[1..].to_i.chr(Encoding::UTF_8)
282
+ else
283
+ codepoint = NAMED_TO_CODEPOINT[body]
284
+ codepoint ? codepoint.chr(Encoding::UTF_8) : match
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end
290
+ end
@@ -121,9 +121,16 @@ module Lutaml
121
121
  @cached_attributes.clear
122
122
  @cached_mappings.clear
123
123
  @cached_attribute_name_counts.clear
124
+ @rule_records_version = (@rule_records_version || 0) + 1
124
125
  @finalized = true
125
126
  end
126
127
 
128
+ # Bumped at finalize; ModelTransform's compiled rule records are
129
+ # keyed on it so late mapping changes rebuild instead of going stale.
130
+ def rule_records_version
131
+ @rule_records_version ||= 0
132
+ end
133
+
127
134
  def finalized?
128
135
  @finalized
129
136
  end
@@ -1334,6 +1341,18 @@ module Lutaml
1334
1341
  !@namespace_class.nil?
1335
1342
  end
1336
1343
 
1344
+ # lutaml-model#154: opt-in decoding of HTML named/numeric entities
1345
+ # (e.g. &copy;, &#8212;) in mapped text and attribute values. XML
1346
+ # documents are otherwise kept byte-faithful: HTML entities are not
1347
+ # defined in XML without a DTD.
1348
+ def html_entities(enabled: true)
1349
+ @decode_html_entities = enabled
1350
+ end
1351
+
1352
+ def decode_html_entities?
1353
+ !!@decode_html_entities
1354
+ end
1355
+
1337
1356
  # Whether namespace was explicitly set via DSL
1338
1357
  def namespace_set?
1339
1358
  !!@namespace_set