lutaml-model 0.8.36 → 0.8.38

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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/bench/bench_sts.rb +9 -3
  3. data/bench/gate_config.rb +5 -4
  4. data/lib/compat/opal/lutaml_model_boot.rb +1 -0
  5. data/lib/lutaml/key_value/transform.rb +52 -5
  6. data/lib/lutaml/key_value/transformation.rb +10 -2
  7. data/lib/lutaml/model/adapter_resolver.rb +14 -2
  8. data/lib/lutaml/model/attribute.rb +15 -9
  9. data/lib/lutaml/model/choice.rb +6 -5
  10. data/lib/lutaml/model/format_registry.rb +3 -1
  11. data/lib/lutaml/model/liquefiable.rb +23 -2
  12. data/lib/lutaml/model/mapping/mapping_rule.rb +46 -11
  13. data/lib/lutaml/model/sequence.rb +47 -2
  14. data/lib/lutaml/model/serialize/attribute_definition.rb +176 -47
  15. data/lib/lutaml/model/serialize/deserialization_context.rb +1 -0
  16. data/lib/lutaml/model/serialize.rb +54 -10
  17. data/lib/lutaml/model/services/transformer.rb +30 -11
  18. data/lib/lutaml/model/store.rb +70 -32
  19. data/lib/lutaml/model/version.rb +1 -1
  20. data/lib/lutaml/xml/format.rb +1 -0
  21. data/lib/lutaml/xml/html_entities.rb +290 -0
  22. data/lib/lutaml/xml/mapping.rb +72 -0
  23. data/lib/lutaml/xml/model_transform.rb +210 -72
  24. data/lib/lutaml/xml/parse_session.rb +38 -0
  25. data/lib/lutaml/yaml/format.rb +1 -0
  26. data/spec/lutaml/model/adapter_resolver_configured_spec.rb +38 -0
  27. data/spec/lutaml/model/custom_method_context_spec.rb +62 -0
  28. data/spec/lutaml/model/liquefiable_lazy_spec.rb +36 -0
  29. data/spec/lutaml/model/mapping_type_validation_spec.rb +61 -0
  30. data/spec/lutaml/model/sequence_choice_compositor_spec.rb +66 -0
  31. data/spec/lutaml/model/store_spec.rb +13 -11
  32. data/spec/lutaml/model/yaml_stream_spec.rb +70 -0
  33. data/spec/lutaml/xml/html_entities_spec.rb +45 -0
  34. metadata +9 -1
@@ -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
@@ -84,6 +84,7 @@ module Lutaml
84
84
  @cached_elements = {}
85
85
  @cached_attributes = {}
86
86
  @cached_mappings = {}
87
+ @cached_attribute_name_counts = {}
87
88
  @finalized = false
88
89
  end
89
90
 
@@ -98,6 +99,17 @@ module Lutaml
98
99
  # Resolve any deferred mapping imports before finalizing
99
100
  ensure_mappings_imported!
100
101
 
102
+ # lutaml-model#687: nested sequence choices tag their target
103
+ # model attributes now that the mapper class is known.
104
+ @element_sequence.each do |seq|
105
+ seq.bind_choice_compositors!(mapper_class)
106
+ end
107
+
108
+ # lutaml-model#296: a mapped rule whose attribute type can never
109
+ # accept the serialization shape fails here, with the attribute
110
+ # and type named, instead of surfacing mid-parse as a cast oddity.
111
+ validate_mapped_attribute_types!(mapper_class)
112
+
101
113
  # Validate mixed content requires collection attribute for content mapping
102
114
  validate_mixed_content_collection!(mapper_class)
103
115
 
@@ -108,9 +120,17 @@ module Lutaml
108
120
  @cached_elements.clear
109
121
  @cached_attributes.clear
110
122
  @cached_mappings.clear
123
+ @cached_attribute_name_counts.clear
124
+ @rule_records_version = (@rule_records_version || 0) + 1
111
125
  @finalized = true
112
126
  end
113
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
+
114
134
  def finalized?
115
135
  @finalized
116
136
  end
@@ -1028,6 +1048,26 @@ module Lutaml
1028
1048
  end
1029
1049
  end
1030
1050
 
1051
+ # Fire InvalidAttributeTypeError at definition time for mapped
1052
+ # rules whose target attribute type is neither a Type::Value nor
1053
+ # Serializable (lutaml-model#296). Undeclared types are left to
1054
+ # deferred-import resolution.
1055
+ def validate_mapped_attribute_types!(mapper_class)
1056
+ (elements + attributes).each do |rule|
1057
+ target = mapper_class.attributes[rule.to]
1058
+ next unless target
1059
+
1060
+ begin
1061
+ resolved = target.type(Lutaml::Model::Config.default_register)
1062
+ rescue Lutaml::Model::UnknownTypeError
1063
+ next
1064
+ end
1065
+ next unless resolved.is_a?(Class)
1066
+
1067
+ target.validate_attr_type!(resolved)
1068
+ end
1069
+ end
1070
+
1031
1071
  def elements(register_id = nil)
1032
1072
  reg_key = register_id || :default
1033
1073
  cached = @cached_elements[reg_key]
@@ -1060,6 +1100,26 @@ module Lutaml
1060
1100
  @raw_mapping
1061
1101
  end
1062
1102
 
1103
+ # lutaml-model#790/#791: whether `rule` is the only attribute rule
1104
+ # claiming its exact name spelling. The answer is a property of the
1105
+ # mapping, not the document, so it is counted once per register
1106
+ # (rules sharing a spelling make each other non-sole).
1107
+ def sole_local_attribute_claimant?(rule, register_id = nil)
1108
+ reg_key = register_id || :default
1109
+ counts = @cached_attribute_name_counts[reg_key]
1110
+ unless counts
1111
+ counts = {}
1112
+ attributes(register_id).each do |r|
1113
+ key = r.name_strings
1114
+ counts[key] = (counts[key] || 0) + 1
1115
+ end
1116
+ counts.freeze if @finalized
1117
+ @cached_attribute_name_counts[reg_key] = counts
1118
+ end
1119
+
1120
+ (counts[rule.name_strings] || 1) == 1
1121
+ end
1122
+
1063
1123
  def mappings(register_id = nil)
1064
1124
  reg_key = register_id || :default
1065
1125
  cached = @cached_mappings[reg_key]
@@ -1281,6 +1341,18 @@ module Lutaml
1281
1341
  !@namespace_class.nil?
1282
1342
  end
1283
1343
 
1344
+ # lutaml-model#154: opt-in decoding of HTML named/numeric entities
1345
+ # (e.g. ©, —) 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
+
1284
1356
  # Whether namespace was explicitly set via DSL
1285
1357
  def namespace_set?
1286
1358
  !!@namespace_set