Coverage for src/lexigram/admin/state/url_sync.py: 83%
58 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
1"""URL state synchronization for Lexigram Admin."""
3from __future__ import annotations
5from typing import TYPE_CHECKING, Any
6import urllib.parse
8if TYPE_CHECKING:
9 from lexigram.admin.state.store import Signal
12class URLStateManager:
13 """Manages synchronization between local state and URL query parameters.
15 This allows for deep linking and maintaining UI state across refreshes.
16 """
18 def __init__(self, base_url: str = "") -> None:
19 self.base_url = base_url
21 def serialize(self, state: dict[str, Any]) -> str:
22 """Serialize state dictionary to a query string."""
23 params = {}
24 for key, value in state.items():
25 if value is None or (isinstance(value, str) and value == ""):
26 continue
27 params[key] = value
29 return urllib.parse.urlencode(params, doseq=True)
31 def deserialize(self, query_string: str) -> dict[str, Any]:
32 """Deserialize query string back to a state dictionary."""
33 params = urllib.parse.parse_qs(query_string.lstrip("?"))
34 state = {}
36 def _coerce(val) -> Any:
37 # Repair stringified lists (URL Bloat Fix)
38 if isinstance(val, str) and val.startswith("[") and val.endswith("]"):
39 try:
40 import ast
42 parsed = ast.literal_eval(val)
43 if isinstance(parsed, list):
44 return [_coerce(str(i)) for i in parsed]
45 except (ValueError, SyntaxError, TypeError):
46 pass
48 # Handle numeric/bool
49 if val.isdigit():
50 return int(val)
51 low = val.lower()
52 if low == "true":
53 return True
54 if low == "false":
55 return False
56 return val
58 for key, values in params.items():
59 if not values:
60 continue
62 # Parse each value in the list
63 parsed_values = []
64 for v in values:
65 res = _coerce(v)
66 if isinstance(res, list):
67 parsed_values.extend(res)
68 else:
69 parsed_values.append(res)
71 # Deduplicate
72 unique = []
73 seen = set()
74 for v in parsed_values:
75 if v not in seen:
76 unique.append(v)
77 seen.add(v)
79 if len(unique) > 1:
80 state[key] = unique
81 elif unique:
82 state[key] = unique[0]
84 return state
86 def bind_signal(
87 self,
88 name: str,
89 signal: Signal[Any],
90 request_query: str | None = None,
91 ) -> None:
92 """Bind a signal to a URL parameter, initializing it from query if present."""
93 if request_query:
94 initial_state = self.deserialize(request_query)
95 if name in initial_state:
96 signal.set(initial_state[name])
98 # In a real environment, we'd add a watch(lambda: some_callback(signal.get()))
99 # that updates the window.location. This is the structural foundation.