| // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:convert'; | |
| import 'exception.dart'; | |
| /// A wrapper for the parameters to a server method. | |
| /// | |
| /// JSON-RPC 2.0 allows parameters that are either a list or a map. This class | |
| /// provides functions that not only assert that the parameters object is the | |
| /// correct type, but also that the expected arguments exist and are themselves | |
| /// the correct type. | |
| /// | |
| /// Example usage: | |
| /// | |
| /// server.registerMethod("subtract", (params) { | |
| /// return params["minuend"].asNum - params["subtrahend"].asNum; | |
| /// }); | |
| class Parameters { | |
| /// The name of the method that this request called. | |
| final String method; | |
| /// The underlying value of the parameters object. | |
| /// | |
| /// If this is accessed for a [Parameter] that was not passed, the request | |
| /// will be automatically rejected. To avoid this, use [Parameter.valueOr]. | |
| final dynamic value; | |
| /// The ID of the request that called [method]. | |
| /// | |
| /// JSON-RPC allows a string, a number, or `null` here. A notification has no | |
| /// ID, and reaches a method as `null` too. Read [isNotification] to tell | |
| /// those apart. | |
| final Object? id; | |
| /// Whether the request that called [method] was a notification. | |
| /// | |
| /// A notification gets no response. Whatever the method returns for one is | |
| /// dropped. | |
| final bool isNotification; | |
| Parameters(this.method, this.value, {this.id, this.isNotification = false}); | |
| /// Returns a single parameter. | |
| /// | |
| /// If [key] is a [String], the request is expected to provide named | |
| /// parameters. If it's an [int], the request is expected to provide | |
| /// positional parameters. Requests that don't do so will be rejected | |
| /// automatically. | |
| /// | |
| /// Whether or not the given parameter exists, this returns a [Parameter] | |
| /// object. If a parameter's value is accessed through a getter like [value] | |
| /// or [Parameter.asNum], the request will be rejected if that parameter | |
| /// doesn't exist. On the other hand, if it's accessed through a method with a | |
| /// default value like [Parameter.valueOr] or [Parameter.asNumOr], the default | |
| /// value will be returned. | |
| Parameter operator [](Object? key) { | |
| if (key is int) { | |
| _assertPositional(); | |
| if (key < value.length) { | |
| return Parameter._(method, value[key], this, key); | |
| } else { | |
| return _MissingParameter(method, this, key); | |
| } | |
| } else if (key is String) { | |
| _assertNamed(); | |
| if (value.containsKey(key)) { | |
| return Parameter._(method, value[key], this, key); | |
| } else { | |
| return _MissingParameter(method, this, key); | |
| } | |
| } else { | |
| throw ArgumentError('Parameters[] only takes an int or a string, was ' | |
| '"$key".'); | |
| } | |
| } | |
| /// Asserts that [value] exists and is a [List] and returns it. | |
| List get asList { | |
| _assertPositional(); | |
| return value; | |
| } | |
| /// Asserts that [value] exists and is a [Map] and returns it. | |
| Map get asMap { | |
| _assertNamed(); | |
| return value; | |
| } | |
| /// Asserts that [value] is a positional argument list. | |
| void _assertPositional() { | |
| if (value is List) return; | |
| throw RpcException.invalidParams('Parameters for method "$method" ' | |
| 'must be passed by position.'); | |
| } | |
| /// Asserts that [value] is a named argument map. | |
| void _assertNamed() { | |
| if (value is Map) return; | |
| throw RpcException.invalidParams('Parameters for method "$method" ' | |
| 'must be passed by name.'); | |
| } | |
| } | |
| /// A wrapper for a single parameter to a server method. | |
| /// | |
| /// This provides numerous functions for asserting the type of the parameter in | |
| /// question. These functions each have a version that asserts that the | |
| /// parameter exists (for example, [asNum] and [asString]) and a version that | |
| /// returns a default value if the parameter doesn't exist (for example, | |
| /// [asNumOr] and [asStringOr]). If an assertion fails, the request is | |
| /// automatically rejected. | |
| /// | |
| /// This extends [Parameters] to make it easy to access nested parameters. For | |
| /// example: | |
| /// | |
| /// // "params.value" is "{'scores': {'home': [5, 10, 17]}}" | |
| /// params['scores']['home'][2].asInt // => 17 | |
| class Parameter extends Parameters { | |
| // The parent parameters, used to construct [_path] and to reach the request | |
| // this parameter came from. | |
| final Parameters _parent; | |
| /// The key used to access `this`, used to construct [_path]. | |
| final Object _key; | |
| /// A human-readable representation of the path of getters used to get this. | |
| /// | |
| /// Named parameters are represented as `.name`, whereas positional parameters | |
| /// are represented as `[index]`. For example: `"foo[0].bar.baz"`. Named | |
| /// parameters that contain characters that are neither alphanumeric, | |
| /// underscores, or hyphens will be JSON-encoded. For example: `"foo | |
| /// bar"."baz.bang"`. If quotes are used for an individual component, they | |
| /// won't be used for the entire string. | |
| /// | |
| /// An exception is made for single-level parameters. A single-level | |
| /// positional parameter is just represented by the index plus one, because | |
| /// "parameter 1" is clearer than "parameter [0]". A single-level named | |
| /// parameter is represented by that name in quotes. | |
| String get _path { | |
| if (_parent is! Parameter) { | |
| return _key is int ? (_key + 1).toString() : jsonEncode(_key); | |
| } | |
| String quoteKey(String key) { | |
| if (key.contains(RegExp(r'[^a-zA-Z0-9_-]'))) return jsonEncode(key); | |
| return key; | |
| } | |
| String computePath(Parameter params) { | |
| if (params._parent is! Parameter) { | |
| return params._key is int | |
| ? '[${params._key}]' | |
| : quoteKey(params._key as String); | |
| } | |
| var path = computePath(params._parent); | |
| return params._key is int | |
| ? '$path[${params._key}]' | |
| : '$path.${quoteKey(params._key as String)}'; | |
| } | |
| return computePath(this); | |
| } | |
| /// Whether this parameter exists. | |
| bool get exists => true; | |
| Parameter._(super.method, super.value, this._parent, this._key) | |
| : super(id: _parent.id, isNotification: _parent.isNotification); | |
| /// Returns [value], or [defaultValue] if this parameter wasn't passed. | |
| dynamic valueOr(Object? defaultValue) => value; | |
| /// Asserts that [value] exists and is a number and returns it. | |
| /// | |
| /// [asNumOr] may be used to provide a default value instead of rejecting the | |
| /// request if [value] doesn't exist. | |
| num get asNum => _getTyped('a number', (value) => value is num); | |
| /// Asserts that [value] is a number and returns it. | |
| /// | |
| /// If [value] doesn't exist, this returns [defaultValue]. | |
| num asNumOr(num defaultValue) => asNum; | |
| /// Asserts that [value] exists and is an integer and returns it. | |
| /// | |
| /// [asIntOr] may be used to provide a default value instead of rejecting the | |
| /// request if [value] doesn't exist. | |
| /// | |
| /// Note that which values count as integers varies between the Dart VM and | |
| /// dart2js. The value `1.0` will be considered an integer under dart2js but | |
| /// not under the VM. | |
| int get asInt => _getTyped('an integer', (value) => value is int); | |
| /// Asserts that [value] is an integer and returns it. | |
| /// | |
| /// If [value] doesn't exist, this returns [defaultValue]. | |
| /// | |
| /// Note that which values count as integers varies between the Dart VM and | |
| /// dart2js. The value `1.0` will be considered an integer under dart2js but | |
| /// not under the VM. | |
| int asIntOr(int defaultValue) => asInt; | |
| /// Asserts that [value] exists and is a boolean and returns it. | |
| /// | |
| /// [asBoolOr] may be used to provide a default value instead of rejecting the | |
| /// request if [value] doesn't exist. | |
| bool get asBool => _getTyped('a boolean', (value) => value is bool); | |
| /// Asserts that [value] is a boolean and returns it. | |
| /// | |
| /// If [value] doesn't exist, this returns [defaultValue]. | |
| bool asBoolOr(bool defaultValue) => asBool; | |
| /// Asserts that [value] exists and is a string and returns it. | |
| /// | |
| /// [asStringOr] may be used to provide a default value instead of rejecting | |
| /// the request if [value] doesn't exist. | |
| String get asString => _getTyped('a string', (value) => value is String); | |
| /// Asserts that [value] is a string and returns it. | |
| /// | |
| /// If [value] doesn't exist, this returns [defaultValue]. | |
| String asStringOr(String defaultValue) => asString; | |
| /// Asserts that [value] exists and is a [List] and returns it. | |
| /// | |
| /// [asListOr] may be used to provide a default value instead of rejecting the | |
| /// request if [value] doesn't exist. | |
| @override | |
| List get asList => _getTyped('an Array', (value) => value is List); | |
| /// Asserts that [value] is a [List] and returns it. | |
| /// | |
| /// If [value] doesn't exist, this returns [defaultValue]. | |
| List asListOr(List defaultValue) => asList; | |
| /// Asserts that [value] exists and is a [Map] and returns it. | |
| /// | |
| /// [asMapOr] may be used to provide a default value instead of rejecting the | |
| /// request if [value] doesn't exist. | |
| @override | |
| Map get asMap => _getTyped('an Object', (value) => value is Map); | |
| /// Asserts that [value] is a [Map] and returns it. | |
| /// | |
| /// If [value] doesn't exist, this returns [defaultValue]. | |
| Map asMapOr(Map defaultValue) => asMap; | |
| /// Asserts that [value] exists, is a string, and can be parsed as a | |
| /// [DateTime] and returns it. | |
| /// | |
| /// [asDateTimeOr] may be used to provide a default value instead of rejecting | |
| /// the request if [value] doesn't exist. | |
| DateTime get asDateTime => _getParsed('date/time', DateTime.parse); | |
| /// Asserts that [value] exists, is a string, and can be parsed as a | |
| /// [DateTime] and returns it. | |
| /// | |
| /// If [value] doesn't exist, this returns [defaultValue]. | |
| DateTime asDateTimeOr(DateTime defaultValue) => asDateTime; | |
| /// Asserts that [value] exists, is a string, and can be parsed as a | |
| /// [Uri] and returns it. | |
| /// | |
| /// [asUriOr] may be used to provide a default value instead of rejecting the | |
| /// request if [value] doesn't exist. | |
| Uri get asUri => _getParsed('URI', Uri.parse); | |
| /// Asserts that [value] exists, is a string, and can be parsed as a | |
| /// [Uri] and returns it. | |
| /// | |
| /// If [value] doesn't exist, this returns [defaultValue]. | |
| Uri asUriOr(Uri defaultValue) => asUri; | |
| /// Get a parameter named [type] that matches [test]. | |
| /// | |
| /// [type] is used for the error message. It should begin with an indefinite | |
| /// article. | |
| dynamic _getTyped(String type, bool Function(dynamic) test) { | |
| if (test(value)) return value; | |
| throw RpcException.invalidParams('Parameter $_path for method ' | |
| '"$method" must be $type, but was ${jsonEncode(value)}.'); | |
| } | |
| dynamic _getParsed(String description, void Function(String) parse) { | |
| var string = asString; | |
| try { | |
| return parse(string); | |
| } on FormatException catch (error) { | |
| // DateTime.parse doesn't actually include any useful information in the | |
| // FormatException, just the string that was being parsed. There's no use | |
| // in including that in the RPC exception. See issue 17753. | |
| var message = error.message; | |
| if (message == string) { | |
| message = ''; | |
| } else { | |
| message = '\n$message'; | |
| } | |
| throw RpcException.invalidParams('Parameter $_path for method ' | |
| '"$method" must be a valid $description, but was ' | |
| '${jsonEncode(string)}.$message'); | |
| } | |
| } | |
| @override | |
| void _assertPositional() { | |
| // Throw the standard exception for a mis-typed list. | |
| asList; | |
| } | |
| @override | |
| void _assertNamed() { | |
| // Throw the standard exception for a mis-typed map. | |
| asMap; | |
| } | |
| } | |
| /// A subclass of [Parameter] representing a missing parameter. | |
| class _MissingParameter extends Parameter { | |
| @override | |
| dynamic get value { | |
| throw RpcException.invalidParams('Request for method "$method" is ' | |
| 'missing required parameter $_path.'); | |
| } | |
| @override | |
| bool get exists => false; | |
| _MissingParameter(String method, Parameters parent, Object key) | |
| : super._(method, null, parent, key); | |
| @override | |
| dynamic valueOr(Object? defaultValue) => defaultValue; | |
| @override | |
| num asNumOr(num defaultValue) => defaultValue; | |
| @override | |
| int asIntOr(int defaultValue) => defaultValue; | |
| @override | |
| bool asBoolOr(bool defaultValue) => defaultValue; | |
| @override | |
| String asStringOr(String defaultValue) => defaultValue; | |
| @override | |
| List asListOr(List defaultValue) => defaultValue; | |
| @override | |
| Map asMapOr(Map defaultValue) => defaultValue; | |
| @override | |
| DateTime asDateTimeOr(DateTime defaultValue) => defaultValue; | |
| @override | |
| Uri asUriOr(Uri defaultValue) => defaultValue; | |
| } |