Coverage for src/lexigram/admin/dashboard/route_integrator.py: 0%

116 statements  

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

1from __future__ import annotations 

2 

3from collections.abc import Sequence 

4import inspect 

5from typing import TYPE_CHECKING, Any 

6 

7from lexigram.admin.navigation.clusters import ( 

8 CLUSTER_GROUP, 

9 cluster_child_href, 

10) 

11from lexigram.contracts.admin.types import ( 

12 ManagementPageDefinition, 

13 SettingsPanelDefinition, 

14) 

15from lexigram.logging import get_logger 

16 

17if TYPE_CHECKING: 

18 from lexigram.admin.core.routing import AdminRouter 

19 from lexigram.admin.dashboard.naming_policy import NamingPolicy 

20 from lexigram.contracts.admin.contributor import BaseAdminContributor 

21 

22logger = get_logger(__name__) 

23 

24 

25from lexigram.admin.dashboard.page_handlers import ( 

26 AdminPageHandler, 

27 _placeholder_page, 

28 StructuredPageHandler, 

29 _resolve_handler, 

30) 

31 

32 

33def _register_pages( 

34 router: AdminRouter, 

35 naming_policy: NamingPolicy, 

36 prefix: str, 

37 pages: list[ManagementPageDefinition], 

38 container: Any = None, 

39) -> None: 

40 """Register management page routes on the admin router. 

41 

42 Routes are registered relative to the admin app mount point 

43 (the prefix is stripped before registration — the admin router 

44 already lives under the admin prefix due to ``AdminRouter.mount()``). 

45 ``registered_internal_paths`` is updated externally so that 

46 ``_ensure_nav_route`` does not create duplicate placeholders. 

47 """ 

48 # prefix is intentionally unused — routes live inside the mounted 

49 # admin app and must be relative to its mount point. 

50 for page in pages: 

51 handler = _resolve_handler(page.handler) 

52 if handler is None: 

53 continue 

54 if inspect.isclass(handler) and container is not None: 

55 handler = AdminPageHandler(handler, container) 

56 else: 

57 handler = StructuredPageHandler(handler) 

58 path = page.route_path 

59 if not path.startswith("/"): 

60 path = f"/{path}" 

61 ns_name = naming_policy.namespaced(page.contributor, page.name) 

62 naming_policy.register("page", ns_name) 

63 router.add_route(path=path, method="GET", handler=handler, name=ns_name) 

64 

65 

66def _register_settings( 

67 router: AdminRouter, 

68 naming_policy: NamingPolicy, 

69 prefix: str, 

70 panels: list[SettingsPanelDefinition], 

71 container: Any = None, 

72) -> None: 

73 """Register settings panel routes on the admin router. 

74 

75 Routes are registered relative to the admin app mount point 

76 (the prefix is stripped before registration — see ``_register_pages``). 

77 """ 

78 for panel in panels: 

79 handler = _resolve_handler(panel.handler) 

80 if handler is None: 

81 continue 

82 if inspect.isclass(handler) and container is not None: 

83 handler = AdminPageHandler(handler, container) 

84 else: 

85 handler = StructuredPageHandler(handler) 

86 path = panel.route_path 

87 if not path.startswith("/"): 

88 path = f"/{path}" 

89 ns_name = naming_policy.namespaced(panel.contributor, panel.name) 

90 naming_policy.register("panel", ns_name) 

91 router.add_route(path=path, method="GET", handler=handler, name=ns_name) 

92 

93 

94class RouteIntegrator: 

95 """Collects ``AdminRouteSpec``, ``ManagementPageDefinition``, and 

96 ``SettingsPanelDefinition`` from contributors and registers them 

97 on the router.""" 

98 

99 def __init__( 

100 self, 

101 *, 

102 router: AdminRouter, 

103 naming_policy: NamingPolicy, 

104 route_prefix: str = "", 

105 container: Any = None, 

106 ) -> None: 

107 self._router = router 

108 self._naming = naming_policy 

109 self._prefix = route_prefix 

110 self._container = container 

111 

112 def register(self, contributors: Sequence[BaseAdminContributor]) -> None: 

113 """Register each contributor's routes, management pages, and 

114 settings panels on the admin router. Nav-item URLs that don't 

115 have a corresponding handler automatically get a placeholder 

116 route so they never 404.""" 

117 registered_internal_paths: set[str] = set() 

118 

119 for c in contributors: 

120 # Routes 

121 for spec in c.get_routes(): 

122 ns_name = self._naming.namespaced(c.package_source, spec.name) 

123 self._naming.register("route", ns_name) 

124 path = spec.path 

125 # Contributor specs carry the full URL (e.g. "/admin/...") 

126 # but routes live inside the mounted admin app, so strip 

127 # the mount prefix like _ensure_nav_route does. 

128 if self._prefix and path.startswith(self._prefix): 

129 path = path[len(self._prefix) :] 

130 if not path: 

131 path = "/" 

132 registered_internal_paths.add(path) 

133 self._router.add_route( 

134 path=path, 

135 method=spec.method, 

136 handler=spec.handler, 

137 name=ns_name, 

138 ) 

139 

140 # Management pages 

141 pages = c.get_management_pages() 

142 if pages: 

143 for page in pages: 

144 internal = page.route_path 

145 if not internal.startswith("/"): 

146 internal = f"/{internal}" 

147 registered_internal_paths.add(internal) 

148 _register_pages( 

149 self._router, 

150 self._naming, 

151 self._prefix, 

152 pages, # type: ignore[arg-type] 

153 container=self._container, 

154 ) 

155 

156 # Settings panels 

157 panels = c.get_settings_panels() 

158 if panels: 

159 for panel in panels: 

160 internal = panel.route_path 

161 if not internal.startswith("/"): 

162 internal = f"/{internal}" 

163 registered_internal_paths.add(internal) 

164 _register_settings( 

165 self._router, 

166 self._naming, 

167 self._prefix, 

168 panels, # type: ignore[arg-type] 

169 container=self._container, 

170 ) 

171 

172 # Auto-register placeholder routes for nav items without handlers. 

173 for c in contributors: 

174 for item in c.get_navigation_items(): 

175 self._ensure_nav_route(item, registered_internal_paths) 

176 for child in item.children or (): 

177 self._ensure_nav_route(child, registered_internal_paths) 

178 

179 # Cluster areas are also reachable under the center namespace 

180 # (e.g. /admin/infrastructure/web), mirroring how settings 

181 # sub-pages nest below /admin/settings. Aliases share the source 

182 # route's handler — real page or placeholder alike. 

183 for c in contributors: 

184 for item in c.get_navigation_items(): 

185 self._register_cluster_alias(item) 

186 for child in item.children or (): 

187 self._register_cluster_alias(child) 

188 

189 def _ensure_nav_route( 

190 self, 

191 item: Any, 

192 registered_paths: set[str], 

193 ) -> None: 

194 """Register a placeholder route for *item* if its URL isn't covered.""" 

195 url = item.url 

196 if not url or url.startswith("http"): 

197 return 

198 

199 internal = url 

200 if self._prefix and internal.startswith(self._prefix): 

201 internal = internal[len(self._prefix) :] 

202 if not internal: 

203 internal = "/" 

204 

205 if internal in registered_paths or url in registered_paths: 

206 return 

207 

208 safe_label = item.label.lower().replace(" ", "_").replace("/", "_") 

209 self._router.add_route( 

210 path=internal, 

211 method="GET", 

212 handler=_placeholder_page, 

213 name=f"placeholder_{safe_label}", 

214 ) 

215 registered_paths.add(internal) 

216 

217 def _register_cluster_alias(self, item: Any) -> None: 

218 """Register a namespaced alias for a cluster group nav item. 

219 

220 Cluster areas live under the center namespace (``/admin/ 

221 infrastructure/web``) in addition to their contributor URL. When 

222 the source URL has a real handler, the alias reuses it; otherwise 

223 the alias falls back to the placeholder page. 

224 """ 

225 if getattr(item, "group", None) != CLUSTER_GROUP: 

226 return 

227 url = item.url 

228 if not url or url.startswith("http"): 

229 return 

230 namespaced = cluster_child_href(url) 

231 if not namespaced or namespaced == url: 

232 return 

233 

234 internal = url 

235 internal_ns = namespaced 

236 if self._prefix and internal.startswith(self._prefix): 

237 internal = internal[len(self._prefix) :] 

238 if self._prefix and internal_ns.startswith(self._prefix): 

239 internal_ns = internal_ns[len(self._prefix) :] 

240 if internal_ns == internal: 

241 return 

242 

243 safe_label = item.label.lower().replace(" ", "_").replace("/", "_") 

244 if not self._router.alias_route( 

245 internal, 

246 internal_ns, 

247 name=f"cluster_alias_{safe_label}", 

248 ): 

249 self._router.add_route( 

250 path=internal_ns, 

251 method="GET", 

252 handler=_placeholder_page, 

253 name=f"cluster_alias_{safe_label}", 

254 )