moxml 0.5.169 → 0.5.171

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: c6d162a122e231644310d60b9f27eef7f3fa89728a6c135a9eb855d56952af5d
4
- data.tar.gz: 7c3c92e7ffe80f35484e364d75808d1a74f4c62db7c1e2222b0d1d2921d3ccac
3
+ metadata.gz: dd5e790791e306829e87770a45424b006c9a535c95c825adfc7d0800816ceb7d
4
+ data.tar.gz: c3348f9fb7c6c0ccf00ab4bc16bba51b0812331d12d8478d2a77e09b8a600e58
5
5
  SHA512:
6
- metadata.gz: 4872ca2eb4456ee02a511196840379610f6c66b590d64ba1a4715b6107c33a912056bd6fe48055d1270c274e9aec62193f88778180f57ebe32da3f27961f871c
7
- data.tar.gz: 4811ea416c2ebd77ff2cbedf4f640a755424e5ae67d3590c63220fd182eff8ad2fb0c58335ea82f90bb8b9c5bca9f408cb4989256a9c74bb6af5b568ecfc457b
6
+ metadata.gz: 2972a94dc0bf75896d29432c1856b949090cbd8547b60c402376f2d715c743c8e029af2181531fbd327d6615a3a6aef5cb83144c5ff2b6389f35bd3ab6dfea59
7
+ data.tar.gz: c89e1be189130b17f0a7f61c67c9caba0624f589c29a2407effba7fb50da72304d33db77001185cb7b079729a7a29140d32e8a492269a58cf748a8e99f0276a2
data/README.adoc CHANGED
@@ -95,7 +95,8 @@ adapter refuses to load below its floor.
95
95
 
96
96
  |Ox
97
97
  |>= 2.14
98
- |Tested against the 2.14 line.
98
+ |DEPRECATED (#491) — warned at configuration; use :leptris (primary)
99
+ or :nokogiri. Removal after the deprecation cycle.
99
100
 
100
101
  |Oga
101
102
  |3.4 / 3.5
@@ -105,8 +106,9 @@ moxml's customized generator and decoder overrides.
105
106
 
106
107
  |LibXML (libxml-ruby)
107
108
  |>= 5.0
108
- |6.x works and is the installed default on Ruby >= 3.2 (libxml-ruby
109
- 6.0 itself requires Ruby >= 3.2); Ruby 3.1 resolves 5.x.
109
+ |DEPRECATED (#491) — warned at configuration; use :nokogiri (the
110
+ same libxml2 engine, governed by the consistency suite). Removal
111
+ after the deprecation cycle.
110
112
 
111
113
  |Leptris (leptris-ruby)
112
114
  |>= 1.9.194.1
@@ -114,10 +116,7 @@ moxml's customized generator and decoder overrides.
114
116
  Raised from 1.9.32 (2026-09-20): every known consumer resolves a
115
117
  modern binding, and the sub-1.9.194 accommodation paths are deleted.
116
118
  Newer bindings unlock more native surface automatically:
117
- 1.9.163.7+ single-result XPath seams, 1.9.174.4+ C-minted identity
118
- wrappers, 1.9.174.6+ one-dispatch subtree walks, 1.9.177+ first-class
119
- entity references (`entity_mode: :keep`), 1.9.193.4+ native plan rows
120
- (`Moxml::Plan`), 1.9.194.1+ hot-read aliases, 1.9.201.1+ the C struct
119
+ 1.9.194.1+ hot-read aliases, 1.9.201.1+ the C struct
121
120
  executor (`Moxml::StructPlan`; the floor is the varargs-mint fix),
122
121
  1.9.204+ native document-level parts (created DOCTYPEs as native
123
122
  nodes, document-PI removal, declaration clear), 1.9.240+ the bulk
@@ -45,6 +45,7 @@ Generated by `bundle exec rake docs:coverage` from the coverage matrix and the d
45
45
  | validation engine metadata | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
46
46
  | validation regex dismissal | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
47
47
  | validation builtin lexical table | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
48
+ | validation schema sets | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
48
49
  | validation substitution dismissal | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
49
50
  | loud capability contracts | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
50
51
  |===
@@ -255,7 +255,7 @@ semantics.
255
255
 
256
256
  === Validation
257
257
 
258
- Pins:: `spec/moxml/xsd_builtin_lexical_spec.rb`, `spec/moxml/xsd_engine_meta_spec.rb`, `spec/moxml/xsd_regex_spec.rb`, `spec/moxml/xsd_spec.rb`, `spec/moxml/xsd_substitution_spec.rb`, `spec/moxml/xsd_underdetection_spec.rb`, `spec/moxml/xsd_version_spec.rb`
258
+ Pins:: `spec/moxml/xsd_builtin_lexical_spec.rb`, `spec/moxml/xsd_engine_meta_spec.rb`, `spec/moxml/xsd_regex_spec.rb`, `spec/moxml/xsd_set_spec.rb`, `spec/moxml/xsd_spec.rb`, `spec/moxml/xsd_substitution_spec.rb`, `spec/moxml/xsd_underdetection_spec.rb`, `spec/moxml/xsd_version_spec.rb`
259
259
 
260
260
  XSD validation through engines that ship a validator (libxml2;
261
261
  leptris tier-1: recognized roots, one-level content descent, lexical
@@ -265,7 +265,13 @@ spaces (collapse for the token family, the unbounded integer family
265
265
  with bignum bounds, calendar and duration grammars, binary, QName
266
266
  with the reserved `xmlns` prefix) from the facade's own table on
267
267
  every adapter — an engine table converges to it, never the reverse;
268
- an unknown name raises `ArgumentError`. `validate` returns
268
+ an unknown name raises `ArgumentError`. Schema-set compilation
269
+ compiles a LIST of documents into one schema (`Context#xsd_set`):
270
+ imports resolve across the whole set, so components declared in
271
+ importing documents — substitution-group members, derived types —
272
+ are visible to the head's content models; the single-document
273
+ faces cannot see them. Adapters without a set-capable engine raise
274
+ `Moxml::NotImplementedError`. `validate` returns
269
275
  accumulated `ValidationError` entries —
270
276
  message always; line/column/path where the engine reports them, nil
271
277
  where it does not; `[]` means valid. Documents outside the engine's
@@ -373,6 +373,12 @@ module Moxml
373
373
  "XSD validation requires an adapter with an XSD engine"
374
374
  end
375
375
 
376
+ def xsd_compile_set(_paths)
377
+ raise Moxml::NotImplementedError,
378
+ "Schema-set compilation requires an XSD engine with the " \
379
+ "compile_set face"
380
+ end
381
+
376
382
  def xsd_declarations(_xsd_handle); end
377
383
 
378
384
  def xsd_compile_error(_xsd_handle); end
@@ -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,
@@ -372,6 +364,23 @@ module Moxml
372
364
  ::Leptris::XML::XSD.compile_file(path)
373
365
  end
374
366
 
367
+ # Schema-set compilation (leptris 1.9.387, moxml#483 item 4):
368
+ # every document compiles into one schema, so imports resolve
369
+ # across the set and substitution-group members declared in
370
+ # importing documents are visible to the head's content
371
+ # models. Path-sourced only — the engine face reads files.
372
+ def xsd_compile_set(paths)
373
+ unless XSD_SUPPORTED &&
374
+ defined?(::Leptris::XML::XSD::Schema) &&
375
+ ::Leptris::XML::XSD::Schema.respond_to?(:compile_set)
376
+ raise Moxml::NotImplementedError,
377
+ "Schema-set compilation requires the leptris 1.9.387+ " \
378
+ "engine (the compile_set face)"
379
+ end
380
+
381
+ ::Leptris::XML::XSD.compile_set(paths)
382
+ end
383
+
375
384
  def relaxng_compile(schema_source)
376
385
  unless RELAXNG_SUPPORTED
377
386
  raise Moxml::NotImplementedError,
@@ -535,17 +544,13 @@ module Moxml
535
544
  # classes do not exist. NATIVE_MUTATIONS_COHERENT is false
536
545
  # without the native layer, so all three evaluate false
537
546
  # there.
538
- NATIVE_IDENTITY =
539
- NATIVE_MUTATIONS_COHERENT &&
540
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.174.4")
547
+ NATIVE_IDENTITY = NATIVE_MUTATIONS_COHERENT
541
548
 
542
549
  NATIVE_C_MINT =
543
550
  NATIVE_IDENTITY &&
544
551
  Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.255.2")
545
552
 
546
- NATIVE_KLASS_CHILDREN =
547
- NATIVE_IDENTITY &&
548
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.193.2")
553
+ NATIVE_KLASS_CHILDREN = NATIVE_IDENTITY
549
554
  end
550
555
 
551
556
  if NATIVE_READ_LAYER
@@ -853,8 +858,7 @@ module Moxml
853
858
  end
854
859
 
855
860
  NATIVE_C_WALK =
856
- defined?(::Leptris::XML::NativeNode) &&
857
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.174.6")
861
+ defined?(::Leptris::XML::NativeNode)
858
862
 
859
863
  # One C visit walk (TODO.perf/27: post-order/abort state in
860
864
  # the C callback, rb_yield direct — no FFI closure per
@@ -1092,8 +1096,7 @@ module Moxml
1092
1096
  # on released 1.9.37–1.9.39; 1.9.40 (engine 1.9.14+) restored
1093
1097
  # the fallback. Older bindings keep the Ruby-engine routing in
1094
1098
  # native_expression?.
1095
- PREFIXED_ATTR_PREDICATES_NATIVE =
1096
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.40")
1099
+ PREFIXED_ATTR_PREDICATES_NATIVE = true
1097
1100
 
1098
1101
  # leptris/leptris#636 (leptris-ruby 1.9.42): libxml2-compatible
1099
1102
  # pretty-print — child-PI lines, DOCTYPE internal-subset layout,
@@ -1101,8 +1104,7 @@ module Moxml
1101
1104
  # unit (`to_xml`'s indent_text takes the unit string). Older
1102
1105
  # floor bindings treat indent_text as a display-form boolean, so
1103
1106
  # the passthrough and the parity pins gate on this.
1104
- LIBXML2_LAYOUT_PARITY =
1105
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.42")
1107
+ LIBXML2_LAYOUT_PARITY = true
1106
1108
 
1107
1109
  # leptris-ruby#109 (1.9.45): Element#to_xml takes the
1108
1110
  # indent-unit string, so moxml's per-child document composition
@@ -1110,13 +1112,11 @@ module Moxml
1110
1112
  # emit spaces until the engine fix (leptris/leptris#658); the
1111
1113
  # boolean display form stays document-level and is never
1112
1114
  # forwarded (the element face raises on it).
1113
- INDENT_UNIT_SUPPORTED =
1114
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.45")
1115
+ INDENT_UNIT_SUPPORTED = true
1115
1116
 
1116
1117
  # leptris-ruby#115 (fixed 1.9.50): the element unit path keeps
1117
1118
  # child comments.
1118
- ELEMENT_UNIT_COMMENTS =
1119
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.50")
1119
+ ELEMENT_UNIT_COMMENTS = true
1120
1120
 
1121
1121
  # leptris/leptris#677: the engine's DROP_WS_TEXT trims boundary
1122
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
@@ -25,7 +25,27 @@ module Moxml
25
25
  headed_ox: "HeadedOx",
26
26
  }.freeze
27
27
 
28
+ # Adapters in their deprecation cycle (#491): warned once per
29
+ # process at configuration, removed after the cycle. The value is
30
+ # the migration guidance shown in the warning.
31
+ DEPRECATED_ADAPTERS = {
32
+ headed_ox: "use :leptris (primary) or :nokogiri; headed_ox is a pure alias of :ox",
33
+ libxml: "use :nokogiri — the same libxml2 engine, governed by the consistency suite",
34
+ ox: "use :leptris (primary) or :nokogiri",
35
+ }.freeze
36
+
28
37
  class << self
38
+ # One-time deprecation notice per adapter per process (#491).
39
+ def warn_deprecated!(name)
40
+ guidance = DEPRECATED_ADAPTERS[name.to_sym]
41
+ return unless guidance
42
+ return if (@deprecation_warned ||= {})[name.to_sym]
43
+
44
+ @deprecation_warned[name.to_sym] = true
45
+ warn "moxml: the :#{name} adapter is deprecated and will be " \
46
+ "removed; #{guidance}"
47
+ end
48
+
29
49
  def load(name)
30
50
  validate_platform!(name)
31
51
  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
@@ -222,6 +223,24 @@ module Moxml
222
223
  Moxml::XSD::Schema.new(handle, self, source)
223
224
  end
224
225
 
226
+ # Compile a SET of schema documents (paths) into one schema:
227
+ # imports resolve across the whole set, so components declared
228
+ # in importing documents (substitution-group members, derived
229
+ # types) are visible to the head's content models — the
230
+ # single-document faces cannot see them (#483 item 4). The
231
+ # Schema's #version / SchemaRules index read the HEAD document
232
+ # (the first path); engine-set semantics cover the rest.
233
+ # Adapters without a set-capable engine raise
234
+ # Moxml::NotImplementedError.
235
+ def xsd_set(*paths)
236
+ paths = paths.first if paths.size == 1 && paths.first.is_a?(Array)
237
+ raise ArgumentError, "schema set needs at least one path" if paths.empty?
238
+
239
+ handle = config.adapter.xsd_compile_set(paths.map(&:to_s))
240
+ source = File.read(paths.first.to_s)
241
+ Moxml::XSD::Schema.new(handle, self, source)
242
+ end
243
+
225
244
  # Compile a RELAX NG schema from source or from a file path (the
226
245
  # engine resolves relative includes against the file's directory).
227
246
  # Adapters without an RNG engine raise Moxml::NotImplementedError.
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.169"
4
+ VERSION = "0.5.171"
5
5
  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
@@ -15,16 +15,18 @@ require "spec_helper"
15
15
  # the RSpec/* cops that forbid local-variable use inside examples.
16
16
  module SaxEntityParityFixtures
17
17
  class CaptureHandler < Moxml::SAX::ElementHandler
18
- attr_reader :first_attrs, :text
18
+ attr_reader :first_attrs, :first_namespaces, :text
19
19
 
20
20
  def initialize
21
21
  super
22
22
  @first_attrs = nil
23
+ @first_namespaces = nil
23
24
  @text = +""
24
25
  end
25
26
 
26
- def on_start_element(_name, attrs = {}, _namespaces = {})
27
+ def on_start_element(_name, attrs = {}, namespaces = {})
27
28
  @first_attrs = attrs.dup if @first_attrs.nil?
29
+ @first_namespaces = namespaces.dup if @first_namespaces.nil?
28
30
  end
29
31
 
30
32
  def on_characters(chunk)
@@ -51,6 +53,19 @@ module SaxEntityParityFixtures
51
53
  "no entities" => ['<doc x="plain"/>', "plain", "plain"],
52
54
  }.freeze
53
55
 
56
+ # Adapters whose SAX bridge reports namespace declarations in the
57
+ # handler's namespaces hash with the URI decoded exactly once.
58
+ SAX_NAMESPACE_ADAPTERS = %i[nokogiri ox rexml headed_ox].freeze
59
+
60
+ # Each row: [input XML, decoded namespace URI]. The parser decodes a
61
+ # declaration's URI exactly once; a decoded "&#38;" is data and must
62
+ # reach the handler as-is.
63
+ NAMESPACE_CASES = {
64
+ "wrapped amp + decimal ref" => ['<f:doc xmlns:f="http://f/?x=1&amp;#38;y"/>', "http://f/?x=1&#38;y"],
65
+ "wrapped amp + amp" => ['<f:doc xmlns:f="http://f/?x=&amp;amp;"/>', "http://f/?x=&amp;"],
66
+ "plain amp" => ['<f:doc xmlns:f="http://f/?x=1&amp;y=2"/>', "http://f/?x=1&y=2"],
67
+ }.freeze
68
+
54
69
  # Each row: [input XML, decoded text, expected text as it appears in
55
70
  # serialized XML].
56
71
  TEXT_CASES = {
@@ -138,6 +153,16 @@ RSpec.describe "SAX/DOM entity parity" do
138
153
  end
139
154
  end
140
155
 
156
+ if SaxEntityParityFixtures::SAX_NAMESPACE_ADAPTERS.include?(adapter)
157
+ SaxEntityParityFixtures::NAMESPACE_CASES.each do |label, (xml, expected)|
158
+ it "decodes namespace URI once via SAX: #{label}" do
159
+ handler = SaxEntityParityFixtures::CaptureHandler.new
160
+ ctx.sax_parse(xml, handler)
161
+ expect(handler.first_namespaces["f"]).to eq(expected)
162
+ end
163
+ end
164
+ end
165
+
141
166
  SaxEntityParityFixtures::TEXT_CASES.each do |label, (xml, expected, expected_serialized)|
142
167
  it "decodes text via SAX: #{label}" do
143
168
  handler = SaxEntityParityFixtures::CaptureHandler.new
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Processing model §Validation (normative): pinned here.
4
+
5
+ require "spec_helper"
6
+
7
+ RSpec.describe "Schema-set compilation" do
8
+ let(:ctx) { Moxml.new(:leptris) }
9
+
10
+ # leptris#1881 / moxml#483 item 4: a head document cannot see
11
+ # substitution-group members declared in schemas that import it;
12
+ # the set face merges every document into one compilation.
13
+ def write_set(dir)
14
+ File.write(File.join(dir, "f.xsd"), <<~XSD)
15
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
16
+ xmlns="urn:F" xmlns:f="urn:F" targetNamespace="urn:F"
17
+ elementFormDefault="qualified">
18
+ <xs:complexType name="foo">
19
+ <xs:sequence><xs:any/></xs:sequence>
20
+ </xs:complexType>
21
+ <xs:element name="foo" type="foo"/>
22
+ <xs:element name="bar">
23
+ <xs:complexType>
24
+ <xs:choice minOccurs="0" maxOccurs="unbounded">
25
+ <xs:element ref="f:foo"/>
26
+ <xs:element ref="f:bar"/>
27
+ </xs:choice>
28
+ </xs:complexType>
29
+ </xs:element>
30
+ </xs:schema>
31
+ XSD
32
+ File.write(File.join(dir, "g.xsd"), <<~XSD)
33
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
34
+ xmlns:g="urn:G" xmlns:f="urn:F" targetNamespace="urn:G"
35
+ elementFormDefault="qualified">
36
+ <xs:import namespace="urn:F" schemaLocation="f.xsd"/>
37
+ <xs:element name="foo1" substitutionGroup="f:foo">
38
+ <xs:complexType>
39
+ <xs:complexContent>
40
+ <xs:extension base="f:foo">
41
+ <xs:attribute name="x" type="xs:int"/>
42
+ </xs:extension>
43
+ </xs:complexContent>
44
+ </xs:complexType>
45
+ </xs:element>
46
+ </xs:schema>
47
+ XSD
48
+ [File.join(dir, "f.xsd"), File.join(dir, "g.xsd")]
49
+ end
50
+
51
+ # The binding carries the face from 1.9.386.0 (engine 1.9.387);
52
+ # published bindings below it loud-fail by contract, so the live
53
+ # examples skip until the ride lands.
54
+ def set_capable?
55
+ defined?(Leptris::XML::XSD::Schema) &&
56
+ Leptris::XML::XSD::Schema.respond_to?(:compile_set)
57
+ end
58
+
59
+ it "admits substitution members declared in importing documents" do
60
+ skip "requires the leptris compile_set face (binding 1.9.386.0+)" unless
61
+ set_capable?
62
+
63
+ Dir.mktmpdir do |dir|
64
+ paths = write_set(dir)
65
+ schema = ctx.xsd_set(*paths)
66
+ doc = ctx.parse(
67
+ '<f:foo xmlns:f="urn:F" xmlns:g="urn:G">' \
68
+ '<f:bar><g:foo1 x="1"><f:bar/></g:foo1></f:bar></f:foo>',
69
+ )
70
+ expect(schema.validate(doc)).to be_empty
71
+ end
72
+ end
73
+
74
+ it "accepts the paths as an array or as arguments" do
75
+ skip "requires the leptris compile_set face (binding 1.9.386.0+)" unless
76
+ set_capable?
77
+
78
+ Dir.mktmpdir do |dir|
79
+ paths = write_set(dir)
80
+ expect(ctx.xsd_set(paths)).to be_a(Moxml::XSD::Schema)
81
+ expect(ctx.xsd_set(*paths)).to be_a(Moxml::XSD::Schema)
82
+ end
83
+ end
84
+
85
+ it "raises on an empty set" do
86
+ expect { ctx.xsd_set }.to raise_error(ArgumentError)
87
+ end
88
+
89
+ it "raises the named error on adapters without a set-capable engine" do
90
+ nk = Moxml.new(:nokogiri)
91
+ Dir.mktmpdir do |dir|
92
+ paths = write_set(dir)
93
+ expect { nk.xsd_set(*paths) }
94
+ .to raise_error(Moxml::NotImplementedError, /compile_set face/)
95
+ end
96
+ end
97
+ end
@@ -101,6 +101,10 @@ module ModelCoverage
101
101
  section: "Validation",
102
102
  pin: "spec/moxml/xsd_builtin_lexical_spec.rb",
103
103
  },
104
+ "validation schema sets" => {
105
+ section: "Validation",
106
+ pin: "spec/moxml/xsd_set_spec.rb",
107
+ },
104
108
  "validation substitution dismissal" => {
105
109
  section: "Validation",
106
110
  pin: "spec/moxml/xsd_substitution_spec.rb",
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.169
4
+ version: 0.5.171
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-10-28 00:00:00.000000000 Z
11
+ date: 2026-10-30 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  Moxml is a unified XML manipulation library that provides a common API
@@ -512,6 +512,7 @@ files:
512
512
  - spec/moxml/adapter/rexml_spec.rb
513
513
  - spec/moxml/adapter/shared_examples/.gitkeep
514
514
  - spec/moxml/adapter/shared_examples/adapter_contract.rb
515
+ - spec/moxml/adapter_deprecation_spec.rb
515
516
  - spec/moxml/adapter_spec.rb
516
517
  - spec/moxml/allocation_benchmark_spec.rb
517
518
  - spec/moxml/allocation_guard_spec.rb
@@ -638,6 +639,7 @@ files:
638
639
  - spec/moxml/xsd_handle_mismatch_spec.rb
639
640
  - spec/moxml/xsd_regex_spec.rb
640
641
  - spec/moxml/xsd_release_spec.rb
642
+ - spec/moxml/xsd_set_spec.rb
641
643
  - spec/moxml/xsd_spec.rb
642
644
  - spec/moxml/xsd_substitution_spec.rb
643
645
  - spec/moxml/xsd_underdetection_spec.rb