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.
Files changed (32) hide show
  1. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/PKG-INFO +23 -1
  2. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/README.md +22 -0
  3. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/pyproject.toml +1 -1
  4. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/__init__.py +1 -1
  5. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/converters.py +4 -2
  6. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/validator.py +6 -1
  7. sdcvalidator-4.4.0/src/sdcvalidator/xsd11_restriction.py +167 -0
  8. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/PKG-INFO +23 -1
  9. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/SOURCES.txt +3 -1
  10. sdcvalidator-4.4.0/tests/test_xsd11_restriction.py +204 -0
  11. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/LICENSE +0 -0
  12. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/setup.cfg +0 -0
  13. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/cli.py +0 -0
  14. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/constants.py +0 -0
  15. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/error_classifier.py +0 -0
  16. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/error_mapper.py +0 -0
  17. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/exceptions.py +0 -0
  18. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/instance_modifier.py +0 -0
  19. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/mcp_server.py +0 -0
  20. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator/schema_checker.py +0 -0
  21. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/dependency_links.txt +0 -0
  22. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/entry_points.txt +0 -0
  23. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/requires.txt +0 -0
  24. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/src/sdcvalidator.egg-info/top_level.txt +0 -0
  25. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/tests/test_converters.py +0 -0
  26. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/tests/test_error_classifier.py +0 -0
  27. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/tests/test_error_mapper.py +0 -0
  28. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/tests/test_instance_modifier.py +0 -0
  29. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/tests/test_mcp_server.py +0 -0
  30. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/tests/test_recovery.py +0 -0
  31. {sdcvalidator-4.3.0 → sdcvalidator-4.4.0}/tests/test_schema_checker.py +0 -0
  32. {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.0
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.3.0"
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.3.0"
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 = XMLSchema11(str(schema_path))
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 = XMLSchema11(str(schema_path))
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
- self.schema = XMLSchema11(str(schema), validation=validation)
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.0
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