Fixed null safety issues

This commit is contained in:
thomashii@dukefirehawk.com 2021-05-09 19:16:15 +08:00
parent 5b3c589c8e
commit 15926ff45d
42 changed files with 242 additions and 207 deletions

View file

@ -11,7 +11,7 @@
* Added merge_map and migrated to 2.0.0 (6/6 tests passed)
* Added mock_request and migrated to 2.0.0 (0/0 tests)
* Migrated angel_framework to 4.0.0 (146/149 tests passed)
* Migrated angel_auth to 4.0.0 (22/32 tests passed)
* Migrated angel_auth to 4.0.0 (21/30 tests passed)
* Migrated angel_configuration to 4.0.0 (6/8 testspassed)
* Migrated angel_validate to 4.0.0 (6/7 tests passed)
* Migrated json_god to 4.0.0 (13/13 tests passed)

View file

@ -30,7 +30,7 @@ class AuthToken {
String? ipAddress;
late DateTime issuedAt;
num? lifeSpan;
num lifeSpan;
var userId;
Map<String, dynamic> payload = {};
@ -41,12 +41,17 @@ class AuthToken {
DateTime? issuedAt,
Map payload = const {}}) {
this.issuedAt = issuedAt ?? DateTime.now();
this.payload.addAll(payload.keys
.fold({}, ((out, k) => out?..[k.toString()] = payload[k])) ??
{});
/*
this.payload.addAll(payload.keys.fold(
{},
((out, k) => out..[k.toString()] = payload[k])
as Map<String, dynamic>? Function(
Map<String, dynamic>?, dynamic)) ??
{});
*/
}
factory AuthToken.fromJson(String jsons) =>
@ -55,10 +60,10 @@ class AuthToken {
factory AuthToken.fromMap(Map data) {
return AuthToken(
ipAddress: data['aud'].toString(),
lifeSpan: data['exp'] as num?,
lifeSpan: data['exp'] as num,
issuedAt: DateTime.parse(data['iat'].toString()),
userId: data['sub'],
payload: data['pld'] as Map? ?? {});
payload: data['pld'] as Map);
}
factory AuthToken.parse(String jwt) {

View file

@ -5,10 +5,10 @@ import 'package:quiver/core.dart';
/// A common class containing parsing and validation logic for third-party authentication configuration.
class ExternalAuthOptions {
/// The user's identifier, otherwise known as an "application id".
final String? clientId;
final String clientId;
/// The user's secret, other known as an "application secret".
final String? clientSecret;
final String clientSecret;
/// The user's redirect URI.
final Uri redirectUri;
@ -17,17 +17,11 @@ class ExternalAuthOptions {
final Set<String> scopes;
ExternalAuthOptions._(
this.clientId, this.clientSecret, this.redirectUri, this.scopes) {
if (clientId == null) {
throw ArgumentError.notNull('clientId');
} else if (clientSecret == null) {
throw ArgumentError.notNull('clientSecret');
}
}
this.clientId, this.clientSecret, this.redirectUri, this.scopes);
factory ExternalAuthOptions(
{required String? clientId,
required String? clientSecret,
{required String clientId,
required String clientSecret,
required redirectUri,
Iterable<String> scopes = const []}) {
if (redirectUri is String) {
@ -50,8 +44,8 @@ class ExternalAuthOptions {
/// * `redirect_uri`
factory ExternalAuthOptions.fromMap(Map map) {
return ExternalAuthOptions(
clientId: map['client_id'] as String?,
clientSecret: map['client_secret'] as String?,
clientId: map['client_id'] as String,
clientSecret: map['client_secret'] as String,
redirectUri: map['redirect_uri'],
scopes: map['scopes'] is Iterable
? ((map['scopes'] as Iterable).map((x) => x.toString()))
@ -72,15 +66,15 @@ class ExternalAuthOptions {
/// Creates a copy of this object, with the specified changes.
ExternalAuthOptions copyWith(
{String? clientId,
String? clientSecret,
{String clientId = '',
String clientSecret = '',
redirectUri,
Iterable<String>? scopes}) {
Iterable<String> scopes = const []}) {
return ExternalAuthOptions(
clientId: clientId ?? this.clientId,
clientSecret: clientSecret ?? this.clientSecret,
clientId: clientId,
clientSecret: clientSecret,
redirectUri: redirectUri ?? this.redirectUri,
scopes: (scopes ??= []).followedBy(this.scopes),
scopes: (scopes).followedBy(this.scopes),
);
}
@ -117,7 +111,7 @@ class ExternalAuthOptions {
secret = clientSecret;
} else {
var codeUnits =
List<int>.filled(asteriskCount ?? clientSecret!.length, $asterisk);
List<int>.filled(asteriskCount ?? clientSecret.length, $asterisk);
secret = String.fromCharCodes(codeUnits);
}

View file

@ -6,12 +6,15 @@ import 'package:angel_framework/angel_framework.dart';
/// [realm] defaults to `'angel_auth'`.
RequestHandler forceBasicAuth<User>({String? realm}) {
return (RequestContext req, ResponseContext res) async {
if (req.container!.has<User>()) {
if (req.container != null) {
var reqContainer = req.container!;
if (reqContainer.has<User>()) {
return true;
} else if (req.container!.has<Future<User>>()) {
await req.container!.makeAsync<User>();
} else if (reqContainer.has<Future<User>>()) {
await reqContainer.makeAsync<User>();
return true;
}
}
res.headers['www-authenticate'] = 'Basic realm="${realm ?? 'angel_auth'}"';
throw AngelHttpException.notAuthenticated();
@ -31,13 +34,18 @@ RequestHandler requireAuthentication<User>() {
}
}
if (req.container!.has<User>() || req.method == 'OPTIONS') {
if (req.container != null) {
var reqContainer = req.container!;
if (reqContainer.has<User>() || req.method == 'OPTIONS') {
return true;
} else if (req.container!.has<Future<User>>()) {
await req.container!.makeAsync<User>();
} else if (reqContainer.has<Future<User>>()) {
await reqContainer.makeAsync<User>();
return true;
} else {
return _reject(res);
}
} else {
return _reject(res);
}
};
}

View file

@ -1,6 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'dart:math' as Math;
import 'dart:math';
import 'package:angel_framework/angel_framework.dart';
import 'package:crypto/crypto.dart';
import 'auth_token.dart';
@ -9,11 +9,11 @@ import 'strategy.dart';
/// Handles authentication within an Angel application.
class AngelAuth<User> {
Hmac? _hs256;
int? _jwtLifeSpan;
final StreamController<User?> _onLogin = StreamController<User>(),
late Hmac _hs256;
late int _jwtLifeSpan;
final StreamController<User> _onLogin = StreamController<User>(),
_onLogout = StreamController<User>();
final Math.Random _random = Math.Random.secure();
final Random _random = Random.secure();
final RegExp _rgxBearer = RegExp(r'^Bearer');
/// If `true` (default), then JWT's will be stored and retrieved from a `token` cookie.
@ -51,23 +51,25 @@ class AngelAuth<User> {
FutureOr Function(User)? serializer;
/// Deserializes a unique identifier into its associated identity. In most cases, this is a user object or model instance.
FutureOr<User> Function(Object?)? deserializer;
FutureOr<User> Function(Object)? deserializer;
/// Fires the result of [deserializer] whenever a user signs in to the application.
Stream<User?> get onLogin => _onLogin.stream;
Stream<User> get onLogin => _onLogin.stream;
/// Fires `req.user`, which is usually the result of [deserializer], whenever a user signs out of the application.
Stream<User?> get onLogout => _onLogout.stream;
Stream<User> get onLogout => _onLogout.stream;
/// The [Hmac] being used to encode JWT's.
Hmac? get hmac => _hs256;
Hmac get hmac => _hs256;
String _randomString(
{int length = 32,
String validChars =
"ABCDEFHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"}) {
'ABCDEFHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_'}) {
var chars = <int>[];
while (chars.length < length) chars.add(_random.nextInt(validChars.length));
while (chars.length < length) {
chars.add(_random.nextInt(validChars.length));
}
return String.fromCharCodes(chars);
}
@ -83,7 +85,7 @@ class AngelAuth<User> {
this.cookieDomain,
this.cookiePath = '/',
this.secureCookies = true,
this.reviveTokenEndpoint = "/auth/token"})
this.reviveTokenEndpoint = '/auth/token'})
: super() {
_hs256 = Hmac(sha256, (jwtKey ?? _randomString()).codeUnits);
_jwtLifeSpan = jwtLifeSpan?.toInt() ?? -1;
@ -110,7 +112,7 @@ class AngelAuth<User> {
app.container!
.registerLazySingleton<Future<_AuthResult<User>>>((container) async {
var req = container.make<RequestContext>()!;
var res = container.make<ResponseContext>();
var res = container.make<ResponseContext>()!;
var result = await _decodeJwt(req, res);
if (result != null) {
return result;
@ -148,8 +150,8 @@ class AngelAuth<User> {
req.container!.registerSingleton<AuthToken>(token);
}
if (allowCookie == true) {
_addProtectedCookie(res!, 'token', token.serialize(_hs256!));
if (allowCookie) {
_addProtectedCookie(res!, 'token', token.serialize(_hs256));
}
}
@ -185,29 +187,29 @@ class AngelAuth<User> {
}
Future<_AuthResult<User>?> _decodeJwt(
RequestContext req, ResponseContext? res) async {
RequestContext req, ResponseContext res) async {
var jwt = getJwt(req);
if (jwt != null) {
var token = AuthToken.validate(jwt, _hs256!);
var token = AuthToken.validate(jwt, _hs256);
if (enforceIp) {
if (req.ip != token.ipAddress) {
throw AngelHttpException.forbidden(
message: "JWT cannot be accessed from this IP address.");
message: 'JWT cannot be accessed from this IP address.');
}
}
if (token.lifeSpan! > -1) {
if (token.lifeSpan > -1) {
var expiry =
token.issuedAt.add(Duration(milliseconds: token.lifeSpan!.toInt()));
token.issuedAt.add(Duration(milliseconds: token.lifeSpan.toInt()));
if (!expiry.isAfter(DateTime.now())) {
throw AngelHttpException.forbidden(message: "Expired JWT.");
throw AngelHttpException.forbidden(message: 'Expired JWT.');
}
}
var user = await deserializer!(token.userId);
var user = await deserializer!(token.userId as Object);
_apply(req, res, token, user);
return _AuthResult(user, token);
}
@ -217,7 +219,7 @@ class AngelAuth<User> {
/// Retrieves a JWT from a request, if any was sent at all.
String? getJwt(RequestContext req) {
if (req.headers!.value('Authorization') != null) {
if (req.headers?.value('Authorization') != null) {
final authHeader = req.headers!.value('Authorization')!;
// Allow Basic auth to fall through
@ -228,7 +230,7 @@ class AngelAuth<User> {
req.cookies.any((cookie) => cookie.name == 'token')) {
return req.cookies.firstWhere((cookie) => cookie.name == 'token').value;
} else if (allowTokenInQuery &&
req.uri!.queryParameters['token'] is String) {
req.uri?.queryParameters['token'] is String) {
return req.uri!.queryParameters['token']?.toString();
}
@ -248,10 +250,10 @@ class AngelAuth<User> {
cookie.secure = true;
}
if (_jwtLifeSpan! > 0) {
cookie.maxAge ??= _jwtLifeSpan! < 0 ? -1 : _jwtLifeSpan! ~/ 1000;
cookie.expires ??=
DateTime.now().add(Duration(milliseconds: _jwtLifeSpan!));
var lifeSpan = _jwtLifeSpan;
if (lifeSpan > 0) {
cookie.maxAge ??= lifeSpan < 0 ? -1 : lifeSpan ~/ 1000;
cookie.expires ??= DateTime.now().add(Duration(milliseconds: lifeSpan));
}
cookie.domain ??= cookieDomain;
@ -270,19 +272,19 @@ class AngelAuth<User> {
jwt = body['token']?.toString();
}
if (jwt == null) {
throw AngelHttpException.forbidden(message: "No JWT provided");
throw AngelHttpException.forbidden(message: 'No JWT provided');
} else {
var token = AuthToken.validate(jwt, _hs256!);
var token = AuthToken.validate(jwt, _hs256);
if (enforceIp) {
if (req.ip != token.ipAddress) {
throw AngelHttpException.forbidden(
message: "JWT cannot be accessed from this IP address.");
message: 'JWT cannot be accessed from this IP address.');
}
}
if (token.lifeSpan! > -1) {
if (token.lifeSpan > -1) {
var expiry = token.issuedAt
.add(Duration(milliseconds: token.lifeSpan!.toInt()));
.add(Duration(milliseconds: token.lifeSpan.toInt()));
if (!expiry.isAfter(DateTime.now())) {
//print(
@ -293,15 +295,15 @@ class AngelAuth<User> {
}
if (allowCookie) {
_addProtectedCookie(res, 'token', token.serialize(_hs256!));
_addProtectedCookie(res, 'token', token.serialize(_hs256));
}
final data = await deserializer!(token.userId);
return {'data': data, 'token': token.serialize(_hs256!)};
final data = await deserializer!(token.userId as Object);
return {'data': data, 'token': token.serialize(_hs256)};
}
} catch (e) {
if (e is AngelHttpException) rethrow;
throw AngelHttpException.badRequest(message: "Malformed JWT");
throw AngelHttpException.badRequest(message: 'Malformed JWT');
}
}
@ -347,7 +349,7 @@ class AngelAuth<User> {
// Create JWT
var token = AuthToken(
userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
var jwt = token.serialize(_hs256!);
var jwt = token.serialize(_hs256);
if (options?.tokenCallback != null) {
if (!req.container!.has<User>()) {
@ -356,7 +358,7 @@ class AngelAuth<User> {
var r = await options!.tokenCallback!(req, res, token, result);
if (r != null) return r;
jwt = token.serialize(_hs256!);
jwt = token.serialize(_hs256);
}
_apply(req, res, token, result);
@ -376,7 +378,7 @@ class AngelAuth<User> {
req.accepts('application/json')) {
var user = hasExisting
? result
: await deserializer!(await serializer!(result));
: await deserializer!((await serializer!(result)) as Object);
_onLogin.add(user);
return {"data": user, "token": jwt};
}
@ -402,38 +404,40 @@ class AngelAuth<User> {
/// Log a user in on-demand.
Future login(AuthToken token, RequestContext req, ResponseContext res) async {
var user = await deserializer!(token.userId);
var user = await deserializer!(token.userId as Object);
_apply(req, res, token, user);
_onLogin.add(user);
if (allowCookie) {
_addProtectedCookie(res, 'token', token.serialize(_hs256!));
_addProtectedCookie(res, 'token', token.serialize(_hs256));
}
}
/// Log a user in on-demand.
Future loginById(userId, RequestContext req, ResponseContext res) async {
var user = await deserializer!(userId);
var user = await deserializer!(userId as Object);
var token =
AuthToken(userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
_apply(req, res, token, user);
_onLogin.add(user);
if (allowCookie) {
_addProtectedCookie(res, 'token', token.serialize(_hs256!));
_addProtectedCookie(res, 'token', token.serialize(_hs256));
}
}
/// Log an authenticated user out.
RequestHandler logout([AngelAuthOptions<User>? options]) {
return (RequestContext req, ResponseContext res) async {
if (req.container!.has<User>()) {
var user = req.container!.make<User>();
if (req.container?.has<User>() == true) {
var user = req.container?.make<User>();
if (user != null) {
_onLogout.add(user);
}
}
if (allowCookie == true) {
res.cookies.removeWhere((cookie) => cookie.name == "token");
res.cookies.removeWhere((cookie) => cookie.name == 'token');
_addProtectedCookie(res, 'token', '""');
}

View file

@ -38,10 +38,13 @@ class LocalAuthStrategy<User> extends AuthStrategy<User> {
User? verificationResult;
if (allowBasic) {
var authHeader = req.headers!.value('authorization') ?? '';
var authHeader = req.headers?.value('authorization') ?? '';
if (_rgxBasic.hasMatch(authHeader)) {
var base64AuthString = _rgxBasic.firstMatch(authHeader)!.group(1)!;
var base64AuthString = _rgxBasic.firstMatch(authHeader)?.group(1);
if (base64AuthString == null) {
return null;
}
var authString = String.fromCharCodes(base64.decode(base64AuthString));
if (_rgxUsrPass.hasMatch(authString)) {
Match usrPassMatch = _rgxUsrPass.firstMatch(authString)!;
@ -51,7 +54,7 @@ class LocalAuthStrategy<User> extends AuthStrategy<User> {
throw AngelHttpException.badRequest(errors: [invalidMessage]);
}
if (verificationResult == false || verificationResult == null) {
if (verificationResult == null) {
res
..statusCode = 401
..headers['www-authenticate'] = 'Basic realm="$realm"';
@ -68,16 +71,16 @@ class LocalAuthStrategy<User> extends AuthStrategy<User> {
.parseBody()
.then((_) => req.bodyAsMap)
.catchError((_) => <String, dynamic>{});
if (body != null) {
if (_validateString(body[usernameField]?.toString()) &&
_validateString(body[passwordField]?.toString())) {
//if (body != null) {
if (_validateString(body[usernameField].toString()) &&
_validateString(body[passwordField].toString())) {
verificationResult = await verifier(
body[usernameField]?.toString(), body[passwordField]?.toString());
}
body[usernameField].toString(), body[passwordField].toString());
}
//}
}
if (verificationResult == false || verificationResult == null) {
if (verificationResult == null) {
if (options.failureRedirect != null &&
options.failureRedirect!.isNotEmpty) {
await res.redirect(options.failureRedirect, code: 401);

View file

@ -7,6 +7,7 @@ environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
angel_framework:
# path: ../framework
git:
url: https://github.com/dukefirehawk/angel.git
ref: sdk-2.12.x_nnbd
@ -23,3 +24,7 @@ dev_dependencies:
logging: ^1.0.0
pedantic: ^1.11.0
test: ^1.17.3
#dependency_overrides:
# angel_container:
# path: ../container/angel_container

View file

@ -1,12 +1,12 @@
import "package:angel_auth/src/auth_token.dart";
import "package:crypto/crypto.dart";
import "package:test/test.dart";
import 'package:angel_auth/src/auth_token.dart';
import 'package:crypto/crypto.dart';
import 'package:test/test.dart';
main() async {
final Hmac hmac = Hmac(sha256, "angel_auth".codeUnits);
void main() async {
final hmac = Hmac(sha256, 'angel_auth'.codeUnits);
test("sample serialization", () {
var token = AuthToken(ipAddress: "localhost", userId: "thosakwe");
test('sample serialization', () {
var token = AuthToken(ipAddress: 'localhost', userId: 'thosakwe');
var jwt = token.serialize(hmac);
print(jwt);
@ -17,7 +17,7 @@ main() async {
});
test('custom payload', () {
var token = AuthToken(ipAddress: "localhost", userId: "thosakwe", payload: {
var token = AuthToken(ipAddress: 'localhost', userId: 'thosakwe', payload: {
"foo": "bar",
"baz": {
"one": 1,

View file

@ -70,6 +70,7 @@ void main() {
);
});
/* Deprecated as clientId and clientSecret cannot be null
test('ensures id not null', () {
expect(
() => ExternalAuthOptions(
@ -89,6 +90,7 @@ void main() {
throwsArgumentError,
);
});
*/
});
group('fromMap()', () {

View file

@ -7,7 +7,7 @@ import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:test/test.dart';
final AngelAuth<Map<String, String>?> auth = AngelAuth<Map<String, String>?>();
final AngelAuth<Map<String, String>> auth = AngelAuth<Map<String, String>>();
var headers = <String, String>{'accept': 'application/json'};
var localOpts = AngelAuthOptions<Map<String, String>>(
failureRedirect: '/failure', successRedirect: '/success');
@ -113,12 +113,13 @@ void main() async {
auth.strategies.clear();
auth.strategies['local'] =
LocalAuthStrategy(verifier, forceBasic: true, realm: 'test');
var response = await client!.get(Uri.parse('$url/hello'), headers: {
var response = await client?.get(Uri.parse('$url/hello'), headers: {
'accept': 'application/json',
'content-type': 'application/json'
});
print(response.headers);
print('Body <${response.body}>');
expect(response.headers['www-authenticate'], equals('Basic realm="test"'));
print('Header = ${response?.headers}');
print('Body <${response?.body}>');
var head = response?.headers['www-authenticate'];
expect(head, equals('Basic realm="test"'));
});
}

View file

@ -3,7 +3,7 @@ import 'dart:io';
import 'package:angel_auth/angel_auth.dart';
import 'package:test/test.dart';
const Duration threeDays = const Duration(days: 3);
const Duration threeDays = Duration(days: 3);
void main() {
late Cookie defaultCookie;

View file

@ -18,7 +18,7 @@ final Parser<Map<String, String>> credentials = chain<String>([
match<String>(':'),
string.opt(),
]).map<Map<String, String>>(
(r) => {'username': r.value![0], 'password': r.value![2]});
(r) => {'username': r.value![0]!, 'password': r.value![2]!});
/// We can actually embed a parser within another parser.
///

View file

@ -24,7 +24,7 @@ Parser<num> calculatorGrammar() {
expr.space(),
match<Null>(op).space() as Parser<num>,
expr.space(),
]).map((r) => f(r.value![0], r.value![2])),
]).map((r) => f(r.value![0]!, r.value![2]!)),
);
}

View file

@ -7,7 +7,7 @@ class _Advance<T> extends Parser<T> {
_Advance(this.parser, this.amount);
@override
ParseResult<T> __parse(ParseArgs args) {
ParseResult<T?> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth()).change(parser: this);
if (result.successful) args.scanner.position += amount;
return result;

View file

@ -21,7 +21,7 @@ class _Any<T> extends Parser<T> {
_Any(this.parsers, this.backtrack, this.errorMessage, this.severity);
@override
ParseResult<T> _parse(ParseArgs args) {
ParseResult<T?> _parse(ParseArgs args) {
var inactive = parsers
.where((p) => !args.trampoline.isActive(p, args.scanner.position));

View file

@ -1,13 +1,13 @@
part of lex.src.combinator;
class _Cache<T> extends Parser<T> {
final Map<int, ParseResult<T>> _cache = {};
final Map<int, ParseResult<T?>> _cache = {};
final Parser<T> parser;
_Cache(this.parser);
@override
ParseResult<T> __parse(ParseArgs args) {
ParseResult<T?> __parse(ParseArgs args) {
return _cache.putIfAbsent(args.scanner.position, () {
return parser._parse(args.increaseDepth());
}).change(parser: this);

View file

@ -3,9 +3,9 @@ part of lex.src.combinator;
/// Expects to parse a sequence of [parsers].
///
/// If [failFast] is `true` (default), then the first failure to parse will abort the parse.
ListParser<T> chain<T>(Iterable<Parser<T>> parsers,
ListParser<T?> chain<T>(Iterable<Parser<T>> parsers,
{bool failFast: true, SyntaxErrorSeverity? severity}) {
return _Chain<T>(
return _Chain<T?>(
parsers, failFast != false, severity ?? SyntaxErrorSeverity.error);
}
@ -17,7 +17,7 @@ class _Alt<T> extends Parser<T> {
_Alt(this.parser, this.errorMessage, this.severity);
@override
ParseResult<T> __parse(ParseArgs args) {
ParseResult<T?> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth());
return result.successful
? result
@ -33,7 +33,7 @@ class _Alt<T> extends Parser<T> {
}
}
class _Chain<T> extends ListParser<T> {
class _Chain<T> extends ListParser<T?> {
final Iterable<Parser<T>> parsers;
final bool failFast;
final SyntaxErrorSeverity severity;
@ -41,9 +41,9 @@ class _Chain<T> extends ListParser<T> {
_Chain(this.parsers, this.failFast, this.severity);
@override
ParseResult<List<T>> __parse(ParseArgs args) {
ParseResult<List<T?>?> __parse(ParseArgs args) {
var errors = <SyntaxError>[];
var results = <T>[];
var results = <T?>[];
var spans = <FileSpan>[];
bool successful = true;
@ -61,9 +61,7 @@ class _Chain<T> extends ListParser<T> {
successful = false;
}
if (result.value != null) {
results.add(result.value!);
}
results.add(result.value);
if (result.span != null) {
spans.add(result.span!);
@ -76,14 +74,14 @@ class _Chain<T> extends ListParser<T> {
span = spans.reduce((a, b) => a.expand(b));
}
return ParseResult<List<T>>(
return ParseResult<List<T?>?>(
args.trampoline,
args.scanner,
this,
successful,
errors,
span: span,
value: List<T>.unmodifiable(results),
value: List<T?>.unmodifiable(results),
);
}

View file

@ -9,7 +9,7 @@ class _Check<T> extends Parser<T> {
_Check(this.parser, this.matcher, this.errorMessage, this.severity);
@override
ParseResult<T> __parse(ParseArgs args) {
ParseResult<T?> __parse(ParseArgs args) {
var matchState = {};
var result = parser._parse(args.increaseDepth()).change(parser: this);
if (!result.successful)

View file

@ -67,9 +67,9 @@ class ParseArgs {
/// A parser combinator, which can parse very complicated grammars in a manageable manner.
abstract class Parser<T> {
ParseResult<T> __parse(ParseArgs args);
ParseResult<T?> __parse(ParseArgs args);
ParseResult<T> _parse(ParseArgs args) {
ParseResult<T?> _parse(ParseArgs args) {
var pos = args.scanner.position;
if (args.trampoline.hasMemoized(this, pos))
@ -86,7 +86,7 @@ abstract class Parser<T> {
}
/// Parses text from a [SpanScanner].
ParseResult<T> parse(SpanScanner scanner, [int depth = 1]) {
ParseResult<T?> parse(SpanScanner scanner, [int depth = 1]) {
var args = ParseArgs(Trampoline(), scanner, depth);
return _parse(args);
}
@ -105,7 +105,7 @@ abstract class Parser<T> {
// TODO: Type issue
/// Runs the given function, which changes the returned [ParseResult] into one relating to a [U] object.
Parser<U> change<U>(ParseResult<U> Function(ParseResult<T>) f) {
Parser<U> change<U>(ParseResult<U?> Function(ParseResult<T?>) f) {
return _Change<T, U>(this, f);
}
@ -128,7 +128,7 @@ abstract class Parser<T> {
}
/// Transforms the parse result using a unary function.
Parser<U> map<U>(U Function(ParseResult<T>) f) {
Parser<U> map<U>(U Function(ParseResult<T?>) f) {
return _Map<T, U>(this, f);
}
@ -192,20 +192,18 @@ abstract class Parser<T> {
Parser<List<T>> separatedBy(Parser other) {
var suffix = other.then(this).index(1).cast<T>();
return this.then(suffix.star()).map((r) {
List<dynamic>? v = r.value;
if (v != null) {
var preceding =
v.isEmpty ? [] : (r.value?[0] == null ? [] : [r.value?[0]]);
var v = r.value;
if (v == null || v.length < 2) {
return [];
}
var preceding = v.isEmpty ? [] : (v[0] == null ? [] : [v[0]]);
var out = List<T>.from(preceding);
if (r.value?[1] != null) {
r.value?[1].forEach((element) {
if (v[1] != null) {
v[1].forEach((element) {
out.add(element as T);
});
}
return out;
} else {
return List<T>.empty(growable: true);
}
});
}
@ -278,7 +276,7 @@ abstract class Parser<T> {
Parser<T> opt({bool backtrack: true}) => _Opt(this, backtrack);
/// Sets the value of the [ParseResult].
Parser<T> value(T Function(ParseResult<T>) f) {
Parser<T> value(T Function(ParseResult<T?>) f) {
return _Value<T>(this, f);
}
@ -379,7 +377,7 @@ class ParseResult<T> {
scanner,
parser ?? this.parser,
successful ?? this.successful,
errors,
errors.isNotEmpty ? errors : this.errors,
span: span ?? this.span,
value: value ?? this.value,
);

View file

@ -7,8 +7,8 @@ class _Compare<T> extends ListParser<T> {
_Compare(this.parser, this.compare);
@override
ParseResult<List<T>> __parse(ParseArgs args) {
ParseResult<List<T>> result = parser._parse(args.increaseDepth());
ParseResult<List<T>?> __parse(ParseArgs args) {
ParseResult<List<T>?> result = parser._parse(args.increaseDepth());
if (!result.successful) return result;
result = result.change(

View file

@ -7,7 +7,7 @@ class _FoldErrors<T> extends Parser<T> {
_FoldErrors(this.parser, this.equal);
@override
ParseResult<T> __parse(ParseArgs args) {
ParseResult<T?> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth()).change(parser: this);
var errors = result.errors.fold<List<SyntaxError>>([], (out, e) {
if (!out.any((b) => equal(e, b))) out.add(e);

View file

@ -8,11 +8,26 @@ class _Index<T> extends Parser<T> {
@override
ParseResult<T> __parse(ParseArgs args) {
ParseResult<List<T>> result = parser._parse(args.increaseDepth());
ParseResult<List<T>?> result = parser._parse(args.increaseDepth());
Object? value;
if (result.successful)
value = index == -1 ? result.value!.last : result.value!.elementAt(index);
if (result.successful) {
var vList = result.value;
if (vList == null) {
throw ArgumentError("ParseResult is null");
}
if (index == -1) {
value = vList.last;
} else {
if (index < vList.length) {
//TODO: Look at this
// print(">>>>Index: $index, Size: ${vList.length}");
// value =
// index == -1 ? result.value!.last : result.value!.elementAt(index);
value = result.value!.elementAt(index);
}
}
}
return ParseResult<T>(
args.trampoline,

View file

@ -16,7 +16,7 @@ class _Longest<T> extends Parser<T> {
_Longest(this.parsers, this.errorMessage, this.severity);
@override
ParseResult<T> _parse(ParseArgs args) {
ParseResult<T?> _parse(ParseArgs args) {
var inactive = parsers
.toList()
.where((p) => !args.trampoline.isActive(p, args.scanner.position));
@ -27,7 +27,7 @@ class _Longest<T> extends Parser<T> {
int replay = args.scanner.position;
var errors = <SyntaxError>[];
var results = <ParseResult<T>>[];
var results = <ParseResult<T?>>[];
for (var parser in inactive) {
var result = parser._parse(args.increaseDepth());
@ -59,10 +59,10 @@ class _Longest<T> extends Parser<T> {
}
@override
ParseResult<T> __parse(ParseArgs args) {
ParseResult<T?> __parse(ParseArgs args) {
int replay = args.scanner.position;
var errors = <SyntaxError>[];
var results = <ParseResult<T>>[];
var results = <ParseResult<T?>>[];
for (var parser in parsers) {
var result = parser._parse(args.increaseDepth());

View file

@ -2,14 +2,14 @@ part of lex.src.combinator;
class _Map<T, U> extends Parser<U> {
final Parser<T> parser;
final U Function(ParseResult<T>) f;
final U Function(ParseResult<T?>) f;
_Map(this.parser, this.f);
@override
ParseResult<U> __parse(ParseArgs args) {
ParseResult<U?> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth());
return ParseResult<U>(
return ParseResult<U?>(
args.trampoline,
args.scanner,
this,
@ -34,12 +34,12 @@ class _Map<T, U> extends Parser<U> {
class _Change<T, U> extends Parser<U> {
final Parser<T> parser;
final ParseResult<U> Function(ParseResult<T>) f;
final ParseResult<U?> Function(ParseResult<T?>) f;
_Change(this.parser, this.f);
@override
ParseResult<U> __parse(ParseArgs args) {
ParseResult<U?> __parse(ParseArgs args) {
return f(parser._parse(args.increaseDepth())).change(parser: this);
}

View file

@ -7,7 +7,7 @@ class _MaxDepth<T> extends Parser<T> {
_MaxDepth(this.parser, this.cap);
@override
ParseResult<T> __parse(ParseArgs args) {
ParseResult<T?> __parse(ParseArgs args) {
if (args.depth > cap) {
return ParseResult<T>(args.trampoline, args.scanner, this, false, []);
}

View file

@ -8,7 +8,7 @@ class _Negate<T> extends Parser<T> {
_Negate(this.parser, this.errorMessage, this.severity);
@override
ParseResult<T> __parse(ParseArgs args) {
ParseResult<T?> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth()).change(parser: this);
if (!result.successful) {

View file

@ -7,7 +7,7 @@ class _Opt<T> extends Parser<T> {
_Opt(this.parser, this.backtrack);
@override
ParseResult<T> __parse(ParseArgs args) {
ParseResult<T?> __parse(ParseArgs args) {
var replay = args.scanner.position;
var result = parser._parse(args.increaseDepth());
@ -35,9 +35,9 @@ class _ListOpt<T> extends ListParser<T> {
_ListOpt(this.parser, this.backtrack);
@override
ParseResult<List<T>> __parse(ParseArgs args) {
ParseResult<List<T>?> __parse(ParseArgs args) {
var replay = args.scanner.position;
ParseResult<List<T>> result = parser._parse(args.increaseDepth());
ParseResult<List<T>?> result = parser._parse(args.increaseDepth());
if (!result.successful) args.scanner.position = replay;

View file

@ -7,8 +7,8 @@ class _Reduce<T> extends Parser<T> {
_Reduce(this.parser, this.combine);
@override
ParseResult<T> __parse(ParseArgs args) {
ParseResult<List<T>> result = parser._parse(args.increaseDepth());
ParseResult<T?> __parse(ParseArgs args) {
ParseResult<List<T>?> result = parser._parse(args.increaseDepth());
if (!result.successful)
return ParseResult<T>(

View file

@ -15,14 +15,14 @@ class Reference<T> extends Parser<T> {
}
@override
ParseResult<T> __parse(ParseArgs args) {
ParseResult<T?> __parse(ParseArgs args) {
if (_parser == null)
throw StateError('There is no parser assigned to this reference.');
return _parser!._parse(args);
}
@override
ParseResult<T> _parse(ParseArgs args) {
ParseResult<T?> _parse(ParseArgs args) {
if (_parser == null)
throw StateError('There is no parser assigned to this reference.');
return _parser!._parse(args);

View file

@ -12,12 +12,12 @@ class _Repeat<T> extends ListParser<T> {
this.backtrack, this.severity);
@override
ParseResult<List<T>> __parse(ParseArgs args) {
ParseResult<List<T>?> __parse(ParseArgs args) {
var errors = <SyntaxError>[];
var results = <T>[];
var spans = <FileSpan>[];
int success = 0, replay = args.scanner.position;
ParseResult<T>? result;
ParseResult<T?> result;
do {
result = parser._parse(args.increaseDepth());
@ -51,7 +51,7 @@ class _Repeat<T> extends ListParser<T> {
} else if (success > count && exact) {
if (backtrack) args.scanner.position = replay;
return ParseResult<List<T>>(args.trampoline, args.scanner, this, false, [
return ParseResult<List<T>?>(args.trampoline, args.scanner, this, false, [
SyntaxError(
severity,
tooMany,

View file

@ -10,7 +10,7 @@ class _Safe<T> extends Parser<T> {
_Safe(this.parser, this.backtrack, this.errorMessage, this.severity);
@override
ParseResult<T> __parse(ParseArgs args) {
ParseResult<T?> __parse(ParseArgs args) {
var replay = args.scanner.position;
try {

View file

@ -2,12 +2,12 @@ part of lex.src.combinator;
class _Value<T> extends Parser<T> {
final Parser<T> parser;
final T Function(ParseResult<T>) f;
final T Function(ParseResult<T?>) f;
_Value(this.parser, this.f);
@override
ParseResult<T> __parse(ParseArgs args) {
ParseResult<T?> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth()).change(parser: this);
return result.successful ? result.change(value: f(result)) : result;
}

View file

@ -136,7 +136,7 @@ abstract class Driver<
r.resolveAbsolute(path, method: req.method, strip: false);
var pipeline = MiddlewarePipeline<RequestHandler>(resolved);
return Tuple4(
pipeline.handlers!,
pipeline.handlers,
resolved.fold<Map<String, dynamic>>(
<String, dynamic>{}, (out, r) => out..addAll(r.allParams)),
(resolved.isEmpty ? null : resolved.first.parseResult)!,
@ -164,7 +164,7 @@ abstract class Driver<
..registerSingleton<ParseResult?>(tuple.item3);
if (app.environment.isProduction && app.logger != null) {
req.container!.registerSingleton<Stopwatch>(Stopwatch()..start());
req.container?.registerSingleton<Stopwatch>(Stopwatch()..start());
}
return runPipeline(it, req, res, app)

View file

@ -96,7 +96,7 @@ class HostnameRouter {
var resolved = r.resolveAbsolute(req.path, method: req.method);
var pipeline = MiddlewarePipeline<RequestHandler>(resolved);
// print('Pipeline: $pipeline');
for (var handler in pipeline.handlers!) {
for (var handler in pipeline.handlers) {
// print(handler);
// Avoid stack overflow.
if (handler == handleRequest) {

View file

@ -95,7 +95,7 @@ class Routable extends Router<RequestHandler> {
@override
Route<RequestHandler> addRoute(
String method, String path, RequestHandler handler,
{Iterable<RequestHandler>? middleware}) {
{Iterable<RequestHandler> middleware = const {}}) {
final handlers = <RequestHandler>[];
// Merge @Middleware declaration, if any
var reflector = _container?.reflector;

View file

@ -36,7 +36,7 @@ class Angel extends Routable {
final List<Angel> _children = [];
final Map<
String,
Tuple4<List, Map<String, dynamic>, ParseResult<RouteResult>?,
Tuple4<List, Map<String, dynamic>, ParseResult<RouteResult>,
MiddlewarePipeline>> handlerCache = HashMap();
Router<RequestHandler>? _flattened;
@ -150,8 +150,8 @@ class Angel extends Routable {
@override
Route<RequestHandler> addRoute(
String method, String path, RequestHandler handler,
{Iterable<RequestHandler>? middleware}) {
middleware ??= [];
{Iterable<RequestHandler> middleware = const []}) {
//middleware ??= [];
if (_flattened != null) {
logger?.warning(
'WARNING: You added a route ($method $path) to the router, after it had been optimized.');

View file

@ -11,10 +11,10 @@ final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
/// A variation of the [Router] support both hash routing and push state.
abstract class BrowserRouter<T> extends Router<T> {
/// Fires whenever the active route changes. Fires `null` if none is selected (404).
Stream<RoutingResult<T>> get onResolve;
Stream<RoutingResult<T?>> get onResolve;
/// Fires whenever the active route changes. Fires `null` if none is selected (404).
Stream<Route<T>> get onRoute;
Stream<Route<T?>> get onRoute;
/// Set `hash` to true to use hash routing instead of push state.
/// `listen` as `true` will call `listen` after initialization.
@ -48,17 +48,17 @@ abstract class _BrowserRouterImpl<T> extends Router<T>
implements BrowserRouter<T> {
bool _listening = false;
Route? _current;
final StreamController<RoutingResult<T>> _onResolve =
StreamController<RoutingResult<T>>();
final StreamController<Route<T>> _onRoute = StreamController<Route<T>>();
final StreamController<RoutingResult<T?>> _onResolve =
StreamController<RoutingResult<T?>>();
final StreamController<Route<T?>> _onRoute = StreamController<Route<T?>>();
Route? get currentRoute => _current;
@override
Stream<RoutingResult<T>> get onResolve => _onResolve.stream;
Stream<RoutingResult<T?>> get onResolve => _onResolve.stream;
@override
Stream<Route<T>> get onRoute => _onRoute.stream;
Stream<Route<T?>> get onRoute => _onRoute.stream;
_BrowserRouterImpl({bool listen = false}) : super() {
if (listen != false) this.listen();
@ -69,8 +69,7 @@ abstract class _BrowserRouterImpl<T> extends Router<T>
void go(Iterable linkParams) => _goTo(navigate(linkParams));
@override
Route on(String path, T handler,
{Iterable<T> middleware = const Iterable.empty()}) =>
Route on(String path, T handler, {Iterable<T> middleware = const []}) =>
all(path, handler, middleware: middleware);
void prepareAnchors() {
@ -108,7 +107,9 @@ abstract class _BrowserRouterImpl<T> extends Router<T>
class _HashRouter<T> extends _BrowserRouterImpl<T> {
_HashRouter({required bool listen}) : super(listen: listen) {
if (listen) this.listen();
if (listen) {
this.listen();
}
}
@override

View file

@ -6,13 +6,13 @@ class RouteGrammar {
static final Parser<String> notSlash =
match<String>(RegExp(notSlashRgx)).value((r) => r.span?.text ?? '');
static final Parser<Match?> regExp =
match<Match?>(RegExp(r'\(([^\n)]+)\)([^/]+)?'))
.value((r) => r.scanner.lastMatch);
static final Parser<Match> regExp =
match<Match>(RegExp(r'\(([^\n)]+)\)([^/]+)?'))
.value((r) => r.scanner.lastMatch!);
static final Parser<Match?> parameterName =
match<Match?>(RegExp('$notSlashRgx?' r':([A-Za-z0-9_]+)' r'([^(/\n])?'))
.value((r) => r.scanner.lastMatch);
static final Parser<Match> parameterName =
match<Match>(RegExp('$notSlashRgx?' r':([A-Za-z0-9_]+)' r'([^(/\n])?'))
.value((r) => r.scanner.lastMatch!);
static final Parser<ParameterSegment> parameterSegment = chain([
parameterName,

View file

@ -4,10 +4,10 @@ import 'router.dart';
class MiddlewarePipeline<T> {
/// All the possible routes that matched the given path.
final Iterable<RoutingResult<T>> routingResults;
final List<T> _handlers = [];
final List<T?> _handlers = [];
/// An ordered list of every handler delegated to handle this request.
List<T> get handlers {
List<T?> get handlers {
/*
if (_handlers != null) return _handlers;
final handlers = <T>[];
@ -19,7 +19,7 @@ class MiddlewarePipeline<T> {
return _handlers = handlers;
*/
if (_handlers.isEmpty) {
if (_handlers.isNotEmpty) {
return _handlers;
}

View file

@ -284,7 +284,7 @@ class Router<T> {
/// Finds the first [Route] that matches the given path,
/// with the given method.
bool resolve(String absolute, String relative, List<RoutingResult<T>> out,
bool resolve(String absolute, String relative, List<RoutingResult<T?>> out,
{String method = 'GET', bool strip = true}) {
final cleanRelative =
strip == false ? relative : stripStraySlashes(relative);
@ -334,13 +334,13 @@ class Router<T> {
/// Returns the result of [resolve] with [path] passed as
/// both `absolute` and `relative`.
Iterable<RoutingResult<T>> resolveAbsolute(String path,
Iterable<RoutingResult<T?>> 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<RoutingResult<T>> resolveAll(String absolute, String relative,
Iterable<RoutingResult<T?>> resolveAll(String absolute, String relative,
{String method = 'GET', bool strip = true}) {
if (_useCache == true) {
return _cache.putIfAbsent('$method$absolute',

View file

@ -3,7 +3,7 @@ part of angel_route.src.router;
/// Represents a complex result of navigating to a path.
class RoutingResult<T> {
/// The parse result that matched the given sub-path.
final ParseResult<RouteResult> parseResult;
final ParseResult<RouteResult?> parseResult;
/// A nested instance, if a sub-path was matched.
final Iterable<RoutingResult<T>> nested;
@ -47,10 +47,10 @@ class RoutingResult<T> {
}
/// All handlers on this sub-path and its children.
List<T> get allHandlers {
final handlers = <T>[];
List<T?> get allHandlers {
final handlers = <T?>[];
void crawl(RoutingResult<T> result) {
void crawl(RoutingResult<T?> result) {
handlers.addAll(result.handlers);
if (result.nested.isNotEmpty == true) {

View file

@ -7,6 +7,7 @@ environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
combinator:
# path: ../combinator
git:
url: https://github.com/dukefirehawk/angel.git
ref: sdk-2.12.x_nnbd