blob: a228489cc3d10703e6333386f9e2f8e2df56c6f4 [file] [log] [blame]
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:async';
import 'package:shelf/shelf.dart';
/// Check if the [regexp] is non-capturing.
bool _isNoCapture(String regexp) {
ArgumentError.checkNotNull(regexp, 'regexp');
// Construct a new regular expression matching anything containing regexp,
// then match with empty-string and count number of groups.
return RegExp('^(?:$regexp)|.*\$').firstMatch('')!.groupCount == 0;
}
/// Entry in the router.
///
/// This class implements the logic for matching the path pattern.
class RouterEntry {
/// Pattern for parsing the route pattern
static final RegExp _parser = RegExp(r'([^<]*)(?:<([^>|]+)(?:\|([^>]*))?>)?');
final String verb, route;
final Function _handler;
final Middleware _middleware;
/// Expression that the request path must match.
///
/// This also captures any parameters in the route pattern.
final RegExp _routePattern;
/// Names for the parameters in the route pattern.
final List<String> _params = [];
/// List of parameter names in the route pattern.
List<String> get params => _params.toList(); // exposed for using generator.
RouterEntry._(this.verb, this.route, this._handler, this._middleware,
this._routePattern);
factory RouterEntry(
String verb,
String route,
Function handler, {
Middleware? middleware,
}) {
middleware = middleware ?? ((Handler fn) => fn);
ArgumentError.checkNotNull(verb, 'verb');
ArgumentError.checkNotNull(route, 'route');
ArgumentError.checkNotNull(handler, 'handler');
if (!route.startsWith('/')) {
throw ArgumentError.value(
route, 'route', 'expected route to start with a slash');
}
final params = <String>[];
var pattern = '';
for (var m in _parser.allMatches(route)) {
pattern += RegExp.escape(m[1]!);
if (m[2] != null) {
params.add(m[2]!);
if (m[3] != null && !_isNoCapture(m[3]!)) {
throw ArgumentError.value(
route, 'route', 'expression for "${m[2]}" is capturing');
}
pattern += '(${m[3] ?? r'[^/]+'})';
}
}
final routePattern = RegExp('^$pattern\$');
return RouterEntry._(verb, route, handler, middleware, routePattern);
}
/// Returns a map from parameter name to value, if the path matches the
/// route pattern. Otherwise returns null.
Map<String, String>? match(String path) {
// Check if path matches the route pattern
var m = _routePattern.firstMatch(path);
if (m == null) {
return null;
}
// Construct map from parameter name to matched value
var params = <String, String>{};
for (var i = 0; i < _params.length; i++) {
// first group is always the full match, we ignore this group.
params[_params[i]] = m[i + 1]!;
}
return params;
}
// invoke handler with given request and params
Future<Response> invoke(Request request, Map<String, String> params) async {
request = request.change(context: {'shelf_router/params': params});
return await _middleware((request) async {
if (_handler is Handler || _params.isEmpty) {
return await _handler(request);
}
return await Function.apply(_handler, [
request,
..._params.map((n) => params[n]),
]);
})(request);
}
}