@dialecte/create 0.0.1
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.
- package/README.md +88 -0
- package/dist/cli/index.js +281 -0
- package/package.json +43 -0
- package/python/generate/__init__.py +1 -0
- package/python/generate/__main__.py +103 -0
- package/python/generate/collector.py +128 -0
- package/python/generate/deriver.py +117 -0
- package/python/generate/emitters/__init__.py +1 -0
- package/python/generate/emitters/constants.py +69 -0
- package/python/generate/emitters/definition.py +49 -0
- package/python/generate/emitters/ts_helpers.py +283 -0
- package/python/generate/emitters/types.py +67 -0
- package/python/generate/extractors/__init__.py +1 -0
- package/python/generate/extractors/attributes.py +83 -0
- package/python/generate/extractors/children.py +175 -0
- package/python/generate/extractors/constraints.py +154 -0
- package/python/generate/extractors/docs.py +32 -0
- package/python/generate/extractors/facets.py +168 -0
- package/python/generate/extractors/namespace.py +59 -0
- package/python/generate/globals.py +99 -0
- package/python/generate/helpers.py +143 -0
- package/python/generate/ir.py +81 -0
- package/python/generate/orphans.py +69 -0
- package/python/generate/xpath_parser.py +167 -0
- package/python/generate/xsi_type.py +150 -0
- package/python/pyproject.toml +15 -0
- package/templates/dialecte/README.md +39 -0
- package/templates/dialecte/_gitignore +4 -0
- package/templates/dialecte/docs/.vitepress/config.ts +15 -0
- package/templates/dialecte/docs/index.md +18 -0
- package/templates/dialecte/env.d.ts +1 -0
- package/templates/dialecte/package.json +45 -0
- package/templates/dialecte/src/__version__/config/dialecte.config.ts +48 -0
- package/templates/dialecte/src/__version__/config/hydrated.types.ts +78 -0
- package/templates/dialecte/src/__version__/config/index.ts +2 -0
- package/templates/dialecte/src/__version__/config/namespaces.ts +6 -0
- package/templates/dialecte/src/__version__/definition/.gitkeep +2 -0
- package/templates/dialecte/src/__version__/definition/index.ts +4 -0
- package/templates/dialecte/src/__version__/dialecte.ts +30 -0
- package/templates/dialecte/src/__version__/extensions/index.ts +3 -0
- package/templates/dialecte/src/__version__/index.ts +2 -0
- package/templates/dialecte/src/__version__/test/hydrated-test.ts +53 -0
- package/templates/dialecte/src/__version__/test/index.ts +1 -0
- package/templates/dialecte/src/index.ts +1 -0
- package/templates/dialecte/tsconfig.build.json +24 -0
- package/templates/dialecte/tsconfig.json +8 -0
- package/templates/dialecte/tsconfig.node.json +13 -0
- package/templates/dialecte/tsconfig.vitest.json +10 -0
- package/templates/dialecte/vite.config.ts +36 -0
- package/templates/dialecte/vitest.config.ts +35 -0
- package/vendor/elementpath-5.1.1-py3-none-any.whl +0 -0
- package/vendor/xmlschema-4.3.1-py3-none-any.whl +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Extract child elements, choices, and text content from an XSD element."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from generate.extractors.constraints import extract_constraints
|
|
5
|
+
from generate.extractors.facets import extract_facets
|
|
6
|
+
from generate.ir import ChildDef, ChoiceGroup, TextContent
|
|
7
|
+
def extract_children(xsd_elem: Any) -> tuple[list[str], bool, dict[str, ChildDef]]:
|
|
8
|
+
"""Extract child element definitions from an XSD element's content model.
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
(child_sequence, has_any_element, child_details)
|
|
12
|
+
|
|
13
|
+
xmlschema API:
|
|
14
|
+
XsdElement.type: XsdComplexType
|
|
15
|
+
XsdComplexType.content: XsdGroup (model group)
|
|
16
|
+
XsdGroup.iter_elements() → Iterator[XsdElement | XsdAnyElement]
|
|
17
|
+
Each yielded element has:
|
|
18
|
+
.local_name: str
|
|
19
|
+
.min_occurs: int
|
|
20
|
+
.max_occurs: int | None
|
|
21
|
+
Content wildcards:
|
|
22
|
+
XsdGroup — may contain XsdAnyElement children
|
|
23
|
+
"""
|
|
24
|
+
return _extract_children_from_content(_get_content_model(xsd_elem))
|
|
25
|
+
def extract_children_from_type(xsd_type: Any) -> tuple[list[str], bool, dict[str, ChildDef]]:
|
|
26
|
+
"""Like ``extract_children`` but reads a complex type's content model directly.
|
|
27
|
+
|
|
28
|
+
Used by xsi:type expansion where children come from a substitutable type
|
|
29
|
+
(selected via ``xsi:type``) rather than the element's own declared type.
|
|
30
|
+
"""
|
|
31
|
+
return _extract_children_from_content(getattr(xsd_type, 'content', None))
|
|
32
|
+
def _extract_children_from_content(content: Any) -> tuple[list[str], bool, dict[str, ChildDef]]:
|
|
33
|
+
"""Shared core: extract child definitions from a content model (XsdGroup)."""
|
|
34
|
+
sequence: list[str] = []
|
|
35
|
+
details: dict[str, ChildDef] = {}
|
|
36
|
+
any_child = False
|
|
37
|
+
|
|
38
|
+
if content is None:
|
|
39
|
+
return sequence, any_child, details
|
|
40
|
+
|
|
41
|
+
seen_names: set[str] = set()
|
|
42
|
+
|
|
43
|
+
for child in _iter_child_elements(content):
|
|
44
|
+
# Check if it's a wildcard (xs:any)
|
|
45
|
+
cls_name = type(child).__name__
|
|
46
|
+
if 'Any' in cls_name and 'Element' in cls_name:
|
|
47
|
+
any_child = True
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
name = getattr(child, 'local_name', None)
|
|
51
|
+
if not name:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
if name in seen_names:
|
|
55
|
+
continue
|
|
56
|
+
seen_names.add(name)
|
|
57
|
+
|
|
58
|
+
min_occ = getattr(child, 'min_occurs', 0)
|
|
59
|
+
max_occ = getattr(child, 'max_occurs', None) # None = unbounded
|
|
60
|
+
|
|
61
|
+
sequence.append(name)
|
|
62
|
+
details[name] = ChildDef(
|
|
63
|
+
required=min_occ > 0,
|
|
64
|
+
min_occurs=min_occ,
|
|
65
|
+
max_occurs=max_occ,
|
|
66
|
+
constraints=extract_constraints(child) or None,
|
|
67
|
+
facets=None, # Rare: child text content facets
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return sequence, any_child, details
|
|
71
|
+
def extract_choices(xsd_elem: Any) -> list[ChoiceGroup]:
|
|
72
|
+
"""Extract xs:choice groups from the content model.
|
|
73
|
+
|
|
74
|
+
xmlschema API:
|
|
75
|
+
XsdGroup.model: str ('sequence' | 'choice' | 'all')
|
|
76
|
+
XsdGroup is iterable — yields XsdElement | XsdGroup | XsdAnyElement
|
|
77
|
+
"""
|
|
78
|
+
content = _get_content_model(xsd_elem)
|
|
79
|
+
if content is None:
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
choices: list[ChoiceGroup] = []
|
|
83
|
+
_walk_groups_for_choices(content, choices)
|
|
84
|
+
return choices
|
|
85
|
+
def extract_text_content(xsd_elem: Any) -> TextContent | None:
|
|
86
|
+
"""Extract text content definition for elements with simple or mixed content.
|
|
87
|
+
|
|
88
|
+
xmlschema API:
|
|
89
|
+
XsdComplexType.has_simple_content() → bool
|
|
90
|
+
XsdComplexType.mixed: bool
|
|
91
|
+
XsdComplexType.content_type_label: str ('simple' | 'mixed' | 'element-only' | 'empty')
|
|
92
|
+
"""
|
|
93
|
+
xsd_type = getattr(xsd_elem, 'type', None)
|
|
94
|
+
if xsd_type is None:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
has_simple = False
|
|
98
|
+
if callable(getattr(xsd_type, 'has_simple_content', None)):
|
|
99
|
+
has_simple = xsd_type.has_simple_content()
|
|
100
|
+
mixed = getattr(xsd_type, 'mixed', False)
|
|
101
|
+
|
|
102
|
+
if not has_simple and not mixed:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
# Find the simple type to extract facets from
|
|
106
|
+
facets_source = (
|
|
107
|
+
getattr(xsd_type, 'content', None)
|
|
108
|
+
or getattr(xsd_type, 'simple_type', None)
|
|
109
|
+
or getattr(xsd_type, 'base_type', None)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
facets = extract_facets(facets_source)
|
|
113
|
+
return TextContent(facets=facets) if facets else TextContent()
|
|
114
|
+
# --- Internal helpers ---
|
|
115
|
+
def _get_content_model(xsd_elem: Any) -> Any:
|
|
116
|
+
"""Get the content model (XsdGroup) from an element's type."""
|
|
117
|
+
xsd_type = getattr(xsd_elem, 'type', None)
|
|
118
|
+
if xsd_type is None:
|
|
119
|
+
return None
|
|
120
|
+
return getattr(xsd_type, 'content', None)
|
|
121
|
+
def _iter_child_elements(content: Any):
|
|
122
|
+
"""Iterate child elements from a content model.
|
|
123
|
+
|
|
124
|
+
xmlschema API:
|
|
125
|
+
XsdGroup.iter_elements() → yields XsdElement | XsdAnyElement
|
|
126
|
+
"""
|
|
127
|
+
iter_fn = getattr(content, 'iter_elements', None)
|
|
128
|
+
if iter_fn and callable(iter_fn):
|
|
129
|
+
yield from iter_fn()
|
|
130
|
+
def iter_child_elements(xsd_elem: Any):
|
|
131
|
+
"""Public helper: iterate XsdElement children of an element for recursive walking."""
|
|
132
|
+
yield from _iter_named_child_elements(_get_content_model(xsd_elem))
|
|
133
|
+
def iter_type_child_elements(xsd_type: Any):
|
|
134
|
+
"""Public helper: iterate XsdElement children declared in a complex type's content."""
|
|
135
|
+
yield from _iter_named_child_elements(getattr(xsd_type, 'content', None))
|
|
136
|
+
def _iter_named_child_elements(content: Any):
|
|
137
|
+
"""Iterate non-wildcard XsdElement children of a content model."""
|
|
138
|
+
if content is None:
|
|
139
|
+
return
|
|
140
|
+
for child in _iter_child_elements(content):
|
|
141
|
+
cls_name = type(child).__name__
|
|
142
|
+
if 'Any' in cls_name and 'Element' in cls_name:
|
|
143
|
+
continue
|
|
144
|
+
yield child
|
|
145
|
+
def _walk_groups_for_choices(group: Any, out: list[ChoiceGroup]) -> None:
|
|
146
|
+
"""Recursively walk model groups to find xs:choice groups.
|
|
147
|
+
|
|
148
|
+
xmlschema API:
|
|
149
|
+
XsdGroup.model: str
|
|
150
|
+
XsdGroup is iterable (yields children)
|
|
151
|
+
"""
|
|
152
|
+
model = getattr(group, 'model', None)
|
|
153
|
+
if model is None:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
if model == 'choice':
|
|
157
|
+
options: list[str] = []
|
|
158
|
+
for item in group:
|
|
159
|
+
name = getattr(item, 'local_name', None)
|
|
160
|
+
if name:
|
|
161
|
+
options.append(name)
|
|
162
|
+
# Recurse into nested groups
|
|
163
|
+
if getattr(item, 'model', None) is not None:
|
|
164
|
+
_walk_groups_for_choices(item, out)
|
|
165
|
+
if options:
|
|
166
|
+
out.append(ChoiceGroup(
|
|
167
|
+
options=sorted(options),
|
|
168
|
+
min_occurs=getattr(group, 'min_occurs', 0),
|
|
169
|
+
max_occurs=getattr(group, 'max_occurs', None),
|
|
170
|
+
))
|
|
171
|
+
else:
|
|
172
|
+
# sequence or all — recurse into nested groups
|
|
173
|
+
for item in group:
|
|
174
|
+
if getattr(item, 'model', None) is not None:
|
|
175
|
+
_walk_groups_for_choices(item, out)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Extract identity constraints (xs:unique, xs:key, xs:keyref) from XSD elements."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from generate.helpers import local_name
|
|
5
|
+
from generate.ir import IdentityConstraint
|
|
6
|
+
from generate.xpath_parser import parse_field, parse_selector
|
|
7
|
+
def extract_constraints(xsd_elem: Any) -> list[IdentityConstraint]:
|
|
8
|
+
"""Extract identity constraints from an XSD element.
|
|
9
|
+
|
|
10
|
+
Tries compiled constraint objects first, then raw XML fallback.
|
|
11
|
+
|
|
12
|
+
xmlschema API (compiled path):
|
|
13
|
+
XsdElement.identities: dict[str, XsdIdentity] — keys/uniques/keyrefs
|
|
14
|
+
XsdIdentity.selector: XsdSelector
|
|
15
|
+
XsdSelector.path: str
|
|
16
|
+
XsdIdentity.fields: list[XsdFieldSelector]
|
|
17
|
+
XsdFieldSelector.path: str
|
|
18
|
+
XsdUnique, XsdKey, XsdKeyref — subclasses
|
|
19
|
+
XsdKeyref.refer: XsdKey | XsdUnique
|
|
20
|
+
.refer.local_name: str
|
|
21
|
+
|
|
22
|
+
xmlschema API (raw XML fallback):
|
|
23
|
+
XsdElement.elem: Element — raw lxml/ET element
|
|
24
|
+
Scan for xs:key, xs:unique, xs:keyref tags
|
|
25
|
+
"""
|
|
26
|
+
constraints: list[IdentityConstraint] = []
|
|
27
|
+
seen: set[tuple[str, str]] = set()
|
|
28
|
+
|
|
29
|
+
# Try compiled identities
|
|
30
|
+
for ic in _iter_identity_constraints(xsd_elem):
|
|
31
|
+
kind = _classify_constraint(ic)
|
|
32
|
+
if kind is None:
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
name = getattr(ic, 'name', '') or ''
|
|
36
|
+
# Use local_name if it's a Clark QName
|
|
37
|
+
if name.startswith('{'):
|
|
38
|
+
name = local_name(name)
|
|
39
|
+
|
|
40
|
+
sig = (kind, name)
|
|
41
|
+
if sig in seen:
|
|
42
|
+
continue
|
|
43
|
+
seen.add(sig)
|
|
44
|
+
|
|
45
|
+
selector_xpath = _get_selector_path(ic)
|
|
46
|
+
field_xpaths = _get_field_paths(ic)
|
|
47
|
+
|
|
48
|
+
selector_paths = parse_selector(selector_xpath)
|
|
49
|
+
field_paths = [parse_field(fp) for fp in field_xpaths]
|
|
50
|
+
|
|
51
|
+
refer = None
|
|
52
|
+
if kind == 'keyref':
|
|
53
|
+
refer = _get_refer_name(ic)
|
|
54
|
+
|
|
55
|
+
constraints.append(IdentityConstraint(
|
|
56
|
+
kind=kind,
|
|
57
|
+
name=name,
|
|
58
|
+
selector=selector_paths,
|
|
59
|
+
fields=field_paths,
|
|
60
|
+
deep='//' in (selector_xpath or ''),
|
|
61
|
+
refer=refer,
|
|
62
|
+
))
|
|
63
|
+
|
|
64
|
+
return constraints
|
|
65
|
+
# --- Internal helpers ---
|
|
66
|
+
def _iter_identity_constraints(xsd_elem: Any):
|
|
67
|
+
"""Yield identity constraint objects from an element.
|
|
68
|
+
|
|
69
|
+
Checks multiple container shapes that xmlschema uses:
|
|
70
|
+
- .identities dict
|
|
71
|
+
- .identity_constraints dict
|
|
72
|
+
- .keys, .uniques, .keyrefs dicts/lists
|
|
73
|
+
"""
|
|
74
|
+
# Preferred: .identities (xmlschema v2+)
|
|
75
|
+
identities = getattr(xsd_elem, 'identities', None)
|
|
76
|
+
if identities:
|
|
77
|
+
if hasattr(identities, 'values'):
|
|
78
|
+
yield from identities.values()
|
|
79
|
+
else:
|
|
80
|
+
yield from identities
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
# Alternative: .identity_constraints
|
|
84
|
+
ic_map = getattr(xsd_elem, 'identity_constraints', None)
|
|
85
|
+
if ic_map:
|
|
86
|
+
if hasattr(ic_map, 'values'):
|
|
87
|
+
yield from ic_map.values()
|
|
88
|
+
else:
|
|
89
|
+
yield from ic_map
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Separate containers
|
|
93
|
+
for attr_name in ('keys', 'uniques', 'keyrefs'):
|
|
94
|
+
container = getattr(xsd_elem, attr_name, None)
|
|
95
|
+
if container:
|
|
96
|
+
if hasattr(container, 'values'):
|
|
97
|
+
yield from container.values()
|
|
98
|
+
else:
|
|
99
|
+
yield from container
|
|
100
|
+
def _classify_constraint(ic: Any) -> str | None:
|
|
101
|
+
"""Determine if an identity constraint is 'unique', 'key', or 'keyref'."""
|
|
102
|
+
# Category attribute
|
|
103
|
+
category = getattr(ic, 'category', None)
|
|
104
|
+
if category:
|
|
105
|
+
cat = str(category).lower()
|
|
106
|
+
if 'key' in cat and 'ref' in cat:
|
|
107
|
+
return 'keyref'
|
|
108
|
+
if 'key' in cat:
|
|
109
|
+
return 'key'
|
|
110
|
+
if 'unique' in cat:
|
|
111
|
+
return 'unique'
|
|
112
|
+
|
|
113
|
+
# Class name fallback
|
|
114
|
+
cls_name = type(ic).__name__.lower()
|
|
115
|
+
if 'keyref' in cls_name:
|
|
116
|
+
return 'keyref'
|
|
117
|
+
if 'key' in cls_name:
|
|
118
|
+
return 'key'
|
|
119
|
+
if 'unique' in cls_name:
|
|
120
|
+
return 'unique'
|
|
121
|
+
|
|
122
|
+
return None
|
|
123
|
+
def _get_selector_path(ic: Any) -> str:
|
|
124
|
+
"""Get XPath selector path from a constraint."""
|
|
125
|
+
selector = getattr(ic, 'selector', None)
|
|
126
|
+
if selector is None:
|
|
127
|
+
return ''
|
|
128
|
+
path = getattr(selector, 'path', None)
|
|
129
|
+
if path:
|
|
130
|
+
return str(path)
|
|
131
|
+
# Fallback: selector might be string itself
|
|
132
|
+
return str(selector) if selector else ''
|
|
133
|
+
def _get_field_paths(ic: Any) -> list[str]:
|
|
134
|
+
"""Get XPath field paths from a constraint."""
|
|
135
|
+
fields = getattr(ic, 'fields', None)
|
|
136
|
+
if not fields:
|
|
137
|
+
return []
|
|
138
|
+
result = []
|
|
139
|
+
for f in fields:
|
|
140
|
+
path = getattr(f, 'path', None)
|
|
141
|
+
if path:
|
|
142
|
+
result.append(str(path))
|
|
143
|
+
elif isinstance(f, str):
|
|
144
|
+
result.append(f)
|
|
145
|
+
return result
|
|
146
|
+
def _get_refer_name(ic: Any) -> str | None:
|
|
147
|
+
"""Get the name of the referred key/unique for a keyref constraint."""
|
|
148
|
+
refer = getattr(ic, 'refer', None)
|
|
149
|
+
if refer is None:
|
|
150
|
+
return None
|
|
151
|
+
name = getattr(refer, 'local_name', None) or getattr(refer, 'name', None)
|
|
152
|
+
if name:
|
|
153
|
+
return local_name(str(name))
|
|
154
|
+
return str(refer)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Extract documentation string from an XSD element."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
def extract_docs(xsd_elem: Any) -> str | None:
|
|
4
|
+
"""Extract the first xs:documentation text from an XSD element's annotations.
|
|
5
|
+
|
|
6
|
+
xmlschema API:
|
|
7
|
+
XsdElement.annotation → XsdAnnotation | None
|
|
8
|
+
XsdAnnotation.documentation → list[XsdDocumentation]
|
|
9
|
+
XsdDocumentation.text → str
|
|
10
|
+
"""
|
|
11
|
+
# Try .annotation (singular) first — xmlschema v2+
|
|
12
|
+
annotation = getattr(xsd_elem, 'annotation', None)
|
|
13
|
+
if annotation is not None:
|
|
14
|
+
docs = getattr(annotation, 'documentation', None)
|
|
15
|
+
if docs:
|
|
16
|
+
for doc in docs:
|
|
17
|
+
text = getattr(doc, 'text', None)
|
|
18
|
+
if text and text.strip():
|
|
19
|
+
return text.strip()
|
|
20
|
+
|
|
21
|
+
# Fallback: .annotations (plural) — older xmlschema
|
|
22
|
+
annotations = getattr(xsd_elem, 'annotations', None)
|
|
23
|
+
if annotations:
|
|
24
|
+
for ann in annotations:
|
|
25
|
+
docs = getattr(ann, 'documentation', None)
|
|
26
|
+
if docs:
|
|
27
|
+
for doc in docs:
|
|
28
|
+
text = getattr(doc, 'text', None)
|
|
29
|
+
if text and text.strip():
|
|
30
|
+
return text.strip()
|
|
31
|
+
|
|
32
|
+
return None
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Extract facets (validation constraints) from an XSD type, walking the base type chain."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from generate.helpers import get_facet_value, local_name, xsd_pattern_to_js
|
|
5
|
+
from generate.ir import Facets
|
|
6
|
+
def extract_facets(xsd_type: Any) -> Facets | None:
|
|
7
|
+
"""Walk base type chain, collect all facets. Return None if empty.
|
|
8
|
+
|
|
9
|
+
xmlschema API used:
|
|
10
|
+
XsdSimpleType.facets: dict[str, XsdFacet] — keyed by Clark-notation QName
|
|
11
|
+
XsdSimpleType.enumeration: iterable of values
|
|
12
|
+
XsdSimpleType.patterns: iterable of XsdPatternFacets
|
|
13
|
+
XsdSimpleType.base_type: XsdSimpleType | XsdComplexType | None
|
|
14
|
+
XsdFacet.value / .v / .min_value / .max_value — scalar facet value
|
|
15
|
+
XsdPatternFacets — iterable, each has .regexps or str()
|
|
16
|
+
XsdEnumerationFacets — iterable, each has .value or is str
|
|
17
|
+
"""
|
|
18
|
+
if xsd_type is None:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
facets = Facets()
|
|
22
|
+
visited: set[int] = set()
|
|
23
|
+
queue: list[Any] = [xsd_type]
|
|
24
|
+
|
|
25
|
+
while queue:
|
|
26
|
+
current = queue.pop(0)
|
|
27
|
+
if current is None or id(current) in visited:
|
|
28
|
+
continue
|
|
29
|
+
visited.add(id(current))
|
|
30
|
+
|
|
31
|
+
_collect_facets_from_type(current, facets)
|
|
32
|
+
|
|
33
|
+
# Walk base type chain
|
|
34
|
+
base = getattr(current, 'base_type', None)
|
|
35
|
+
if base is not None:
|
|
36
|
+
queue.append(base)
|
|
37
|
+
|
|
38
|
+
# Walk union member types
|
|
39
|
+
member_types = getattr(current, 'member_types', None)
|
|
40
|
+
if member_types:
|
|
41
|
+
for mt in member_types:
|
|
42
|
+
if mt is not None and id(mt) not in visited:
|
|
43
|
+
queue.append(mt)
|
|
44
|
+
|
|
45
|
+
# Walk simple_type (for complexType with simpleContent)
|
|
46
|
+
simple = getattr(current, 'simple_type', None)
|
|
47
|
+
if simple is not None and id(simple) not in visited:
|
|
48
|
+
queue.append(simple)
|
|
49
|
+
|
|
50
|
+
return None if facets.is_empty() else facets
|
|
51
|
+
def _collect_facets_from_type(current: Any, facets: Facets) -> None:
|
|
52
|
+
"""Read facets dict from a single type level and populate the Facets dataclass."""
|
|
53
|
+
raw_facets = getattr(current, 'facets', None)
|
|
54
|
+
if not raw_facets:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# Ensure it's dict-like
|
|
58
|
+
items = raw_facets.items() if hasattr(raw_facets, 'items') else []
|
|
59
|
+
|
|
60
|
+
for name, facet in items:
|
|
61
|
+
local = local_name(str(name))
|
|
62
|
+
match local:
|
|
63
|
+
case 'enumeration':
|
|
64
|
+
new_enums = _extract_enumeration(current, facet)
|
|
65
|
+
if new_enums:
|
|
66
|
+
if facets.enumeration is None:
|
|
67
|
+
facets.enumeration = new_enums
|
|
68
|
+
else:
|
|
69
|
+
existing = set(facets.enumeration)
|
|
70
|
+
facets.enumeration.extend(v for v in new_enums if v not in existing)
|
|
71
|
+
case 'pattern':
|
|
72
|
+
new_patterns = _extract_patterns(current, facet)
|
|
73
|
+
if new_patterns:
|
|
74
|
+
if facets.pattern is None:
|
|
75
|
+
facets.pattern = new_patterns
|
|
76
|
+
else:
|
|
77
|
+
existing = set(facets.pattern)
|
|
78
|
+
facets.pattern.extend(p for p in new_patterns if p not in existing)
|
|
79
|
+
case 'minLength':
|
|
80
|
+
if facets.min_length is None:
|
|
81
|
+
facets.min_length = get_facet_value(facet)
|
|
82
|
+
case 'maxLength':
|
|
83
|
+
if facets.max_length is None:
|
|
84
|
+
facets.max_length = get_facet_value(facet)
|
|
85
|
+
case 'length':
|
|
86
|
+
if facets.length is None:
|
|
87
|
+
facets.length = get_facet_value(facet)
|
|
88
|
+
case 'minInclusive':
|
|
89
|
+
if facets.min_inclusive is None:
|
|
90
|
+
facets.min_inclusive = get_facet_value(facet)
|
|
91
|
+
case 'maxInclusive':
|
|
92
|
+
if facets.max_inclusive is None:
|
|
93
|
+
facets.max_inclusive = get_facet_value(facet)
|
|
94
|
+
case 'minExclusive':
|
|
95
|
+
if facets.min_exclusive is None:
|
|
96
|
+
facets.min_exclusive = get_facet_value(facet)
|
|
97
|
+
case 'maxExclusive':
|
|
98
|
+
if facets.max_exclusive is None:
|
|
99
|
+
facets.max_exclusive = get_facet_value(facet)
|
|
100
|
+
case 'totalDigits':
|
|
101
|
+
if facets.total_digits is None:
|
|
102
|
+
facets.total_digits = get_facet_value(facet)
|
|
103
|
+
case 'fractionDigits':
|
|
104
|
+
if facets.fraction_digits is None:
|
|
105
|
+
facets.fraction_digits = get_facet_value(facet)
|
|
106
|
+
case 'whiteSpace':
|
|
107
|
+
if facets.white_space is None:
|
|
108
|
+
facets.white_space = get_facet_value(facet)
|
|
109
|
+
def _extract_enumeration(xsd_type: Any, facet: Any) -> list[str]:
|
|
110
|
+
"""Extract enumeration values from a type or its facet object.
|
|
111
|
+
|
|
112
|
+
xmlschema stores enumerations in multiple ways:
|
|
113
|
+
- XsdSimpleType.enumeration → list of values directly
|
|
114
|
+
- XsdEnumerationFacets → iterable, .values or .enumeration
|
|
115
|
+
- Fallback: scan raw elem for xs:enumeration value=...
|
|
116
|
+
"""
|
|
117
|
+
# Direct enumeration property on the type
|
|
118
|
+
enum = getattr(xsd_type, 'enumeration', None)
|
|
119
|
+
if enum:
|
|
120
|
+
return [str(v) for v in enum]
|
|
121
|
+
|
|
122
|
+
# Facet object approaches
|
|
123
|
+
values = getattr(facet, 'values', None)
|
|
124
|
+
if values:
|
|
125
|
+
return [str(v) for v in values]
|
|
126
|
+
|
|
127
|
+
enum_list = getattr(facet, 'enumeration', None)
|
|
128
|
+
if enum_list:
|
|
129
|
+
result = []
|
|
130
|
+
for item in enum_list:
|
|
131
|
+
v = getattr(item, 'value', None)
|
|
132
|
+
result.append(str(v) if v is not None else str(item))
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
# Raw XML fallback
|
|
136
|
+
elem = getattr(xsd_type, 'elem', None)
|
|
137
|
+
if elem is not None:
|
|
138
|
+
result = []
|
|
139
|
+
for child in elem.iter():
|
|
140
|
+
tag = getattr(child, 'tag', '')
|
|
141
|
+
if 'enumeration' in str(tag):
|
|
142
|
+
val = child.get('value')
|
|
143
|
+
if val is not None:
|
|
144
|
+
result.append(val)
|
|
145
|
+
if result:
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
return []
|
|
149
|
+
def _extract_patterns(xsd_type: Any, facet: Any) -> list[str]:
|
|
150
|
+
"""Extract regex pattern strings from a type or facet.
|
|
151
|
+
|
|
152
|
+
xmlschema API:
|
|
153
|
+
XsdPatternFacets.regexps: list[str] — the actual regex strings
|
|
154
|
+
XsdSimpleType.patterns: XsdPatternFacets — same object as facet dict entry
|
|
155
|
+
"""
|
|
156
|
+
# XsdPatternFacets (the facet dict entry) has .regexps with actual regex strings
|
|
157
|
+
regexps = getattr(facet, 'regexps', None)
|
|
158
|
+
if regexps:
|
|
159
|
+
return [xsd_pattern_to_js(str(r)) for r in regexps]
|
|
160
|
+
|
|
161
|
+
# Fallback: via type.patterns property (same object, different access path)
|
|
162
|
+
patterns = getattr(xsd_type, 'patterns', None)
|
|
163
|
+
if patterns:
|
|
164
|
+
regexps = getattr(patterns, 'regexps', None)
|
|
165
|
+
if regexps:
|
|
166
|
+
return [xsd_pattern_to_js(str(r)) for r in regexps]
|
|
167
|
+
|
|
168
|
+
return []
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Extract Namespace from an XSD element."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from generate.helpers import local_name, namespace_uri
|
|
5
|
+
from generate.ir import Namespace
|
|
6
|
+
def extract_namespace(xsd_elem: Any) -> Namespace:
|
|
7
|
+
"""Build a Namespace from an XsdElement.
|
|
8
|
+
|
|
9
|
+
Uses:
|
|
10
|
+
- xsd_elem.target_namespace → URI
|
|
11
|
+
- xsd_elem.schema.namespaces → prefix lookup
|
|
12
|
+
- xsd_elem.prefixed_name fallback
|
|
13
|
+
|
|
14
|
+
xmlschema API:
|
|
15
|
+
XsdComponent.target_namespace: str
|
|
16
|
+
XMLSchemaBase.namespaces: dict[str, str] (prefix → uri)
|
|
17
|
+
"""
|
|
18
|
+
uri = getattr(xsd_elem, 'target_namespace', '') or ''
|
|
19
|
+
prefix = _resolve_prefix(xsd_elem, uri)
|
|
20
|
+
return Namespace(prefix=prefix, uri=uri)
|
|
21
|
+
def extract_attr_namespace(xsd_attr: Any) -> Namespace | None:
|
|
22
|
+
"""Build a Namespace for an XsdAttribute, or None if it's in the element's own namespace.
|
|
23
|
+
|
|
24
|
+
A namespace-qualified attribute has a non-empty target_namespace that differs
|
|
25
|
+
from its parent element's target_namespace.
|
|
26
|
+
|
|
27
|
+
xmlschema API:
|
|
28
|
+
XsdAttribute.target_namespace: str
|
|
29
|
+
XsdAttribute.qualified: bool
|
|
30
|
+
XsdAttribute.name: str (Clark notation '{uri}local')
|
|
31
|
+
"""
|
|
32
|
+
name = getattr(xsd_attr, 'name', '') or ''
|
|
33
|
+
attr_ns = namespace_uri(name)
|
|
34
|
+
|
|
35
|
+
if not attr_ns:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
uri = attr_ns
|
|
39
|
+
prefix = _resolve_prefix(xsd_attr, uri)
|
|
40
|
+
if not prefix:
|
|
41
|
+
return None
|
|
42
|
+
return Namespace(prefix=prefix, uri=uri)
|
|
43
|
+
def _resolve_prefix(component: Any, uri: str) -> str:
|
|
44
|
+
"""Find the prefix for a namespace URI by walking up to the schema's namespace map."""
|
|
45
|
+
if not uri:
|
|
46
|
+
return ''
|
|
47
|
+
|
|
48
|
+
schema = getattr(component, 'schema', None)
|
|
49
|
+
if schema is None:
|
|
50
|
+
return ''
|
|
51
|
+
|
|
52
|
+
namespaces = getattr(schema, 'namespaces', {}) or {}
|
|
53
|
+
for pfx, ns_uri in namespaces.items():
|
|
54
|
+
if ns_uri == uri and not pfx:
|
|
55
|
+
return ''
|
|
56
|
+
for pfx, ns_uri in namespaces.items():
|
|
57
|
+
if ns_uri == uri and pfx:
|
|
58
|
+
return pfx
|
|
59
|
+
return ''
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Inject explicitly mapped global attributes from the schema into target elements.
|
|
2
|
+
|
|
3
|
+
Reads ``attribute-mapping.json`` from the same directory as the entry XSD.
|
|
4
|
+
Format::
|
|
5
|
+
|
|
6
|
+
{
|
|
7
|
+
"ElementName": {
|
|
8
|
+
"prefix:localName": "namespace-uri",
|
|
9
|
+
...
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
The key ``prefix:localName`` becomes the attribute key in ``attr_sequence`` and
|
|
14
|
+
``attributes``. The namespace URI is used to look up the global ``xs:attribute``
|
|
15
|
+
declaration in ``schema.maps.attributes`` via Clark notation ``{uri}localName``.
|
|
16
|
+
|
|
17
|
+
No heuristics — only explicitly declared mappings are injected.
|
|
18
|
+
Pattern mirrors ``orphans.py``.
|
|
19
|
+
"""
|
|
20
|
+
import json
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from generate.extractors.facets import extract_facets
|
|
25
|
+
from generate.extractors.namespace import extract_attr_namespace
|
|
26
|
+
from generate.ir import AttributeDef, ElementDef
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_attr_mapping(entry_path: Path) -> dict[str, dict[str, str]]:
|
|
30
|
+
"""Read attribute-mapping.json from the same directory as the entry XSD.
|
|
31
|
+
|
|
32
|
+
Returns empty dict if the file does not exist.
|
|
33
|
+
"""
|
|
34
|
+
mapping_file = entry_path.parent / 'attribute-mapping.json'
|
|
35
|
+
if not mapping_file.exists():
|
|
36
|
+
return {}
|
|
37
|
+
with open(mapping_file) as f:
|
|
38
|
+
return json.load(f)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def inject_mapped_attributes(
|
|
42
|
+
schema: Any,
|
|
43
|
+
elements: dict[str, ElementDef],
|
|
44
|
+
mapping: dict[str, dict[str, str]],
|
|
45
|
+
) -> int:
|
|
46
|
+
"""Inject global attributes into elements as declared in the mapping.
|
|
47
|
+
|
|
48
|
+
For each ``(element_name, attr_key, ns_uri)`` in the mapping:
|
|
49
|
+
- Looks up the global attr in ``schema.maps.attributes`` by Clark name ``{ns_uri}local``.
|
|
50
|
+
- Builds an ``AttributeDef`` from the global attr declaration.
|
|
51
|
+
- Injects into ``element.attributes[attr_key]`` and ``element.attr_sequence``.
|
|
52
|
+
- Idempotent: skips if key already present.
|
|
53
|
+
|
|
54
|
+
Returns total number of attributes injected.
|
|
55
|
+
"""
|
|
56
|
+
maps = getattr(schema, 'maps', None)
|
|
57
|
+
global_attrs = getattr(maps, 'attributes', None) if maps else {}
|
|
58
|
+
|
|
59
|
+
injected = 0
|
|
60
|
+
|
|
61
|
+
for element_name, attr_entries in mapping.items():
|
|
62
|
+
elem = elements.get(element_name)
|
|
63
|
+
if elem is None:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
for key, ns_uri in attr_entries.items():
|
|
67
|
+
if key in elem.attributes:
|
|
68
|
+
continue # already present
|
|
69
|
+
|
|
70
|
+
# Derive local name from key (strip prefix if present)
|
|
71
|
+
local = key.split(':', 1)[-1]
|
|
72
|
+
clark = f'{{{ns_uri}}}{local}'
|
|
73
|
+
xsd_attr = global_attrs.get(clark)
|
|
74
|
+
if xsd_attr is None:
|
|
75
|
+
continue # not found in schema — skip silently
|
|
76
|
+
|
|
77
|
+
attr_ns = extract_attr_namespace(xsd_attr)
|
|
78
|
+
fixed = getattr(xsd_attr, 'fixed', None)
|
|
79
|
+
default = getattr(xsd_attr, 'default', None) if fixed is None else None
|
|
80
|
+
use = getattr(xsd_attr, 'use', 'optional')
|
|
81
|
+
attr_type = getattr(xsd_attr, 'type', None)
|
|
82
|
+
|
|
83
|
+
elem.attributes[key] = AttributeDef(
|
|
84
|
+
required=use == 'required',
|
|
85
|
+
default=default,
|
|
86
|
+
fixed=fixed,
|
|
87
|
+
namespace=attr_ns,
|
|
88
|
+
facets=extract_facets(attr_type),
|
|
89
|
+
)
|
|
90
|
+
elem.attr_sequence.append(key)
|
|
91
|
+
injected += 1
|
|
92
|
+
|
|
93
|
+
if any(k in elem.attr_sequence for k in attr_entries):
|
|
94
|
+
elem.attr_sequence.sort()
|
|
95
|
+
|
|
96
|
+
return injected
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
return injected
|