moxml 0.5.157 → 0.5.159
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 +4 -4
- data/.github/workflows/performance-release.yml +24 -8
- data/benchmark/README.md +10 -10
- data/benchmark/scoreboard.json +38 -38
- data/docs/_pages/performance.adoc +13 -15
- data/lib/moxml/adapter/leptris.rb +34 -0
- data/lib/moxml/context.rb +6 -2
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xsd/schema_rules.rb +335 -0
- data/lib/moxml/xsd.rb +15 -2
- data/spec/moxml/xsd_underdetection_spec.rb +174 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 824fc75872bed077049fca259a7b667634c2938e4a22c3899d7c898a0a585c98
|
|
4
|
+
data.tar.gz: f639f83b2c39247bb825ce017932ada226d9fb8b11ad6567893f1cb00babaa7f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eca084853becfa0d0dfaa80e88f492f1d4541e32a97aed2480f441db3bd33990d7c0f6ab2879db8aa9fb92216bcc6782afa573384e4126724801f38094a53c99
|
|
7
|
+
data.tar.gz: 6429a255c098e7291fd45b0034d23abbe7a3acc1fccbdb9956f1ac2e3002890415388d3e060b1d20869d3783d38f986917e633597b8e05dcd3b3effffc9e7cf6
|
|
@@ -3,12 +3,23 @@ name: performance-release
|
|
|
3
3
|
# Every release re-measures the scoreboard and appends the table to
|
|
4
4
|
# the release notes (TODO.sota/04). Triggered by the release
|
|
5
5
|
# workflow's completion; idempotent — a re-run replaces the marked
|
|
6
|
-
# section instead of stacking it.
|
|
6
|
+
# section instead of stacking it. Manual dispatch can backfill any
|
|
7
|
+
# release: `ref` pins the measured tree, `tag` pins the notes
|
|
8
|
+
# target (both default to the automatic release-run behavior).
|
|
7
9
|
on:
|
|
8
10
|
workflow_run:
|
|
9
11
|
workflows: ["release"]
|
|
10
12
|
types: [completed]
|
|
11
13
|
workflow_dispatch:
|
|
14
|
+
inputs:
|
|
15
|
+
ref:
|
|
16
|
+
description: "Tree to measure (branch/tag/SHA); default: the released tree"
|
|
17
|
+
required: false
|
|
18
|
+
type: string
|
|
19
|
+
tag:
|
|
20
|
+
description: "Release whose notes receive the section; default: the triggering release"
|
|
21
|
+
required: false
|
|
22
|
+
type: string
|
|
12
23
|
|
|
13
24
|
concurrency:
|
|
14
25
|
group: ${{ github.workflow }}
|
|
@@ -30,8 +41,9 @@ jobs:
|
|
|
30
41
|
with:
|
|
31
42
|
# Pin to the exact tree the release run published, so the
|
|
32
43
|
# measured scoreboard describes the released code even when
|
|
33
|
-
# main moves between the release and this report.
|
|
34
|
-
|
|
44
|
+
# main moves between the release and this report. A manual
|
|
45
|
+
# dispatch may pin any tree (backfills).
|
|
46
|
+
ref: ${{ inputs.ref || github.event.workflow_run.head_sha || 'main' }}
|
|
35
47
|
submodules: "recursive"
|
|
36
48
|
|
|
37
49
|
- name: Set up Ruby
|
|
@@ -99,11 +111,15 @@ jobs:
|
|
|
99
111
|
# run — NOT "latest" (a rerun of an old run would mis-aim)
|
|
100
112
|
# and NOT the gemspec at the pinned tree (the checkout is
|
|
101
113
|
# the pre-bump tree; the bump lands on main during the
|
|
102
|
-
# release run itself).
|
|
103
|
-
|
|
104
|
-
TAG
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
# release run itself). A manual dispatch may pin the tag
|
|
115
|
+
# directly (backfills).
|
|
116
|
+
TAG="${{ inputs.tag }}"
|
|
117
|
+
if [ -z "$TAG" ]; then
|
|
118
|
+
TRIGGERED_AT="${{ github.event.workflow_run.created_at }}"
|
|
119
|
+
TAG=$(gh api "repos/${GITHUB_REPOSITORY}/releases" \
|
|
120
|
+
--jq "map(select(.created_at >= \"${TRIGGERED_AT}\")) | sort_by(.created_at) | last | .tag_name")
|
|
121
|
+
TAG=${TAG:-$(gh release view --json tagName --jq .tagName)}
|
|
122
|
+
fi
|
|
107
123
|
echo "Reporting against $TAG"
|
|
108
124
|
gh release view "$TAG" --json body --jq .body > /tmp/body.md
|
|
109
125
|
ruby -e '
|
data/benchmark/README.md
CHANGED
|
@@ -92,16 +92,16 @@ rewrites this block and `docs/_pages/performance.adoc`):
|
|
|
92
92
|
<!-- SCOREBOARD:BEGIN -->
|
|
93
93
|
| Row | Ratio | Kind | Status |
|
|
94
94
|
| --- | ----- | ---- | ------ |
|
|
95
|
-
| `parse` |
|
|
96
|
-
| `serialize` | 1.
|
|
97
|
-
| `xpath //field (count)` |
|
|
98
|
-
| `xpath //field (each)` | 0.
|
|
99
|
-
| `attr pairs listing` | 7.
|
|
100
|
-
| `build add_element` | 0.
|
|
101
|
-
| `walk+read (warm)` | 0.
|
|
102
|
-
| `walk+read (cold)` | 0.
|
|
103
|
-
| `children recursion (cold)` | 0.
|
|
104
|
-
| `plan records (typed)` | 1.
|
|
95
|
+
| `parse` | 3.59x | parity | met |
|
|
96
|
+
| `serialize` | 1.83x | parity | met |
|
|
97
|
+
| `xpath //field (count)` | 4.79x | parity | met |
|
|
98
|
+
| `xpath //field (each)` | 0.44x | structural | tracked (below parity) |
|
|
99
|
+
| `attr pairs listing` | 7.46x | parity | met |
|
|
100
|
+
| `build add_element` | 0.57x | structural | tracked (below parity) |
|
|
101
|
+
| `walk+read (warm)` | 0.64x | structural | tracked (below parity) |
|
|
102
|
+
| `walk+read (cold)` | 0.17x | structural | tracked (below parity), paused: leptris-ruby#527 (1.9.380 crossing regression) |
|
|
103
|
+
| `children recursion (cold)` | 0.13x | structural | tracked (below parity), paused: leptris-ruby#527 (1.9.380 crossing regression) |
|
|
104
|
+
| `plan records (typed)` | 1.14x | data | informational |
|
|
105
105
|
<!-- SCOREBOARD:END -->
|
|
106
106
|
|
|
107
107
|
The open rows are the known structural gap: every node the facade
|
data/benchmark/scoreboard.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"protocol": {
|
|
3
|
-
"measured": "2026-10-
|
|
4
|
-
"ruby": "3.3.
|
|
3
|
+
"measured": "2026-10-23T15:54:45Z",
|
|
4
|
+
"ruby": "3.3.13",
|
|
5
5
|
"adapter": "leptris",
|
|
6
|
-
"adapter_version": "1.9.
|
|
6
|
+
"adapter_version": "1.9.381.0",
|
|
7
7
|
"nokogiri": "1.19.4",
|
|
8
8
|
"reps": 9,
|
|
9
9
|
"warmup": 2,
|
|
10
10
|
"clock": "CLOCK_PROCESS_CPUTIME_ID",
|
|
11
|
-
"load": 1.
|
|
11
|
+
"load": 1.87
|
|
12
12
|
},
|
|
13
13
|
"rows": [
|
|
14
14
|
{
|
|
15
15
|
"row": "parse",
|
|
16
16
|
"kind": "parity",
|
|
17
17
|
"parity": true,
|
|
18
|
-
"baseline_us":
|
|
19
|
-
"candidate_us":
|
|
20
|
-
"ratio":
|
|
18
|
+
"baseline_us": 1027.8,
|
|
19
|
+
"candidate_us": 286.6,
|
|
20
|
+
"ratio": 3.586,
|
|
21
21
|
"baseline_allocs": 7,
|
|
22
22
|
"candidate_allocs": 18
|
|
23
23
|
},
|
|
@@ -25,39 +25,39 @@
|
|
|
25
25
|
"row": "serialize",
|
|
26
26
|
"kind": "parity",
|
|
27
27
|
"parity": true,
|
|
28
|
-
"baseline_us":
|
|
29
|
-
"candidate_us":
|
|
30
|
-
"ratio": 1.
|
|
28
|
+
"baseline_us": 517.0,
|
|
29
|
+
"candidate_us": 281.9,
|
|
30
|
+
"ratio": 1.834,
|
|
31
31
|
"baseline_allocs": 30,
|
|
32
32
|
"candidate_allocs": 14
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"row": "xpath //field (count)",
|
|
36
|
-
"kind": "
|
|
37
|
-
"parity":
|
|
38
|
-
"baseline_us":
|
|
39
|
-
"candidate_us":
|
|
40
|
-
"ratio":
|
|
36
|
+
"kind": "parity",
|
|
37
|
+
"parity": true,
|
|
38
|
+
"baseline_us": 121.9,
|
|
39
|
+
"candidate_us": 25.4,
|
|
40
|
+
"ratio": 4.791,
|
|
41
41
|
"baseline_allocs": 13,
|
|
42
|
-
"candidate_allocs":
|
|
42
|
+
"candidate_allocs": 8
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"row": "xpath //field (each)",
|
|
46
46
|
"kind": "structural",
|
|
47
47
|
"parity": false,
|
|
48
|
-
"baseline_us":
|
|
49
|
-
"candidate_us":
|
|
50
|
-
"ratio": 0.
|
|
48
|
+
"baseline_us": 361.9,
|
|
49
|
+
"candidate_us": 822.7,
|
|
50
|
+
"ratio": 0.44,
|
|
51
51
|
"baseline_allocs": 1213,
|
|
52
|
-
"candidate_allocs":
|
|
52
|
+
"candidate_allocs": 10
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"row": "attr pairs listing",
|
|
56
56
|
"kind": "parity",
|
|
57
57
|
"parity": true,
|
|
58
|
-
"baseline_us":
|
|
59
|
-
"candidate_us":
|
|
60
|
-
"ratio": 7.
|
|
58
|
+
"baseline_us": 502.0,
|
|
59
|
+
"candidate_us": 67.3,
|
|
60
|
+
"ratio": 7.458,
|
|
61
61
|
"baseline_allocs": 3300,
|
|
62
62
|
"candidate_allocs": 0
|
|
63
63
|
},
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
"row": "build add_element",
|
|
66
66
|
"kind": "structural",
|
|
67
67
|
"parity": false,
|
|
68
|
-
"baseline_us":
|
|
69
|
-
"candidate_us":
|
|
70
|
-
"ratio": 0.
|
|
68
|
+
"baseline_us": 5532.8,
|
|
69
|
+
"candidate_us": 9658.2,
|
|
70
|
+
"ratio": 0.573,
|
|
71
71
|
"baseline_allocs": 20358,
|
|
72
72
|
"candidate_allocs": 11363
|
|
73
73
|
},
|
|
@@ -75,9 +75,9 @@
|
|
|
75
75
|
"row": "walk+read (warm)",
|
|
76
76
|
"kind": "structural",
|
|
77
77
|
"parity": false,
|
|
78
|
-
"baseline_us":
|
|
79
|
-
"candidate_us":
|
|
80
|
-
"ratio": 0.
|
|
78
|
+
"baseline_us": 1805.4,
|
|
79
|
+
"candidate_us": 2797.9,
|
|
80
|
+
"ratio": 0.645,
|
|
81
81
|
"baseline_allocs": 4503,
|
|
82
82
|
"candidate_allocs": 2
|
|
83
83
|
},
|
|
@@ -85,9 +85,9 @@
|
|
|
85
85
|
"row": "walk+read (cold)",
|
|
86
86
|
"kind": "structural",
|
|
87
87
|
"parity": false,
|
|
88
|
-
"baseline_us":
|
|
89
|
-
"candidate_us":
|
|
90
|
-
"ratio": 0.
|
|
88
|
+
"baseline_us": 2938.6,
|
|
89
|
+
"candidate_us": 17354.2,
|
|
90
|
+
"ratio": 0.169,
|
|
91
91
|
"baseline_allocs": 6011,
|
|
92
92
|
"candidate_allocs": 10232
|
|
93
93
|
},
|
|
@@ -95,9 +95,9 @@
|
|
|
95
95
|
"row": "children recursion (cold)",
|
|
96
96
|
"kind": "structural",
|
|
97
97
|
"parity": false,
|
|
98
|
-
"baseline_us":
|
|
99
|
-
"candidate_us":
|
|
100
|
-
"ratio": 0.
|
|
98
|
+
"baseline_us": 3346.5,
|
|
99
|
+
"candidate_us": 25949.4,
|
|
100
|
+
"ratio": 0.129,
|
|
101
101
|
"baseline_allocs": 5711,
|
|
102
102
|
"candidate_allocs": 8732
|
|
103
103
|
},
|
|
@@ -105,9 +105,9 @@
|
|
|
105
105
|
"row": "plan records (typed)",
|
|
106
106
|
"kind": "data",
|
|
107
107
|
"parity": false,
|
|
108
|
-
"baseline_us":
|
|
109
|
-
"candidate_us":
|
|
110
|
-
"ratio": 1.
|
|
108
|
+
"baseline_us": 2901.1,
|
|
109
|
+
"candidate_us": 2554.9,
|
|
110
|
+
"ratio": 1.136,
|
|
111
111
|
"baseline_allocs": 8112,
|
|
112
112
|
"candidate_allocs": 13232
|
|
113
113
|
}
|
|
@@ -12,28 +12,26 @@ the marked block below from a live measurement; see
|
|
|
12
12
|
`benchmark/README.md` for the protocol and row definitions.
|
|
13
13
|
|
|
14
14
|
// SCOREBOARD:BEGIN
|
|
15
|
-
Generated
|
|
15
|
+
Generated by `SCORE_PUBLISH=1 bundle exec ruby benchmark/scoreboard.rb`. Protocol: CPU-time medians of an interleaved A/B run (raw nokogiri vs moxml on leptris); the ratio is baseline / candidate, so > 1.0 means the facade is faster. Warmups discarded, GC between samples.
|
|
16
16
|
|
|
17
|
-
Measurement: Ruby 3.3.
|
|
17
|
+
Measurement: Ruby 3.3.13, leptris 1.9.381.0, nokogiri 1.19.4, 2026-10-23T15:54:45Z, 1-minute load 1.87, 9 repetitions.
|
|
18
18
|
|
|
19
19
|
[cols="2,1,1,1",options="header"]
|
|
20
20
|
|===
|
|
21
21
|
| Row | Ratio | Kind | Status
|
|
22
|
-
| `parse` |
|
|
23
|
-
| `serialize` | 1.
|
|
24
|
-
| `xpath //field (count)` |
|
|
25
|
-
| `xpath //field (each)` | 0.
|
|
26
|
-
| `attr pairs listing` | 7.
|
|
27
|
-
| `build add_element` | 0.
|
|
28
|
-
| `walk+read (warm)` | 0.
|
|
29
|
-
| `walk+read (cold)` | 0.
|
|
30
|
-
| `children recursion (cold)` | 0.
|
|
31
|
-
| `plan records (typed)` | 1.
|
|
22
|
+
| `parse` | 3.59x | parity | met
|
|
23
|
+
| `serialize` | 1.83x | parity | met
|
|
24
|
+
| `xpath //field (count)` | 4.79x | parity | met
|
|
25
|
+
| `xpath //field (each)` | 0.44x | structural | tracked (below parity)
|
|
26
|
+
| `attr pairs listing` | 7.46x | parity | met
|
|
27
|
+
| `build add_element` | 0.57x | structural | tracked (below parity)
|
|
28
|
+
| `walk+read (warm)` | 0.64x | structural | tracked (below parity)
|
|
29
|
+
| `walk+read (cold)` | 0.17x | structural | tracked (below parity), paused: leptris-ruby#527 (1.9.380 crossing regression)
|
|
30
|
+
| `children recursion (cold)` | 0.13x | structural | tracked (below parity), paused: leptris-ruby#527 (1.9.380 crossing regression)
|
|
31
|
+
| `plan records (typed)` | 1.14x | data | informational
|
|
32
32
|
|===
|
|
33
33
|
|
|
34
|
-
*Parity rows* must stay at or above the 0.95 measurement floor. *Structural rows* are measured and trend-locked (10% regression tolerance) while their gaps remain open. *Data API* rows are informational.
|
|
35
|
-
|
|
36
|
-
NOTE: the 1.9.380 engine line regressed the crossing-bound rows ~2x (and the XPath evaluator itself) against 1.9.37x — tracked as leptris-ruby#527; this baseline records the 380.1 reality the lock now guards.
|
|
34
|
+
*Parity rows* must stay at or above the 0.95 measurement floor. *Structural rows* are measured and trend-locked (10% regression tolerance) while their gaps remain open: per-node wrapper minting costs (#312 endgame) and engine construction. *Data API* rows are informational.
|
|
37
35
|
// SCOREBOARD:END
|
|
38
36
|
|
|
39
37
|
=== Memory management
|
|
@@ -555,6 +555,11 @@ module Moxml
|
|
|
555
555
|
# face is a C method at 50-84ns).
|
|
556
556
|
ELEMENT = Class.new(::Leptris::XML::NativeNode) do
|
|
557
557
|
alias_method :native_name, :name
|
|
558
|
+
# The C nn_document (the BINDING document handle) is
|
|
559
|
+
# captured before Moxml::Node#document shadows it —
|
|
560
|
+
# the construction fast path needs the binding doc,
|
|
561
|
+
# not a facade wrapper (TODO.sota/04 bridge tax).
|
|
562
|
+
alias_method :native_document, :document
|
|
558
563
|
include ::Moxml::Node
|
|
559
564
|
include ::Moxml::Element
|
|
560
565
|
|
|
@@ -2623,6 +2628,35 @@ module Moxml
|
|
|
2623
2628
|
def create_element_with_attrs(parent, name, attrs)
|
|
2624
2629
|
drop_columnar_children(parent)
|
|
2625
2630
|
validate_element_name(name)
|
|
2631
|
+
# NativeNode parents need NO FFI twin (TODO.sota/04 bridge
|
|
2632
|
+
# tax): the bridge existed only to obtain c_address +
|
|
2633
|
+
# document, and the NativeNode answers both natively
|
|
2634
|
+
# (address / document). The twin mint per building parent
|
|
2635
|
+
# (FFI::Pointer + binding Node.wrap + registry insert) is
|
|
2636
|
+
# pure construction overhead.
|
|
2637
|
+
if parent.is_a?(::Leptris::XML::NativeNode)
|
|
2638
|
+
doc = parent.respond_to?(:native_document) &&
|
|
2639
|
+
parent.native_document or return nil
|
|
2640
|
+
return nil unless ::Leptris::XML::Native.respond_to?(:create_element_with_attrs)
|
|
2641
|
+
|
|
2642
|
+
flat = attrs.flatten
|
|
2643
|
+
if NATIVE_C_MINT &&
|
|
2644
|
+
::Leptris::XML::Native.respond_to?(:create_element_with_attrs_wrapped)
|
|
2645
|
+
minted = ::Leptris::XML::Native.create_element_with_attrs_wrapped(
|
|
2646
|
+
doc, parent.address, name.to_s, flat
|
|
2647
|
+
)
|
|
2648
|
+
return minted unless minted.nil?
|
|
2649
|
+
end
|
|
2650
|
+
|
|
2651
|
+
addr = ::Leptris::XML::Native.create_element_with_attrs(
|
|
2652
|
+
doc.c_address, parent.address, name.to_s, flat
|
|
2653
|
+
)
|
|
2654
|
+
return nil if addr.nil? || addr.zero?
|
|
2655
|
+
|
|
2656
|
+
return native_twin(
|
|
2657
|
+
doc, ::Leptris::XML::Node.wrap(::FFI::Pointer.new(addr), doc)
|
|
2658
|
+
)
|
|
2659
|
+
end
|
|
2626
2660
|
binding_parent = to_binding(parent)
|
|
2627
2661
|
doc = binding_parent.document or return nil
|
|
2628
2662
|
return nil unless ::Leptris::XML::Native.respond_to?(:create_element_with_attrs)
|
data/lib/moxml/context.rb
CHANGED
|
@@ -207,15 +207,19 @@ module Moxml
|
|
|
207
207
|
# @raise [Moxml::NotImplementedError] on adapters without a validator
|
|
208
208
|
def xsd(schema_source)
|
|
209
209
|
handle = config.adapter.xsd_compile(schema_source.to_s)
|
|
210
|
-
Moxml::XSD::Schema.new(handle, self)
|
|
210
|
+
Moxml::XSD::Schema.new(handle, self, schema_source.to_s)
|
|
211
211
|
end
|
|
212
212
|
|
|
213
213
|
# Compile an XSD schema from a file path — <xs:include href>
|
|
214
214
|
# resolves relative to the file's directory (the engine
|
|
215
215
|
# resolves it; reading the file into #xsd would lose that).
|
|
216
|
+
# The #452 rule index reads this document only; included
|
|
217
|
+
# schema parts stay engine-validated (the index can only
|
|
218
|
+
# under-report).
|
|
216
219
|
def xsd_file(path)
|
|
217
220
|
handle = config.adapter.xsd_compile_file(path.to_s)
|
|
218
|
-
|
|
221
|
+
source = File.read(path.to_s)
|
|
222
|
+
Moxml::XSD::Schema.new(handle, self, source)
|
|
219
223
|
end
|
|
220
224
|
|
|
221
225
|
# Compile a RELAX NG schema from source or from a file path (the
|
data/lib/moxml/version.rb
CHANGED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module XSD
|
|
5
|
+
# Ruby-side post-validation rules closing the tier-1
|
|
6
|
+
# under-detections tracked in #452: an engine may accept, inside
|
|
7
|
+
# its recognized scope, instances that violate
|
|
8
|
+
#
|
|
9
|
+
# * a strict `xs:any` wildcard with an undeclared element,
|
|
10
|
+
# * a strict `xs:anyAttribute` with an undeclared attribute,
|
|
11
|
+
# * `xsi:nil` on an element whose declaration is not nillable.
|
|
12
|
+
#
|
|
13
|
+
# The rules run from an index of the schema SOURCE (this gem's
|
|
14
|
+
# own XML machinery — no engine involvement) and merge with the
|
|
15
|
+
# engine's findings; exact duplicates are dropped. Findings are
|
|
16
|
+
# one-directional: the index covers one schema document at
|
|
17
|
+
# tier-1's one-level content depth, so a missing piece can only
|
|
18
|
+
# under-report, never false-report — the same lax-scope posture
|
|
19
|
+
# tier-1 already has.
|
|
20
|
+
module SchemaRules
|
|
21
|
+
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
|
22
|
+
|
|
23
|
+
# A wildcard's namespace attribute, per XML Schema
|
|
24
|
+
# Structures §3.10.2 (##any default; processContents default
|
|
25
|
+
# strict). :tns / :local are resolved against the indexed
|
|
26
|
+
# schema at check time.
|
|
27
|
+
class Wildcard
|
|
28
|
+
def initialize(value, process)
|
|
29
|
+
@process = (process || "strict").to_sym
|
|
30
|
+
case value
|
|
31
|
+
when nil, "##any", ""
|
|
32
|
+
@mode = :any
|
|
33
|
+
when "##other"
|
|
34
|
+
@mode = :other
|
|
35
|
+
else
|
|
36
|
+
@mode = :list
|
|
37
|
+
@namespaces = value.split(/\s+/).reject(&:empty?).freeze
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def strict?
|
|
42
|
+
@process == :strict
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def covers?(namespace)
|
|
46
|
+
case @mode
|
|
47
|
+
when :any then true
|
|
48
|
+
when :other
|
|
49
|
+
namespace != :local && namespace != :tns
|
|
50
|
+
else
|
|
51
|
+
@namespaces.any? { |entry| entry_covers?(entry, namespace) }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def entry_covers?(entry, namespace)
|
|
58
|
+
namespace == case entry
|
|
59
|
+
when "##targetNamespace" then :tns
|
|
60
|
+
when "##local" then :local
|
|
61
|
+
else entry
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
ContentModel = Struct.new(:declared_children, :any,
|
|
67
|
+
:declared_attributes, :any_attribute)
|
|
68
|
+
SchemaIndex = Struct.new(:target_namespace, :element_default,
|
|
69
|
+
:attribute_default, :elements,
|
|
70
|
+
:attributes, :contents)
|
|
71
|
+
|
|
72
|
+
class << self
|
|
73
|
+
# Index one schema document. Returns nil when the source
|
|
74
|
+
# cannot be indexed — the engine's answer then stands alone
|
|
75
|
+
# (today's behavior).
|
|
76
|
+
def index(schema_source, context)
|
|
77
|
+
root = context.parse(schema_source).root
|
|
78
|
+
return nil unless root&.name == "schema"
|
|
79
|
+
|
|
80
|
+
index = SchemaIndex.new(
|
|
81
|
+
root["targetNamespace"].to_s,
|
|
82
|
+
root["elementFormDefault"] == "qualified",
|
|
83
|
+
root["attributeFormDefault"] == "qualified",
|
|
84
|
+
{}, {}, {}
|
|
85
|
+
)
|
|
86
|
+
child_elements(root).each do |decl|
|
|
87
|
+
case decl.name
|
|
88
|
+
when "element" then index_global_element(decl, index)
|
|
89
|
+
when "attribute" then index_global_attribute(decl, index)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
index
|
|
93
|
+
rescue StandardError
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Post-validation findings for one instance document.
|
|
98
|
+
def findings(doc, index)
|
|
99
|
+
return [] unless index
|
|
100
|
+
|
|
101
|
+
root = doc.root
|
|
102
|
+
return [] unless root
|
|
103
|
+
|
|
104
|
+
root_qname = qname(root)
|
|
105
|
+
declaration = index.elements[root_qname]
|
|
106
|
+
model = index.contents[root_qname]
|
|
107
|
+
findings = []
|
|
108
|
+
findings << nil_finding(root, declaration) if declaration
|
|
109
|
+
findings.concat(attribute_findings(root, model, index))
|
|
110
|
+
if model
|
|
111
|
+
child_elements(root).each do |child|
|
|
112
|
+
findings.concat(child_findings(child, model, index))
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
findings.compact
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def index_global_element(decl, index)
|
|
121
|
+
name = decl["name"] or return
|
|
122
|
+
qname = "{#{index.target_namespace}}#{name}"
|
|
123
|
+
index.elements[qname] = { nillable: decl["nillable"] == "true" }
|
|
124
|
+
type_attr = decl.attribute("type")
|
|
125
|
+
model = if type_attr.nil? || type_attr.value.empty?
|
|
126
|
+
inline_model(decl, index)
|
|
127
|
+
else
|
|
128
|
+
named_model(decl, index)
|
|
129
|
+
end
|
|
130
|
+
index.contents[qname] = model if model
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def index_global_attribute(decl, index)
|
|
134
|
+
name = decl["name"] or return
|
|
135
|
+
index.attributes["{#{index.target_namespace}}#{name}"] = true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def inline_model(decl, index)
|
|
139
|
+
type_node = child_elements(decl).find do |child|
|
|
140
|
+
child.name == "complexType"
|
|
141
|
+
end
|
|
142
|
+
content_model(type_node, index) if type_node
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# A named complexType reference, indexed only when the
|
|
146
|
+
# definition sits directly under this schema element.
|
|
147
|
+
def named_model(decl, index)
|
|
148
|
+
type_name = decl.attribute("type")&.value.to_s.split(":").last
|
|
149
|
+
return nil unless type_name
|
|
150
|
+
|
|
151
|
+
type_node = child_elements(decl.parent).find do |child|
|
|
152
|
+
child.name == "complexType" && child["name"] == type_name
|
|
153
|
+
end
|
|
154
|
+
content_model(type_node, index) if type_node
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def content_model(type_node, index)
|
|
158
|
+
model = ContentModel.new([], nil, [], nil)
|
|
159
|
+
collect_model(type_node, index, model)
|
|
160
|
+
model
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Particle presence across the type's subtree (sequence /
|
|
164
|
+
# choice / all / complexContent wrappers flattened): we only
|
|
165
|
+
# ADD findings for strict-wildcard matches, so flattening
|
|
166
|
+
# can only under-report, never over-report. Cardinality and
|
|
167
|
+
# order stay the engine's job.
|
|
168
|
+
def collect_model(node, index, model)
|
|
169
|
+
child_elements(node).each do |child|
|
|
170
|
+
case child.name
|
|
171
|
+
when "element"
|
|
172
|
+
add_particle(child, index, model,
|
|
173
|
+
model.declared_children)
|
|
174
|
+
when "attribute"
|
|
175
|
+
add_particle(child, index, model,
|
|
176
|
+
model.declared_attributes)
|
|
177
|
+
when "any"
|
|
178
|
+
model.any ||= Wildcard.new(child["namespace"],
|
|
179
|
+
child["processContents"])
|
|
180
|
+
when "anyAttribute"
|
|
181
|
+
model.any_attribute ||=
|
|
182
|
+
Wildcard.new(child["namespace"],
|
|
183
|
+
child["processContents"])
|
|
184
|
+
else
|
|
185
|
+
collect_model(child, index, model)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def add_particle(particle, index, _model, sink)
|
|
191
|
+
ref = particle["ref"]
|
|
192
|
+
name = ref&.split(":")&.last || particle["name"]
|
|
193
|
+
return unless name
|
|
194
|
+
|
|
195
|
+
sink << if ref
|
|
196
|
+
"{#{index.target_namespace}}#{name}"
|
|
197
|
+
else
|
|
198
|
+
"{#{form_namespace(particle, index)}}#{name}"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Particles declare with form="qualified"/"unqualified",
|
|
203
|
+
# else the schema's elementFormDefault (elements) /
|
|
204
|
+
# attributeFormDefault (attributes) — both default
|
|
205
|
+
# unqualified.
|
|
206
|
+
def form_namespace(particle, index)
|
|
207
|
+
default = if particle.name == "element"
|
|
208
|
+
index.element_default
|
|
209
|
+
else
|
|
210
|
+
index.attribute_default
|
|
211
|
+
end
|
|
212
|
+
case particle["form"]
|
|
213
|
+
when "qualified" then index.target_namespace
|
|
214
|
+
when "unqualified" then ""
|
|
215
|
+
else default ? index.target_namespace : ""
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def child_findings(child, model, index)
|
|
220
|
+
qname = qname(child)
|
|
221
|
+
findings = []
|
|
222
|
+
if model.declared_children.include?(qname)
|
|
223
|
+
declaration = index.elements[qname]
|
|
224
|
+
findings << nil_finding(child, declaration) if declaration
|
|
225
|
+
elsif undeclared_strict_wildcard_match?(child, model, index,
|
|
226
|
+
qname)
|
|
227
|
+
findings << Moxml::XSD::ValidationError.new(
|
|
228
|
+
message: "element <#{local(qname)}> matches a strict " \
|
|
229
|
+
"wildcard but is not declared",
|
|
230
|
+
path: path_of(child),
|
|
231
|
+
)
|
|
232
|
+
end
|
|
233
|
+
findings.concat(
|
|
234
|
+
attribute_findings(child, index.contents[qname], index),
|
|
235
|
+
)
|
|
236
|
+
findings
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def undeclared_strict_wildcard_match?(_child, model, index, qname)
|
|
240
|
+
wildcard = model.any
|
|
241
|
+
return false unless wildcard&.strict?
|
|
242
|
+
return false if index.elements.key?(qname)
|
|
243
|
+
|
|
244
|
+
namespace = resolve_namespace(qname, index)
|
|
245
|
+
wildcard.covers?(namespace) && !index.elements.key?(qname)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def attribute_findings(element, model, index)
|
|
249
|
+
return [] unless model
|
|
250
|
+
|
|
251
|
+
any_attribute = model.any_attribute
|
|
252
|
+
element.attributes.filter_map do |attribute|
|
|
253
|
+
next if attribute.namespace&.uri == XSI_NS
|
|
254
|
+
|
|
255
|
+
qname = attribute_qname(attribute)
|
|
256
|
+
declared = model.declared_attributes.include?(qname) ||
|
|
257
|
+
index.attributes.key?(qname)
|
|
258
|
+
next if declared
|
|
259
|
+
next unless any_attribute&.strict?
|
|
260
|
+
next if index.attributes.key?(qname)
|
|
261
|
+
next unless any_attribute.covers?(
|
|
262
|
+
resolve_namespace(qname, index),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
Moxml::XSD::ValidationError.new(
|
|
266
|
+
message: "attribute #{local(qname)} matches a strict " \
|
|
267
|
+
"wildcard but is not declared",
|
|
268
|
+
path: path_of(element),
|
|
269
|
+
)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def nil_finding(element, declaration)
|
|
274
|
+
return if declaration[:nillable]
|
|
275
|
+
|
|
276
|
+
value = element.attributes.find do |attribute|
|
|
277
|
+
attribute.namespace&.uri == XSI_NS &&
|
|
278
|
+
attribute.name == "nil"
|
|
279
|
+
end&.value
|
|
280
|
+
return unless %w[true 1].include?(value)
|
|
281
|
+
|
|
282
|
+
Moxml::XSD::ValidationError.new(
|
|
283
|
+
message: "element <#{element.name}> is not nillable but " \
|
|
284
|
+
"carries xsi:nil=\"#{value}\"",
|
|
285
|
+
path: path_of(element),
|
|
286
|
+
)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# {uri}local → the wildcard vocabulary: :tns when the
|
|
290
|
+
# namespace is the schema's target, :local when empty, the
|
|
291
|
+
# URI otherwise.
|
|
292
|
+
def resolve_namespace(qname, index)
|
|
293
|
+
namespace = namespace_of(qname)
|
|
294
|
+
if namespace.empty?
|
|
295
|
+
:local
|
|
296
|
+
elsif namespace == index.target_namespace
|
|
297
|
+
:tns
|
|
298
|
+
else
|
|
299
|
+
namespace
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def qname(node)
|
|
304
|
+
"{#{node.namespace&.uri}}#{node.name}"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def attribute_qname(attribute)
|
|
308
|
+
"{#{attribute.namespace&.uri}}#{attribute.name}"
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def namespace_of(qname)
|
|
312
|
+
qname[/^\{(.*)\}/, 1].to_s
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def local(qname)
|
|
316
|
+
qname[/\}(.*)$/, 1].to_s
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def child_elements(node)
|
|
320
|
+
node.children.select(&:element?)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def path_of(node)
|
|
324
|
+
segments = [node.name]
|
|
325
|
+
current = node
|
|
326
|
+
while (parent = current.parent)&.element?
|
|
327
|
+
segments.unshift(parent.name)
|
|
328
|
+
current = parent
|
|
329
|
+
end
|
|
330
|
+
segments.join("/")
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
data/lib/moxml/xsd.rb
CHANGED
|
@@ -28,6 +28,8 @@ module Moxml
|
|
|
28
28
|
module XSD
|
|
29
29
|
SUPPORTED_ADAPTERS = %i[leptris nokogiri].freeze
|
|
30
30
|
|
|
31
|
+
autoload :SchemaRules, "moxml/xsd/schema_rules"
|
|
32
|
+
|
|
31
33
|
# One validation finding, harmonized across adapters (#362.2):
|
|
32
34
|
# +message+ always present; +line+ / +column+ when the engine
|
|
33
35
|
# reports them; +path+ (the element path) when the engine
|
|
@@ -89,9 +91,11 @@ module Moxml
|
|
|
89
91
|
"schema released (deterministic drop); compile a new one"
|
|
90
92
|
end
|
|
91
93
|
|
|
92
|
-
def initialize(handle, context)
|
|
94
|
+
def initialize(handle, context, schema_source = nil)
|
|
93
95
|
@handle = handle
|
|
94
96
|
@context = context
|
|
97
|
+
@rules_index =
|
|
98
|
+
schema_source && SchemaRules.index(schema_source, context)
|
|
95
99
|
assert_adapter_matches_handle!
|
|
96
100
|
end
|
|
97
101
|
|
|
@@ -143,7 +147,16 @@ module Moxml
|
|
|
143
147
|
parsed
|
|
144
148
|
end
|
|
145
149
|
end
|
|
146
|
-
@context.config.adapter.xsd_validate(
|
|
150
|
+
engine_errors = @context.config.adapter.xsd_validate(
|
|
151
|
+
@handle, doc.native
|
|
152
|
+
)
|
|
153
|
+
return engine_errors unless @rules_index
|
|
154
|
+
|
|
155
|
+
# The #452 post-validation rules only ever add findings for
|
|
156
|
+
# documents the engine accepted; exact duplicates are
|
|
157
|
+
# dropped for engines that already detect them.
|
|
158
|
+
(engine_errors +
|
|
159
|
+
SchemaRules.findings(doc, @rules_index)).uniq
|
|
147
160
|
end
|
|
148
161
|
|
|
149
162
|
def valid?(node_or_xml)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# #452: tier-1 engines can accept, inside their recognized scope,
|
|
6
|
+
# instances that violate strict wildcards or xsi:nil. The
|
|
7
|
+
# Ruby-side SchemaRules post-validation closes those gaps from the
|
|
8
|
+
# schema source; findings merge with the engine's (duplicates
|
|
9
|
+
# dropped), so every adapter answers invalid for the four
|
|
10
|
+
# corpus shapes.
|
|
11
|
+
RSpec.describe "Moxml::XSD tier-1 under-detections (#452)" do
|
|
12
|
+
%i[nokogiri leptris].each do |adapter_name|
|
|
13
|
+
describe "on #{adapter_name}" do
|
|
14
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
15
|
+
|
|
16
|
+
before do
|
|
17
|
+
skip "#{adapter_name} requires an XSD-capable engine" unless
|
|
18
|
+
Moxml::XSD.supported?(ctx)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe "strict xs:any with an undeclared element" do
|
|
22
|
+
let(:schema) do
|
|
23
|
+
ctx.xsd(<<~XSD)
|
|
24
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
25
|
+
targetNamespace="http://example.com/w"
|
|
26
|
+
elementFormDefault="qualified">
|
|
27
|
+
<xs:element name="container">
|
|
28
|
+
<xs:complexType>
|
|
29
|
+
<xs:sequence>
|
|
30
|
+
<xs:any namespace="##targetNamespace"
|
|
31
|
+
processContents="strict" minOccurs="0"/>
|
|
32
|
+
</xs:sequence>
|
|
33
|
+
</xs:complexType>
|
|
34
|
+
</xs:element>
|
|
35
|
+
<xs:element name="known" type="xs:string"/>
|
|
36
|
+
</xs:schema>
|
|
37
|
+
XSD
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "rejects the undeclared wildcard match" do
|
|
41
|
+
doc = ctx.parse(
|
|
42
|
+
'<container xmlns="http://example.com/w"><unknown>x</unknown></container>',
|
|
43
|
+
)
|
|
44
|
+
expect(schema.valid?(doc)).to be(false)
|
|
45
|
+
expect(schema.validate(doc).map(&:message)).to include(
|
|
46
|
+
/matches a strict wildcard but is not declared/,
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "accepts the declared wildcard match" do
|
|
51
|
+
doc = ctx.parse(
|
|
52
|
+
'<container xmlns="http://example.com/w"><known>x</known></container>',
|
|
53
|
+
)
|
|
54
|
+
expect(schema.valid?(doc)).to be(true)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "leaves lax wildcards to the engine" do
|
|
58
|
+
lax = ctx.xsd(<<~XSD)
|
|
59
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
60
|
+
targetNamespace="http://example.com/w"
|
|
61
|
+
elementFormDefault="qualified">
|
|
62
|
+
<xs:element name="container">
|
|
63
|
+
<xs:complexType>
|
|
64
|
+
<xs:sequence>
|
|
65
|
+
<xs:any namespace="##targetNamespace"
|
|
66
|
+
processContents="lax" minOccurs="0"/>
|
|
67
|
+
</xs:sequence>
|
|
68
|
+
</xs:complexType>
|
|
69
|
+
</xs:element>
|
|
70
|
+
</xs:schema>
|
|
71
|
+
XSD
|
|
72
|
+
doc = ctx.parse(
|
|
73
|
+
'<container xmlns="http://example.com/w"><unknown>x</unknown></container>',
|
|
74
|
+
)
|
|
75
|
+
expect(lax.valid?(doc)).to be(true)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe "strict xs:anyAttribute with an undeclared attribute" do
|
|
80
|
+
let(:schema) do
|
|
81
|
+
ctx.xsd(<<~XSD)
|
|
82
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
83
|
+
targetNamespace="http://example.com/aa"
|
|
84
|
+
elementFormDefault="qualified">
|
|
85
|
+
<xs:element name="settings">
|
|
86
|
+
<xs:complexType>
|
|
87
|
+
<xs:complexContent>
|
|
88
|
+
<xs:restriction base="xs:anyType">
|
|
89
|
+
<xs:attribute name="lang"/>
|
|
90
|
+
<xs:anyAttribute namespace="##targetNamespace ##local"
|
|
91
|
+
processContents="strict"/>
|
|
92
|
+
</xs:restriction>
|
|
93
|
+
</xs:complexContent>
|
|
94
|
+
</xs:complexType>
|
|
95
|
+
</xs:element>
|
|
96
|
+
</xs:schema>
|
|
97
|
+
XSD
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "rejects the undeclared wildcard match" do
|
|
101
|
+
doc = ctx.parse(
|
|
102
|
+
'<settings xmlns="http://example.com/aa" lang="en" bogus="1">v</settings>',
|
|
103
|
+
)
|
|
104
|
+
expect(schema.valid?(doc)).to be(false)
|
|
105
|
+
expect(schema.validate(doc).map(&:message)).to include(
|
|
106
|
+
/attribute bogus matches a strict wildcard but is not declared/,
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "accepts the declared attribute" do
|
|
111
|
+
# self-closing: the empty-content-type reading of the
|
|
112
|
+
# restriction is the engine's answer, not this rule's
|
|
113
|
+
doc = ctx.parse(
|
|
114
|
+
'<settings xmlns="http://example.com/aa" lang="en"/>',
|
|
115
|
+
)
|
|
116
|
+
expect(schema.valid?(doc)).to be(true)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe "xsi:nil on a non-nillable element" do
|
|
121
|
+
let(:schema) do
|
|
122
|
+
ctx.xsd(<<~XSD)
|
|
123
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
124
|
+
elementFormDefault="qualified">
|
|
125
|
+
<xs:element name="memo" type="xs:string" nillable="true"/>
|
|
126
|
+
<xs:element name="strict" type="xs:string"/>
|
|
127
|
+
</xs:schema>
|
|
128
|
+
XSD
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "rejects xsi:nil on the non-nillable declaration" do
|
|
132
|
+
doc = ctx.parse(
|
|
133
|
+
'<strict xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>',
|
|
134
|
+
)
|
|
135
|
+
expect(schema.valid?(doc)).to be(false)
|
|
136
|
+
expect(schema.validate(doc).map(&:message)).to include(
|
|
137
|
+
/is not nillable but carries xsi:nil/,
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "accepts xsi:nil on the nillable declaration" do
|
|
142
|
+
doc = ctx.parse(
|
|
143
|
+
'<memo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>',
|
|
144
|
+
)
|
|
145
|
+
expect(schema.valid?(doc)).to be(true)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "accepts the element without xsi:nil" do
|
|
149
|
+
doc = ctx.parse("<strict>text</strict>")
|
|
150
|
+
expect(schema.valid?(doc)).to be(true)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
describe "wrong-namespace root" do
|
|
155
|
+
let(:schema) do
|
|
156
|
+
ctx.xsd(<<~XSD)
|
|
157
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
158
|
+
targetNamespace="http://example.com/q"
|
|
159
|
+
elementFormDefault="qualified">
|
|
160
|
+
<xs:element name="note" type="xs:string"/>
|
|
161
|
+
</xs:schema>
|
|
162
|
+
XSD
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "rejects the root in the wrong namespace" do
|
|
166
|
+
doc = ctx.parse(
|
|
167
|
+
'<note xmlns="http://example.com/other">hi</note>',
|
|
168
|
+
)
|
|
169
|
+
expect(schema.valid?(doc)).to be(false)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
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.
|
|
4
|
+
version: 0.5.159
|
|
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-
|
|
11
|
+
date: 2026-10-24 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
|
|
@@ -318,6 +318,7 @@ files:
|
|
|
318
318
|
- lib/moxml/xpath/ruby/generator.rb
|
|
319
319
|
- lib/moxml/xpath/ruby/node.rb
|
|
320
320
|
- lib/moxml/xsd.rb
|
|
321
|
+
- lib/moxml/xsd/schema_rules.rb
|
|
321
322
|
- lib/moxml/xslt.rb
|
|
322
323
|
- lib/moxml/xslt/stylesheet.rb
|
|
323
324
|
- moxml.gemspec
|
|
@@ -631,6 +632,7 @@ files:
|
|
|
631
632
|
- spec/moxml/xsd_handle_mismatch_spec.rb
|
|
632
633
|
- spec/moxml/xsd_release_spec.rb
|
|
633
634
|
- spec/moxml/xsd_spec.rb
|
|
635
|
+
- spec/moxml/xsd_underdetection_spec.rb
|
|
634
636
|
- spec/moxml/xslt/stylesheet_spec.rb
|
|
635
637
|
- spec/performance/README.md
|
|
636
638
|
- spec/performance/benchmark_spec.rb
|