Coverage for src/lexigram/web/routing/groups.py: 27%

30 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 04:37 +0800

1"""Route groups for organizing controllers with shared configuration. 

2 

3Provides RouteGroup for grouping controllers with shared prefix, guards, and interceptors. 

4""" 

5 

6from __future__ import annotations 

7 

8from typing import TYPE_CHECKING, Any 

9 

10if TYPE_CHECKING: 

11 from collections.abc import Callable 

12 

13 from lexigram.web.routing.controllers import Controller 

14 

15 

16class RouteGroup: 

17 """Groups controllers with shared configuration. 

18 

19 Usage: 

20 admin_group = RouteGroup( 

21 prefix="/api/v1/admin", 

22 controllers=[UserController, RoleController], 

23 guards=[AuthGuard, RoleGuard("admin")], 

24 interceptors=[AuditLogInterceptor], 

25 ) 

26 

27 # Register with WebProvider 

28 provider = WebProvider( 

29 route_groups=[admin_group], 

30 ) 

31 """ 

32 

33 def __init__( 

34 self, 

35 prefix: str, 

36 controllers: list[type[Controller]], 

37 guards: list[Any] | None = None, 

38 interceptors: list[Any] | None = None, 

39 middleware: list[Any] | None = None, 

40 ): 

41 self.prefix = prefix 

42 self.controllers = controllers 

43 self.guards = guards or [] 

44 self.interceptors = interceptors or [] 

45 self.middleware = middleware or [] 

46 

47 def apply_guards(self, controller_cls: type[Controller]) -> type[Controller]: 

48 """Apply group's guards to controller by creating a subclass.""" 

49 existing_guards = getattr(controller_cls, "_guards", []) 

50 return type( 

51 f"{controller_cls.__name__}_with_guards", 

52 (controller_cls,), 

53 {"_guards": existing_guards + self.guards}, 

54 ) 

55 

56 def apply_interceptors(self, controller_cls: type[Controller]) -> type[Controller]: 

57 """Apply group's interceptors to controller by creating a subclass.""" 

58 existing_interceptors = getattr(controller_cls, "_interceptors", []) 

59 return type( 

60 f"{controller_cls.__name__}_with_interceptors", 

61 (controller_cls,), 

62 {"_interceptors": existing_interceptors + self.interceptors}, 

63 ) 

64 

65 def finalize(self) -> list[type[Controller]]: 

66 """Apply group configuration to all controllers by creating subclasses.""" 

67 results = [] 

68 for controller in self.controllers: 

69 controller = self.apply_guards(controller) 

70 controller = self.apply_interceptors(controller) 

71 

72 # Get existing prefix and combine 

73 existing_prefix = getattr(controller, "prefix", "") 

74 if existing_prefix: 

75 # Combine prefixes 

76 combined_prefix = self.prefix.rstrip("/") + existing_prefix 

77 else: 

78 combined_prefix = self.prefix 

79 

80 # Create final subclass with prefix 

81 results.append( 

82 type( 

83 f"{controller.__name__}_finalized", 

84 (controller,), 

85 {"prefix": combined_prefix}, 

86 ), 

87 ) 

88 

89 return results 

90 

91 

92def group( 

93 prefix: str, 

94 guards: list[Any] | None = None, 

95 interceptors: list[Any] | None = None, 

96 middleware: list[Any] | None = None, 

97) -> Callable[[list[type[Controller]]], RouteGroup]: 

98 """Create a route group. 

99 

100 Usage: 

101 @group("/api/v1/admin", guards=[AuthGuard]) 

102 class AdminController(Controller): 

103 ... 

104 """ 

105 

106 def decorator(controllers: list[type[Controller]]) -> RouteGroup: 

107 return RouteGroup( 

108 prefix=prefix, 

109 controllers=controllers, 

110 guards=guards, 

111 interceptors=interceptors, 

112 middleware=middleware, 

113 ) 

114 

115 return decorator