sdcvalidator 4.3.0__tar.gz → 4.4.0__tar.gz
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.
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/PKG-INFO +23 -1
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/README.md +22 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/pyproject.toml +1 -1
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/__init__.py +1 -1
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/converters.py +4 -2
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/validator.py +6 -1
- sdcvalidator-4.4.0/src/sdcvalidator/xsd11_restriction.py +167 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/PKG-INFO +23 -1
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/SOURCES.txt +3 -1
- sdcvalidator-4.4.0/tests/test_xsd11_restriction.py +204 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/LICENSE +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/setup.cfg +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/cli.py +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/constants.py +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/error_classifier.py +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/error_mapper.py +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/exceptions.py +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/instance_modifier.py +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/mcp_server.py +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/schema_checker.py +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/dependency_links.txt +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/entry_points.txt +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/requires.txt +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/top_level.txt +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/tests/test_converters.py +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/tests/test_error_classifier.py +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/tests/test_error_mapper.py +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/tests/test_instance_modifier.py +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/tests/test_mcp_server.py +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/tests/test_recovery.py +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/tests/test_schema_checker.py +0 -0
- {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/tests/test_validator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sdcvalidator
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4.0
|
|
4
4
|
Summary: SDC4 structural validator with ExceptionalValue recovery — thin wrapper over xmlschema
|
|
5
5
|
Author: Semantic Data Charter Foundation
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -144,6 +144,24 @@ sdcvalidator-xml2json instance.xml -o output.json
|
|
|
144
144
|
sdcvalidator-json2xml data.json schema.xsd -o output.xml
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
## MCP Server
|
|
148
|
+
|
|
149
|
+
`sdcvalidator` ships a stdio MCP (Model Context Protocol) server so any MCP-capable agent can validate SDC4 data without importing the Python library. It implements JSON-RPC 2.0 directly over stdio — no external MCP SDK dependency.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
sdcvalidator-mcp serve --mcp
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Tools
|
|
156
|
+
|
|
157
|
+
| Tool | Purpose |
|
|
158
|
+
|------|---------|
|
|
159
|
+
| `validate_instance` | Validate an XML instance against its SDC4 XSD schema. Returns pass/fail with error count and classified errors. |
|
|
160
|
+
| `validate_and_report` | Validate an instance and return a detailed report with two-tier (structural vs semantic) error classification. |
|
|
161
|
+
| `check_schema_compliance` | Check whether an XSD schema follows SDC4 principles (restriction only, no `xsd:extension`). Does not validate instances. |
|
|
162
|
+
|
|
163
|
+
`validate_instance` and `validate_and_report` take `schema_path` and `instance_path`, plus an optional `check_compliance` boolean (default `true`). `check_schema_compliance` takes `schema_path` only.
|
|
164
|
+
|
|
147
165
|
## Two-Tier Error Classification
|
|
148
166
|
|
|
149
167
|
| Tier | Type | Examples | Action |
|
|
@@ -157,6 +175,10 @@ SDC4 data models must use `xsd:restriction` only — never `xsd:extension`. This
|
|
|
157
175
|
|
|
158
176
|
The validator checks this by default and rejects schemas that violate this principle.
|
|
159
177
|
|
|
178
|
+
## Status
|
|
179
|
+
|
|
180
|
+
Production-ready. Available on [PyPI](https://pypi.org/project/sdcvalidator/) under Apache 2.0.
|
|
181
|
+
|
|
160
182
|
## License
|
|
161
183
|
|
|
162
184
|
Apache License 2.0 — see [LICENSE](LICENSE).
|
|
@@ -122,6 +122,24 @@ sdcvalidator-xml2json instance.xml -o output.json
|
|
|
122
122
|
sdcvalidator-json2xml data.json schema.xsd -o output.xml
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
## MCP Server
|
|
126
|
+
|
|
127
|
+
`sdcvalidator` ships a stdio MCP (Model Context Protocol) server so any MCP-capable agent can validate SDC4 data without importing the Python library. It implements JSON-RPC 2.0 directly over stdio — no external MCP SDK dependency.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
sdcvalidator-mcp serve --mcp
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Tools
|
|
134
|
+
|
|
135
|
+
| Tool | Purpose |
|
|
136
|
+
|------|---------|
|
|
137
|
+
| `validate_instance` | Validate an XML instance against its SDC4 XSD schema. Returns pass/fail with error count and classified errors. |
|
|
138
|
+
| `validate_and_report` | Validate an instance and return a detailed report with two-tier (structural vs semantic) error classification. |
|
|
139
|
+
| `check_schema_compliance` | Check whether an XSD schema follows SDC4 principles (restriction only, no `xsd:extension`). Does not validate instances. |
|
|
140
|
+
|
|
141
|
+
`validate_instance` and `validate_and_report` take `schema_path` and `instance_path`, plus an optional `check_compliance` boolean (default `true`). `check_schema_compliance` takes `schema_path` only.
|
|
142
|
+
|
|
125
143
|
## Two-Tier Error Classification
|
|
126
144
|
|
|
127
145
|
| Tier | Type | Examples | Action |
|
|
@@ -135,6 +153,10 @@ SDC4 data models must use `xsd:restriction` only — never `xsd:extension`. This
|
|
|
135
153
|
|
|
136
154
|
The validator checks this by default and rejects schemas that violate this principle.
|
|
137
155
|
|
|
156
|
+
## Status
|
|
157
|
+
|
|
158
|
+
Production-ready. Available on [PyPI](https://pypi.org/project/sdcvalidator/) under Apache 2.0.
|
|
159
|
+
|
|
138
160
|
## License
|
|
139
161
|
|
|
140
162
|
Apache License 2.0 — see [LICENSE](LICENSE).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sdcvalidator"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.4.0"
|
|
8
8
|
description = "SDC4 structural validator with ExceptionalValue recovery — thin wrapper over xmlschema"
|
|
9
9
|
license = "Apache-2.0"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -14,7 +14,7 @@ Thin wrapper over xmlschema with two-tier error classification and the SDC4
|
|
|
14
14
|
"quarantine-and-tag" recovery pattern (ISO 21090 ExceptionalValues).
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
__version__ = "4.
|
|
17
|
+
__version__ = "4.4.0"
|
|
18
18
|
|
|
19
19
|
# etree_tostring is re-exported from xmlschema so callers can serialize the
|
|
20
20
|
# ElementTree returned by validate_with_recovery() without importing xmlschema
|
|
@@ -18,6 +18,8 @@ from xml.etree import ElementTree as ET
|
|
|
18
18
|
|
|
19
19
|
from xmlschema import XMLSchema11
|
|
20
20
|
|
|
21
|
+
from .xsd11_restriction import build_xsd11_schema
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
def xml_to_json(
|
|
23
25
|
xml_path: Union[str, Path],
|
|
@@ -43,7 +45,7 @@ def xml_to_json(
|
|
|
43
45
|
if schema_path is None:
|
|
44
46
|
raise ValueError("schema_path is required for xml_to_json conversion")
|
|
45
47
|
|
|
46
|
-
schema =
|
|
48
|
+
schema = build_xsd11_schema(str(schema_path))
|
|
47
49
|
return schema.to_dict(str(xml_path))
|
|
48
50
|
|
|
49
51
|
|
|
@@ -73,7 +75,7 @@ def json_to_xml(
|
|
|
73
75
|
elif isinstance(json_data, str):
|
|
74
76
|
json_data = json.loads(json_data)
|
|
75
77
|
|
|
76
|
-
schema =
|
|
78
|
+
schema = build_xsd11_schema(str(schema_path))
|
|
77
79
|
element = schema.encode(json_data)
|
|
78
80
|
|
|
79
81
|
output_path = Path(output_path)
|
|
@@ -24,6 +24,8 @@ from xml.etree import ElementTree as ET
|
|
|
24
24
|
from dataclasses import dataclass, field
|
|
25
25
|
|
|
26
26
|
from xmlschema import XMLSchema11, XMLResource
|
|
27
|
+
|
|
28
|
+
from .xsd11_restriction import build_xsd11_schema
|
|
27
29
|
from xmlschema.validators.exceptions import XMLSchemaValidationError
|
|
28
30
|
|
|
29
31
|
from .error_classifier import ErrorClassifier
|
|
@@ -100,7 +102,10 @@ class SDC4Validator:
|
|
|
100
102
|
raise SDC4SchemaValidationError(error_msg)
|
|
101
103
|
|
|
102
104
|
if isinstance(schema, (str, Path)):
|
|
103
|
-
|
|
105
|
+
# build_xsd11_schema preserves strict validation for every case except
|
|
106
|
+
# the valid XSD 1.1 substitution-group restriction that xmlschema
|
|
107
|
+
# false-rejects at build time (see xsd11_restriction).
|
|
108
|
+
self.schema = build_xsd11_schema(str(schema), validation=validation)
|
|
104
109
|
else:
|
|
105
110
|
self.schema = schema
|
|
106
111
|
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 Semantic Data Charter Foundation
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
"""
|
|
11
|
+
XSD 1.1 substitution-group restriction support.
|
|
12
|
+
|
|
13
|
+
SDC4 data models restrict a content model that references an abstract
|
|
14
|
+
substitution-group head (``sdc4:Item``) down to the specific member elements a
|
|
15
|
+
model defines. For example the reference-model cluster content ``label?, Item*``
|
|
16
|
+
is restricted to ``label?, ms-A?, ms-B?, ...`` where each ``ms-*`` is a member
|
|
17
|
+
of ``Item``'s substitution group.
|
|
18
|
+
|
|
19
|
+
This is a valid restriction under **XSD 1.1 Part 1, Section 3.4.6.4 "Content type
|
|
20
|
+
restricts (Complex Content)"**, which defines restriction in terms of the
|
|
21
|
+
instances a content model accepts. XSD 1.1 removed the XSD 1.0 particle
|
|
22
|
+
name-matching rule (``NameAndTypeOK``). A conformant XSD 1.1 processor accepts
|
|
23
|
+
the construct, and the Apache Xerces-J XML Schema 1.1 reference build does accept
|
|
24
|
+
it. See ``SDCRM/docs/VALIDATORS.md`` and
|
|
25
|
+
https://www.w3.org/TR/xmlschema11-1/#derivation-ok-restriction .
|
|
26
|
+
|
|
27
|
+
``xmlschema`` (through at least 4.x) still applies the removed XSD 1.0 rule and
|
|
28
|
+
rejects this construct at schema-build time with "the derived group is an illegal
|
|
29
|
+
restriction". This module recognises that specific false positive, structurally,
|
|
30
|
+
so the schema can be built while genuinely invalid restrictions stay fatal.
|
|
31
|
+
|
|
32
|
+
Recognition is not message-matching alone: for the reported complex type we
|
|
33
|
+
confirm that every element in the derived content model either matches a base
|
|
34
|
+
element by name (an ordinary restriction xmlschema already checks) or is a member
|
|
35
|
+
of a substitution group whose head appears in the base content model, with
|
|
36
|
+
compatible occurrences. A restriction to a non-member element, or one that widens
|
|
37
|
+
occurrences, is not recognised and remains a fatal error.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from typing import Union
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
from xmlschema import XMLSchema11
|
|
44
|
+
from xmlschema.validators.exceptions import XMLSchemaParseError
|
|
45
|
+
|
|
46
|
+
# xmlschema phrasings for the complex-content restriction rejection.
|
|
47
|
+
_RESTRICTION_MARKERS = ("illegal restriction", "not a valid restriction")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _occurs_within(member, head) -> bool:
|
|
51
|
+
"""True if ``member``'s occurrence range is inside ``head``'s range, i.e. the
|
|
52
|
+
member is a valid occurrence-restriction of the head particle."""
|
|
53
|
+
if member.min_occurs < head.min_occurs:
|
|
54
|
+
return False
|
|
55
|
+
if head.max_occurs is None: # head is unbounded, any bound is within it
|
|
56
|
+
return True
|
|
57
|
+
if member.max_occurs is None:
|
|
58
|
+
return False
|
|
59
|
+
return member.max_occurs <= head.max_occurs
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def is_substitution_group_restriction_false_positive(error) -> bool:
|
|
63
|
+
"""Return True iff ``error`` is xmlschema wrongly rejecting a valid XSD 1.1
|
|
64
|
+
substitution-group-member restriction (Section 3.4.6.4).
|
|
65
|
+
|
|
66
|
+
Narrow by construction: returns False for genuinely invalid restrictions
|
|
67
|
+
(restricting to a non-member element, or widening an element's occurrences),
|
|
68
|
+
so it never masks a real schema defect.
|
|
69
|
+
"""
|
|
70
|
+
if not isinstance(error, XMLSchemaParseError):
|
|
71
|
+
return False
|
|
72
|
+
message = (getattr(error, "message", "") or "").lower()
|
|
73
|
+
if not any(marker in message for marker in _RESTRICTION_MARKERS):
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
complex_type = getattr(error, "validator", None)
|
|
77
|
+
base_type = getattr(complex_type, "base_type", None)
|
|
78
|
+
derived_group = getattr(complex_type, "content", None)
|
|
79
|
+
base_group = getattr(base_type, "content", None)
|
|
80
|
+
if complex_type is None or base_type is None:
|
|
81
|
+
return False
|
|
82
|
+
if derived_group is None or base_group is None:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
# Only the element-only sequence-restricts-sequence case is in scope.
|
|
87
|
+
if getattr(derived_group, "model", None) != "sequence":
|
|
88
|
+
return False
|
|
89
|
+
if getattr(base_group, "model", None) != "sequence":
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
substitution_groups = complex_type.maps.substitution_groups
|
|
93
|
+
|
|
94
|
+
base_named = {} # name -> base element declaration
|
|
95
|
+
base_heads = [] # substitution-group head particles in the base
|
|
96
|
+
for particle in base_group.iter_model():
|
|
97
|
+
name = getattr(particle, "name", None)
|
|
98
|
+
if name is None:
|
|
99
|
+
return False # a wildcard or nested group in the base — out of scope
|
|
100
|
+
base_named[name] = particle
|
|
101
|
+
# An element heads a substitution group when its name keys the map.
|
|
102
|
+
# The head need not be abstract (S3Model's 'Items' head is not).
|
|
103
|
+
if name in substitution_groups:
|
|
104
|
+
base_heads.append(particle)
|
|
105
|
+
|
|
106
|
+
for derived in derived_group.iter_model():
|
|
107
|
+
dname = getattr(derived, "name", None)
|
|
108
|
+
if dname is None:
|
|
109
|
+
return False # wildcard/nested group in the restriction — out of scope
|
|
110
|
+
|
|
111
|
+
if dname in base_named:
|
|
112
|
+
# Ordinary same-name restriction; xmlschema's own element check
|
|
113
|
+
# decides whether it is valid (occurrence tightening, fixed value,
|
|
114
|
+
# validly-derived type). If it is not, this is a real error.
|
|
115
|
+
if not derived.is_restriction(base_named[dname]):
|
|
116
|
+
return False
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# Not a same-name element: it must substitute for a base head element
|
|
120
|
+
# and its occurrences must fall inside that head's occurrences.
|
|
121
|
+
matched = False
|
|
122
|
+
for head in base_heads:
|
|
123
|
+
members = substitution_groups.get(head.name) or ()
|
|
124
|
+
if any(getattr(m, "name", None) == dname for m in members):
|
|
125
|
+
matched = _occurs_within(derived, head)
|
|
126
|
+
break
|
|
127
|
+
if not matched:
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
return True
|
|
131
|
+
except Exception:
|
|
132
|
+
# Any unexpected shape: be conservative and treat as a real error.
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def build_xsd11_schema(
|
|
137
|
+
source: Union[str, Path, "XMLSchema11"], validation: str = "strict"
|
|
138
|
+
) -> XMLSchema11:
|
|
139
|
+
"""Build an ``XMLSchema11`` while tolerating only the known-valid XSD 1.1
|
|
140
|
+
substitution-group restriction that xmlschema false-rejects.
|
|
141
|
+
|
|
142
|
+
Strict mode is preserved for every other case: a clean schema builds once in
|
|
143
|
+
strict mode; a schema that fails strict only because of the substitution-group
|
|
144
|
+
false positive is rebuilt in lax mode and returned; any genuine error keeps
|
|
145
|
+
the build fatal. ``lax`` and ``skip`` are passed through unchanged.
|
|
146
|
+
|
|
147
|
+
A lax-built schema still enforces the restriction during instance validation
|
|
148
|
+
(valid members are accepted, non-members rejected); only xmlschema's incorrect
|
|
149
|
+
build-time check is bypassed.
|
|
150
|
+
"""
|
|
151
|
+
if validation != "strict":
|
|
152
|
+
return XMLSchema11(source, validation=validation)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
return XMLSchema11(source, validation="strict")
|
|
156
|
+
except XMLSchemaParseError as strict_error:
|
|
157
|
+
if not is_substitution_group_restriction_false_positive(strict_error):
|
|
158
|
+
raise
|
|
159
|
+
schema = XMLSchema11(source, validation="lax")
|
|
160
|
+
genuine = [
|
|
161
|
+
e for e in schema.all_errors
|
|
162
|
+
if not is_substitution_group_restriction_false_positive(e)
|
|
163
|
+
]
|
|
164
|
+
if genuine:
|
|
165
|
+
# Real problems remain; surface the first one, not the false positive.
|
|
166
|
+
raise genuine[0]
|
|
167
|
+
return schema
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sdcvalidator
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4.0
|
|
4
4
|
Summary: SDC4 structural validator with ExceptionalValue recovery — thin wrapper over xmlschema
|
|
5
5
|
Author: Semantic Data Charter Foundation
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -144,6 +144,24 @@ sdcvalidator-xml2json instance.xml -o output.json
|
|
|
144
144
|
sdcvalidator-json2xml data.json schema.xsd -o output.xml
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
## MCP Server
|
|
148
|
+
|
|
149
|
+
`sdcvalidator` ships a stdio MCP (Model Context Protocol) server so any MCP-capable agent can validate SDC4 data without importing the Python library. It implements JSON-RPC 2.0 directly over stdio — no external MCP SDK dependency.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
sdcvalidator-mcp serve --mcp
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Tools
|
|
156
|
+
|
|
157
|
+
| Tool | Purpose |
|
|
158
|
+
|------|---------|
|
|
159
|
+
| `validate_instance` | Validate an XML instance against its SDC4 XSD schema. Returns pass/fail with error count and classified errors. |
|
|
160
|
+
| `validate_and_report` | Validate an instance and return a detailed report with two-tier (structural vs semantic) error classification. |
|
|
161
|
+
| `check_schema_compliance` | Check whether an XSD schema follows SDC4 principles (restriction only, no `xsd:extension`). Does not validate instances. |
|
|
162
|
+
|
|
163
|
+
`validate_instance` and `validate_and_report` take `schema_path` and `instance_path`, plus an optional `check_compliance` boolean (default `true`). `check_schema_compliance` takes `schema_path` only.
|
|
164
|
+
|
|
147
165
|
## Two-Tier Error Classification
|
|
148
166
|
|
|
149
167
|
| Tier | Type | Examples | Action |
|
|
@@ -157,6 +175,10 @@ SDC4 data models must use `xsd:restriction` only — never `xsd:extension`. This
|
|
|
157
175
|
|
|
158
176
|
The validator checks this by default and rejects schemas that violate this principle.
|
|
159
177
|
|
|
178
|
+
## Status
|
|
179
|
+
|
|
180
|
+
Production-ready. Available on [PyPI](https://pypi.org/project/sdcvalidator/) under Apache 2.0.
|
|
181
|
+
|
|
160
182
|
## License
|
|
161
183
|
|
|
162
184
|
Apache License 2.0 — see [LICENSE](LICENSE).
|
|
@@ -12,6 +12,7 @@ src/sdcvalidator/instance_modifier.py
|
|
|
12
12
|
src/sdcvalidator/mcp_server.py
|
|
13
13
|
src/sdcvalidator/schema_checker.py
|
|
14
14
|
src/sdcvalidator/validator.py
|
|
15
|
+
src/sdcvalidator/xsd11_restriction.py
|
|
15
16
|
src/sdcvalidator.egg-info/PKG-INFO
|
|
16
17
|
src/sdcvalidator.egg-info/SOURCES.txt
|
|
17
18
|
src/sdcvalidator.egg-info/dependency_links.txt
|
|
@@ -25,4 +26,5 @@ tests/test_instance_modifier.py
|
|
|
25
26
|
tests/test_mcp_server.py
|
|
26
27
|
tests/test_recovery.py
|
|
27
28
|
tests/test_schema_checker.py
|
|
28
|
-
tests/test_validator.py
|
|
29
|
+
tests/test_validator.py
|
|
30
|
+
tests/test_xsd11_restriction.py
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 Semantic Data Charter Foundation
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
"""
|
|
11
|
+
Tests for XSD 1.1 substitution-group restriction support.
|
|
12
|
+
|
|
13
|
+
The reference behaviour is the Apache Xerces-J XML Schema 1.1 build, which
|
|
14
|
+
accepts a Data Model that restricts an abstract substitution-group head to
|
|
15
|
+
specific member elements (valid per XSD 1.1 Part 1 Section 3.4.6.4) and rejects
|
|
16
|
+
genuinely invalid restrictions. build_xsd11_schema must match that: accept the
|
|
17
|
+
valid construct that stock xmlschema false-rejects, keep real errors fatal, and
|
|
18
|
+
still enforce the restriction during instance validation.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import pytest
|
|
22
|
+
from xmlschema.validators.exceptions import XMLSchemaParseError
|
|
23
|
+
|
|
24
|
+
from sdcvalidator.xsd11_restriction import (
|
|
25
|
+
build_xsd11_schema,
|
|
26
|
+
is_substitution_group_restriction_false_positive,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# A reference model with an ABSTRACT substitution-group head (like sdc4:Item),
|
|
31
|
+
# concrete member types that reach the head's type through an extension (like
|
|
32
|
+
# ClusterType/XdAdapterType extending ItemType), and a cluster whose content is
|
|
33
|
+
# `label?, Item*`.
|
|
34
|
+
ABSTRACT_HEAD_RM = """<?xml version="1.0" encoding="UTF-8"?>
|
|
35
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
36
|
+
xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning"
|
|
37
|
+
xmlns:t="urn:test" targetNamespace="urn:test"
|
|
38
|
+
elementFormDefault="qualified" vc:minVersion="1.1">
|
|
39
|
+
<xsd:complexType abstract="true" name="ItemType"><xsd:sequence/></xsd:complexType>
|
|
40
|
+
<xsd:element abstract="true" name="Item" type="t:ItemType"/>
|
|
41
|
+
<xsd:complexType name="MidType"><xsd:complexContent><xsd:extension base="t:ItemType">
|
|
42
|
+
<xsd:sequence><xsd:element name="v" type="xsd:string" minOccurs="0"/></xsd:sequence>
|
|
43
|
+
</xsd:extension></xsd:complexContent></xsd:complexType>
|
|
44
|
+
<xsd:complexType name="MemberType"><xsd:complexContent><xsd:restriction base="t:MidType">
|
|
45
|
+
<xsd:sequence><xsd:element name="v" type="xsd:string" minOccurs="0"/></xsd:sequence>
|
|
46
|
+
</xsd:restriction></xsd:complexContent></xsd:complexType>
|
|
47
|
+
<xsd:element name="ms-a" substitutionGroup="t:Item" type="t:MemberType"/>
|
|
48
|
+
<xsd:element name="ms-b" substitutionGroup="t:Item" type="t:MemberType"/>
|
|
49
|
+
<xsd:complexType name="ClusterType"><xsd:complexContent><xsd:extension base="t:ItemType">
|
|
50
|
+
<xsd:sequence>
|
|
51
|
+
<xsd:element name="label" type="xsd:string" minOccurs="0"/>
|
|
52
|
+
<xsd:element ref="t:Item" minOccurs="0" maxOccurs="unbounded"/>
|
|
53
|
+
</xsd:sequence></xsd:extension></xsd:complexContent></xsd:complexType>
|
|
54
|
+
{derived}
|
|
55
|
+
<xsd:element name="root" type="t:DataCluster"/>
|
|
56
|
+
</xsd:schema>
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# The valid restriction that stock xmlschema false-rejects: a fixed-value leading
|
|
60
|
+
# label plus specific substitution-group members in place of the Item head.
|
|
61
|
+
VALID_DERIVED = """
|
|
62
|
+
<xsd:complexType name="DataCluster"><xsd:complexContent><xsd:restriction base="t:ClusterType">
|
|
63
|
+
<xsd:sequence>
|
|
64
|
+
<xsd:element name="label" type="xsd:string" minOccurs="1" maxOccurs="1" fixed="Fixed Label"/>
|
|
65
|
+
<xsd:element ref="t:ms-a" minOccurs="0" maxOccurs="1"/>
|
|
66
|
+
<xsd:element ref="t:ms-b" minOccurs="0" maxOccurs="1"/>
|
|
67
|
+
</xsd:sequence></xsd:restriction></xsd:complexContent></xsd:complexType>
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# Genuinely invalid: restrict a required head slot to an element that is NOT a
|
|
71
|
+
# member of the head's substitution group. xmlschema raises "illegal restriction"
|
|
72
|
+
# here, and the recognizer must decline to suppress it (stranger is not a member),
|
|
73
|
+
# so the error stays fatal.
|
|
74
|
+
INVALID_NONMEMBER_SCHEMA = """<?xml version="1.0" encoding="UTF-8"?>
|
|
75
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
76
|
+
xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning"
|
|
77
|
+
xmlns:t="urn:test" targetNamespace="urn:test"
|
|
78
|
+
elementFormDefault="qualified" vc:minVersion="1.1">
|
|
79
|
+
<xsd:complexType abstract="true" name="ItemType"><xsd:sequence/></xsd:complexType>
|
|
80
|
+
<xsd:element abstract="true" name="Item" type="t:ItemType"/>
|
|
81
|
+
<xsd:complexType name="MemberType"><xsd:complexContent><xsd:restriction base="t:ItemType">
|
|
82
|
+
<xsd:sequence/></xsd:restriction></xsd:complexContent></xsd:complexType>
|
|
83
|
+
<xsd:element name="ms-a" substitutionGroup="t:Item" type="t:MemberType"/>
|
|
84
|
+
<!-- stranger is derived from ItemType but is NOT in Item's substitution group -->
|
|
85
|
+
<xsd:element name="stranger" type="t:MemberType"/>
|
|
86
|
+
<xsd:complexType name="BaseType"><xsd:sequence>
|
|
87
|
+
<xsd:element ref="t:Item" minOccurs="1" maxOccurs="1"/>
|
|
88
|
+
</xsd:sequence></xsd:complexType>
|
|
89
|
+
<xsd:complexType name="DataCluster"><xsd:complexContent><xsd:restriction base="t:BaseType">
|
|
90
|
+
<xsd:sequence>
|
|
91
|
+
<xsd:element ref="t:stranger" minOccurs="1" maxOccurs="1"/>
|
|
92
|
+
</xsd:sequence></xsd:restriction></xsd:complexContent></xsd:complexType>
|
|
93
|
+
<xsd:element name="root" type="t:DataCluster"/>
|
|
94
|
+
</xsd:schema>
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
# A reference model whose substitution-group head element is NOT abstract (like
|
|
98
|
+
# S3Model's 'Items'); only its type is abstract.
|
|
99
|
+
NONABSTRACT_HEAD_SCHEMA = """<?xml version="1.0" encoding="UTF-8"?>
|
|
100
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
101
|
+
xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning"
|
|
102
|
+
xmlns:t="urn:test" targetNamespace="urn:test"
|
|
103
|
+
elementFormDefault="qualified" vc:minVersion="1.1">
|
|
104
|
+
<xsd:complexType abstract="true" name="ItemsType"><xsd:sequence/></xsd:complexType>
|
|
105
|
+
<xsd:element name="Items" type="t:ItemsType"/>
|
|
106
|
+
<xsd:complexType name="MemberType"><xsd:complexContent><xsd:restriction base="t:ItemsType">
|
|
107
|
+
<xsd:sequence/></xsd:restriction></xsd:complexContent></xsd:complexType>
|
|
108
|
+
<xsd:element name="ms-a" substitutionGroup="t:Items" type="t:MemberType"/>
|
|
109
|
+
<xsd:element name="ms-b" substitutionGroup="t:Items" type="t:MemberType"/>
|
|
110
|
+
<xsd:complexType name="ClusterType"><xsd:sequence>
|
|
111
|
+
<xsd:element name="label" type="xsd:string" minOccurs="0"/>
|
|
112
|
+
<xsd:element ref="t:Items" minOccurs="0" maxOccurs="unbounded"/>
|
|
113
|
+
</xsd:sequence></xsd:complexType>
|
|
114
|
+
<xsd:complexType name="DataCluster"><xsd:complexContent><xsd:restriction base="t:ClusterType">
|
|
115
|
+
<xsd:sequence>
|
|
116
|
+
<xsd:element name="label" type="xsd:string" minOccurs="1" maxOccurs="1" fixed="X"/>
|
|
117
|
+
<xsd:element ref="t:ms-a" minOccurs="0" maxOccurs="1"/>
|
|
118
|
+
<xsd:element ref="t:ms-b" minOccurs="0" maxOccurs="1"/>
|
|
119
|
+
</xsd:sequence></xsd:restriction></xsd:complexContent></xsd:complexType>
|
|
120
|
+
<xsd:element name="root" type="t:DataCluster"/>
|
|
121
|
+
</xsd:schema>
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _write(tmp_path, name, content):
|
|
126
|
+
p = tmp_path / name
|
|
127
|
+
p.write_text(content)
|
|
128
|
+
return str(p)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_valid_substitution_restriction_builds(tmp_path):
|
|
132
|
+
"""The valid substitution-group restriction that stock xmlschema rejects
|
|
133
|
+
must build under strict validation."""
|
|
134
|
+
path = _write(tmp_path, "valid.xsd", ABSTRACT_HEAD_RM.format(derived=VALID_DERIVED))
|
|
135
|
+
schema = build_xsd11_schema(path) # strict by default
|
|
136
|
+
assert "root" in schema.elements
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_nonabstract_head_builds(tmp_path):
|
|
140
|
+
"""A substitution-group head that is not declared abstract (S3Model 'Items'
|
|
141
|
+
style) is still recognised."""
|
|
142
|
+
path = _write(tmp_path, "s3m.xsd", NONABSTRACT_HEAD_SCHEMA)
|
|
143
|
+
schema = build_xsd11_schema(path)
|
|
144
|
+
assert "root" in schema.elements
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_valid_restriction_enforced_on_instances(tmp_path):
|
|
148
|
+
"""A lax-built schema must still enforce the restriction: valid members are
|
|
149
|
+
accepted, non-members rejected."""
|
|
150
|
+
path = _write(tmp_path, "valid.xsd", ABSTRACT_HEAD_RM.format(derived=VALID_DERIVED))
|
|
151
|
+
schema = build_xsd11_schema(path)
|
|
152
|
+
schema.validate('<root xmlns="urn:test"><label>Fixed Label</label><ms-a/></root>')
|
|
153
|
+
with pytest.raises(Exception):
|
|
154
|
+
schema.validate('<root xmlns="urn:test"><label>Fixed Label</label><intruder/></root>')
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_nonmember_restriction_stays_fatal(tmp_path):
|
|
158
|
+
"""Restricting the head slot to a non-member element is a real error and must
|
|
159
|
+
remain fatal."""
|
|
160
|
+
path = _write(tmp_path, "nonmember.xsd", INVALID_NONMEMBER_SCHEMA)
|
|
161
|
+
with pytest.raises(XMLSchemaParseError):
|
|
162
|
+
build_xsd11_schema(path)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_widened_cardinality_stays_fatal(tmp_path):
|
|
166
|
+
"""Widening an element's occurrences is not a valid restriction and must
|
|
167
|
+
remain fatal."""
|
|
168
|
+
schema_xsd = """<?xml version="1.0" encoding="UTF-8"?>
|
|
169
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
170
|
+
xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning"
|
|
171
|
+
xmlns:t="urn:test" targetNamespace="urn:test" vc:minVersion="1.1">
|
|
172
|
+
<xsd:complexType name="BaseType"><xsd:sequence>
|
|
173
|
+
<xsd:element name="a" type="xsd:string" minOccurs="0" maxOccurs="1"/>
|
|
174
|
+
</xsd:sequence></xsd:complexType>
|
|
175
|
+
<xsd:complexType name="DerivedType"><xsd:complexContent>
|
|
176
|
+
<xsd:restriction base="t:BaseType"><xsd:sequence>
|
|
177
|
+
<xsd:element name="a" type="xsd:string" minOccurs="0" maxOccurs="3"/>
|
|
178
|
+
</xsd:sequence></xsd:restriction></xsd:complexContent></xsd:complexType>
|
|
179
|
+
<xsd:element name="root" type="t:DerivedType"/>
|
|
180
|
+
</xsd:schema>
|
|
181
|
+
"""
|
|
182
|
+
path = _write(tmp_path, "widen.xsd", schema_xsd)
|
|
183
|
+
with pytest.raises(XMLSchemaParseError):
|
|
184
|
+
build_xsd11_schema(path)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_clean_schema_unaffected(tmp_path):
|
|
188
|
+
"""A schema with no restriction issues builds normally."""
|
|
189
|
+
schema_xsd = """<?xml version="1.0" encoding="UTF-8"?>
|
|
190
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
191
|
+
xmlns:t="urn:test" targetNamespace="urn:test">
|
|
192
|
+
<xsd:element name="root" type="xsd:string"/>
|
|
193
|
+
</xsd:schema>
|
|
194
|
+
"""
|
|
195
|
+
path = _write(tmp_path, "clean.xsd", schema_xsd)
|
|
196
|
+
schema = build_xsd11_schema(path)
|
|
197
|
+
assert "root" in schema.elements
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_recognizer_ignores_non_restriction_errors():
|
|
201
|
+
"""The recognizer must not treat an unrelated error as the false positive."""
|
|
202
|
+
class Other(Exception):
|
|
203
|
+
message = "something else entirely"
|
|
204
|
+
assert is_substitution_group_restriction_false_positive(Other()) is False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|