Expose SCD XML structure outline
This commit is contained in:
@@ -75,22 +75,20 @@ def query_without_line_comments(text: str) -> str:
|
|||||||
def node_path(root: ET.Element, target: ET.Element) -> str:
|
def node_path(root: ET.Element, target: ET.Element) -> str:
|
||||||
"""Produce a stable, human-readable evidence path without XML prefixes."""
|
"""Produce a stable, human-readable evidence path without XML prefixes."""
|
||||||
|
|
||||||
def visit(node: ET.Element, prefix: str) -> str | None:
|
def visit(node: ET.Element, current: str) -> str | None:
|
||||||
name = local_name(node.tag)
|
|
||||||
current = f"{prefix}/{name}" if prefix else f"/{name}"
|
|
||||||
if node is target:
|
if node is target:
|
||||||
return current
|
return current
|
||||||
positions: dict[str, int] = {}
|
positions: dict[str, int] = {}
|
||||||
for child in node:
|
for child in node:
|
||||||
child_name = local_name(child.tag)
|
child_name = local_name(child.tag)
|
||||||
positions[child_name] = positions.get(child_name, 0) + 1
|
positions[child_name] = positions.get(child_name, 0) + 1
|
||||||
child_prefix = f"{current}[{positions[child_name]}]"
|
suffix = f"[{positions[child_name]}]" if positions[child_name] > 1 else ""
|
||||||
found = visit(child, child_prefix)
|
found = visit(child, f"{current}/{child_name}{suffix}")
|
||||||
if found:
|
if found:
|
||||||
return found
|
return found
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return visit(root, "") or "/"
|
return visit(root, f"/{local_name(root.tag)}[1]") or "/"
|
||||||
|
|
||||||
|
|
||||||
def xml_from_scd_payload(data: bytes) -> tuple[ET.Element | None, dict[str, Any]]:
|
def xml_from_scd_payload(data: bytes) -> tuple[ET.Element | None, dict[str, Any]]:
|
||||||
@@ -184,6 +182,21 @@ def inspect_scd_payload(data: bytes, *, sections: list[str] | None = None) -> di
|
|||||||
"container": {**container, "code": "SCD_SCHEMA_NODE_NOT_FOUND"},
|
"container": {**container, "code": "SCD_SCHEMA_NODE_NOT_FOUND"},
|
||||||
"sections": {name: [] for name in requested},
|
"sections": {name: [] for name in requested},
|
||||||
}
|
}
|
||||||
|
# Keep a complete, evidence-backed map of the XML shape alongside the
|
||||||
|
# curated semantic sections below. 1C releases and SCD variants may add
|
||||||
|
# nodes that this adapter does not yet assign a business meaning to. A
|
||||||
|
# tag/count/path outline makes those nodes visible to callers without
|
||||||
|
# guessing that (for example) a form property is a dataset or layout.
|
||||||
|
top_level_counts: dict[str, int] = {}
|
||||||
|
top_level_paths: dict[str, list[str]] = {}
|
||||||
|
for child in schema:
|
||||||
|
tag = local_name(child.tag)
|
||||||
|
top_level_counts[tag] = top_level_counts.get(tag, 0) + 1
|
||||||
|
top_level_paths.setdefault(tag, []).append(node_path(schema, child))
|
||||||
|
xml_tag_counts: dict[str, int] = {}
|
||||||
|
for node in schema.iter():
|
||||||
|
tag = local_name(node.tag)
|
||||||
|
xml_tag_counts[tag] = xml_tag_counts.get(tag, 0) + 1
|
||||||
node_names = {
|
node_names = {
|
||||||
"parameters": {"parameter"},
|
"parameters": {"parameter"},
|
||||||
"datasets": {"dataSet"},
|
"datasets": {"dataSet"},
|
||||||
@@ -220,6 +233,14 @@ def inspect_scd_payload(data: bytes, *, sections: list[str] | None = None) -> di
|
|||||||
query_references.append({"dataset": dataset.get("name"), "parameters": references})
|
query_references.append({"dataset": dataset.get("name"), "parameters": references})
|
||||||
analysis = {
|
analysis = {
|
||||||
"kind": "raw_query_parameter_token_scan",
|
"kind": "raw_query_parameter_token_scan",
|
||||||
|
"schema_outline": {
|
||||||
|
"kind": "exact_xml_tag_inventory",
|
||||||
|
"top_level": [
|
||||||
|
{"tag": tag, "count": top_level_counts[tag], "paths": top_level_paths[tag]}
|
||||||
|
for tag in sorted(top_level_counts, key=str.casefold)
|
||||||
|
],
|
||||||
|
"all_tag_counts": dict(sorted(xml_tag_counts.items(), key=lambda item: item[0].casefold())),
|
||||||
|
},
|
||||||
"declared_parameters": declared,
|
"declared_parameters": declared,
|
||||||
"query_parameter_references": query_references,
|
"query_parameter_references": query_references,
|
||||||
"referenced_not_declared_in_schema": sorted(
|
"referenced_not_declared_in_schema": sorted(
|
||||||
|
|||||||
@@ -231,6 +231,19 @@ def test_scd_payload_decoder_reads_xml_after_platform_prefix() -> None:
|
|||||||
assert result["sections"]["calculated_fields"][0]["expression"] == "Сумма * 1.2"
|
assert result["sections"]["calculated_fields"][0]["expression"] == "Сумма * 1.2"
|
||||||
assert result["sections"]["variants"][0]["name"] == "Основной"
|
assert result["sections"]["variants"][0]["name"] == "Основной"
|
||||||
assert result["analysis"]["query_parameter_references"] == [{"dataset": "ОсновнойНабор", "parameters": ["Период"]}]
|
assert result["analysis"]["query_parameter_references"] == [{"dataset": "ОсновнойНабор", "parameters": ["Период"]}]
|
||||||
|
assert result["analysis"]["schema_outline"] == {
|
||||||
|
"kind": "exact_xml_tag_inventory",
|
||||||
|
"top_level": [
|
||||||
|
{"tag": "calculatedField", "count": 1, "paths": ["/dataCompositionSchema[1]/calculatedField"]},
|
||||||
|
{"tag": "dataSet", "count": 1, "paths": ["/dataCompositionSchema[1]/dataSet"]},
|
||||||
|
{"tag": "field", "count": 2, "paths": ["/dataCompositionSchema[1]/field", "/dataCompositionSchema[1]/field[2]"]},
|
||||||
|
{"tag": "parameter", "count": 1, "paths": ["/dataCompositionSchema[1]/parameter"]},
|
||||||
|
{"tag": "resource", "count": 1, "paths": ["/dataCompositionSchema[1]/resource"]},
|
||||||
|
{"tag": "settingsVariant", "count": 1, "paths": ["/dataCompositionSchema[1]/settingsVariant"]},
|
||||||
|
{"tag": "totalField", "count": 1, "paths": ["/dataCompositionSchema[1]/totalField"]},
|
||||||
|
],
|
||||||
|
"all_tag_counts": {"calculatedField": 1, "dataCompositionSchema": 1, "dataPath": 2, "dataSet": 1, "expression": 2, "field": 2, "name": 5, "parameter": 1, "query": 1, "resource": 1, "settingsVariant": 1, "totalField": 1, "valueType": 1},
|
||||||
|
}
|
||||||
assert result["analysis"]["referenced_not_declared_in_schema"] == []
|
assert result["analysis"]["referenced_not_declared_in_schema"] == []
|
||||||
assert result["analysis"]["total_field_references"] == {"fields": ["Сумма"], "missing_from_declared_fields": [], "status": "checked"}
|
assert result["analysis"]["total_field_references"] == {"fields": ["Сумма"], "missing_from_declared_fields": [], "status": "checked"}
|
||||||
assert result["analysis"]["settings_context"] == {"status": "not_present", "sections": {}}
|
assert result["analysis"]["settings_context"] == {"status": "not_present", "sections": {}}
|
||||||
|
|||||||
Reference in New Issue
Block a user