| // 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]. |
| get value => _value; |
| final _value; |
| |
| Parameters(this.method, this._value); |
| |
| /// 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 [](key) { |
| if (key is int) { |
| _assertPositional(); |
| if (key < value.length) { |
| return new Parameter._(method, value[key], this, key); |
| } else { |
| return new _MissingParameter(method, this, key); |
| } |
| } else if (key is String) { |
| _assertNamed(); |
| if (value.containsKey(key)) { |
| return new Parameter._(method, value[key], this, key); |
| } else { |
| return new _MissingParameter(method, this, key); |
| } |
| } else { |
| throw new 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 new 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 new 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]. |
| final Parameters _parent; |
| |
| /// The key used to access [this], used to construct [_path]. |
| final _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() : JSON.encode(_key); |
| } |
| |
| quoteKey(key) { |
| if (key.contains(new RegExp(r'[^a-zA-Z0-9_-]'))) return JSON.encode(key); |
| return key; |
| } |
| |
| computePath(params) { |
| if (params._parent is! Parameter) { |
| return params._key is int ? "[${params._key}]" : quoteKey(params._key); |
| } |
| |
| var path = computePath(params._parent); |
| return params._key is int |
| ? "$path[${params._key}]" |
| : "$path.${quoteKey(params._key)}"; |
| } |
| |
| return computePath(this); |
| } |
| |
| /// Whether this parameter exists. |
| bool get exists => true; |
| |
| Parameter._(String method, value, this._parent, this._key) |
| : super(method, value); |
| |
| /// Returns [value], or [defaultValue] if this parameter wasn't passed. |
| valueOr(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. |
| 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. |
| 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 [named] that matches [test], or the value of calling |
| /// [orElse]. |
| /// |
| /// [type] is used for the error message. It should begin with an indefinite |
| /// article. |
| _getTyped(String type, bool test(value)) { |
| if (test(value)) return value; |
| throw new RpcException.invalidParams('Parameter $_path for method ' |
| '"$method" must be $type, but was ${JSON.encode(value)}.'); |
| } |
| |
| _getParsed(String description, parse(String value)) { |
| 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 new RpcException.invalidParams('Parameter $_path for method ' |
| '"$method" must be a valid $description, but was ' |
| '${JSON.encode(string)}.$message'); |
| } |
| } |
| |
| void _assertPositional() { |
| // Throw the standard exception for a mis-typed list. |
| asList; |
| } |
| |
| void _assertNamed() { |
| // Throw the standard exception for a mis-typed map. |
| asMap; |
| } |
| } |
| |
| /// A subclass of [Parameter] representing a missing parameter. |
| class _MissingParameter extends Parameter { |
| get value { |
| throw new RpcException.invalidParams('Request for method "$method" is ' |
| 'missing required parameter $_path.'); |
| } |
| |
| bool get exists => false; |
| |
| _MissingParameter(String method, Parameters parent, key) |
| : super._(method, null, parent, key); |
| |
| valueOr(defaultValue) => defaultValue; |
| |
| num asNumOr(num defaultValue) => defaultValue; |
| |
| int asIntOr(int defaultValue) => defaultValue; |
| |
| bool asBoolOr(bool defaultValue) => defaultValue; |
| |
| String asStringOr(String defaultValue) => defaultValue; |
| |
| List asListOr(List defaultValue) => defaultValue; |
| |
| Map asMapOr(Map defaultValue) => defaultValue; |
| |
| DateTime asDateTimeOr(DateTime defaultValue) => defaultValue; |
| |
| Uri asUriOr(Uri defaultValue) => defaultValue; |
| } |