foxygit / RPA-Remisser Log in
commits tags

/rpa/migrate.py · 5.1 KB

raw
"""Migrerar det gamla nästlade `{steps}`-formatet till nod/kant-grafen som
grafmotorn (rpa/graph_engine.py) kör.

Gammalt:  steps: [ {type, ...}, {type:"loop", steps:[...]}, {type:"if", branches:[{condition, steps:[]}], else:[...]} ]
Nytt:     nodes: [{id, type, position, ...fält}],  edges: [{id, source, target, sourceHandle, targetHandle}]

Handtag: vanlig nod in/out; `if` -> in + case0..caseN + else; `loop` -> in + each + done.
jump_to_label försvinner - föregångarens kant pekas om till etikett-noden.
"""

from __future__ import annotations

import uuid

_CONTAINER_KEYS = ("steps", "branches", "else")


def _uid() -> str:
    return uuid.uuid4().hex[:12]


def _flat_node(step: dict, pos: dict | None) -> dict:
    """Nod med stegets platta fält (utan behållar-nycklar). `if` behåller
    branches men bara deras condition (inte steps)."""
    node = {k: v for k, v in step.items() if k not in _CONTAINER_KEYS}
    node.setdefault("id", _uid())
    node["position"] = pos or {"x": 0, "y": 0}
    if step.get("type") == "if":
        node["branches"] = [{"condition": b.get("condition", {})} for b in step.get("branches", [])]
    return node


class _Builder:
    def __init__(self, layout: dict | None):
        self.nodes: list[dict] = []
        self.edges: list[dict] = []
        self.layout = layout or {}
        self._y = 0

    def _pos(self, step: dict) -> dict:
        sid = step.get("id")
        if sid and sid in self.layout:
            return self.layout[sid]
        self._y += 130
        return {"x": 0, "y": self._y}

    def _edge(self, src: str, src_h: str, tgt: str, tgt_h: str = "in") -> None:
        self.edges.append({
            "id": f"e-{_uid()}",
            "source": src, "sourceHandle": src_h,
            "target": tgt, "targetHandle": tgt_h,
        })

    def chain(self, steps: list) -> tuple[str | None, list[tuple[str, str]]]:
        """Bygger noder för en steglista. Returnerar (entry_id, open_ends) där
        open_ends är [(node_id, handle), ...] som ska kopplas vidare."""
        label_map: dict[str, str] = {}
        prepared: list[dict] = []
        for s in steps:
            s = dict(s)
            s.setdefault("id", _uid())
            prepared.append(s)
            if s.get("label"):
                label_map[s["label"]] = s["id"]

        entry: str | None = None
        open_ends: list[tuple[str, str]] = []

        for s in prepared:
            if s.get("type") == "jump_to_label":
                tgt = label_map.get(s.get("target", ""))
                if tgt:
                    for nid, h in open_ends:
                        self._edge(nid, h, tgt)
                open_ends = []  # framåtflödet stannar här
                continue

            b_entry, b_open = self._block(s)
            if entry is None:
                entry = b_entry
            for nid, h in open_ends:
                self._edge(nid, h, b_entry)
            open_ends = b_open

        return entry, open_ends

    def _block(self, step: dict) -> tuple[str, list[tuple[str, str]]]:
        t = step.get("type")
        node = _flat_node(step, self._pos(step))
        self.nodes.append(node)
        nid = node["id"]

        if t == "loop":
            body_entry, body_open = self.chain(step.get("steps", []))
            if body_entry:
                self._edge(nid, "each", body_entry)
                for bn, bh in body_open:  # kroppens slut loopar tillbaka
                    self._edge(bn, bh, nid)
            return nid, [(nid, "done")]

        if t == "if":
            open_ends: list[tuple[str, str]] = []
            for i, br in enumerate(step.get("branches", [])):
                b_entry, b_open = self.chain(br.get("steps", []))
                if b_entry:
                    self._edge(nid, f"case{i}", b_entry)
                    open_ends.extend(b_open)
                else:
                    open_ends.append((nid, f"case{i}"))
            els = step.get("else")
            if els is not None:
                e_entry, e_open = self.chain(els)
                if e_entry:
                    self._edge(nid, "else", e_entry)
                    open_ends.extend(e_open)
                else:
                    open_ends.append((nid, "else"))
            else:
                # inget else i gamla modellen = "fortsätt" -> else-handtaget öppet
                open_ends.append((nid, "else"))
            return nid, open_ends

        return nid, [(nid, "out")]


def steps_to_graph(steps: list, layout: dict | None = None) -> dict:
    b = _Builder(layout)
    entry, _open = b.chain(steps or [])
    return {"nodes": b.nodes, "edges": b.edges, "start": entry}


def flow_to_graph(flow: dict) -> dict:
    """Ett helt flöde: behåll settings/inputs/input_sets, konvertera steps."""
    if flow.get("nodes") is not None:
        return flow  # redan graf
    g = steps_to_graph(flow.get("steps", []), flow.get("layout"))
    return {
        "format": "graph",
        "nodes": g["nodes"],
        "edges": g["edges"],
        "start": g["start"],
        "settings": flow.get("settings", {}),
        "inputs": flow.get("inputs", []),
        "input_sets": flow.get("input_sets", {}),
        "layout": {},
    }