moxml 0.5.170 → 0.5.172

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +6 -7
  3. data/Rakefile +23 -0
  4. data/conformance/moxml-conformance.json +413 -0
  5. data/lib/moxml/adapter/customized_leptris/declaration.rb +9 -0
  6. data/lib/moxml/adapter/customized_leptris/doctype.rb +8 -0
  7. data/lib/moxml/adapter/customized_leptris/entity_reference.rb +8 -0
  8. data/lib/moxml/adapter/customized_leptris/text_segment.rb +8 -0
  9. data/lib/moxml/adapter/customized_leptris.rb +5 -0
  10. data/lib/moxml/adapter/customized_libxml/cdata.rb +2 -0
  11. data/lib/moxml/adapter/customized_libxml/comment.rb +2 -0
  12. data/lib/moxml/adapter/customized_libxml/declaration.rb +2 -0
  13. data/lib/moxml/adapter/customized_libxml/element.rb +2 -0
  14. data/lib/moxml/adapter/customized_libxml/entity_reference.rb +2 -0
  15. data/lib/moxml/adapter/customized_libxml/node.rb +2 -0
  16. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -0
  17. data/lib/moxml/adapter/customized_libxml/text.rb +2 -0
  18. data/lib/moxml/adapter/customized_libxml.rb +3 -0
  19. data/lib/moxml/adapter/customized_oga/entity_decoder.rb +8 -0
  20. data/lib/moxml/adapter/customized_oga/raw_value_override.rb +8 -0
  21. data/lib/moxml/adapter/customized_oga/xml_declaration.rb +8 -0
  22. data/lib/moxml/adapter/customized_oga/xml_generator.rb +1 -1
  23. data/lib/moxml/adapter/customized_oga.rb +4 -0
  24. data/lib/moxml/adapter/customized_ox/attribute.rb +5 -0
  25. data/lib/moxml/adapter/customized_ox/entity_reference.rb +5 -0
  26. data/lib/moxml/adapter/customized_ox/namespace.rb +5 -0
  27. data/lib/moxml/adapter/customized_ox/text.rb +6 -0
  28. data/lib/moxml/adapter/customized_ox.rb +3 -0
  29. data/lib/moxml/adapter/customized_rexml/entity_reference.rb +5 -0
  30. data/lib/moxml/adapter/customized_rexml/formatter.rb +6 -0
  31. data/lib/moxml/adapter/customized_rexml.rb +4 -0
  32. data/lib/moxml/adapter/leptris.rb +15 -32
  33. data/lib/moxml/adapter/nokogiri.rb +12 -4
  34. data/lib/moxml/adapter.rb +33 -0
  35. data/lib/moxml/context.rb +1 -0
  36. data/lib/moxml/version.rb +1 -1
  37. data/lib/moxml/xsd/builtin_lexical.rb +100 -0
  38. data/spec/moxml/adapter/customized_lifecycle_audit_spec.rb +49 -0
  39. data/spec/moxml/adapter_deprecation_spec.rb +49 -0
  40. data/spec/moxml/adapter_registry_spec.rb +44 -0
  41. data/spec/moxml/customized_lifecycle_spec.rb +47 -0
  42. data/spec/moxml/sax_entity_parity_spec.rb +27 -2
  43. data/spec/moxml/xsd_conformance_cases_spec.rb +37 -0
  44. metadata +8 -2
@@ -1,6 +1,6 @@
1
1
  # rubocop:disable Style/FrozenStringLiteralComment
2
2
 
3
- require "oga"
3
+ # Lifecycle record: engine gap (upstream oga); see the patched-class URL below.require "oga"
4
4
 
5
5
  # monkey patch the Oga generator because it's not configurable
6
6
  # https://github.com/yorickpeterse/oga/blob/main/lib/oga/xml/generator.rb
@@ -2,6 +2,10 @@
2
2
 
3
3
  module Moxml
4
4
  module Adapter
5
+ # Lifecycle record (#495): every override serves the :oga adapter,
6
+ # deprecated in moxml#491 and deleted with the Opal surface in
7
+ # moxml#489 phase 4. No separate upstream ask -- the adapter's
8
+ # fate governs this file.
5
9
  module CustomizedOga
6
10
  autoload :EntityDecoderOverride, "moxml/adapter/customized_oga/entity_decoder"
7
11
  autoload :RawValueOverride, "moxml/adapter/customized_oga/raw_value_override"
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Engine gap (upstream ox): an ::Ox::Attribute carries no parent
4
+ # back-reference and no prefix slots, so the facade's attribute node
5
+ # contract needs this Node subclass. Upstream tracker:
6
+ # https://github.com/ohler55/ox
7
+
3
8
  module Moxml
4
9
  module Adapter
5
10
  module CustomizedOx
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Parse-path alias: ox expands the built-in entities at parse time and
4
+ # has no reference nodes, so parsed references are the facade's
5
+ # Moxml::Entity::Reference values. Lifecycle: the :ox adapter is
6
+ # deprecated (#491) — this record dies with it.
7
+
3
8
  module Moxml
4
9
  module Adapter
5
10
  module CustomizedOx
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Engine gap (upstream ox): ox has no Namespace node class — prefixes
4
+ # live as attribute spellings — so the facade's namespace node contract
5
+ # needs this value object. Upstream tracker:
6
+ # https://github.com/ohler55/ox
7
+
3
8
  module Moxml
4
9
  module Adapter
5
10
  module CustomizedOx
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Engine gap (upstream ox): Ox uses Strings for text content, but a
4
+ # String cannot carry a @parent back-reference; this ::Ox::Node subclass
5
+ # gives text nodes node identity. NOTE: an ::Ox::Node subclass that is
6
+ # neither ::Ox::Element nor ::Ox::Document changes Ox's dump behavior.
7
+ # Upstream tracker: https://github.com/ohler55/ox
8
+
3
9
  module Moxml
4
10
  module Adapter
5
11
  module CustomizedOx
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Moxml
4
4
  module Adapter
5
+ # Lifecycle record (#495): engine-gap shims for the :ox adapter,
6
+ # deprecated in moxml#491 -- removed with it after the cycle.
7
+ # No separate upstream ask; the adapter's fate governs.
5
8
  module CustomizedOx
6
9
  autoload :Attribute, "moxml/adapter/customized_ox/attribute"
7
10
  autoload :EntityReference, "moxml/adapter/customized_ox/entity_reference"
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Parse-path alias: rexml expands the built-in entities at parse time,
4
+ # so parsed references are the facade's Moxml::Entity::Reference values.
5
+ # Lifecycle: the :rexml adapter is Opal-only and dies with the Opal
6
+ # surface (#489 phase 4); third-party engine, no upstream ask.
7
+
3
8
  module Moxml
4
9
  module Adapter
5
10
  module CustomizedRexml
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Custom REXML formatter that fixes indentation and wrapping issues.
4
+ # Engine gap (upstream rexml): REXML's Formatters::Pretty cannot be
5
+ # configured to the facade's whitespace contract. Third-party engine,
6
+ # no upstream ask; the :rexml adapter is Opal-only and dies with the
7
+ # Opal surface (#489 phase 4).
8
+
3
9
  require "rexml/formatters/pretty"
4
10
 
5
11
  module Moxml
@@ -2,6 +2,10 @@
2
2
 
3
3
  module Moxml
4
4
  module Adapter
5
+ # Lifecycle record (#495): engine-gap shims for the :rexml
6
+ # adapter (Opal fallback; deleted with the Opal surface in
7
+ # moxml#489 phase 4). No separate upstream ask; the adapter's
8
+ # fate governs this file.
5
9
  module CustomizedRexml
6
10
  autoload :EntityReference,
7
11
  "moxml/adapter/customized_rexml/entity_reference"
@@ -41,8 +41,7 @@ module Moxml
41
41
  # leptris_node_digest shipped in bindings 1.9.99 (libleptris
42
42
  # 1.9.99, engine #869): on-demand Merkle subtree hash, zero
43
43
  # cost when unused. Below it, Node#digest answers nil.
44
- DIGEST_SUPPORTED =
45
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.99")
44
+ DIGEST_SUPPORTED = true
46
45
 
47
46
  # 1.9.163.7 added the XPath seams (TODO.perf/15+16):
48
47
  # Searchable#at_xpath materializes nodeset entry 0 and frees
@@ -52,8 +51,7 @@ module Moxml
52
51
  # raw pointers, skipping the per-call EvaluationContext
53
52
  # (~2us on every multi-result query).
54
53
  NATIVE_XPATH_SEAMS =
55
- defined?(::Leptris::XML::Native) &&
56
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.7")
54
+ defined?(::Leptris::XML::Native)
57
55
 
58
56
  # Opt-in native read layer (leptris-ruby#185, bindings >= 1.9.162.6):
59
57
  # TypedData node wrappers with C-bound hot reads and bulk
@@ -68,8 +66,7 @@ module Moxml
68
66
  nil
69
67
  end
70
68
  NATIVE_READ_LAYER =
71
- defined?(::Leptris::XML::NativeNode) &&
72
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.162.6")
69
+ defined?(::Leptris::XML::NativeNode)
73
70
 
74
71
  # leptris-ruby#529: the whole XPath node-set result as an Array
75
72
  # of registered natives in ONE C crossing (Document#
@@ -118,8 +115,7 @@ module Moxml
118
115
  # (1.9.162.x answered ASCII-8BIT — the reads retagged with a
119
116
  # dup+force_encoding per call).
120
117
  NATIVE_STRINGS_UTF8 =
121
- defined?(::Leptris::XML::NativeNode) &&
122
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.2")
118
+ defined?(::Leptris::XML::NativeNode)
123
119
 
124
120
  # 1.9.163.5 (leptris-ruby#204/#208): native mutations are
125
121
  # version-coherent with the binding (both surfaces' memos
@@ -127,8 +123,7 @@ module Moxml
127
123
  # add_child are adoptable end-to-end; NativeNode grew a
128
124
  # document accessor and line numbers.
129
125
  NATIVE_MUTATIONS_COHERENT =
130
- defined?(::Leptris::XML::NativeNode) &&
131
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.5")
126
+ defined?(::Leptris::XML::NativeNode)
132
127
 
133
128
  # Identity mode (#230 stage 2): the contract modules extended
134
129
  # onto natives shadow the C surface (Element#[] over
@@ -170,16 +165,14 @@ module Moxml
170
165
  # 1.9.181 (lockstep): Node#source_position — {line, col_start,
171
166
  # col_end} from the engine's source-position API (upstream
172
167
  # #1124); created nodes report zeros.
173
- NATIVE_SOURCE_POSITION =
174
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.181")
168
+ NATIVE_SOURCE_POSITION = true
175
169
 
176
170
  # Entity-reference preservation (leptris-ruby#212 / upstream
177
171
  # #1094): 1.9.177 ships ParseOptions.keep_entity_refs plus
178
172
  # first-class EntityReference nodes — the marker machinery
179
173
  # becomes bypassable when the Context's entity_mode is :keep.
180
174
  NATIVE_ENTITY_REFS =
181
- defined?(::Leptris::XML::EntityReference) &&
182
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.177")
175
+ defined?(::Leptris::XML::EntityReference)
183
176
 
184
177
  # 1.9.163.2 moved the native bulk children into C
185
178
  # (Native.bulk_children) and fixed the 512 truncation
@@ -189,8 +182,7 @@ module Moxml
189
182
  # C bulk path (version-memoized, cheaper than a per-call
190
183
  # count+copy).
191
184
  NATIVE_BULK_FIXED =
192
- defined?(::Leptris::XML::NativeNode) &&
193
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.163.2")
185
+ defined?(::Leptris::XML::NativeNode)
194
186
 
195
187
  # Bulk SAX record drain (leptris#1298): leptris_sax_records_parse
196
188
  # + Leptris::XML::SAX::Records — one crossing per document,
@@ -552,17 +544,13 @@ module Moxml
552
544
  # classes do not exist. NATIVE_MUTATIONS_COHERENT is false
553
545
  # without the native layer, so all three evaluate false
554
546
  # there.
555
- NATIVE_IDENTITY =
556
- NATIVE_MUTATIONS_COHERENT &&
557
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.174.4")
547
+ NATIVE_IDENTITY = NATIVE_MUTATIONS_COHERENT
558
548
 
559
549
  NATIVE_C_MINT =
560
550
  NATIVE_IDENTITY &&
561
551
  Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.255.2")
562
552
 
563
- NATIVE_KLASS_CHILDREN =
564
- NATIVE_IDENTITY &&
565
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.193.2")
553
+ NATIVE_KLASS_CHILDREN = NATIVE_IDENTITY
566
554
  end
567
555
 
568
556
  if NATIVE_READ_LAYER
@@ -870,8 +858,7 @@ module Moxml
870
858
  end
871
859
 
872
860
  NATIVE_C_WALK =
873
- defined?(::Leptris::XML::NativeNode) &&
874
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.174.6")
861
+ defined?(::Leptris::XML::NativeNode)
875
862
 
876
863
  # One C visit walk (TODO.perf/27: post-order/abort state in
877
864
  # the C callback, rb_yield direct — no FFI closure per
@@ -1109,8 +1096,7 @@ module Moxml
1109
1096
  # on released 1.9.37–1.9.39; 1.9.40 (engine 1.9.14+) restored
1110
1097
  # the fallback. Older bindings keep the Ruby-engine routing in
1111
1098
  # native_expression?.
1112
- PREFIXED_ATTR_PREDICATES_NATIVE =
1113
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.40")
1099
+ PREFIXED_ATTR_PREDICATES_NATIVE = true
1114
1100
 
1115
1101
  # leptris/leptris#636 (leptris-ruby 1.9.42): libxml2-compatible
1116
1102
  # pretty-print — child-PI lines, DOCTYPE internal-subset layout,
@@ -1118,8 +1104,7 @@ module Moxml
1118
1104
  # unit (`to_xml`'s indent_text takes the unit string). Older
1119
1105
  # floor bindings treat indent_text as a display-form boolean, so
1120
1106
  # the passthrough and the parity pins gate on this.
1121
- LIBXML2_LAYOUT_PARITY =
1122
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.42")
1107
+ LIBXML2_LAYOUT_PARITY = true
1123
1108
 
1124
1109
  # leptris-ruby#109 (1.9.45): Element#to_xml takes the
1125
1110
  # indent-unit string, so moxml's per-child document composition
@@ -1127,13 +1112,11 @@ module Moxml
1127
1112
  # emit spaces until the engine fix (leptris/leptris#658); the
1128
1113
  # boolean display form stays document-level and is never
1129
1114
  # forwarded (the element face raises on it).
1130
- INDENT_UNIT_SUPPORTED =
1131
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.45")
1115
+ INDENT_UNIT_SUPPORTED = true
1132
1116
 
1133
1117
  # leptris-ruby#115 (fixed 1.9.50): the element unit path keeps
1134
1118
  # child comments.
1135
- ELEMENT_UNIT_COMMENTS =
1136
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.50")
1119
+ ELEMENT_UNIT_COMMENTS = true
1137
1120
 
1138
1121
  # leptris/leptris#677: the engine's DROP_WS_TEXT trims boundary
1139
1122
  # whitespace of non-blank text nodes. Probed at load rather
@@ -800,11 +800,19 @@ module Moxml
800
800
  @handler.on_end_document
801
801
  end
802
802
 
803
- def start_element(name, attributes = [])
804
- attr_hash, ns_hash = split_attributes_and_namespaces(attributes) do |v|
805
- Moxml::Adapter::Base.decode_entities(v)
803
+ # libxml hands over namespace declaration URIs already decoded,
804
+ # while attribute values still carry their entity references;
805
+ # decoding the URIs again corrupts "&" into "&".
806
+ def start_element_namespace(name, attrs = [], prefix = nil, _uri = nil,
807
+ namespaces = [])
808
+ attr_hash = {}
809
+ attrs.each do |attr|
810
+ key = attr.prefix ? "#{attr.prefix}:#{attr.localname}" : attr.localname
811
+ attr_hash[key] = Moxml::Adapter::Base.decode_entities(attr.value)
806
812
  end
807
- @handler.on_start_element(name, attr_hash, ns_hash)
813
+ ns_hash = namespaces.empty? ? EMPTY_NAMESPACES : namespaces.to_h
814
+ qualified = prefix ? "#{prefix}:#{name}" : name
815
+ @handler.on_start_element(qualified, attr_hash, ns_hash)
808
816
  end
809
817
 
810
818
  def end_element(name)
data/lib/moxml/adapter.rb CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  module Moxml
4
4
  module Adapter
5
+ # Extension contract (#494): adding an adapter is exactly the trio
6
+ # below — nothing else, anywhere. The dispatch is open for
7
+ # extension and closed against edit: registration is data, the
8
+ # load path is generic, and spec/moxml/adapter_registry_spec.rb
9
+ # walks the registry mechanically so a skipped leg fails there.
10
+ #
11
+ # 1. one entry in AVAILABLE_ADAPTERS,
12
+ # 2. a requireable file at moxml/adapter/<name> (a
13
+ # `Customized<Name>` namespace module is optional),
14
+ # 3. a subclass of Adapter::Base in that file.
15
+ #
16
+ # Deprecated adapters live in DEPRECATED_ADAPTERS (still fully
17
+ # registered and loadable) until their removal milestone.
5
18
  autoload :Base, "moxml/adapter/base"
6
19
  autoload :CustomizedOga, "moxml/adapter/customized_oga"
7
20
  autoload :CustomizedOx, "moxml/adapter/customized_ox"
@@ -25,7 +38,27 @@ module Moxml
25
38
  headed_ox: "HeadedOx",
26
39
  }.freeze
27
40
 
41
+ # Adapters in their deprecation cycle (#491): warned once per
42
+ # process at configuration, removed after the cycle. The value is
43
+ # the migration guidance shown in the warning.
44
+ DEPRECATED_ADAPTERS = {
45
+ headed_ox: "use :leptris (primary) or :nokogiri; headed_ox is a pure alias of :ox",
46
+ libxml: "use :nokogiri — the same libxml2 engine, governed by the consistency suite",
47
+ ox: "use :leptris (primary) or :nokogiri",
48
+ }.freeze
49
+
28
50
  class << self
51
+ # One-time deprecation notice per adapter per process (#491).
52
+ def warn_deprecated!(name)
53
+ guidance = DEPRECATED_ADAPTERS[name.to_sym]
54
+ return unless guidance
55
+ return if (@deprecation_warned ||= {})[name.to_sym]
56
+
57
+ @deprecation_warned[name.to_sym] = true
58
+ warn "moxml: the :#{name} adapter is deprecated and will be " \
59
+ "removed; #{guidance}"
60
+ end
61
+
29
62
  def load(name)
30
63
  validate_platform!(name)
31
64
  require_adapter(name)
data/lib/moxml/context.rb CHANGED
@@ -26,6 +26,7 @@ module Moxml
26
26
  attr_reader :config
27
27
 
28
28
  def initialize(adapter = nil)
29
+ Adapter.warn_deprecated!(adapter) if adapter
29
30
  @config = Config.new(adapter)
30
31
  # Bumped on any namespace-scope mutation; Element scope caches
31
32
  # compare against it so every wrapper sees changes without
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.170"
4
+ VERSION = "0.5.172"
5
5
  end
@@ -255,6 +255,106 @@ module Moxml
255
255
  "xs:NOTATION" => ->(lexical) { qname?(collapsed(lexical)) },
256
256
  }.freeze
257
257
 
258
+ # The cross-language conformance dataset (#496): the model's
259
+ # builtin-lexical answers as data, pinned BY the Ruby suite
260
+ # (spec/moxml/xsd_conformance_cases_spec.rb) and exported to
261
+ # conformance/moxml-conformance.json by `rake conformance:export`
262
+ # — moxml-rs and moxml-ts run the same rows. Each row:
263
+ # [type, lexical, expected].
264
+ CONFORMANCE_CASES = [
265
+ # token family / collapse
266
+ ["xs:token", "a b", true],
267
+ ["xs:ID", " id ", true],
268
+ ["xs:NMTOKEN", "\tx ", true],
269
+ ["xs:ID", "1d", false],
270
+ # unbounded integers
271
+ ["xs:integer", "1" * 29, true],
272
+ ["xs:positiveInteger", "1" * 29, true],
273
+ ["xs:nonNegativeInteger", "1" * 29, true],
274
+ ["xs:negativeInteger", "-1#{'0' * 20}", true],
275
+ ["xs:nonPositiveInteger", "0", true],
276
+ ["xs:nonPositiveInteger", "-0", true],
277
+ # bounded integers
278
+ ["xs:long", "9223372036854775807", true],
279
+ ["xs:long", "9223372036854775808", false],
280
+ ["xs:unsignedLong", "18446744073709551615", true],
281
+ ["xs:unsignedLong", "18446744073709551616", false],
282
+ ["xs:byte", "128", false],
283
+ ["xs:unsignedByte", "256", false],
284
+ # float/decimal spellings
285
+ ["xs:float", "+INF", true],
286
+ ["xs:double", "-INF", true],
287
+ ["xs:float", "NaN", true],
288
+ ["xs:float", "1.5E-3", true],
289
+ ["xs:decimal", "5.", true],
290
+ ["xs:decimal", ".5", true],
291
+ ["xs:decimal", "1e3", false],
292
+ # calendar
293
+ ["xs:date", "2024-02-29", true],
294
+ ["xs:date", "1999-02-29", false],
295
+ ["xs:date", "2024-04-31", false],
296
+ ["xs:date", "2024-13-01", false],
297
+ ["xs:date", "not-a-date", false],
298
+ ["xs:dateTime", "2024-02-29T23:59:59Z", true],
299
+ ["xs:dateTime", "1999-02-29T00:00:00", false],
300
+ ["xs:dateTime", "not-a-date", false],
301
+ ["xs:dateTimeStamp", "2024-02-29T00:00:00Z", true],
302
+ ["xs:dateTimeStamp", "2024-02-29T00:00:00", false],
303
+ ["xs:time", "23:59:59.999+05:30", true],
304
+ ["xs:time", "24:00:00", false],
305
+ ["xs:time", "25:20:00", false],
306
+ ["xs:time", "13:20:60", false],
307
+ ["xs:gYearMonth", "2024-02Z", true],
308
+ ["xs:gYearMonth", "2024-13", false],
309
+ ["xs:gMonthDay", "--02-29", true],
310
+ ["xs:gMonthDay", "--02-30", false],
311
+ ["xs:gDay", "---31", true],
312
+ ["xs:gDay", "---32", false],
313
+ # duration
314
+ ["xs:duration", "P1Y2M3DT4H5M6.5S", true],
315
+ ["xs:duration", "PT0S", true],
316
+ ["xs:duration", "-P1D", true],
317
+ ["xs:duration", "P", false],
318
+ ["xs:duration", "PT", false],
319
+ ["xs:duration", "P1Y2MT", false],
320
+ ["xs:duration", "P200.5Y", false],
321
+ ["xs:dayTimeDuration", "P1DT2H", true],
322
+ ["xs:dayTimeDuration", "P1Y", false],
323
+ ["xs:yearMonthDuration", "P1Y2M", true],
324
+ ["xs:yearMonthDuration", "P1D", false],
325
+ ["xs:yearMonthDuration", "P1YT1H", false],
326
+ # binary
327
+ ["xs:base64Binary", "aGVsbG8=", true],
328
+ ["xs:base64Binary", "abcd", true],
329
+ ["xs:base64Binary", "abcde", false],
330
+ ["xs:base64Binary", "a b", false],
331
+ ["xs:hexBinary", "0A", true],
332
+ ["xs:hexBinary", "0AB", false],
333
+ # QName family
334
+ ["xs:QName", "foo", true],
335
+ ["xs:QName", "ns:foo", true],
336
+ ["xs:QName", "xmlns:xsi", false],
337
+ ["xs:QName", ":foo", false],
338
+ ["xs:QName", "a:b:c", false],
339
+ ["xs:NOTATION", "foo", true],
340
+ # lists: items, not characters
341
+ ["xs:IDREFS", "a b", true],
342
+ ["xs:IDREFS", "a", true],
343
+ ["xs:IDREFS", "", false],
344
+ ["xs:NMTOKENS", "a.b-c:d e", true],
345
+ ["xs:ENTITIES", "a 1", false],
346
+ # open lexical spaces
347
+ ["xs:string", "anything <>&", true],
348
+ ["xs:anyURI", "http://example.com/a b", true],
349
+ ["xs:boolean", "true", true],
350
+ ["xs:boolean", "0", true],
351
+ ["xs:boolean", "TRUE", false],
352
+ ["xs:language", "en-US", true],
353
+ ["xs:language", "en_US", false],
354
+ ["xs:Name", "a:b", true],
355
+ ["xs:NCName", "a:b", false],
356
+ ].map(&:freeze).freeze
357
+
258
358
  private_constant :CHECKERS
259
359
  end
260
360
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # #495: the Customized* adapter layer is either a removable-lifecycle
6
+ # record (an engine gap with a tracked upstream issue) or it dissolves
7
+ # into Base. This audit makes the lifecycle record checkable instead of
8
+ # folklore: every override file must cite its issue (an #NNN reference
9
+ # for our engines, or an upstream tracker URL for third-party engines
10
+ # we do not file against), and every Customized module must be wired
11
+ # into exactly its adapter (no dead records).
12
+ RSpec.describe "Customized adapter lifecycle audit" do
13
+ let(:override_files) do
14
+ Dir[File.expand_path("../../lib/moxml/adapter/customized_*/*.rb", __dir__)].sort
15
+ end
16
+
17
+ it "cites a tracked issue or upstream tracker in every override file" do
18
+ broken = override_files.reject do |file|
19
+ head = File.readlines(file).first(25).join
20
+ head.match?(/#\s*#\d{2,5}\b/) || head.include?("github.com/")
21
+ end
22
+
23
+ expect(broken).to be_empty, <<~MSG
24
+ Customized* override files without a lifecycle citation:
25
+ #{broken.join("\n")}
26
+ Each file's header must cite the open upstream issue that explains
27
+ why the override exists (#NNN for leptris; the upstream tracker URL
28
+ for third-party engines), or the #491 deprecation that retires it.
29
+ MSG
30
+ end
31
+
32
+ it "wires every Customized module into exactly its adapter" do
33
+ shims = Dir[File.expand_path("../../lib/moxml/adapter/customized_*.rb", __dir__)]
34
+
35
+ shims.each do |shim|
36
+ module_name = File.read(shim)[/module (Customized\w+)/, 1]
37
+ adapter_file = File.expand_path(
38
+ "../../lib/moxml/adapter/#{module_name.delete_prefix('Customized').downcase}.rb",
39
+ __dir__,
40
+ )
41
+
42
+ expect(File).to exist(adapter_file),
43
+ "#{module_name} has no #{adapter_file} to customize"
44
+
45
+ expect(File.read(adapter_file)).to include(module_name),
46
+ "#{module_name} is not wired into its adapter"
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::Adapter do
6
+ describe ".warn_deprecated!" do
7
+ # The once-per-process state is module-level; isolate each
8
+ # example from prior loads and from later suites.
9
+ around do |example|
10
+ described_class.instance_variable_set(:@deprecation_warned, {})
11
+ example.run
12
+ described_class.instance_variable_set(:@deprecation_warned, {})
13
+ end
14
+
15
+ it "warns once per deprecated adapter per process" do
16
+ expect do
17
+ Moxml.new(:headed_ox)
18
+ end.to output(%r{:headed_ox adapter is deprecated}).to_stderr
19
+ expect do
20
+ Moxml.new(:headed_ox)
21
+ end.not_to output.to_stderr
22
+ end
23
+
24
+ it "guides every deprecated adapter toward the governed matrix" do
25
+ described_class::DEPRECATED_ADAPTERS.each_value do |guidance|
26
+ expect(guidance).to match(/:nokogiri|:leptris/)
27
+ end
28
+ end
29
+
30
+ it "does not warn for the governed matrix" do
31
+ expect do
32
+ Moxml.new(:leptris)
33
+ Moxml.new(:nokogiri)
34
+ end.not_to output(/deprecated/).to_stderr
35
+ end
36
+
37
+ it "does not warn for unknown names" do
38
+ expect do
39
+ described_class.warn_deprecated!(:definitely_not_an_adapter)
40
+ end.not_to output.to_stderr
41
+ end
42
+
43
+ it "keeps the deprecated adapters loadable through the cycle" do
44
+ described_class::DEPRECATED_ADAPTERS.each_key do |name|
45
+ expect(described_class.available?(name)).to be(true)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # The registry's extension contract (#494): adding an adapter is the
6
+ # documented trio — one AVAILABLE_ADAPTERS entry, one requireable
7
+ # adapter file, one Base subclass. The dispatch is open for extension:
8
+ # this spec walks the registry mechanically so a new adapter that
9
+ # skips any leg fails here, not in a consumer.
10
+ RSpec.describe "Adapter registry extension contract" do
11
+ it "loads every registered adapter through the public face" do
12
+ Moxml::Adapter::AVAILABLE_ADAPTERS.each do |name|
13
+ adapter = Moxml::Adapter.load(name)
14
+ expect(adapter).not_to be_nil
15
+ end
16
+ end
17
+
18
+ it "resolves every registered adapter to an Adapter::Base subclass" do
19
+ Moxml::Adapter::AVAILABLE_ADAPTERS.each do |name|
20
+ adapter = Moxml::Adapter.load(name)
21
+ expect(adapter).to be <= Moxml::Adapter::Base
22
+ end
23
+ end
24
+
25
+ it "answers available? truthfully for every registered adapter" do
26
+ Moxml::Adapter::AVAILABLE_ADAPTERS.each do |name|
27
+ expect(Moxml::Adapter.available?(name)).to be(true)
28
+ end
29
+ end
30
+
31
+ it "rejects unregistered names before any require runs" do
32
+ expect do
33
+ Moxml::Adapter.load(:definitely_not_an_adapter)
34
+ end.to raise_error(Moxml::AdapterError, /not available on this platform/)
35
+ end
36
+
37
+ it "keeps the deprecated adapters inside the registry with guidance" do
38
+ described = Moxml::Adapter::DEPRECATED_ADAPTERS
39
+ expect(described.keys).to all(satisfy { |name| Moxml::Adapter::AVAILABLE_ADAPTERS.include?(name) })
40
+ described.each_value do |guidance|
41
+ expect(guidance).to match(/:nokogiri|:leptris/)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # The customized_* layer's doctrine (#495): each namespace module is
6
+ # either a tracked shim (an engine gap with a stated resolution) or an
7
+ # adapter-fate case (dies with its deprecated adapter). A Customized*
8
+ # file exists only when its header says which — the record is
9
+ # checkable, not folklore.
10
+ RSpec.describe "customized adapter layer lifecycle" do
11
+ def customized_files
12
+ Dir["lib/moxml/adapter/customized_*.rb"]
13
+ end
14
+
15
+ it "has customized namespaces to govern" do
16
+ expect(customized_files).not_to be_empty
17
+ end
18
+
19
+ it "cites its lifecycle in every module header" do
20
+ customized_files.each do |path|
21
+ header = File.readlines(path).first(30).join
22
+ expect(header).to match(/#\d{3}/),
23
+ "#{path} carries no lifecycle citation (#495)"
24
+ expect(header).to match(/Lifecycle record|no native representation/),
25
+ "#{path} header does not state its lifecycle"
26
+ end
27
+ end
28
+
29
+ it "marks adapter-fate cases as deprecated or Opal-bound" do
30
+ adapter_fate = customized_files.reject { |p| p.include?("leptris") }
31
+ expect(adapter_fate).not_to be_empty
32
+ adapter_fate.each do |path|
33
+ header = File.readlines(path).first(30).join
34
+ expect(header).to match(/moxml#491|moxml#489/),
35
+ "#{path} is an adapter-fate case but cites neither " \
36
+ "its deprecation (#491) nor the Opal removal (#489)"
37
+ end
38
+ end
39
+
40
+ it "keeps the tracked shim pointed at its resolution" do
41
+ header = File.readlines("lib/moxml/adapter/customized_leptris.rb")
42
+ .first(30).join
43
+ expect(header).to include("tracked shim"), "the leptris shim must say so"
44
+ expect(header).to include("dissolve into Base"),
45
+ "the leptris shim must state its end state"
46
+ end
47
+ end