Skip to content

Commit 935e0dd

Browse files
committed
docs: Add section on routing to typed context pipeline design
- Move DESIGN_typed_context_pipeline into design folder - Add runnable simplified example code in design/appendix_a.dart
1 parent 95f7ffc commit 935e0dd

File tree

2 files changed

+379
-1
lines changed

2 files changed

+379
-1
lines changed

DESIGN_typed_context_pipeline.md renamed to design/DESIGN_typed_context_pipeline.md

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,10 +310,142 @@ This typed pipeline introduces a shift from the traditional `Middleware = Handle
310310
* **Gained**: Strong compile-time type safety for the data flowing through the request context. This significantly reduces a class of runtime errors due to misconfigured pipelines.
311311
* **Different Flexibility**: Some dynamic flexibility found in the `Handler Function(Handler)` pattern (e.g., complex around-logic, dynamically choosing the next handler in the chain from within a middleware) is handled differently (e.g., via exceptions, or by moving logic into handlers). For many common middleware tasks (logging, data enrichment, simple auth checks), the typed pipeline offers a clearer and safer model.
312312

313-
## 8. General Considerations
313+
## 8. Integrating with Routing
314+
315+
The typed context pipeline is designed to prepare a rich, type-safe context that can then be utilized by a routing system to dispatch requests to appropriate endpoint handlers. Relic's `Router<T>` class can be seamlessly integrated with this pipeline.
316+
317+
The core idea is that the `PipelineBuilder` sets up a chain of common middleware (e.g., authentication, session management, logging). The final function passed to `PipelineBuilder.build(...)` will be a "routing dispatcher." This dispatcher uses the context prepared by the common middleware, performs route matching, potentially adds route-specific data (like path parameters) to the context, and then executes the endpoint handler chosen by the router.
318+
319+
### 8.1. Context for Route Parameters
320+
321+
Endpoint handlers often need access to path parameters extracted during routing (e.g., the `:id` in `/users/:id`). A `ContextProperty` and corresponding view should be defined for these.
322+
323+
```dart
324+
// Example: Data class for route parameters
325+
class RouteParameters {
326+
final Map<Symbol, String> params;
327+
RouteParameters(this.params);
328+
329+
String? operator [](Symbol key) => params[key];
330+
// Potentially add other useful accessors
331+
}
332+
333+
// Example: Private ContextProperty for route parameters
334+
// (Typically defined in a routing-related module/library)
335+
final _routeParametersProperty =
336+
ContextProperty<RouteParameters>('relic.routing.parameters');
337+
```
338+
339+
### 8.2. Context Views for Endpoint Handlers
340+
341+
Endpoint handlers will require a context view that provides access to both the common context data (prepared by the initial pipeline) and the specific `RouteParameters`.
342+
343+
```dart
344+
// Example: A view that combines UserContext (from common pipeline) and RouteParameters
345+
// (Assumes UserContextView is already defined)
346+
extension type UserRouteContextView(RequestContext _relicContext) implements UserContextView {
347+
RouteParameters get routeParams => _routeParametersProperty.get(_relicContext);
348+
349+
// This method will be called by the routing dispatcher after parameters are extracted.
350+
void attachRouteParameters(RouteParameters params) {
351+
_routeParametersProperty.set(_relicContext, params);
352+
}
353+
}
354+
355+
// Other combinations can be created as needed (e.g., BaseRouteContextView, UserSessionRouteContextView).
356+
```
357+
358+
### 8.3. Router's Generic Type `T`
359+
360+
The generic type `T` in `Router<T>` will represent the actual endpoint handler functions. These functions will expect an enriched context view that includes common context data and route parameters.
361+
362+
For example, `T` could be:
363+
`FutureOr<Response> Function(UserRouteContextView context)`
364+
365+
### 8.4. The Routing Dispatcher Function
366+
367+
This function is passed to `PipelineBuilder.build(...)`. It receives the context prepared by the common middleware chain (e.g., `UserContextView`). Its responsibilities are:
368+
1. Use the incoming request details (from the context) to perform a route lookup via `Router<T>`.
369+
2. Handle cases where no route is matched (e.g., by throwing a `RouteNotFoundException` or returning a 404 `Response`).
370+
3. If a route is matched, extract the endpoint handler and any path parameters.
371+
4. Create the specific context view required by the endpoint handler (e.g., `UserRouteContextView`), attaching the extracted `RouteParameters` to it.
372+
5. Execute the chosen endpoint handler with this enriched context.
373+
374+
```dart
375+
// Example: Routing Dispatcher
376+
// Assume 'myAppRouter' is an instance of Router<FutureOr<Response> Function(UserRouteContextView)>
377+
// Assume 'UserContextView' is the output view from the common middleware pipeline.
378+
379+
FutureOr<Response> routingDispatcher(UserContextView commonContext) {
380+
final request = commonContext.request;
381+
382+
// Perform route lookup using Relic's Router.
383+
final lookupResult = myAppRouter.lookup(
384+
request.method.convert(), // Or however method is represented
385+
request.uri.path);
386+
387+
if (lookupResult == null || lookupResult.value == null) {
388+
// Option 1: Throw a specific RouteNotFoundException for centralized error handling.
389+
throw RouteNotFoundException(
390+
'Route not found for ${request.method.value} ${request.uri.path}');
391+
// Option 2: Directly return a 404 Response (less flexible for global error handling).
392+
// return Response.notFound(...);
393+
}
394+
395+
final endpointHandler = lookupResult.value;
396+
final pathParams = RouteParameters(lookupResult.parameters);
397+
398+
// Create the specific context view for the endpoint handler by wrapping the same
399+
// underlying RequestContext and attaching the extracted route parameters.
400+
final endpointContext = UserRouteContextView(commonContext._relicContext);
401+
endpointContext.attachRouteParameters(pathParams);
402+
403+
// Execute the chosen endpoint handler.
404+
return endpointHandler(endpointContext);
405+
}
406+
```
407+
408+
### 8.5. Pipeline Setup with Routing
409+
410+
The `PipelineBuilder` is used to construct the common middleware chain, with the `routingDispatcher` as the final step.
411+
412+
```dart
413+
// Example: In your server setup
414+
void setupServer(Router<FutureOr<Response> Function(UserRouteContextView)> myAppRouter) { // Pass your router
415+
final requestHandler = PipelineBuilder.start() // Input: BaseContextView
416+
.add(authenticationMiddleware) // Output: UserContextView
417+
// ... other common middleware (e.g., session, logging) ...
418+
// The output view of the last common middleware must match
419+
// the input view expected by `routingDispatcher`.
420+
.build(routingDispatcher); // `routingDispatcher` uses UserContextView
421+
422+
// This `requestHandler` can now be used with Relic's server mechanism.
423+
// e.g., relicServe(requestHandler, ...);
424+
}
425+
```
426+
427+
### 8.6. Implications
428+
429+
* **Separation of Concerns**: Common middleware (auth, logging, sessions) are managed by the `PipelineBuilder`, preparing a general-purpose typed context. The `routingDispatcher` then handles routing-specific concerns and further context enrichment (route parameters) for the final endpoint handlers.
430+
* **Type Safety End-to-End**: Endpoint handlers receive a context view that is guaranteed by the type system to contain all necessary data from both the common pipeline and the routing process.
431+
* **Flexibility**: This pattern allows different sets of common middleware to be composed for distinct parts of an application. By creating multiple `PipelineBuilder` instances, each tailored with specific middleware and culminating in a different routing dispatcher (or final handler), an application can support varied requirements across its modules.
432+
433+
For example, an application might have:
434+
* An `/api/v1` section with `apiAuthMiddleware` leading to an API router, producing an `ApiUserContextView`. The `PipelineBuilder.build(...)` for this would result in an `apiV1RequestHandler: FutureOr<Response> Function(NewContext)`.
435+
* An `/admin` section with `adminAuthMiddleware` and `sessionMiddleware` leading to an admin router, producing an `AdminSessionContextView`. This would result in an `adminRequestHandler: FutureOr<Response> Function(NewContext)`.
436+
* A `/public` section with `cachingMiddleware` leading to a simpler router, using a `BaseContextView`. This would result in a `publicRequestHandler: FutureOr<Response> Function(NewContext)`.
437+
438+
A top-level Relic `Router<FutureOr<Response> Function(NewContext)>` can then be used to select the appropriate pre-built pipeline handler based on path prefixes (e.g., requests to `/api/v1/**` lead to invoking `apiV1RequestHandler`). The main server entry point would create the initial `NewContext`, look up the target pipeline handler using this top-level router, and then pass the `NewContext` to the chosen handler. This top-level router doesn't deal with the typed context views itself but delegates to handlers that encapsulate their own typed pipelines. This maintains type safety within each specialized pipeline while allowing for a clean, router-based architecture at the highest level. *See Appendix A for a conceptual code sketch illustrating this top-level routing approach.*
439+
440+
## 9. General Considerations
314441

315442
* **PipelineBuilder Complexity**: The implementation of `PipelineBuilder`, especially its generic typing, is somewhat complex, but this complexity is encapsulated for the end-user.
316443
* **Boilerplate for `ContextProperty` and Views**: Each new piece of context data requires defining a `ContextProperty` instance and corresponding view methods. However, this is more structured and less error-prone than raw `Expando` usage.
317444
* **Learning Curve**: Developers using the framework will need to understand context views, `ContextProperty`, the role of `requestToken`, the pipeline builder, and the implications of the new middleware paradigm.
318445
* **Discipline with `requestToken`**: The `ContextProperty` helper ensures that data is keyed off the stable `token` within the `RequestContext`, mitigating direct misuse of `Expando`s with transient `RequestContext` instances themselves as keys.
319446
* **Middleware Return Types**: Middleware authors must be careful to return the correct context view type that accurately reflects the data they've attached via `ContextProperty` and the `requestToken`.
447+
448+
449+
## Appendix A: Conceptual Code Example for Top-Level Routing
450+
451+
This appendix provides a conceptual, runnable (with stubs) Dart code sketch to illustrate how different `PipelineBuilder` instances can create specialized request handling chains, and how a top-level router can direct traffic to the appropriate chain, all starting with a common `NewContext`. See [appendix_a.dart](appendix_a.dart).

design/appendix_a.dart

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
// ignore_for_file: avoid_print
2+
3+
import 'dart:async';
4+
import 'package:relic/src/router/router.dart';
5+
6+
// === Core Stubs (simplified) ===
7+
class Request {
8+
final Uri uri;
9+
final Method method;
10+
final Map<String, String> headers;
11+
Request({required this.uri, required this.method, this.headers = const {}});
12+
}
13+
14+
class Response {
15+
final int statusCode;
16+
final String body;
17+
Response(this.statusCode, this.body);
18+
19+
static Response ok(final String body) => Response(200, body);
20+
static Response notFound(final String body) => Response(404, body);
21+
static Response unauthorized(final String body) => Response(401, body);
22+
}
23+
24+
class RequestContext {
25+
final Request request;
26+
final Object token; // Stable unique token
27+
RequestContext(this.request, this.token);
28+
}
29+
30+
class NewContext extends RequestContext {
31+
NewContext(super.request, super.token);
32+
}
33+
34+
// === ContextProperty and Views Stubs ===
35+
class ContextProperty<T extends Object> {
36+
final Expando<T> _expando;
37+
final String? _debugName;
38+
39+
ContextProperty([this._debugName]) : _expando = Expando<T>(_debugName);
40+
T get(final RequestContext ctx) {
41+
final val = _expando[ctx.token];
42+
if (val == null) {
43+
throw StateError('Property ${_debugName ?? T.toString()} not found');
44+
}
45+
return val;
46+
}
47+
48+
void set(final RequestContext ctx, final T val) => _expando[ctx.token] = val;
49+
}
50+
51+
extension type BaseContextView(RequestContext _relicContext) {
52+
Request get request => _relicContext.request;
53+
}
54+
55+
// User data and view
56+
class User {
57+
final String id;
58+
final String name;
59+
User(this.id, this.name);
60+
}
61+
62+
final _userProperty = ContextProperty<User>('user');
63+
extension type UserContextView(RequestContext _relicContext)
64+
implements BaseContextView {
65+
User get user => _userProperty.get(_relicContext);
66+
void attachUser(final User user) => _userProperty.set(_relicContext, user);
67+
}
68+
69+
// Admin data and view
70+
class AdminRole {
71+
final String roleName;
72+
AdminRole(this.roleName);
73+
}
74+
75+
final _adminRoleProperty = ContextProperty<AdminRole>('admin_role');
76+
extension type AdminContextView(RequestContext _relicContext)
77+
implements UserContextView {
78+
// Admin also has User
79+
AdminRole get adminRole => _adminRoleProperty.get(_relicContext);
80+
void attachAdminRole(final AdminRole role) =>
81+
_adminRoleProperty.set(_relicContext, role);
82+
}
83+
84+
// === PipelineBuilder Stub ===
85+
class PipelineBuilder<TInView extends BaseContextView, TOutView> {
86+
final TOutView Function(TInView) _chain;
87+
PipelineBuilder._(this._chain);
88+
89+
static PipelineBuilder<BaseContextView, BaseContextView> start() {
90+
return PipelineBuilder._((final BaseContextView view) => view);
91+
}
92+
93+
PipelineBuilder<TInView, TNextOutView> add<TNextOutView>(
94+
final TNextOutView Function(TOutView currentView) middleware,
95+
) {
96+
return PipelineBuilder<TInView, TNextOutView>._(
97+
(final TInView initialView) {
98+
final previousOutput = _chain(initialView);
99+
return middleware(previousOutput);
100+
});
101+
}
102+
103+
FutureOr<Response> Function(NewContext initialContext) build(
104+
final FutureOr<Response> Function(TOutView finalView) handler,
105+
) {
106+
final TOutView Function(BaseContextView) builtChain =
107+
_chain as TOutView Function(BaseContextView);
108+
return (final NewContext initialContext) {
109+
final initialView = BaseContextView(initialContext)
110+
as TInView; // Cast for the chain start
111+
final finalView = builtChain(initialView);
112+
return handler(finalView);
113+
};
114+
}
115+
}
116+
117+
// === Placeholder Middleware ===
118+
// API Auth: Adds User, returns UserContextView
119+
UserContextView apiAuthMiddleware(final BaseContextView inputView) {
120+
print('API Auth Middleware Running for ${inputView.request.uri.path}');
121+
if (inputView.request.headers['X-API-Key'] == 'secret-api-key') {
122+
final userView = UserContextView(inputView._relicContext);
123+
userView.attachUser(User('api_user_123', 'API User'));
124+
return userView;
125+
}
126+
throw Response(401, 'API Key Required'); // Short-circuiting via exception
127+
}
128+
129+
// Admin Auth: Adds User and AdminRole, returns AdminContextView
130+
AdminContextView adminAuthMiddleware(final BaseContextView inputView) {
131+
print('Admin Auth Middleware Running for ${inputView.request.uri.path}');
132+
if (inputView.request.headers['X-Admin-Token'] ==
133+
'super-secret-admin-token') {
134+
final userView = UserContextView(inputView._relicContext);
135+
userView.attachUser(User('admin_user_007', 'Admin User'));
136+
137+
final adminView = AdminContextView(inputView._relicContext);
138+
adminView.attachAdminRole(AdminRole('super_admin'));
139+
return adminView;
140+
}
141+
throw Response(401, 'Admin Token Required');
142+
}
143+
144+
T generalLoggingMiddleware<T extends BaseContextView>(final T inputView) {
145+
// Weird analyzer bug inputView cannot be null here.
146+
// Compiler and interpreter don't complain. Trying:
147+
// final req = inputView!.request;
148+
// won't work ¯\_(ツ)_/¯
149+
// ignore: unchecked_use_of_nullable_value
150+
final req = inputView.request;
151+
print('Logging: ${req.method} ${req.uri.path}');
152+
return inputView;
153+
}
154+
155+
// === Endpoint Handlers ===
156+
FutureOr<Response> handleApiUserDetails(final UserContextView context) {
157+
print('Handling API User Details for ${context.user.name}');
158+
return Response.ok('API User: ${context.user.name} (id: ${context.user.id})');
159+
}
160+
161+
FutureOr<Response> handleAdminDashboard(final AdminContextView context) {
162+
print(
163+
'Handling Admin Dashboard for ${context.user.name} (${context.adminRole.roleName})');
164+
return Response.ok(
165+
'Admin: ${context.user.name}, Role: ${context.adminRole.roleName}');
166+
}
167+
168+
FutureOr<Response> handlePublicInfo(final BaseContextView context) {
169+
print('Handling Public Info for ${context.request.uri.path}');
170+
return Response.ok('This is public information.');
171+
}
172+
173+
typedef Handler = FutureOr<Response> Function(NewContext);
174+
175+
void main() async {
176+
// === 1. Build Specialized Pipeline Handlers ===
177+
final apiHandler = PipelineBuilder.start()
178+
.add(generalLoggingMiddleware)
179+
.add(apiAuthMiddleware)
180+
.build(handleApiUserDetails);
181+
182+
final adminHandler = PipelineBuilder.start()
183+
.add(generalLoggingMiddleware)
184+
.add(adminAuthMiddleware)
185+
.build(handleAdminDashboard);
186+
187+
final publicHandler = PipelineBuilder.start()
188+
.add(generalLoggingMiddleware)
189+
.build(handlePublicInfo);
190+
191+
// === 2. Configure Top-Level Router ===
192+
final topLevelRouter = Router<Handler>()
193+
..any('/api/users/**', apiHandler)
194+
..any('/admin/dashboard/**', adminHandler)
195+
..any('/public/**', publicHandler);
196+
197+
// === 3. Main Server Request Handler ===
198+
FutureOr<Response> mainServerRequestHandler(final Request request) {
199+
final initialContext = NewContext(request, Object());
200+
print('\nProcessing ${request.method} ${request.uri.path}');
201+
202+
try {
203+
final targetPipelineHandler =
204+
topLevelRouter.lookup(request.method, request.uri.path)?.value;
205+
206+
if (targetPipelineHandler != null) {
207+
return targetPipelineHandler(initialContext);
208+
} else {
209+
print('No top-level route matched.');
210+
return Response.notFound('Service endpoint not found.');
211+
}
212+
} on Response catch (e) {
213+
print('Request short-circuited with response: ${e.statusCode}');
214+
return e;
215+
} catch (e) {
216+
print('Unhandled error: $e');
217+
return Response(500, 'Internal Server Error');
218+
}
219+
}
220+
221+
// === Simulate some requests ===
222+
final requests = [
223+
Request(
224+
uri: Uri.parse('/api/users/123'),
225+
method: Method.get,
226+
headers: {'X-API-Key': 'secret-api-key'},
227+
),
228+
Request(
229+
uri: Uri.parse('/api/users/456'),
230+
method: Method.get,
231+
headers: {'X-API-Key': 'wrong-key'},
232+
),
233+
Request(
234+
uri: Uri.parse('/admin/dashboard'),
235+
method: Method.get,
236+
headers: {'X-Admin-Token': 'super-secret-admin-token'},
237+
),
238+
Request(uri: Uri.parse('/public/info'), method: Method.get),
239+
Request(uri: Uri.parse('/unknown/path'), method: Method.get),
240+
];
241+
242+
for (final req in requests) {
243+
final res = await mainServerRequestHandler(req);
244+
print('Response for ${req.uri.path}: ${res.statusCode} - ${res.body}');
245+
}
246+
}

0 commit comments

Comments
 (0)