Skip to content

Commit e8ae118

Browse files
committed
feat(utils,websocket): create reuseable websocket utils and add transform function for playground to transform websocket route info
1 parent 0bb215c commit e8ae118

File tree

3 files changed

+268
-167
lines changed

3 files changed

+268
-167
lines changed

chanx/playground/utils.py

Lines changed: 28 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
"""
2-
Utility functions for WebSocket route discovery and inspection.
2+
WebSocket playground utilities.
33
4-
This module provides tools to dynamically discover WebSocket routes
5-
in a Django Channels application and generate example messages for
6-
WebSocket consumer endpoints.
4+
This module provides specialized utilities for the WebSocket playground,
5+
transforming route information into a format suitable for display and interaction.
76
"""
87

98
import inspect
@@ -17,14 +16,14 @@
1716
get_type_hints,
1817
)
1918

20-
from channels.routing import URLRouter, get_default_application
2119
from django.http import HttpRequest
2220

2321
from polyfactory.factories.pydantic_factory import ModelFactory
2422
from pydantic import BaseModel
2523

2624
from chanx.messages.base import BaseIncomingMessage
2725
from chanx.utils.logging import logger
26+
from chanx.utils.websocket import RouteInfo, get_websocket_routes, transform_routes
2827

2928

3029
class MessageExample(TypedDict):
@@ -51,185 +50,51 @@ class WebSocketRoute(TypedDict, total=False):
5150
message_examples: list[MessageExample]
5251

5352

54-
def get_websocket_routes(request: HttpRequest | None = None) -> list[WebSocketRoute]:
53+
def get_playground_websocket_routes(
54+
request: HttpRequest | None = None,
55+
) -> list[WebSocketRoute]:
5556
"""
56-
Extract all WebSocket routes from the ASGI application.
57+
Get WebSocket routes formatted for the playground.
5758
58-
This function traverses the Django Channels routing configuration
59-
to discover all available WebSocket endpoints, their paths, and
60-
generates example messages for each endpoint based on the
61-
message schema defined in the consumer.
59+
Uses the core WebSocket route discovery mechanism and transforms the routes
60+
into a format suitable for the playground UI, including example messages.
6261
6362
Args:
6463
request: The HTTP request object, used to determine the current domain.
6564
If None, defaults to localhost:8000.
6665
6766
Returns:
68-
A list of dictionaries containing route information including URL,
69-
description, and example messages.
67+
A list of WebSocketRoute objects with UI-friendly information.
7068
"""
71-
endpoints: list[WebSocketRoute] = []
69+
# Get raw routes from the core utility
70+
raw_routes = get_websocket_routes(request)
7271

73-
# Determine the WebSocket base URL based on the request
74-
ws_base_url: str = _get_websocket_base_url(request)
72+
# Transform routes into the format needed for the playground
73+
return transform_routes(raw_routes, _transform_route_for_playground)
7574

76-
# Extract the WebSocket protocol handler from the ASGI application
77-
application = get_default_application()
78-
if hasattr(application, "application_mapping"):
79-
# If it's a ProtocolTypeRouter
80-
ws_app: Any = application.application_mapping.get("websocket")
8175

82-
# Extract routes
83-
if ws_app:
84-
_traverse_middleware(ws_app, "", endpoints, ws_base_url)
85-
86-
return endpoints
87-
88-
89-
def _get_websocket_base_url(request: HttpRequest | None) -> str:
90-
"""
91-
Determine the WebSocket base URL based on the request.
92-
93-
Constructs a WebSocket URL (ws:// or wss://) based on the
94-
domain in the request object.
95-
96-
Args:
97-
request: The HTTP request object.
98-
99-
Returns:
100-
The WebSocket base URL (ws:// or wss:// followed by domain).
101-
"""
102-
if request is None:
103-
return "ws://localhost:8000"
104-
105-
# Get the current domain from the request
106-
domain: str = request.get_host()
107-
108-
# Determine if we should use secure WebSockets (wss://) based on the request
109-
is_secure: bool = request.is_secure()
110-
protocol: str = "wss://" if is_secure else "ws://"
111-
112-
return f"{protocol}{domain}"
113-
114-
115-
def _traverse_middleware(
116-
app: Any, prefix: str, endpoints: list[WebSocketRoute], ws_base_url: str
117-
) -> None:
118-
"""
119-
Traverse through middleware layers to find the URLRouter.
120-
121-
Recursively explores the middleware stack to find URLRouter instances
122-
and extract route information from them. It uses the fact that
123-
middleware typically stores the inner app as self.inner.
124-
125-
Args:
126-
app: The current application or middleware to traverse.
127-
prefix: URL prefix accumulated so far.
128-
endpoints: List to store discovered endpoints.
129-
ws_base_url: Base URL for WebSocket connections.
76+
def _transform_route_for_playground(route: RouteInfo) -> WebSocketRoute:
13077
"""
131-
# Skip if app is None
132-
if app is None:
133-
return
134-
135-
# If it's a URLRouter, extract routes
136-
if isinstance(app, URLRouter):
137-
_extract_routes_from_router(app, prefix, endpoints, ws_base_url)
138-
return
139-
140-
# Try to access the inner application (standard middleware pattern)
141-
inner_app: Any | None = getattr(app, "inner", None)
78+
Transform a raw route into a playground-friendly format.
14279
143-
# If inner isn't found, try other common attributes that might hold the next app
144-
if inner_app is None:
145-
for attr_name in ["app", "application"]:
146-
inner_app = getattr(app, attr_name, None)
147-
if inner_app is not None:
148-
break
149-
150-
# If we found an inner app, continue traversal
151-
if inner_app is not None:
152-
_traverse_middleware(inner_app, prefix, endpoints, ws_base_url)
153-
154-
155-
def _extract_routes_from_router(
156-
router: URLRouter, prefix: str, endpoints: list[WebSocketRoute], ws_base_url: str
157-
) -> None:
158-
"""
159-
Extract routes from a URLRouter object.
160-
161-
Processes each route in the router, extracting path patterns and
162-
handler information, and recursively traversing nested routers.
163-
164-
Args:
165-
router: The router to extract routes from.
166-
prefix: URL prefix accumulated so far.
167-
endpoints: List to store discovered endpoints.
168-
ws_base_url: Base URL for WebSocket connections.
169-
"""
170-
for route in router.routes:
171-
try:
172-
# Get the pattern string
173-
pattern: str = _get_pattern_string(route)
174-
175-
# Build the full path
176-
full_path: str = f"{prefix}{pattern}"
177-
178-
# Get the handler
179-
handler: Any = route.callback
180-
181-
# If it's another router, recurse into it
182-
if isinstance(handler, URLRouter):
183-
_extract_routes_from_router(handler, full_path, endpoints, ws_base_url)
184-
else:
185-
# For consumers, get info with message examples
186-
endpoint_info: WebSocketRoute = _get_handler_info(
187-
handler, full_path, ws_base_url
188-
)
189-
endpoints.append(endpoint_info)
190-
except AttributeError as e:
191-
# More specific error for attribute issues
192-
logger.exception(
193-
f"AttributeError while parsing route: {ws_base_url}/{prefix}. Error: {str(e)}"
194-
)
195-
except Exception as e:
196-
# For other unexpected errors
197-
logger.exception(
198-
f"Error parsing route: {ws_base_url}/{prefix}. Error: {str(e)}"
199-
)
200-
201-
202-
def _get_pattern_string(route: Any) -> str:
203-
"""
204-
Extract pattern string from a route object.
205-
206-
Handles different route pattern implementations to extract
207-
the URL pattern string.
80+
This function extracts metadata from a WebSocket consumer and formats it
81+
for display in the playground UI, including generating example messages.
20882
20983
Args:
210-
route: The route object to extract pattern from.
84+
route: The RouteInfo dataclass instance
21185
21286
Returns:
213-
The cleaned URL pattern string.
87+
A WebSocketRoute with UI-friendly information
21488
"""
215-
if hasattr(route, "pattern"):
216-
# For URLRoute
217-
if hasattr(route.pattern, "pattern"):
218-
pattern: str = route.pattern.pattern
219-
else:
220-
# For RoutePattern
221-
pattern = str(route.pattern)
222-
else:
223-
pattern = str(route)
224-
225-
# Clean up the pattern string
226-
pattern = pattern.replace("^", "").replace("$", "")
227-
return pattern
89+
# Get handler info with examples for the playground
90+
return _get_handler_info(
91+
handler=route.handler, path=route.path, ws_base_url=route.base_url
92+
)
22893

22994

23095
def _get_handler_info(handler: Any, path: str, ws_base_url: str) -> WebSocketRoute:
23196
"""
232-
Extract information about a route handler.
97+
Extract information about a route handler for the playground.
23398
23499
Extracts metadata from a WebSocket consumer including its name,
235100
description, and message schema, and generates example messages.
@@ -240,8 +105,7 @@ def _get_handler_info(handler: Any, path: str, ws_base_url: str) -> WebSocketRou
240105
ws_base_url: Base URL for WebSocket connections.
241106
242107
Returns:
243-
Information about the handler including name, URL, description,
244-
and example messages.
108+
Information about the handler formatted for the playground.
245109
"""
246110
# Default values
247111
name: str = getattr(handler, "__name__", "Unknown")
@@ -283,7 +147,7 @@ def _create_example(msg_type: type[BaseModel]) -> MessageExample:
283147
Returns:
284148
A formatted example with name, description, and sample data.
285149
"""
286-
description = inspect.getdoc(msg_type) or f"Example of {msg_type.__name__}"
150+
description: str = inspect.getdoc(msg_type) or f"Example of {msg_type.__name__}"
287151

288152
# Create the example using the factory
289153
factory = ModelFactory.create_factory(model=msg_type)

chanx/playground/views.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from chanx.utils.logging import logger
1111

12-
from .utils import WebSocketRoute, get_websocket_routes
12+
from .utils import WebSocketRoute, get_playground_websocket_routes
1313

1414

1515
class WebSocketPlaygroundView(TemplateView):
@@ -57,8 +57,10 @@ class WebSocketInfoView(APIView):
5757

5858
def get(self, request: HttpRequest) -> Response:
5959
try:
60-
# Get available WebSocket endpoints using the utility function
61-
available_endpoints: list[WebSocketRoute] = get_websocket_routes(request)
60+
# Get available WebSocket endpoints using the new playground utility function
61+
available_endpoints: list[WebSocketRoute] = get_playground_websocket_routes(
62+
request
63+
)
6264

6365
# Use the list serializer directly
6466
serializer = WebSocketRouteListSerializer(available_endpoints)

0 commit comments

Comments
 (0)