library angel_route.src.router; import 'dart:async'; import 'package:combinator/combinator.dart'; import 'package:string_scanner/string_scanner.dart'; import '../string_util.dart'; import 'routing_exception.dart'; part 'grammar.dart'; part 'route.dart'; part 'routing_result.dart'; part 'symlink_route.dart'; //final RegExp _param = RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?'); //final RegExp _rgxEnd = RegExp(r'\$+$'); //final RegExp _rgxStart = RegExp(r'^\^+'); //final RegExp _rgxStraySlashes = // RegExp(r'(^((\\+/)|(/))+)|(((\\+/)|(/))+$)'); //final RegExp _slashDollar = RegExp(r'/+\$'); final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)'); /// An abstraction over complex [Route] trees. Use this instead of the raw API. :) class Router { final Map>> _cache = {}; //final List<_ChainedRouter> _chained = []; final List _middleware = []; final Map> _mounted = {}; final List> _routes = []; bool _useCache = false; List get middleware => List.unmodifiable(_middleware); Map> get mounted => Map>.unmodifiable(_mounted); List> get routes { return _routes.fold>>([], (out, route) { if (route is SymlinkRoute) { var childRoutes = route.router.routes.fold>>([], (out, r) { return out ..add( route.path.isEmpty ? r : Route.join(route, r), ); }); return out..addAll(childRoutes); } else { return out..add(route); } }); } /// Provide a `root` to make this Router revolve around a pre-defined route. /// Not recommended. Router(); /// Enables the use of a cache to eliminate the overhead of consecutive resolutions of the same path. void enableCache() { _useCache = true; } /// Adds a route that responds to the given path /// for requests with the given method (case-insensitive). /// Provide '*' as the method to respond to all methods. Route addRoute(String method, String path, T handler, {Iterable? middleware}) { if (_useCache == true) { throw StateError('Cannot add routes after caching is enabled.'); } // Check if any mounted routers can match this final handlers = [handler]; middleware ??= []; handlers.insertAll(0, middleware); final route = Route(path, method: method, handlers: handlers); _routes.add(route); return route; } /// Prepends the given [middleware] to any routes created /// by the resulting router. /// /// The resulting router can be chained, too. _ChainedRouter chain(Iterable middleware) { var piped = _ChainedRouter(this, middleware); var route = SymlinkRoute('/', piped); _routes.add(route); return piped; } /// Returns a [Router] with a duplicated version of this tree. Router clone() { final router = Router(); final newMounted = Map>.from(mounted); for (var route in routes) { if (route is! SymlinkRoute) { router._routes.add(route.clone()); } else if (route is SymlinkRoute) { final newRouter = route.router.clone(); newMounted[route.path] = newRouter; final symlink = SymlinkRoute(route.path, newRouter); router._routes.add(symlink); } } return router.._mounted.addAll(newMounted); } /// Creates a visual representation of the route hierarchy and /// passes it to a callback. If none is provided, `print` is called. void dumpTree( {Function(String tree)? callback, String header = 'Dumping route tree:', String tab = ' '}) { final buf = StringBuffer(); var tabs = 0; if (header.isNotEmpty) { buf.writeln(header); } buf.writeln(''); void indent() { for (var i = 0; i < tabs; i++) { buf.write(tab); } } void dumpRouter(Router router) { indent(); tabs++; for (var route in router.routes) { indent(); buf.write('- '); if (route is! SymlinkRoute) buf.write('${route.method} '); buf.write('${route.path.isNotEmpty ? route.path : '/'}'); if (route is SymlinkRoute) { buf.writeln(); dumpRouter(route.router); } else { buf.writeln(' => ${route.handlers.length} handler(s)'); } } tabs--; } dumpRouter(this); (callback ?? print)(buf.toString()); } /// Creates a route, and allows you to add child routes to it /// via a [Router] instance. /// /// Returns the created route. /// You can also register middleware within the router. SymlinkRoute group(String path, void Function(Router router) callback, {Iterable middleware = const Iterable.empty(), String name = ''}) { final router = Router().._middleware.addAll(middleware); callback(router); return mount(path, router)..name = name; } /// Asynchronous equivalent of [group]. Future> groupAsync( String path, FutureOr Function(Router router) callback, {Iterable middleware = const Iterable.empty(), String name = ''}) async { final router = Router().._middleware.addAll(middleware); await callback(router); return mount(path, router)..name = name; } /// Generates a URI string based on the given input. /// Handy when you have named routes. /// /// Each item in `linkParams` should be a [Route], /// `String` or `Map`. /// /// Strings should be route names, namespaces, or paths. /// Maps should be parameters, which will be filled /// into the previous route. /// /// Paths and segments should correspond to the way /// you declared them. /// /// For example, if you declared a route group on /// `'users/:id'`, it would not be resolved if you /// passed `'users'` in [linkParams]. /// /// Leading and trailing slashes are automatically /// removed. /// /// Set [absolute] to `true` to insert a forward slash /// before the generated path. /// /// Example: /// ```dart /// router.navigate(['users/:id', {'id': '1337'}, 'profile']); /// ``` String navigate(Iterable linkParams, {bool absolute = true}) { final segments = []; Router search = this; Route? lastRoute; for (final param in linkParams) { var resolved = false; if (param is String) { // Search by name for (var route in search.routes) { if (route.name == param) { segments.add(route.path.replaceAll(_straySlashes, '')); lastRoute = route; if (route is SymlinkRoute) { search = route.router; } resolved = true; break; } } // Search by path if (!resolved) { var scanner = SpanScanner(param.replaceAll(_straySlashes, '')); for (var route in search.routes) { var pos = scanner.position; var parseResult = route.parser?.parse(scanner); if (parseResult != null) { if (parseResult.successful && scanner.isDone) { segments.add(route.path.replaceAll(_straySlashes, '')); lastRoute = route; if (route is SymlinkRoute) { search = route.router; } resolved = true; break; } else { scanner.position = pos; } } else { scanner.position = pos; } } } if (!resolved) { throw RoutingException( 'Cannot resolve route for link param "$param".'); } } else if (param is Route) { segments.add(param.path.replaceAll(_straySlashes, '')); } else if (param is Map) { if (lastRoute == null) { throw RoutingException( 'Maps in link params must be preceded by a Route or String.'); } else { segments.removeLast(); segments.add(lastRoute.makeUri(param).replaceAll(_straySlashes, '')); } } else { throw RoutingException( 'Link param $param is not Route, String, or Map.'); } } return absolute ? '/${segments.join('/').replaceAll(_straySlashes, '')}' : segments.join('/'); } /// Finds the first [Route] that matches the given path, /// with the given method. bool resolve(String absolute, String relative, List> out, {String method = 'GET', bool strip = true}) { final cleanRelative = strip == false ? relative : stripStraySlashes(relative); var scanner = SpanScanner(cleanRelative); bool crawl(Router r) { var success = false; for (var route in r.routes) { var pos = scanner.position; if (route is SymlinkRoute) { if (route.parser != null) { var pp = route.parser!; if (pp.parse(scanner).successful) { var s = crawl(route.router); if (s) success = true; } } scanner.position = pos; } else if (route.method == '*' || route.method == method) { var parseResult = route.parser?.parse(scanner); if (parseResult != null) { if (parseResult.successful && scanner.isDone) { var tailResult = parseResult.value?.tail ?? ''; print(tailResult); var result = RoutingResult( parseResult: parseResult, params: parseResult.value!.params, shallowRoute: route, shallowRouter: this, tail: tailResult + scanner.rest); out.add(result); success = true; } } scanner.position = pos; } } return success; } return crawl(this); } /// Returns the result of [resolve] with [path] passed as /// both `absolute` and `relative`. Iterable> resolveAbsolute(String path, {String method = 'GET', bool strip = true}) => resolveAll(path, path, method: method, strip: strip); /// Finds every possible [Route] that matches the given path, /// with the given method. Iterable> resolveAll(String absolute, String relative, {String method = 'GET', bool strip = true}) { if (_useCache == true) { return _cache.putIfAbsent('$method$absolute', () => _resolveAll(absolute, relative, method: method, strip: strip)); } return _resolveAll(absolute, relative, method: method, strip: strip); } Iterable> _resolveAll(String absolute, String relative, {String method = 'GET', bool strip = true}) { var results = >[]; resolve(absolute, relative, results, method: method, strip: strip); // _printDebug( // 'Results of $method "/${absolute.replaceAll(_straySlashes, '')}": ${results.map((r) => r.route).toList()}'); return results; } /// Incorporates another [Router]'s routes into this one's. SymlinkRoute mount(String path, Router router) { final route = SymlinkRoute(path, router); _mounted[route.path] = router; _routes.add(route); //route._head = RegExp(route.matcher.pattern.replaceAll(_rgxEnd, '')); return route; } /// Adds a route that responds to any request matching the given path. Route all(String path, T handler, {Iterable? middleware}) { return addRoute('*', path, handler, middleware: middleware); } /// Adds a route that responds to a DELETE request. Route delete(String path, T handler, {Iterable? middleware}) { return addRoute('DELETE', path, handler, middleware: middleware); } /// Adds a route that responds to a GET request. Route get(String path, T handler, {Iterable? middleware}) { return addRoute('GET', path, handler, middleware: middleware); } /// Adds a route that responds to a HEAD request. Route head(String path, T handler, {Iterable? middleware}) { return addRoute('HEAD', path, handler, middleware: middleware); } /// Adds a route that responds to a OPTIONS request. Route options(String path, T handler, {Iterable? middleware}) { return addRoute('OPTIONS', path, handler, middleware: middleware); } /// Adds a route that responds to a POST request. Route post(String path, T handler, {Iterable? middleware}) { return addRoute('POST', path, handler, middleware: middleware); } /// Adds a route that responds to a PATCH request. Route patch(String path, T handler, {Iterable? middleware}) { return addRoute('PATCH', path, handler, middleware: middleware); } /// Adds a route that responds to a PUT request. Route put(String path, T handler, {Iterable? middleware}) { return addRoute('PUT', path, handler, middleware: middleware); } } class _ChainedRouter extends Router { final List _handlers = []; Router _root; _ChainedRouter.empty() : _root = Router(); _ChainedRouter(this._root, Iterable middleware) { _handlers.addAll(middleware); } @override Route addRoute(String method, String path, handler, {Iterable? middleware}) { middleware ??= []; var route = super.addRoute(method, path, handler, middleware: [..._handlers, ...middleware]); //_root._routes.add(route); return route; } @override SymlinkRoute group(String path, void Function(Router router) callback, {Iterable? middleware, String name = ''}) { middleware ??= []; final router = _ChainedRouter(_root, [..._handlers, ...middleware]); callback(router); return mount(path, router)..name = name; } @override Future> groupAsync( String path, FutureOr Function(Router router) callback, {Iterable? middleware, String name = ''}) async { middleware ??= []; final router = _ChainedRouter(_root, [..._handlers, ...middleware]); await callback(router); return mount(path, router)..name = name; } @override SymlinkRoute mount(String path, Router router) { final route = super.mount(path, router); route.router._middleware.insertAll(0, _handlers); //_root._routes.add(route); return route; } @override _ChainedRouter chain(Iterable middleware) { final piped = _ChainedRouter.empty().._root = _root; piped._handlers.addAll([..._handlers, ...middleware]); var route = SymlinkRoute('/', piped); _routes.add(route); return piped; } } /// Optimizes a router by condensing all its routes into one level. Router flatten(Router router) { var flattened = Router(); for (var route in router.routes) { if (route is SymlinkRoute) { var base = route.path.replaceAll(_straySlashes, ''); var child = flatten(route.router); for (var route in child.routes) { var path = route.path.replaceAll(_straySlashes, ''); var joined = '$base/$path'.replaceAll(_straySlashes, ''); flattened.addRoute(route.method, joined.replaceAll(_straySlashes, ''), route.handlers.last, middleware: route.handlers.take(route.handlers.length - 1).toList()); } } else { flattened.addRoute(route.method, route.path, route.handlers.last, middleware: route.handlers.take(route.handlers.length - 1).toList()); } } return flattened..enableCache(); }