472 lines
17 KiB
Python
472 lines
17 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
xsd_til_yaml.py
|
||
|
||
Genererer YAML-skelet fra en XSD-fil til brug med udpak_semistruktur.
|
||
|
||
Producerer:
|
||
- skabeloner.yaml – feltskabeloner for alle complexTypes
|
||
- nøgler.yaml – nøgle-placeholders for overliggende unbounded-niveauer
|
||
- udtræk_<navn>.yaml – én fil per maxOccurs="unbounded" element
|
||
|
||
Kør med:
|
||
python3 xsd_til_yaml.py --xsd bankdata.xsd --output ./yaml_output/
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
import os
|
||
import argparse
|
||
from xml.etree import ElementTree as ET
|
||
from dataclasses import dataclass, field
|
||
from typing import Optional
|
||
|
||
# XML Schema namespace
|
||
XS = "http://www.w3.org/2001/XMLSchema"
|
||
|
||
|
||
# ============================================================
|
||
# Datastrukturer
|
||
# ============================================================
|
||
|
||
@dataclass
|
||
class XsdFelt:
|
||
"""Repræsenterer ét felt (attribut eller element) i en complexType."""
|
||
navn: str
|
||
er_attribut: bool
|
||
xsd_type: str = "xs:string"
|
||
påkrævet: bool = False
|
||
|
||
@property
|
||
def felt_ref(self) -> str:
|
||
"""Returnerer felt-referencen som den skrives i YAML."""
|
||
return f"@{self.navn}" if self.er_attribut else self.navn
|
||
|
||
|
||
@dataclass
|
||
class XsdKompleksType:
|
||
"""Repræsenterer en complexType fra XSD."""
|
||
navn: str
|
||
felter: list[XsdFelt] = field(default_factory=list)
|
||
|
||
|
||
@dataclass
|
||
class XsdUdboundElement:
|
||
"""Repræsenterer et maxOccurs=unbounded element – bliver til en output-fil."""
|
||
element_navn: str # fx "postering"
|
||
type_navn: str # fx "PosteringType"
|
||
rod_sti: str # fx "bank.kunder.kunde.konti.konto.posteringer.postering"
|
||
overliggende: list[str] # navne på overliggende unbounded elementer
|
||
# fx ["kunde", "konto"]
|
||
|
||
|
||
# ============================================================
|
||
# XSD Parser
|
||
# ============================================================
|
||
|
||
class XsdParser:
|
||
"""Parser en XSD-fil og udtrækker complexTypes og unbounded elementer."""
|
||
|
||
def __init__(self, xsd_sti: str):
|
||
self.xsd_sti = xsd_sti
|
||
self.tree = ET.parse(xsd_sti)
|
||
self.root = self.tree.getroot()
|
||
|
||
# Navngivne complexTypes fra XSD
|
||
self.komplekse_typer: dict[str, XsdKompleksType] = {}
|
||
|
||
# Unbounded elementer med rod-sti og overliggende niveauer
|
||
self.unbounded_elementer: list[XsdUdboundElement] = []
|
||
|
||
def parse(self) -> None:
|
||
"""Kør den komplette parsing."""
|
||
self._parse_navngivne_typer()
|
||
self._find_unbounded_elementer()
|
||
|
||
def _xs(self, tag: str) -> str:
|
||
"""Returnerer fuldt kvalificeret XS-tag."""
|
||
return f"{{{XS}}}{tag}"
|
||
|
||
def _parse_navngivne_typer(self) -> None:
|
||
"""Find alle navngivne complexTypes på topniveau."""
|
||
for ct in self.root.findall(self._xs("complexType")):
|
||
navn = ct.get("name")
|
||
if not navn:
|
||
continue
|
||
felter = self._udpak_felter(ct)
|
||
self.komplekse_typer[navn] = XsdKompleksType(navn=navn, felter=felter)
|
||
|
||
def _udpak_felter(self, ct_node) -> list[XsdFelt]:
|
||
"""Udpakker alle felter (attributter + simple elementer) fra en complexType."""
|
||
felter = []
|
||
|
||
# Attributter – bliver til @navn i YAML
|
||
for attr in ct_node.findall(self._xs("attribute")):
|
||
navn = attr.get("name")
|
||
xsd_type = attr.get("type", "xs:string")
|
||
påkrævet = attr.get("use") == "required"
|
||
felter.append(XsdFelt(
|
||
navn=navn,
|
||
er_attribut=True,
|
||
xsd_type=xsd_type,
|
||
påkrævet=påkrævet
|
||
))
|
||
|
||
# Sequence-elementer
|
||
for seq in ct_node.findall(self._xs("sequence")):
|
||
for el in seq.findall(self._xs("element")):
|
||
el_type = el.get("type", "xs:string")
|
||
el_navn = el.get("name")
|
||
max_occ = el.get("maxOccurs", "1")
|
||
|
||
# Spring komplekse typer og unbounded over – de håndteres separat
|
||
if max_occ == "unbounded":
|
||
continue
|
||
if el_type and not el_type.startswith("xs:"):
|
||
continue
|
||
|
||
felter.append(XsdFelt(
|
||
navn=el_navn,
|
||
er_attribut=False,
|
||
xsd_type=el_type or "xs:string",
|
||
påkrævet=True
|
||
))
|
||
|
||
return felter
|
||
|
||
def _find_unbounded_elementer(self) -> None:
|
||
"""
|
||
Gennemgår XSD rekursivt og finder alle maxOccurs=unbounded elementer.
|
||
Bygger rod-stier og registrerer overliggende unbounded niveauer.
|
||
"""
|
||
# Find rod-elementet
|
||
rod_el = self.root.find(self._xs("element"))
|
||
if rod_el is None:
|
||
return
|
||
|
||
rod_navn = rod_el.get("name", "rod")
|
||
rod_type = rod_el.get("type")
|
||
|
||
self._traverse(
|
||
node=rod_el,
|
||
sti=[rod_navn],
|
||
overliggende_unbounded=[],
|
||
rod_type=rod_type
|
||
)
|
||
|
||
def _hent_type_node(self, type_navn: str):
|
||
"""Henter en navngiven complexType-node fra XSD."""
|
||
for ct in self.root.findall(self._xs("complexType")):
|
||
if ct.get("name") == type_navn:
|
||
return ct
|
||
return None
|
||
|
||
def _traverse(
|
||
self,
|
||
node,
|
||
sti: list[str],
|
||
overliggende_unbounded: list[str],
|
||
rod_type: Optional[str] = None
|
||
) -> None:
|
||
"""
|
||
Rekursiv gennemgang af XSD-strukturen.
|
||
Finder unbounded elementer og bygger rod-stier.
|
||
"""
|
||
# Find complexType-noden at arbejde med
|
||
if rod_type:
|
||
ct_node = self._hent_type_node(rod_type)
|
||
else:
|
||
ct_node = node.find(self._xs("complexType"))
|
||
|
||
if ct_node is None:
|
||
return
|
||
|
||
# Gennemgå sequence-elementer
|
||
for seq in ct_node.findall(self._xs("sequence")):
|
||
for el in seq.findall(self._xs("element")):
|
||
el_navn = el.get("name")
|
||
el_type = el.get("type")
|
||
max_occ = el.get("maxOccurs", "1")
|
||
|
||
ny_sti = sti + [el_navn]
|
||
|
||
if max_occ == "unbounded":
|
||
# Dette element bliver en output-fil
|
||
rod_sti = ".".join(ny_sti)
|
||
self.unbounded_elementer.append(XsdUdboundElement(
|
||
element_navn=el_navn,
|
||
type_navn=el_type or "",
|
||
rod_sti=rod_sti,
|
||
overliggende=list(overliggende_unbounded)
|
||
))
|
||
# Fortsæt rekursivt med dette som nyt overliggende niveau
|
||
self._traverse(
|
||
node=el,
|
||
sti=ny_sti,
|
||
overliggende_unbounded=overliggende_unbounded + [el_navn],
|
||
rod_type=el_type
|
||
)
|
||
else:
|
||
# Ikke unbounded – fortsæt ned hvis det er en kompleks type
|
||
if el_type and not el_type.startswith("xs:"):
|
||
self._traverse(
|
||
node=el,
|
||
sti=ny_sti,
|
||
overliggende_unbounded=overliggende_unbounded,
|
||
rod_type=el_type
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Hjælpefunktion: XSD type → YAML type
|
||
# ============================================================
|
||
|
||
def xsd_type_til_yaml(xsd_type: str) -> dict:
|
||
"""Konverterer en XSD-type til YAML kolonne-attributter."""
|
||
mapping = {
|
||
"xs:string": {},
|
||
"xs:decimal": {"type": "decimal", "decimaler": 2},
|
||
"xs:integer": {"type": "integer"},
|
||
"xs:int": {"type": "integer"},
|
||
"xs:boolean": {"type": "boolean"},
|
||
"xs:date": {"type": "date", "dato_ind": "%Y-%m-%d", "dato_ud": "%d-%m-%Y"},
|
||
"xs:dateTime": {"type": "date", "dato_ind": "%Y-%m-%dT%H:%M:%S", "dato_ud": "SYBASE"},
|
||
}
|
||
return mapping.get(xsd_type, {})
|
||
|
||
|
||
# ============================================================
|
||
# CLI
|
||
# ============================================================
|
||
|
||
def byg_argument_parser() -> argparse.ArgumentParser:
|
||
parser = argparse.ArgumentParser(
|
||
description="Genererer YAML-skelet fra XSD til udpak_semistruktur."
|
||
)
|
||
parser.add_argument("--xsd", required=True, help="Sti til XSD-filen")
|
||
parser.add_argument("--output", required=True, help="Output-mappe til YAML-filer")
|
||
parser.add_argument("--prefix", default="udtræk",
|
||
help="Prefix til udtræk-filnavne (standard: udtræk)")
|
||
return parser
|
||
|
||
|
||
# ============================================================
|
||
# YAML Generatorer
|
||
# ============================================================
|
||
|
||
def _yaml_type_linjer(xsd_type: str, indryk: str) -> list[str]:
|
||
"""Returnerer YAML-linjer for typekonvertering baseret på XSD-type."""
|
||
attrs = xsd_type_til_yaml(xsd_type)
|
||
linjer = []
|
||
for k, v in attrs.items():
|
||
if isinstance(v, str):
|
||
linjer.append(f'{indryk}{k}: "{v}"')
|
||
else:
|
||
linjer.append(f'{indryk}{k}: {v}')
|
||
return linjer
|
||
|
||
|
||
def generer_skabeloner_yaml(xsd: XsdParser, output_mappe: str) -> None:
|
||
"""Genererer skabeloner.yaml med feltskabeloner for alle relevante complexTypes."""
|
||
linjer = []
|
||
linjer.append("# =============================================================================")
|
||
linjer.append("# skabeloner.yaml")
|
||
linjer.append("#")
|
||
linjer.append("# Auto-genereret feltskabeloner fra XSD.")
|
||
linjer.append("# Relative feltnavne – brug prefix_felt naar skabelonen anvendes.")
|
||
linjer.append("# =============================================================================")
|
||
linjer.append("")
|
||
linjer.append("kolonne_skabeloner:")
|
||
linjer.append("")
|
||
|
||
relevante = {navn: kt for navn, kt in xsd.komplekse_typer.items() if kt.felter}
|
||
|
||
for type_navn, kt in relevante.items():
|
||
skabelon_navn = type_navn.replace("Type", "").lower()
|
||
linjer.append(f" # Felter fra {type_navn}")
|
||
linjer.append(f" {skabelon_navn}:")
|
||
|
||
for felt in kt.felter:
|
||
linjer.append(f" - navn: {felt.navn}")
|
||
linjer.append(f' felt: "{felt.felt_ref}"')
|
||
for type_linje in _yaml_type_linjer(felt.xsd_type, " "):
|
||
linjer.append(type_linje)
|
||
if felt.påkrævet:
|
||
linjer.append(f" påkrævet: true")
|
||
linjer.append("")
|
||
|
||
sti = os.path.join(output_mappe, "skabeloner.yaml")
|
||
with open(sti, "w", encoding="utf-8") as f:
|
||
f.write("\n".join(linjer))
|
||
print(f" Skrev: {sti}")
|
||
|
||
|
||
def generer_nøgler_yaml(xsd: XsdParser, output_mappe: str) -> None:
|
||
"""Genererer nøgler.yaml med placeholders for forretningsnøgler."""
|
||
overliggende_niveauer = set()
|
||
for el in xsd.unbounded_elementer:
|
||
for ov in el.overliggende:
|
||
overliggende_niveauer.add(ov)
|
||
|
||
if not overliggende_niveauer:
|
||
print(" Ingen overliggende niveauer – nøgler.yaml ikke genereret.")
|
||
return
|
||
|
||
linjer = []
|
||
linjer.append("# =============================================================================")
|
||
linjer.append("# nøgler.yaml")
|
||
linjer.append("#")
|
||
linjer.append("# Nøgle-skabeloner der binder output-filer til overliggende niveauer.")
|
||
linjer.append("# UDFYLD: Erstat __FELT__ med den korrekte forretningsnøgle.")
|
||
linjer.append("# =============================================================================")
|
||
linjer.append("")
|
||
linjer.append("kolonne_skabeloner:")
|
||
linjer.append("")
|
||
|
||
skrevne = set()
|
||
for el in xsd.unbounded_elementer:
|
||
if not el.overliggende:
|
||
continue
|
||
rod_dele = el.rod_sti.split(".")
|
||
for ov_navn in el.overliggende:
|
||
skabelon_navn = f"{ov_navn}_nøgle"
|
||
if skabelon_navn in skrevne:
|
||
continue
|
||
skrevne.add(skabelon_navn)
|
||
try:
|
||
idx = rod_dele.index(ov_navn)
|
||
ov_rod_sti = ".".join(rod_dele[:idx + 1])
|
||
except ValueError:
|
||
ov_rod_sti = f"__ROD_STI_TIL_{ov_navn.upper()}__"
|
||
|
||
linjer.append(f" # Nøgle fra {ov_navn}-niveau")
|
||
linjer.append(f" # TODO: Angiv den korrekte forretningsnøgle for {ov_navn}")
|
||
linjer.append(f" {skabelon_navn}:")
|
||
linjer.append(f" - navn: __FELT__")
|
||
linjer.append(" felt: __FELT__ # fx '@id' eller 'nr'")
|
||
linjer.append(f" rod: {ov_rod_sti}")
|
||
linjer.append("")
|
||
|
||
sti = os.path.join(output_mappe, "nøgler.yaml")
|
||
with open(sti, "w", encoding="utf-8") as f:
|
||
f.write("\n".join(linjer))
|
||
print(f" Skrev: {sti}")
|
||
|
||
|
||
def generer_udtræk_yaml(xsd: XsdParser, output_mappe: str, prefix: str) -> None:
|
||
"""Genererer én udtræk-yaml per unbounded element."""
|
||
for el in xsd.unbounded_elementer:
|
||
linjer = []
|
||
linjer.append("# =============================================================================")
|
||
linjer.append(f"# {prefix}_{el.element_navn}.yaml")
|
||
linjer.append("#")
|
||
linjer.append(f"# Auto-genereret udtræk for {el.element_navn}.")
|
||
if el.overliggende:
|
||
linjer.append(f"# Udfyld nøgle-skabeloner i nøgler.yaml inden brug.")
|
||
linjer.append("# =============================================================================")
|
||
linjer.append("")
|
||
linjer.append("output_filer:")
|
||
linjer.append("")
|
||
linjer.append(f" - rod: {el.rod_sti}")
|
||
linjer.append(f" kolonner:")
|
||
|
||
if el.overliggende:
|
||
linjer.append(f" # Nøgler fra overliggende niveauer – udfyld i nøgler.yaml")
|
||
for ov_navn in el.overliggende:
|
||
linjer.append(f" - skabelon: {ov_navn}_nøgle")
|
||
linjer.append("")
|
||
|
||
if el.type_navn and el.type_navn in xsd.komplekse_typer:
|
||
kt = xsd.komplekse_typer[el.type_navn]
|
||
if kt.felter:
|
||
skabelon_navn = el.type_navn.replace("Type", "").lower()
|
||
linjer.append(f" # Felter fra {el.element_navn} – se skabeloner.yaml")
|
||
linjer.append(f" - skabelon: {skabelon_navn}")
|
||
|
||
linjer.append("")
|
||
linjer.append(f" outputs:")
|
||
linjer.append(f" - fil_navn: \"{el.element_navn}_{{yyyy}}{{mm}}{{dd}}.txt\"")
|
||
linjer.append(f" overskrifter: true")
|
||
linjer.append("")
|
||
|
||
fil_navn = f"{prefix}_{el.element_navn}.yaml"
|
||
sti = os.path.join(output_mappe, fil_navn)
|
||
with open(sti, "w", encoding="utf-8") as f:
|
||
f.write("\n".join(linjer))
|
||
print(f" Skrev: {sti}")
|
||
|
||
|
||
def generer_hoved_config(xsd: XsdParser, output_mappe: str, prefix: str) -> None:
|
||
"""Genererer en hoved-config der inkluderer alle genererede filer."""
|
||
xsd_basis = os.path.splitext(os.path.basename(xsd.xsd_sti))[0]
|
||
linjer = []
|
||
linjer.append("# =============================================================================")
|
||
linjer.append(f"# config_{xsd_basis}.yaml")
|
||
linjer.append("#")
|
||
linjer.append("# Auto-genereret hoved-config. Tilpas config-sektionen.")
|
||
linjer.append("# =============================================================================")
|
||
linjer.append("")
|
||
linjer.append("include:")
|
||
linjer.append(" - skabeloner.yaml")
|
||
linjer.append(" - nøgler.yaml")
|
||
for el in xsd.unbounded_elementer:
|
||
linjer.append(f" - {prefix}_{el.element_navn}.yaml")
|
||
linjer.append("")
|
||
linjer.append("config:")
|
||
linjer.append(f" input_fil: {xsd_basis}.xml")
|
||
linjer.append(" output_path: __OUTPUT_PATH__")
|
||
linjer.append(" logfil: log_{yyyy}{mm}{dd}.txt")
|
||
linjer.append(" log_niveau: info")
|
||
linjer.append(" log_output: begge")
|
||
linjer.append(" encoding: utf-8")
|
||
linjer.append(" separator: \"\\t\"")
|
||
linjer.append(" skrivetilstand: w")
|
||
linjer.append(" dato_ind: \"%Y-%m-%d\"")
|
||
linjer.append(" dato_ud: \"%d-%m-%Y\"")
|
||
|
||
fil_navn = f"config_{xsd_basis}.yaml"
|
||
sti = os.path.join(output_mappe, fil_navn)
|
||
with open(sti, "w", encoding="utf-8") as f:
|
||
f.write("\n".join(linjer))
|
||
print(f" Skrev: {sti}")
|
||
|
||
|
||
def main():
|
||
parser = byg_argument_parser()
|
||
args = parser.parse_args()
|
||
|
||
if not os.path.exists(args.xsd):
|
||
print(f"FEJL: XSD-filen '{args.xsd}' findes ikke.")
|
||
return
|
||
|
||
os.makedirs(args.output, exist_ok=True)
|
||
|
||
print(f"Parser XSD: {args.xsd}")
|
||
xsd = XsdParser(args.xsd)
|
||
xsd.parse()
|
||
|
||
print(f"\nFandt {len(xsd.komplekse_typer)} komplekse typer:")
|
||
for navn in xsd.komplekse_typer:
|
||
kt = xsd.komplekse_typer[navn]
|
||
print(f" {navn}: {len(kt.felter)} felter")
|
||
|
||
print(f"\nFandt {len(xsd.unbounded_elementer)} unbounded elementer:")
|
||
for el in xsd.unbounded_elementer:
|
||
print(f" {el.element_navn} ({el.rod_sti})")
|
||
if el.overliggende:
|
||
print(f" Overliggende: {el.overliggende}")
|
||
|
||
print(f"\nGenererer YAML-filer i: {args.output}")
|
||
generer_skabeloner_yaml(xsd, args.output)
|
||
generer_nøgler_yaml(xsd, args.output)
|
||
generer_udtræk_yaml(xsd, args.output, args.prefix)
|
||
generer_hoved_config(xsd, args.output, args.prefix)
|
||
|
||
print("\nFærdig! Husk at:")
|
||
print(" 1. Udfyld nøgler.yaml med de korrekte forretningsnøgler")
|
||
print(" 2. Ret output_path i hoved-config")
|
||
print(" 3. Tjek at rod-stier passer til din XML-struktur")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|