@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,117 @@
|
|
|
1
|
+
"""Deriver — compute transitive graphs and derived constants from collected IR.
|
|
2
|
+
|
|
3
|
+
Phase 3 of the pipeline: Parse → Collect → **Derive** → Emit.
|
|
4
|
+
"""
|
|
5
|
+
from generate.ir import ElementDef, IdentityConstraint
|
|
6
|
+
def derive_graph(
|
|
7
|
+
elements: dict[str, ElementDef],
|
|
8
|
+
) -> tuple[dict[str, list[str]], dict[str, list[str]]]:
|
|
9
|
+
"""Compute transitive DESCENDANTS and ANCESTORS from CHILDREN/PARENTS.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
(descendants, ancestors) — each is a dict mapping element name to sorted list.
|
|
13
|
+
"""
|
|
14
|
+
children_map = {name: list(e.children.keys()) for name, e in elements.items()}
|
|
15
|
+
parents_map = {name: list(e.parents) for name, e in elements.items()}
|
|
16
|
+
|
|
17
|
+
def transitive(graph: dict[str, list[str]], start: str) -> list[str]:
|
|
18
|
+
result: list[str] = []
|
|
19
|
+
visited: set[str] = set()
|
|
20
|
+
queue = list(graph.get(start, []))
|
|
21
|
+
while queue:
|
|
22
|
+
node = queue.pop(0)
|
|
23
|
+
if node in visited:
|
|
24
|
+
continue
|
|
25
|
+
visited.add(node)
|
|
26
|
+
result.append(node)
|
|
27
|
+
queue.extend(graph.get(node, []))
|
|
28
|
+
return sorted(result)
|
|
29
|
+
|
|
30
|
+
descendants = {name: transitive(children_map, name) for name in elements}
|
|
31
|
+
ancestors = {name: transitive(parents_map, name) for name in elements}
|
|
32
|
+
return descendants, ancestors
|
|
33
|
+
def derive_root_element(elements: dict[str, ElementDef], override: str | None = None) -> str:
|
|
34
|
+
"""Find the document root element.
|
|
35
|
+
|
|
36
|
+
If *override* is provided, use it directly (raising if absent).
|
|
37
|
+
Otherwise expect exactly one parentless element.
|
|
38
|
+
"""
|
|
39
|
+
if override:
|
|
40
|
+
if override not in elements:
|
|
41
|
+
raise ValueError(f'Root override {override!r} not found in elements')
|
|
42
|
+
return override
|
|
43
|
+
|
|
44
|
+
roots = sorted(name for name, e in elements.items() if not e.parents)
|
|
45
|
+
if len(roots) != 1:
|
|
46
|
+
raise ValueError(f'Expected exactly 1 root element, found: {roots}')
|
|
47
|
+
return roots[0]
|
|
48
|
+
def derive_singleton_elements(elements: dict[str, ElementDef], root_name: str) -> list[str]:
|
|
49
|
+
"""Find elements that can appear at most once in the entire document.
|
|
50
|
+
|
|
51
|
+
An element is a document singleton if:
|
|
52
|
+
- It has maxOccurs <= 1 in every parent context (local singleton), AND
|
|
53
|
+
- All of its parents are also document singletons (transitive).
|
|
54
|
+
|
|
55
|
+
Computed via fixpoint: start from root, propagate singleton status downward.
|
|
56
|
+
"""
|
|
57
|
+
# Step 1: local singletons — maxOccurs <= 1 everywhere
|
|
58
|
+
local_singleton: set[str] = set(elements.keys())
|
|
59
|
+
for elem in elements.values():
|
|
60
|
+
for child_name, child_def in elem.children.items():
|
|
61
|
+
if child_def.max_occurs is None or child_def.max_occurs > 1:
|
|
62
|
+
local_singleton.discard(child_name)
|
|
63
|
+
|
|
64
|
+
# Step 2: fixpoint — only keep elements whose entire parent chain is singleton
|
|
65
|
+
doc_singletons: set[str] = {root_name}
|
|
66
|
+
changed = True
|
|
67
|
+
while changed:
|
|
68
|
+
changed = False
|
|
69
|
+
for name, elem in elements.items():
|
|
70
|
+
if name in doc_singletons or name not in local_singleton:
|
|
71
|
+
continue
|
|
72
|
+
if elem.parents and all(p in doc_singletons for p in elem.parents):
|
|
73
|
+
doc_singletons.add(name)
|
|
74
|
+
changed = True
|
|
75
|
+
|
|
76
|
+
return sorted(doc_singletons)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def derive_identity_fields(elements: dict[str, ElementDef]) -> dict[str, list[str]]:
|
|
80
|
+
"""Precompute which attributes participate in identity constraints per element.
|
|
81
|
+
|
|
82
|
+
Scans all element-level constraints, resolves selector targets,
|
|
83
|
+
and assigns field attributes to targeted elements.
|
|
84
|
+
Returns element name -> sorted list of attribute names used in unique/key fields.
|
|
85
|
+
"""
|
|
86
|
+
result: dict[str, set[str]] = {}
|
|
87
|
+
for element in elements.values():
|
|
88
|
+
for constraint in element.constraints:
|
|
89
|
+
for target in _resolve_constraint_targets(constraint, elements):
|
|
90
|
+
result.setdefault(target, set())
|
|
91
|
+
result[target] |= _extract_attribute_fields(constraint)
|
|
92
|
+
return {name: sorted(fields) for name, fields in result.items() if fields}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _resolve_constraint_targets(
|
|
96
|
+
constraint: IdentityConstraint, elements: dict[str, ElementDef]
|
|
97
|
+
) -> set[str]:
|
|
98
|
+
"""Find which element names a constraint's selector targets."""
|
|
99
|
+
if constraint.kind == 'keyref':
|
|
100
|
+
return set()
|
|
101
|
+
targets: set[str] = set()
|
|
102
|
+
for path in constraint.selector:
|
|
103
|
+
for step in path.steps:
|
|
104
|
+
if step.kind == 'name' and step.value in elements:
|
|
105
|
+
targets.add(step.value)
|
|
106
|
+
return targets
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _extract_attribute_fields(constraint: IdentityConstraint) -> set[str]:
|
|
110
|
+
"""Extract attribute names from a constraint's field targets."""
|
|
111
|
+
if constraint.kind == 'keyref':
|
|
112
|
+
return set()
|
|
113
|
+
return {
|
|
114
|
+
f.target.value
|
|
115
|
+
for f in constraint.fields
|
|
116
|
+
if f.target.is_attribute and f.target.value
|
|
117
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Emitters — one per output file
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Emit constants.generated.ts — lookup tables and derived constants."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from generate.emitters.ts_helpers import ts_key, ts_record, ts_string_array
|
|
5
|
+
from generate.ir import ElementDef
|
|
6
|
+
def emit_constants(
|
|
7
|
+
elements: dict[str, ElementDef],
|
|
8
|
+
descendants: dict[str, list[str]],
|
|
9
|
+
ancestors: dict[str, list[str]],
|
|
10
|
+
root_element: str,
|
|
11
|
+
singleton_elements: list[str],
|
|
12
|
+
out: Path,
|
|
13
|
+
) -> None:
|
|
14
|
+
"""Write constants.generated.ts with all lookup tables."""
|
|
15
|
+
names = sorted(elements.keys())
|
|
16
|
+
|
|
17
|
+
lines = [
|
|
18
|
+
'// Auto-generated by generate.py — do not edit',
|
|
19
|
+
'',
|
|
20
|
+
"import type { AvailableElement, AttributesOf } from './types.generated'",
|
|
21
|
+
'',
|
|
22
|
+
f'export const ELEMENT_NAMES = {ts_string_array(names)} as const',
|
|
23
|
+
'',
|
|
24
|
+
f'export const CHILDREN = {ts_record(_child_map(elements))} as const',
|
|
25
|
+
'',
|
|
26
|
+
f'export const PARENTS = {ts_record(_parent_map(elements))} as const',
|
|
27
|
+
'',
|
|
28
|
+
f'export const DESCENDANTS = {ts_record(descendants)} as const',
|
|
29
|
+
'',
|
|
30
|
+
f'export const ANCESTORS = {ts_record(ancestors)} as const',
|
|
31
|
+
'',
|
|
32
|
+
f"export const ROOT_ELEMENT = '{root_element}' as const",
|
|
33
|
+
'',
|
|
34
|
+
f'export const SINGLETON_ELEMENTS = {ts_string_array(singleton_elements)} as const',
|
|
35
|
+
'',
|
|
36
|
+
f'export const ATTRIBUTES = {_attr_obj_ts(elements)} as const',
|
|
37
|
+
'',
|
|
38
|
+
f'export const REQUIRED_ATTRIBUTES = {ts_record(_required_attr_map(elements))} as const satisfies Record<AvailableElement, readonly string[]>',
|
|
39
|
+
'',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
out.write_text('\n'.join(lines), encoding='utf-8')
|
|
43
|
+
def _child_map(elements: dict[str, ElementDef]) -> dict[str, list[str]]:
|
|
44
|
+
"""Element name → sorted child element names."""
|
|
45
|
+
return {name: list(e.child_sequence) for name, e in elements.items()}
|
|
46
|
+
def _parent_map(elements: dict[str, ElementDef]) -> dict[str, list[str]]:
|
|
47
|
+
"""Element name → sorted parent element names."""
|
|
48
|
+
return {name: sorted(e.parents) for name, e in elements.items()}
|
|
49
|
+
def _attr_obj_ts(elements: dict[str, ElementDef]) -> str:
|
|
50
|
+
"""Generate ATTRIBUTES runtime object: each element → keyed record with '' values cast via AttributesOf."""
|
|
51
|
+
lines = ['{']
|
|
52
|
+
for name in sorted(elements.keys()):
|
|
53
|
+
elem = elements[name]
|
|
54
|
+
if not elem.attr_sequence:
|
|
55
|
+
lines.append(f"\t{name}: {{}} as AttributesOf<'{name}'>,")
|
|
56
|
+
else:
|
|
57
|
+
lines.append(f'\t{name}: {{')
|
|
58
|
+
for attr_name in elem.attr_sequence:
|
|
59
|
+
key = ts_key(attr_name)
|
|
60
|
+
lines.append(f"\t\t{key}: '' as string,")
|
|
61
|
+
lines.append(f"\t}} as AttributesOf<'{name}'>,")
|
|
62
|
+
lines.append('}')
|
|
63
|
+
return '\n'.join(lines)
|
|
64
|
+
def _required_attr_map(elements: dict[str, ElementDef]) -> dict[str, list[str]]:
|
|
65
|
+
"""Element name → required attribute names (sorted)."""
|
|
66
|
+
return {
|
|
67
|
+
name: sorted(aname for aname, adef in e.attributes.items() if adef.required)
|
|
68
|
+
for name, e in elements.items()
|
|
69
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Emit definition.generated.ts — the DEFINITION constant."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from generate.emitters.ts_helpers import (
|
|
5
|
+
ts_attr_block,
|
|
6
|
+
ts_child_block,
|
|
7
|
+
ts_constraints,
|
|
8
|
+
ts_namespace,
|
|
9
|
+
ts_string,
|
|
10
|
+
ts_text_content,
|
|
11
|
+
)
|
|
12
|
+
from generate.ir import ElementDef
|
|
13
|
+
def emit_definition(elements: dict[str, ElementDef], out: Path) -> None:
|
|
14
|
+
"""Write the DEFINITION constant to definition.generated.ts.
|
|
15
|
+
|
|
16
|
+
Sparse — only non-default fields emitted per element.
|
|
17
|
+
"""
|
|
18
|
+
lines = [
|
|
19
|
+
'// Auto-generated by generate.py — do not edit',
|
|
20
|
+
'',
|
|
21
|
+
'export const DEFINITION = {',
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
for name in sorted(elements):
|
|
25
|
+
elem = elements[name]
|
|
26
|
+
lines.append(f'\t{name}: {{')
|
|
27
|
+
lines.append(f"\t\ttag: '{name}',")
|
|
28
|
+
lines.append(f'\t\tnamespace: {ts_namespace(elem.namespace)},')
|
|
29
|
+
if elem.documentation:
|
|
30
|
+
lines.append(f'\t\tdocumentation: {ts_string(elem.documentation)},')
|
|
31
|
+
lines.append(f'\t\tparents: {_inline_string_array(elem.parents)},')
|
|
32
|
+
lines.append(f'\t\tattributes: {ts_attr_block(elem, "\t\t\t")},')
|
|
33
|
+
lines.append(f'\t\tchildren: {ts_child_block(elem, "\t\t\t")},')
|
|
34
|
+
if elem.constraints:
|
|
35
|
+
lines.append(f'\t\tconstraints: {ts_constraints(elem.constraints, "\t\t\t")},')
|
|
36
|
+
if elem.text_content:
|
|
37
|
+
lines.append(f'\t\ttextContent: {ts_text_content(elem.text_content, "\t\t\t")},')
|
|
38
|
+
lines.append('\t},')
|
|
39
|
+
|
|
40
|
+
lines.append('} as const')
|
|
41
|
+
lines.append('')
|
|
42
|
+
|
|
43
|
+
out.write_text('\n'.join(lines), encoding='utf-8')
|
|
44
|
+
def _inline_string_array(items: list[str]) -> str:
|
|
45
|
+
"""Short array on one line."""
|
|
46
|
+
if not items:
|
|
47
|
+
return '[]'
|
|
48
|
+
inner = ', '.join(f"'{i}'" for i in items)
|
|
49
|
+
return f'[{inner}]'
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Shared emission helpers for converting IR to TypeScript literals."""
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from generate.ir import (
|
|
6
|
+
AttributeDef,
|
|
7
|
+
ChildDef,
|
|
8
|
+
ChoiceGroup,
|
|
9
|
+
ElementDef,
|
|
10
|
+
Facets,
|
|
11
|
+
IdentityConstraint,
|
|
12
|
+
Namespace,
|
|
13
|
+
TextContent,
|
|
14
|
+
)
|
|
15
|
+
from generate.xpath_parser import FieldPath, FieldTarget, SelectorPath, XPathStep
|
|
16
|
+
def sparse(d: dict[str, Any]) -> dict[str, Any]:
|
|
17
|
+
"""Drop keys whose values are default (None, False, [], 0, {})."""
|
|
18
|
+
return {
|
|
19
|
+
k: v
|
|
20
|
+
for k, v in d.items()
|
|
21
|
+
if v is not None and v is not False and v != [] and v != 0 and v != {}
|
|
22
|
+
}
|
|
23
|
+
def ts_string(s: str) -> str:
|
|
24
|
+
"""Emit a TS string literal, escaping quotes and backslashes."""
|
|
25
|
+
return json.dumps(s, ensure_ascii=False)
|
|
26
|
+
def ts_string_array(items: list[str], indent: str = '') -> str:
|
|
27
|
+
"""Emit a TS readonly string array literal."""
|
|
28
|
+
if not items:
|
|
29
|
+
return '[]'
|
|
30
|
+
inner = ', '.join(ts_string(i) for i in items)
|
|
31
|
+
return f'[{inner}]'
|
|
32
|
+
def ts_record(data: dict[str, list[str]], indent: str = '\t') -> str:
|
|
33
|
+
"""Emit a TS Record<string, string[]> literal."""
|
|
34
|
+
if not data:
|
|
35
|
+
return '{}'
|
|
36
|
+
lines = ['{']
|
|
37
|
+
for key in sorted(data.keys()):
|
|
38
|
+
arr = ts_string_array(data[key])
|
|
39
|
+
lines.append(f'{indent}{ts_key(key)}: {arr},')
|
|
40
|
+
lines.append('}')
|
|
41
|
+
return '\n'.join(lines)
|
|
42
|
+
def ts_key(name: str) -> str:
|
|
43
|
+
"""Quote a TS object key if it contains special characters."""
|
|
44
|
+
if ':' in name or '-' in name or ' ' in name or not name.isidentifier():
|
|
45
|
+
return ts_string(name)
|
|
46
|
+
return name
|
|
47
|
+
def ts_namespace(ns: Namespace) -> str:
|
|
48
|
+
"""Emit a Namespace object literal."""
|
|
49
|
+
return f"{{ prefix: {ts_string(ns.prefix)}, uri: {ts_string(ns.uri)} }}"
|
|
50
|
+
def ts_facets(facets: Facets | None, indent: str = '\t\t\t') -> str:
|
|
51
|
+
"""Emit a sparse Facets literal."""
|
|
52
|
+
if facets is None or facets.is_empty():
|
|
53
|
+
return 'undefined'
|
|
54
|
+
|
|
55
|
+
fields: dict[str, Any] = {}
|
|
56
|
+
if facets.enumeration is not None:
|
|
57
|
+
fields['enumeration'] = facets.enumeration
|
|
58
|
+
if facets.pattern is not None:
|
|
59
|
+
fields['pattern'] = facets.pattern
|
|
60
|
+
if facets.min_length is not None:
|
|
61
|
+
fields['minLength'] = facets.min_length
|
|
62
|
+
if facets.max_length is not None:
|
|
63
|
+
fields['maxLength'] = facets.max_length
|
|
64
|
+
if facets.length is not None:
|
|
65
|
+
fields['length'] = facets.length
|
|
66
|
+
if facets.min_inclusive is not None:
|
|
67
|
+
fields['minInclusive'] = facets.min_inclusive
|
|
68
|
+
if facets.max_inclusive is not None:
|
|
69
|
+
fields['maxInclusive'] = facets.max_inclusive
|
|
70
|
+
if facets.min_exclusive is not None:
|
|
71
|
+
fields['minExclusive'] = facets.min_exclusive
|
|
72
|
+
if facets.max_exclusive is not None:
|
|
73
|
+
fields['maxExclusive'] = facets.max_exclusive
|
|
74
|
+
if facets.total_digits is not None:
|
|
75
|
+
fields['totalDigits'] = facets.total_digits
|
|
76
|
+
if facets.fraction_digits is not None:
|
|
77
|
+
fields['fractionDigits'] = facets.fraction_digits
|
|
78
|
+
if facets.white_space is not None:
|
|
79
|
+
fields['whiteSpace'] = facets.white_space
|
|
80
|
+
|
|
81
|
+
if not fields:
|
|
82
|
+
return 'undefined'
|
|
83
|
+
|
|
84
|
+
return _emit_object(fields, indent)
|
|
85
|
+
def ts_attr_block(elem: ElementDef, indent: str = '\t\t') -> str:
|
|
86
|
+
"""Emit the attributes block for an element."""
|
|
87
|
+
lines = ['{']
|
|
88
|
+
lines.append(f'{indent}sequence: {ts_string_array(elem.attr_sequence)},')
|
|
89
|
+
if elem.attr_any:
|
|
90
|
+
lines.append(f'{indent}any: true,')
|
|
91
|
+
lines.append(f'{indent}details: {{')
|
|
92
|
+
for key in elem.attr_sequence:
|
|
93
|
+
attr = elem.attributes[key]
|
|
94
|
+
lines.append(f'{indent}\t{ts_key(key)}: {_ts_attr_def(attr, indent + "\t\t")},')
|
|
95
|
+
lines.append(f'{indent}}},')
|
|
96
|
+
if elem.identity_fields:
|
|
97
|
+
lines.append(f'{indent}identityFields: {ts_string_array(elem.identity_fields)},')
|
|
98
|
+
lines.append(f'{indent[:-1]}}}')
|
|
99
|
+
return '\n'.join(lines)
|
|
100
|
+
def ts_child_block(elem: ElementDef, indent: str = '\t\t') -> str:
|
|
101
|
+
"""Emit the children block for an element."""
|
|
102
|
+
lines = ['{']
|
|
103
|
+
lines.append(f'{indent}sequence: {ts_string_array(elem.child_sequence)},')
|
|
104
|
+
if elem.child_any:
|
|
105
|
+
lines.append(f'{indent}any: true,')
|
|
106
|
+
lines.append(f'{indent}details: {{')
|
|
107
|
+
for key in elem.child_sequence:
|
|
108
|
+
child = elem.children[key]
|
|
109
|
+
lines.append(f'{indent}\t{ts_key(key)}: {_ts_child_def(child, indent + "\t\t")},')
|
|
110
|
+
lines.append(f'{indent}}},')
|
|
111
|
+
if elem.choices:
|
|
112
|
+
lines.append(f'{indent}choices: {_ts_choices(elem.choices, indent + "\t")},')
|
|
113
|
+
lines.append(f'{indent[:-1]}}}')
|
|
114
|
+
return '\n'.join(lines)
|
|
115
|
+
def ts_constraints(constraints: list[IdentityConstraint], indent: str = '\t\t') -> str:
|
|
116
|
+
"""Emit an array of constraints."""
|
|
117
|
+
if not constraints:
|
|
118
|
+
return '[]'
|
|
119
|
+
lines = ['[']
|
|
120
|
+
for c in constraints:
|
|
121
|
+
lines.append(f'{indent}{_ts_constraint(c, indent + "\t")},')
|
|
122
|
+
lines.append(f'{indent[:-1]}]')
|
|
123
|
+
return '\n'.join(lines)
|
|
124
|
+
def ts_text_content(tc: TextContent, indent: str = '\t\t') -> str:
|
|
125
|
+
"""Emit a TextContent literal."""
|
|
126
|
+
f = ts_facets(tc.facets, indent + '\t')
|
|
127
|
+
if f == 'undefined':
|
|
128
|
+
return '{}'
|
|
129
|
+
return f'{{ facets: {f} }}'
|
|
130
|
+
# --- Private helpers ---
|
|
131
|
+
def _ts_attr_def(attr: AttributeDef, indent: str) -> str:
|
|
132
|
+
"""Emit a sparse AttributeDefinition literal."""
|
|
133
|
+
fields: dict[str, Any] = {}
|
|
134
|
+
if attr.required:
|
|
135
|
+
fields['required'] = True
|
|
136
|
+
if attr.default is not None:
|
|
137
|
+
fields['default'] = attr.default
|
|
138
|
+
if attr.fixed is not None:
|
|
139
|
+
fields['fixed'] = attr.fixed
|
|
140
|
+
if attr.namespace is not None:
|
|
141
|
+
fields['namespace'] = attr.namespace
|
|
142
|
+
if attr.facets is not None and not attr.facets.is_empty():
|
|
143
|
+
fields['facets'] = attr.facets
|
|
144
|
+
|
|
145
|
+
if not fields:
|
|
146
|
+
return '{}'
|
|
147
|
+
return _emit_object(fields, indent)
|
|
148
|
+
def _ts_child_def(child: ChildDef, indent: str) -> str:
|
|
149
|
+
"""Emit a sparse ChildDefinition literal."""
|
|
150
|
+
fields: dict[str, Any] = {}
|
|
151
|
+
if child.required:
|
|
152
|
+
fields['required'] = True
|
|
153
|
+
if child.min_occurs != 0:
|
|
154
|
+
fields['minOccurs'] = child.min_occurs
|
|
155
|
+
if child.max_occurs is not None:
|
|
156
|
+
fields['maxOccurs'] = child.max_occurs
|
|
157
|
+
if child.constraints:
|
|
158
|
+
fields['constraints'] = child.constraints
|
|
159
|
+
|
|
160
|
+
if not fields:
|
|
161
|
+
return '{}'
|
|
162
|
+
return _emit_object(fields, indent)
|
|
163
|
+
def _ts_constraint(c: IdentityConstraint, indent: str) -> str:
|
|
164
|
+
"""Emit a single IdentityConstraint literal."""
|
|
165
|
+
fields: dict[str, Any] = {'kind': c.kind, 'name': c.name}
|
|
166
|
+
if c.refer:
|
|
167
|
+
fields['refer'] = c.refer
|
|
168
|
+
if c.deep:
|
|
169
|
+
fields['deep'] = True
|
|
170
|
+
# Emit as raw TS — handled below for structured types
|
|
171
|
+
parts: list[str] = []
|
|
172
|
+
for key, value in fields.items():
|
|
173
|
+
parts.append(f'{ts_key(key)}: {_emit_value(value, indent)}')
|
|
174
|
+
parts.append(f'selector: {_ts_selector_paths(c.selector)}')
|
|
175
|
+
parts.append(f'fields: {_ts_field_paths(c.fields)}')
|
|
176
|
+
inner = ', '.join(parts)
|
|
177
|
+
return '{ ' + inner + ' }'
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _ts_selector_paths(paths: list[SelectorPath]) -> str:
|
|
181
|
+
"""Emit selector paths array."""
|
|
182
|
+
if not paths:
|
|
183
|
+
return '[]'
|
|
184
|
+
items = [_ts_selector_path(p) for p in paths]
|
|
185
|
+
return '[' + ', '.join(items) + ']'
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _ts_selector_path(p: SelectorPath) -> str:
|
|
189
|
+
"""Emit a single SelectorPath literal."""
|
|
190
|
+
parts: list[str] = []
|
|
191
|
+
if p.deep:
|
|
192
|
+
parts.append('deep: true')
|
|
193
|
+
parts.append(f'steps: {_ts_xpath_steps(p.steps)}')
|
|
194
|
+
return '{ ' + ', '.join(parts) + ' }'
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _ts_field_paths(fields: list[FieldPath]) -> str:
|
|
198
|
+
"""Emit field paths array."""
|
|
199
|
+
if not fields:
|
|
200
|
+
return '[]'
|
|
201
|
+
items = [_ts_field_path(f) for f in fields]
|
|
202
|
+
return '[' + ', '.join(items) + ']'
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _ts_field_path(f: FieldPath) -> str:
|
|
206
|
+
"""Emit a single FieldPath literal."""
|
|
207
|
+
parts: list[str] = []
|
|
208
|
+
if f.deep:
|
|
209
|
+
parts.append('deep: true')
|
|
210
|
+
if f.steps:
|
|
211
|
+
parts.append(f'steps: {_ts_xpath_steps(f.steps)}')
|
|
212
|
+
parts.append(f'target: {_ts_field_target(f.target)}')
|
|
213
|
+
return '{ ' + ', '.join(parts) + ' }'
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _ts_xpath_steps(steps: tuple[XPathStep, ...]) -> str:
|
|
217
|
+
"""Emit an array of XPathStep literals."""
|
|
218
|
+
if not steps:
|
|
219
|
+
return '[]'
|
|
220
|
+
items = [_ts_xpath_step(s) for s in steps]
|
|
221
|
+
return '[' + ', '.join(items) + ']'
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _ts_xpath_step(s: XPathStep) -> str:
|
|
225
|
+
"""Emit a single XPathStep."""
|
|
226
|
+
if s.value is not None:
|
|
227
|
+
return '{ ' + f"kind: '{s.kind}', value: {ts_string(s.value)}" + ' }'
|
|
228
|
+
return '{ ' + f"kind: '{s.kind}'" + ' }'
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _ts_field_target(t: FieldTarget) -> str:
|
|
232
|
+
"""Emit a FieldTarget literal."""
|
|
233
|
+
parts = [f"kind: '{t.kind}'"]
|
|
234
|
+
if t.value is not None:
|
|
235
|
+
parts.append(f'value: {ts_string(t.value)}')
|
|
236
|
+
if t.is_attribute:
|
|
237
|
+
parts.append('isAttribute: true')
|
|
238
|
+
return '{ ' + ', '.join(parts) + ' }'
|
|
239
|
+
def _ts_choices(choices: list[ChoiceGroup], indent: str) -> str:
|
|
240
|
+
"""Emit ChoiceGroup array."""
|
|
241
|
+
lines = ['[']
|
|
242
|
+
for cg in choices:
|
|
243
|
+
fields: dict[str, Any] = {'options': cg.options}
|
|
244
|
+
if cg.min_occurs != 0:
|
|
245
|
+
fields['minOccurs'] = cg.min_occurs
|
|
246
|
+
if cg.max_occurs is not None:
|
|
247
|
+
fields['maxOccurs'] = cg.max_occurs
|
|
248
|
+
lines.append(f'{indent}{_emit_object(fields, indent + "\t")},')
|
|
249
|
+
lines.append(f'{indent[:-1]}]')
|
|
250
|
+
return '\n'.join(lines)
|
|
251
|
+
def _emit_object(fields: dict[str, Any], indent: str) -> str:
|
|
252
|
+
"""Emit a TS object literal from a dict, handling typed values."""
|
|
253
|
+
if not fields:
|
|
254
|
+
return '{}'
|
|
255
|
+
|
|
256
|
+
parts: list[str] = []
|
|
257
|
+
for key, value in fields.items():
|
|
258
|
+
ts_val = _emit_value(value, indent)
|
|
259
|
+
parts.append(f'{ts_key(key)}: {ts_val}')
|
|
260
|
+
|
|
261
|
+
if len(parts) == 1:
|
|
262
|
+
return '{ ' + parts[0] + ' }'
|
|
263
|
+
inner = ', '.join(parts)
|
|
264
|
+
return '{ ' + inner + ' }'
|
|
265
|
+
def _emit_value(value: Any, indent: str) -> str:
|
|
266
|
+
"""Emit a TS value from a Python value."""
|
|
267
|
+
if isinstance(value, bool):
|
|
268
|
+
return 'true' if value else 'false'
|
|
269
|
+
if isinstance(value, int):
|
|
270
|
+
return str(value)
|
|
271
|
+
if isinstance(value, str):
|
|
272
|
+
return ts_string(value)
|
|
273
|
+
if isinstance(value, list):
|
|
274
|
+
if all(isinstance(v, str) for v in value):
|
|
275
|
+
return ts_string_array(value)
|
|
276
|
+
if all(isinstance(v, IdentityConstraint) for v in value):
|
|
277
|
+
return ts_constraints(value, indent)
|
|
278
|
+
return ts_string_array([str(v) for v in value])
|
|
279
|
+
if isinstance(value, Namespace):
|
|
280
|
+
return ts_namespace(value)
|
|
281
|
+
if isinstance(value, Facets):
|
|
282
|
+
return ts_facets(value, indent)
|
|
283
|
+
return str(value)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Emit types.generated.ts — per-element attribute type interfaces."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from generate.emitters.ts_helpers import ts_key
|
|
5
|
+
from generate.ir import AttributeDef, ElementDef
|
|
6
|
+
def emit_types(elements: dict[str, ElementDef], out: Path) -> None:
|
|
7
|
+
"""Write types.generated.ts with attribute type interfaces.
|
|
8
|
+
|
|
9
|
+
Namespace-qualified keys (e.g. 'eIEC61850-6-100:version') are quoted in the TS interface.
|
|
10
|
+
Enumeration facets generate union types.
|
|
11
|
+
"""
|
|
12
|
+
lines = [
|
|
13
|
+
'// Auto-generated by generate.py — do not edit',
|
|
14
|
+
'',
|
|
15
|
+
"import type { ELEMENT_NAMES, REQUIRED_ATTRIBUTES } from './constants.generated'",
|
|
16
|
+
'',
|
|
17
|
+
'export type AvailableElement = (typeof ELEMENT_NAMES)[number]',
|
|
18
|
+
'',
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
sorted_names = sorted(elements)
|
|
22
|
+
|
|
23
|
+
for name in sorted_names:
|
|
24
|
+
elem = elements[name]
|
|
25
|
+
lines.append(f'export type Attributes{name} = {{')
|
|
26
|
+
for attr_name in elem.attr_sequence:
|
|
27
|
+
attr = elem.attributes[attr_name]
|
|
28
|
+
ts_type = _attr_to_ts_type(attr)
|
|
29
|
+
optional = '?' if not attr.required else ''
|
|
30
|
+
key = ts_key(attr_name)
|
|
31
|
+
lines.append(f'\t{key}{optional}: {ts_type}')
|
|
32
|
+
lines.append('}')
|
|
33
|
+
lines.append('')
|
|
34
|
+
|
|
35
|
+
lines.append('export type AttributesMap = {')
|
|
36
|
+
for name in sorted_names:
|
|
37
|
+
lines.append(f'\t{name}: Attributes{name}')
|
|
38
|
+
lines.append('}')
|
|
39
|
+
lines.append('')
|
|
40
|
+
lines.append('export type AttributesOf<T extends AvailableElement> = AttributesMap[T]')
|
|
41
|
+
lines.append('')
|
|
42
|
+
lines.append('export type RequiredAttributeNames<T extends AvailableElement> =')
|
|
43
|
+
lines.append('\t(typeof REQUIRED_ATTRIBUTES)[T][number]')
|
|
44
|
+
lines.append('export type OptionalAttributeNames<T extends AvailableElement> = Exclude<')
|
|
45
|
+
lines.append('\tkeyof AttributesOf<T>,')
|
|
46
|
+
lines.append('\tRequiredAttributeNames<T>')
|
|
47
|
+
lines.append('>')
|
|
48
|
+
lines.append('')
|
|
49
|
+
|
|
50
|
+
out.write_text('\n'.join(lines), encoding='utf-8')
|
|
51
|
+
def _attr_to_ts_type(attr: AttributeDef) -> str:
|
|
52
|
+
"""Generate a TS type annotation from an attribute definition.
|
|
53
|
+
|
|
54
|
+
- Enumerations → union of literal strings + (string & {}) for extensibility
|
|
55
|
+
- Fixed values → literal string type
|
|
56
|
+
- Default → string
|
|
57
|
+
"""
|
|
58
|
+
facets = attr.facets
|
|
59
|
+
|
|
60
|
+
if facets and facets.enumeration:
|
|
61
|
+
literals = ' | '.join(f"'{v}'" for v in facets.enumeration)
|
|
62
|
+
return f'{literals} | (string & {{}})'
|
|
63
|
+
|
|
64
|
+
if attr.fixed:
|
|
65
|
+
return f"'{attr.fixed}'"
|
|
66
|
+
|
|
67
|
+
return 'string'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Extractors — one per XSD concern
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Extract attributes from an XSD element."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from generate.extractors.facets import extract_facets
|
|
5
|
+
from generate.extractors.namespace import extract_attr_namespace
|
|
6
|
+
from generate.helpers import local_name
|
|
7
|
+
from generate.ir import AttributeDef, Namespace
|
|
8
|
+
|
|
9
|
+
# W3C XML namespace attrs (xml:base, xml:lang, xml:space, xml:id, etc.) are
|
|
10
|
+
# implicitly valid on every XML element and have no meaning in IEC schemas.
|
|
11
|
+
_XML_NS_URI = 'http://www.w3.org/XML/1998/namespace'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def extract_attributes(xsd_elem: Any) -> tuple[list[str], bool, dict[str, AttributeDef]]:
|
|
15
|
+
"""Extract attributes from an XSD element.
|
|
16
|
+
|
|
17
|
+
Uses qualify-on-collision keying: bare local name by default; a namespace-prefixed
|
|
18
|
+
key (``prefix:local``) only when two or more attributes on the same element share
|
|
19
|
+
the same local name (e.g. SCL ``version`` and 6-100 ``version``).
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
(attr_sequence, has_any_attribute, attribute_details)
|
|
23
|
+
|
|
24
|
+
xmlschema API:
|
|
25
|
+
XsdElement.attributes: XsdAttributeGroup (dict-like, name → XsdAttribute)
|
|
26
|
+
XsdAttribute.use: str ('optional' | 'required' | 'prohibited')
|
|
27
|
+
XsdAttribute.default: str | None
|
|
28
|
+
XsdAttribute.fixed: str | None
|
|
29
|
+
XsdAttribute.type: XsdSimpleType
|
|
30
|
+
XsdAttributeGroup[attr_name]: XsdAttribute
|
|
31
|
+
|
|
32
|
+
Wildcards:
|
|
33
|
+
XsdElement.attributes.wildcard → XsdAnyAttribute | None (xs:anyAttribute)
|
|
34
|
+
"""
|
|
35
|
+
sequence: list[str] = []
|
|
36
|
+
details: dict[str, AttributeDef] = {}
|
|
37
|
+
any_attr = False
|
|
38
|
+
|
|
39
|
+
attributes = getattr(xsd_elem, 'attributes', None)
|
|
40
|
+
if attributes is None:
|
|
41
|
+
return sequence, any_attr, details
|
|
42
|
+
|
|
43
|
+
# Pass 1: collect raw (local_name, namespace, xsd_attr) for collision detection
|
|
44
|
+
raw: list[tuple[str, Namespace | None, Any]] = []
|
|
45
|
+
for attr_name, xsd_attr in attributes.items():
|
|
46
|
+
if attr_name is None:
|
|
47
|
+
continue
|
|
48
|
+
ns = extract_attr_namespace(xsd_attr)
|
|
49
|
+
if ns and ns.uri == _XML_NS_URI:
|
|
50
|
+
continue # skip W3C XML namespace attrs (xml:base, xml:lang, xml:space, xml:id)
|
|
51
|
+
raw.append((local_name(attr_name), ns, xsd_attr))
|
|
52
|
+
|
|
53
|
+
# Detect local-name collisions
|
|
54
|
+
local_count: dict[str, int] = {}
|
|
55
|
+
for ln, _, _ in raw:
|
|
56
|
+
local_count[ln] = local_count.get(ln, 0) + 1
|
|
57
|
+
collision_locals = {ln for ln, cnt in local_count.items() if cnt > 1}
|
|
58
|
+
|
|
59
|
+
# Pass 2: build keys and AttributeDefs
|
|
60
|
+
for ln, ns, xsd_attr in raw:
|
|
61
|
+
key = f'{ns.prefix}:{ln}' if (ln in collision_locals and ns and ns.prefix) else ln
|
|
62
|
+
|
|
63
|
+
fixed = getattr(xsd_attr, 'fixed', None)
|
|
64
|
+
default = getattr(xsd_attr, 'default', None) if fixed is None else None
|
|
65
|
+
use = getattr(xsd_attr, 'use', 'optional')
|
|
66
|
+
attr_type = getattr(xsd_attr, 'type', None)
|
|
67
|
+
|
|
68
|
+
details[key] = AttributeDef(
|
|
69
|
+
required=use == 'required',
|
|
70
|
+
default=default,
|
|
71
|
+
fixed=fixed,
|
|
72
|
+
namespace=ns,
|
|
73
|
+
facets=extract_facets(attr_type),
|
|
74
|
+
)
|
|
75
|
+
sequence.append(key)
|
|
76
|
+
|
|
77
|
+
sequence.sort()
|
|
78
|
+
|
|
79
|
+
# Check xs:anyAttribute
|
|
80
|
+
wildcard = getattr(attributes, 'wildcard', None)
|
|
81
|
+
any_attr = wildcard is not None
|
|
82
|
+
|
|
83
|
+
return sequence, any_attr, details
|