Coverage for src/lexigram/admin/state/url_sync.py: 0%

58 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 23:18 +0800

1"""URL state synchronization for Lexigram Admin.""" 

2 

3from __future__ import annotations 

4 

5from typing import TYPE_CHECKING, Any 

6import urllib.parse 

7 

8if TYPE_CHECKING: 

9 from lexigram.admin.state.store import Signal 

10 

11 

12class URLStateManager: 

13 """Manages synchronization between local state and URL query parameters. 

14 

15 This allows for deep linking and maintaining UI state across refreshes. 

16 """ 

17 

18 def __init__(self, base_url: str = "") -> None: 

19 self.base_url = base_url 

20 

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 

28 

29 return urllib.parse.urlencode(params, doseq=True) 

30 

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 = {} 

35 

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 

41 

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 

47 

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 

57 

58 for key, values in params.items(): 

59 if not values: 

60 continue 

61 

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) 

70 

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) 

78 

79 if len(unique) > 1: 

80 state[key] = unique 

81 elif unique: 

82 state[key] = unique[0] 

83 

84 return state 

85 

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]) 

97 

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.