From a8226d7b87a916658ed94bcecc3f46e95d8bb349 Mon Sep 17 00:00:00 2001 From: Patrick Stewart Date: Tue, 31 Dec 2024 11:53:22 -0700 Subject: [PATCH] add: adding routing styles to routing package 43 pass 6 fail --- packages/routing/lib/src/routing_style.dart | 117 +++++++++ .../routing/lib/src/styles/express_style.dart | 157 ++++++++++++ .../routing/lib/src/styles/laravel_style.dart | 165 ++++++++++++ packages/routing/test/routing_style_test.dart | 237 ++++++++++++++++++ .../test/styles/laravel_style_test.dart | 206 +++++++++++++++ 5 files changed, 882 insertions(+) create mode 100644 packages/routing/lib/src/routing_style.dart create mode 100644 packages/routing/lib/src/styles/express_style.dart create mode 100644 packages/routing/lib/src/styles/laravel_style.dart create mode 100644 packages/routing/test/routing_style_test.dart create mode 100644 packages/routing/test/styles/laravel_style_test.dart diff --git a/packages/routing/lib/src/routing_style.dart b/packages/routing/lib/src/routing_style.dart new file mode 100644 index 0000000..9182c7e --- /dev/null +++ b/packages/routing/lib/src/routing_style.dart @@ -0,0 +1,117 @@ +import 'router.dart'; + +/// Base interface for all routing styles. +/// +/// This allows different routing patterns to be implemented while preserving +/// the core routing functionality. Each style wraps around the base [Router] +/// implementation, providing its own API while utilizing the underlying routing +/// system. +abstract class RoutingStyle { + /// The underlying router instance that handles actual routing. + Router get router; + + /// Unique identifier for this routing style. + String get styleName; + + /// Initialize the routing style. + /// + /// This is called when the style is activated through the registry. + /// Use this to set up any style-specific configuration or state. + void initialize(); + + /// Clean up any resources used by this style. + /// + /// This is called when switching to a different style or shutting down. + void dispose() {} +} + +/// Registry for managing different routing styles. +/// +/// The registry maintains a collection of available routing styles and handles +/// switching between them. It ensures only one style is active at a time while +/// preserving the underlying routing configuration. +class RoutingStyleRegistry { + final Map> _styles = {}; + RoutingStyle? _activeStyle; + final Router _baseRouter; + + /// Creates a new routing style registry. + /// + /// The registry maintains its own base router instance that all styles + /// will wrap around, ensuring routing state is preserved when switching styles. + RoutingStyleRegistry() : _baseRouter = Router(); + + /// Register a new routing style. + /// + /// Each style must have a unique [styleName]. Attempting to register a style + /// with a name that's already registered will throw an exception. + /// + /// ```dart + /// registry.registerStyle(ExpressStyle(registry.baseRouter)); + /// registry.registerStyle(LaravelStyle(registry.baseRouter)); + /// ``` + void registerStyle(RoutingStyle style) { + if (_styles.containsKey(style.styleName)) { + throw StateError( + 'A style with name "${style.styleName}" is already registered'); + } + _styles[style.styleName] = style; + } + + /// Activate a registered routing style. + /// + /// This makes the specified style active, initializing it and making its + /// routing pattern available. Any previously active style will be disposed. + /// + /// ```dart + /// registry.useStyle('express'); // Use Express-style routing + /// registry.useStyle('laravel'); // Switch to Laravel-style routing + /// ``` + /// + /// Throws a [StateError] if the style name is not registered. + void useStyle(String styleName) { + if (!_styles.containsKey(styleName)) { + throw StateError('No routing style registered with name "$styleName"'); + } + + // Dispose previous style if exists + _activeStyle?.dispose(); + + // Activate new style + _activeStyle = _styles[styleName]; + _activeStyle!.initialize(); + } + + /// The currently active routing style. + /// + /// Returns null if no style is active. + RoutingStyle? get activeStyle => _activeStyle; + + /// The underlying router instance. + /// + /// This router is shared across all styles, maintaining the routing state + /// even when switching between different routing patterns. + Router get baseRouter => _baseRouter; +} + +/// Base interface for middleware style adapters. +/// +/// This allows different routing styles to adapt their middleware patterns +/// to work with the platform's middleware system. +abstract class MiddlewareStyle { + /// Convert framework-specific middleware to platform middleware. + /// + /// This allows each routing style to define how its middleware format + /// should be converted to work with the platform's middleware system. + /// + /// ```dart + /// // Example Laravel-style middleware adaptation + /// T adaptMiddleware(dynamic middleware) { + /// if (middleware is String) { + /// return resolveMiddlewareFromContainer(middleware); + /// } + /// return middleware as T; + /// } + /// ``` + T adaptMiddleware(dynamic originalMiddleware); +} diff --git a/packages/routing/lib/src/styles/express_style.dart b/packages/routing/lib/src/styles/express_style.dart new file mode 100644 index 0000000..5f10830 --- /dev/null +++ b/packages/routing/lib/src/styles/express_style.dart @@ -0,0 +1,157 @@ +import '../router.dart'; +import '../routing_style.dart'; + +/// Express-style routing implementation. +/// +/// This is the default routing style that maintains compatibility with the +/// existing Express-like routing pattern. It provides the familiar app.get(), +/// app.post(), etc. methods while utilizing the underlying routing system. +class ExpressStyle implements RoutingStyle { + final Router _router; + + @override + Router get router => _router; + + @override + String get styleName => 'express'; + + /// Creates a new Express-style router. + ExpressStyle(this._router); + + @override + void initialize() { + // Express style is the default, so no special initialization needed + } + + @override + void dispose() { + // Express style doesn't need special cleanup + } + + /// Register a route that handles GET requests. + /// + /// ```dart + /// app.get('/users', (req, res) { + /// // Handle GET request + /// }); + /// ``` + Route get(String path, T handler, {List middleware = const []}) { + return _router.get(path, handler, middleware: middleware); + } + + /// Register a route that handles POST requests. + /// + /// ```dart + /// app.post('/users', (req, res) { + /// // Handle POST request + /// }); + /// ``` + Route post(String path, T handler, {List middleware = const []}) { + return _router.post(path, handler, middleware: middleware); + } + + /// Register a route that handles PUT requests. + /// + /// ```dart + /// app.put('/users/:id', (req, res) { + /// // Handle PUT request + /// }); + /// ``` + Route put(String path, T handler, {List middleware = const []}) { + return _router.put(path, handler, middleware: middleware) as Route; + } + + /// Register a route that handles DELETE requests. + /// + /// ```dart + /// app.delete('/users/:id', (req, res) { + /// // Handle DELETE request + /// }); + /// ``` + Route delete(String path, T handler, {List middleware = const []}) { + return _router.delete(path, handler, middleware: middleware); + } + + /// Register a route that handles PATCH requests. + /// + /// ```dart + /// app.patch('/users/:id', (req, res) { + /// // Handle PATCH request + /// }); + /// ``` + Route patch(String path, T handler, {List middleware = const []}) { + return _router.patch(path, handler, middleware: middleware); + } + + /// Register a route that handles HEAD requests. + /// + /// ```dart + /// app.head('/status', (req, res) { + /// // Handle HEAD request + /// }); + /// ``` + Route head(String path, T handler, {List middleware = const []}) { + return _router.head(path, handler, middleware: middleware); + } + + /// Register a route that handles OPTIONS requests. + /// + /// ```dart + /// app.options('/api', (req, res) { + /// // Handle OPTIONS request + /// }); + /// ``` + Route options(String path, T handler, {List middleware = const []}) { + return _router.options(path, handler, middleware: middleware); + } + + /// Register a route that handles all HTTP methods. + /// + /// ```dart + /// app.all('/any', (req, res) { + /// // Handle any HTTP method + /// }); + /// ``` + Route all(String path, T handler, {List middleware = const []}) { + return _router.all(path, handler, middleware: middleware); + } + + /// Use middleware for all routes. + /// + /// ```dart + /// app.use((req, res, next) { + /// // Middleware logic + /// next(); + /// }); + /// ``` + void use(T middleware) { + _router.chain([middleware]); + } + + /// Create a route group with optional prefix and middleware. + /// + /// ```dart + /// app.group('/api', (router) { + /// router.get('/users', handler); + /// router.post('/users', createHandler); + /// }, middleware: [authMiddleware]); + /// ``` + void group(String prefix, void Function(ExpressStyle router) callback, + {List middleware = const []}) { + _router.group(prefix, (router) { + callback(ExpressStyle(router)); + }, middleware: middleware); + } +} + +/// Express middleware adapter. +/// +/// This adapter maintains compatibility with Express-style middleware, +/// which is already in the format expected by the platform. +class ExpressMiddlewareStyle implements MiddlewareStyle { + @override + T adaptMiddleware(dynamic originalMiddleware) { + // Express middleware is already in the correct format + return originalMiddleware as T; + } +} diff --git a/packages/routing/lib/src/styles/laravel_style.dart b/packages/routing/lib/src/styles/laravel_style.dart new file mode 100644 index 0000000..a5d089b --- /dev/null +++ b/packages/routing/lib/src/styles/laravel_style.dart @@ -0,0 +1,165 @@ +import '../router.dart'; +import '../routing_style.dart'; + +/// Laravel-style routing implementation. +/// +/// This style provides a Laravel-like routing pattern while utilizing the +/// underlying routing system. It demonstrates how different routing styles +/// can be implemented on top of the core routing functionality. +class LaravelStyle implements RoutingStyle { + final Router _router; + + @override + Router get router => _router; + + @override + String get styleName => 'laravel'; + + /// Creates a new Laravel-style router. + LaravelStyle(this._router); + + @override + void initialize() { + // Laravel style doesn't need special initialization + } + + @override + void dispose() { + // Laravel style doesn't need special cleanup + } + + /// Register a route with a specific HTTP method. + /// + /// ```dart + /// Route::get('/users', handler); + /// Route::post('/users', handler); + /// ``` + Route route(String method, String path, T handler, + {List middleware = const []}) { + return _router.addRoute(method.toUpperCase(), path, handler, + middleware: middleware); + } + + /// Register a GET route. + /// + /// ```dart + /// Route::get('/users', handler); + /// ``` + Route get(String path, T handler, {List middleware = const []}) { + return route('GET', path, handler, middleware: middleware); + } + + /// Register a POST route. + /// + /// ```dart + /// Route::post('/users', handler); + /// ``` + Route post(String path, T handler, {List middleware = const []}) { + return route('POST', path, handler, middleware: middleware); + } + + /// Register a PUT route. + /// + /// ```dart + /// Route::put('/users/{id}', handler); + /// ``` + Route put(String path, T handler, {List middleware = const []}) { + return route('PUT', path, handler, middleware: middleware); + } + + /// Register a DELETE route. + /// + /// ```dart + /// Route::delete('/users/{id}', handler); + /// ``` + Route delete(String path, T handler, {List middleware = const []}) { + return route('DELETE', path, handler, middleware: middleware); + } + + /// Register a PATCH route. + /// + /// ```dart + /// Route::patch('/users/{id}', handler); + /// ``` + Route patch(String path, T handler, {List middleware = const []}) { + return route('PATCH', path, handler, middleware: middleware); + } + + /// Create a route group with shared attributes. + /// + /// ```dart + /// Route::group({ + /// 'prefix': '/api', + /// 'middleware': ['auth'], + /// }, () { + /// Route::get('/users', handler); + /// Route::post('/users', handler); + /// }); + /// ``` + void group(Map attributes, void Function() callback) { + var prefix = attributes['prefix'] as String? ?? ''; + var middleware = attributes['middleware'] as List? ?? const []; + + _router.group(prefix, (groupRouter) { + // Store current router + var parentRouter = _router; + // Create new style instance for group + var groupStyle = LaravelStyle(groupRouter); + // Set current instance as the active one + _activeInstance = groupStyle; + // Execute callback + callback(); + // Restore parent instance + _activeInstance = this; + }, middleware: middleware); + } + + // Track active instance for group context + static LaravelStyle? _activeInstance; + + // Forward calls to active instance + LaravelStyle get _current => _activeInstance as LaravelStyle? ?? this; + + /// Add a name to the last registered route. + /// + /// ```dart + /// Route::get('/users', handler).name('users.index'); + /// ``` + Route name(String name) { + var lastRoute = _router.routes.last; + lastRoute.name = name; + return lastRoute; + } + + /// Register middleware for all routes. + /// + /// ```dart + /// Route::middleware(['auth', 'throttle']); + /// ``` + void middleware(List middleware) { + _router.chain(middleware); + } +} + +/// Laravel middleware adapter. +/// +/// This adapter converts Laravel-style middleware (strings or callables) +/// to the platform's middleware format. +class LaravelMiddlewareStyle implements MiddlewareStyle { + final Map _middlewareMap; + + LaravelMiddlewareStyle(this._middlewareMap); + + @override + T adaptMiddleware(dynamic originalMiddleware) { + if (originalMiddleware is String) { + var factory = _middlewareMap[originalMiddleware]; + if (factory == null) { + throw StateError( + 'No middleware registered for key "$originalMiddleware"'); + } + return factory(); + } + return originalMiddleware as T; + } +} diff --git a/packages/routing/test/routing_style_test.dart b/packages/routing/test/routing_style_test.dart new file mode 100644 index 0000000..64de436 --- /dev/null +++ b/packages/routing/test/routing_style_test.dart @@ -0,0 +1,237 @@ +import 'package:platform_routing/src/router.dart'; +import 'package:platform_routing/src/routing_style.dart'; +import 'package:platform_routing/src/styles/express_style.dart'; +import 'package:test/test.dart'; + +// Test style implementation +class _TestStyle implements RoutingStyle { + final Router _router; + final void Function() _onDispose; + + _TestStyle(this._router, this._onDispose); + + @override + Router get router => _router; + + @override + String get styleName => 'test'; + + @override + void initialize() {} + + @override + void dispose() { + _onDispose(); + } +} + +void main() { + group('RoutingStyleRegistry', () { + late RoutingStyleRegistry registry; + + setUp(() { + registry = RoutingStyleRegistry(); + }); + + test('registers and activates styles', () { + var expressStyle = ExpressStyle(registry.baseRouter); + + // Register style + registry.registerStyle(expressStyle); + expect(registry.activeStyle, isNull); + + // Activate style + registry.useStyle('express'); + expect(registry.activeStyle, equals(expressStyle)); + }); + + test('throws on duplicate style registration', () { + var style1 = ExpressStyle(registry.baseRouter); + var style2 = ExpressStyle(registry.baseRouter); + + registry.registerStyle(style1); + expect( + () => registry.registerStyle(style2), + throwsStateError, + ); + }); + + test('throws when activating unregistered style', () { + expect( + () => registry.useStyle('nonexistent'), + throwsStateError, + ); + }); + + test('disposes previous style when switching', () { + var disposed = false; + + var testStyle = _TestStyle(registry.baseRouter, () => disposed = true); + var expressStyle = ExpressStyle(registry.baseRouter); + + registry.registerStyle(testStyle); + registry.registerStyle(expressStyle); + + // Activate test style + registry.useStyle('test'); + expect(disposed, isFalse); + + // Switch to express style + registry.useStyle('express'); + expect(disposed, isTrue); + }); + }); + + group('ExpressStyle', () { + late RoutingStyleRegistry registry; + late ExpressStyle style; + + setUp(() { + registry = RoutingStyleRegistry(); + style = ExpressStyle(registry.baseRouter); + registry.registerStyle(style); + registry.useStyle('express'); + }); + + test('maintains express-style routing pattern', () { + var handlerCalled = false; + var middlewareCalled = false; + + // Register middleware + style.use((req, res, next) { + middlewareCalled = true; + next(); + }); + + // Register route + style.get('/test', (req, res) { + handlerCalled = true; + }); + + // Simulate request + var results = registry.baseRouter.resolveAbsolute('/test', method: 'GET'); + expect(results, isNotEmpty); + + // Execute handlers + for (var result in results) { + for (var handler in result.handlers) { + handler(null, null); + } + } + + expect(middlewareCalled, isTrue); + expect(handlerCalled, isTrue); + }); + + test('supports route groups', () { + var routes = []; + + style.group('/api', (router) { + router.get('/users', (req, res) { + routes.add('/api/users'); + }); + + router.post('/users', (req, res) { + routes.add('/api/users'); + }); + }); + + // Verify routes were registered + var getResults = + registry.baseRouter.resolveAbsolute('/api/users', method: 'GET'); + var postResults = + registry.baseRouter.resolveAbsolute('/api/users', method: 'POST'); + + expect(getResults, isNotEmpty); + expect(postResults, isNotEmpty); + }); + + test('supports all HTTP methods', () { + var methods = [ + 'GET', + 'POST', + 'PUT', + 'DELETE', + 'PATCH', + 'HEAD', + 'OPTIONS' + ]; + var calledMethods = []; + + // Register routes for each method + for (var method in methods) { + switch (method) { + case 'GET': + style.get('/test', (req, res) => calledMethods.add(method)); + break; + case 'POST': + style.post('/test', (req, res) => calledMethods.add(method)); + break; + case 'PUT': + style.put('/test', (req, res) => calledMethods.add(method)); + break; + case 'DELETE': + style.delete('/test', (req, res) => calledMethods.add(method)); + break; + case 'PATCH': + style.patch('/test', (req, res) => calledMethods.add(method)); + break; + case 'HEAD': + style.head('/test', (req, res) => calledMethods.add(method)); + break; + case 'OPTIONS': + style.options('/test', (req, res) => calledMethods.add(method)); + break; + } + } + + // Verify each method resolves + for (var method in methods) { + var results = + registry.baseRouter.resolveAbsolute('/test', method: method); + expect(results, isNotEmpty, reason: 'Method $method should resolve'); + + // Execute handler + for (var result in results) { + for (var handler in result.handlers) { + handler(null, null); + } + } + } + + // Verify each method was called + expect(calledMethods, containsAll(methods)); + }); + + test('supports middleware in route groups', () { + var middlewareCalled = false; + var handlerCalled = false; + + style.group('/api', (router) { + router.get('/test', (req, res) { + handlerCalled = true; + }); + }, middleware: [ + (req, res, next) { + middlewareCalled = true; + next(); + } + ]); + + // Simulate request + var results = + registry.baseRouter.resolveAbsolute('/api/test', method: 'GET'); + expect(results, isNotEmpty); + + // Execute handlers + for (var result in results) { + for (var handler in result.handlers) { + handler(null, null); + } + } + + expect(middlewareCalled, isTrue); + expect(handlerCalled, isTrue); + }); + }); +} diff --git a/packages/routing/test/styles/laravel_style_test.dart b/packages/routing/test/styles/laravel_style_test.dart new file mode 100644 index 0000000..7495dc8 --- /dev/null +++ b/packages/routing/test/styles/laravel_style_test.dart @@ -0,0 +1,206 @@ +import 'package:platform_routing/src/routing_style.dart'; +import 'package:platform_routing/src/styles/laravel_style.dart'; +import 'package:test/test.dart'; + +typedef NextFunction = void Function(); +typedef MiddlewareFunction = void Function(dynamic, dynamic, NextFunction); +typedef RouteHandler = void Function(dynamic, dynamic); + +void executeHandler(Function? handler) { + if (handler == null) return; + + if (handler is RouteHandler) { + handler(null, null); + } else if (handler is MiddlewareFunction) { + handler(null, null, () {}); + } +} + +void main() { + group('LaravelStyle', () { + late RoutingStyleRegistry registry; + late LaravelStyle style; + + setUp(() { + registry = RoutingStyleRegistry(); + style = LaravelStyle(registry.baseRouter); + registry.registerStyle(style); + registry.useStyle('laravel'); + }); + + test('supports Laravel-style route registration', () { + var handlerCalled = false; + var middlewareCalled = false; + + // Register middleware + middleware(req, res, NextFunction next) { + middlewareCalled = true; + next(); + } + + style.middleware([middleware]); + + // Register route + handler(req, res) { + handlerCalled = true; + } + + style.get('/test', handler).name!; + + // Simulate request + var results = registry.baseRouter.resolveAbsolute('/test', method: 'GET'); + expect(results, isNotEmpty); + + // Execute handlers + for (var result in results) { + for (var handler in result.handlers) { + executeHandler(handler); + } + } + + expect(middlewareCalled, isTrue); + expect(handlerCalled, isTrue); + }); + + test('supports Laravel-style route groups', () { + var routes = []; + var groupMiddlewareCalled = false; + + // Register middleware + middleware(req, res, NextFunction next) { + groupMiddlewareCalled = true; + next(); + } + + style.group({ + 'prefix': '/api', + 'middleware': [middleware], + }, () { + getHandler(req, res) { + routes.add('/api/users'); + } + + postHandler(req, res) { + routes.add('/api/users'); + } + + style.get('/users', getHandler).name!; + style.post('/users', postHandler).name!; + }); + + // Verify routes were registered + var getResults = + registry.baseRouter.resolveAbsolute('/api/users', method: 'GET'); + var postResults = + registry.baseRouter.resolveAbsolute('/api/users', method: 'POST'); + + expect(getResults, isNotEmpty); + expect(postResults, isNotEmpty); + + // Verify route names + var namedRoute = registry.baseRouter.routes + .firstWhere((r) => r.name == 'api.users.index'); + expect(namedRoute, isNotNull); + + // Execute handlers to verify middleware + for (var result in getResults) { + for (var handler in result.handlers) { + executeHandler(handler); + } + } + + expect(groupMiddlewareCalled, isTrue); + expect(routes, contains('/api/users')); + }); + + test('supports all HTTP methods', () { + var methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']; + var calledMethods = []; + + // Register routes for each method + for (var method in methods) { + handler(req, res) { + calledMethods.add(method); + } + + style.route(method, '/test', handler).name!; + } + + // Verify each method resolves + for (var method in methods) { + var results = + registry.baseRouter.resolveAbsolute('/test', method: method); + expect(results, isNotEmpty, reason: 'Method $method should resolve'); + + // Execute handler + for (var result in results) { + for (var handler in result.handlers) { + executeHandler(handler); + } + } + } + + // Verify each method was called + expect(calledMethods, containsAll(methods)); + }); + + test('supports named routes', () { + handler(req, res) {} + + style.get('/users', handler).name!; + style.post('/users', handler).name!; + style.get('/users/:id', handler).name!; + + var routes = registry.baseRouter.routes; + expect(routes.any((r) => r.name == 'users.index'), isTrue); + expect(routes.any((r) => r.name == 'users.store'), isTrue); + expect(routes.any((r) => r.name == 'users.show'), isTrue); + }); + + test('supports middleware string resolution', () { + var authCalled = false; + var throttleCalled = false; + + MiddlewareFunction createAuthMiddleware() { + return (req, res, NextFunction next) { + authCalled = true; + next(); + }; + } + + MiddlewareFunction createThrottleMiddleware() { + return (req, res, NextFunction next) { + throttleCalled = true; + next(); + }; + } + + var middlewareAdapter = LaravelMiddlewareStyle({ + 'auth': createAuthMiddleware, + 'throttle': createThrottleMiddleware, + }); + + // Test string to middleware conversion + var authMiddleware = + middlewareAdapter.adaptMiddleware('auth') as MiddlewareFunction; + var throttleMiddleware = + middlewareAdapter.adaptMiddleware('throttle') as MiddlewareFunction; + + // Execute middleware + doNext() {} + authMiddleware(null, null, doNext); + throttleMiddleware(null, null, doNext); + + expect(authCalled, isTrue); + expect(throttleCalled, isTrue); + }); + + test('throws on unknown middleware string', () { + var adapter = LaravelMiddlewareStyle({}); + expect( + () => adapter.adaptMiddleware('unknown'), + throwsStateError, + ); + }); + }); +}