From 0cd48525a16605a7ac8f46d160cbdbdf2bed468b Mon Sep 17 00:00:00 2001 From: Spencer Mortensen Date: Fri, 8 Jan 2016 22:53:01 -0500 Subject: [PATCH] Extended support for custom exceptions --- src/Exception.php | 107 +++++++++++++++++- .../{Evaluation.php => Application.php} | 73 ++++++------ src/Exception/Argument.php | 3 +- src/Exception/Implementation.php | 90 +++++++++++++++ src/Exception/Method.php | 3 +- src/Server.php | 26 ++++- tests/Api.php | 36 +++++- tests/ServerTest.php | 97 +++++++++++----- 8 files changed, 356 insertions(+), 79 deletions(-) rename src/Exception/{Evaluation.php => Application.php} (57%) create mode 100644 src/Exception/Implementation.php diff --git a/src/Exception.php b/src/Exception.php index 7e528a1..e5aa201 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -24,6 +24,111 @@ namespace Datto\JsonRpc; -interface Exception +abstract class Exception extends \Exception { + /** @var null|boolean|integer|float|string|array */ + private $data; + + /** + * @param string $message + * Short description of the error that occurred. This message SHOULD + * be limited to a single, concise sentence. + * + * @param int $code + * Integer identifying the type of error that occurred. This code MUST + * follow the JSON-RPC 2.0 requirements for error codes: + * + * @link http://www.jsonrpc.org/specification#error_object + * + * @param null|boolean|integer|float|string|array $data + * An optional primitive value that contains additional information about + * the error.You're free to define the format of this data (e.g. you could + * supply an array with detailed error information). Alternatively, you may + * omit this field by supplying a null value. + */ + public function __construct($message, $code, $data = null) + { + if (!self::isValidMessage($message)) { + $message = ''; + } + + if (!self::isValidCode($code)) { + $code = 1; + } + + if (!self::isValidData($data)) { + $data = null; + } + + parent::__construct($message, $code); + + $this->data = $data; + } + + /** + * @return null|boolean|integer|float|string|array + * Returns the (optional) data property of the error object. + */ + public function getData() + { + return $this->data; + } + + /** + * Determines whether a value can be used as an error message. + * + * @param string $input + * Short description of the error that occurred. This message SHOULD + * be limited to a single, concise sentence. + * + * @return bool + * Returns true iff the value can be used as an error message. + */ + private static function isValidMessage($input) + { + return is_string($input); + } + + /** + * Determines whether a value can be used as an error code. + * + * @param $code + * Integer identifying the type of error that occurred. This code MUST + * follow the JSON-RPC 2.0 requirements for error codes: + * + * @link http://www.jsonrpc.org/specification#error_object + * + * @return bool + * Returns true iff the value can be used as an error code. + */ + private static function isValidCode($code) + { + return is_int($code); + } + + /** + * Determines whether a value can be used as the data value in an error + * object. + * + * @param null|boolean|integer|float|string|array $input + * An optional primitive value that contains additional information about + * the error.You're free to define the format of this data (e.g. you could + * supply an array with detailed error information). Alternatively, you may + * omit this field by supplying a null value. + * + * @return bool + * Returns true iff the value can be used as the data value in an error + * object. + */ + private static function isValidData($input) + { + $type = gettype($input); + + return ($type === 'array') + || ($type === 'string') + || ($type === 'double') + || ($type === 'integer') + || ($type === 'boolean') + || ($type === 'NULL'); + } } diff --git a/src/Exception/Evaluation.php b/src/Exception/Application.php similarity index 57% rename from src/Exception/Evaluation.php rename to src/Exception/Application.php index 265c414..9564202 100644 --- a/src/Exception/Evaluation.php +++ b/src/Exception/Application.php @@ -24,20 +24,26 @@ namespace Datto\JsonRpc\Exception; -use Exception; use Datto\JsonRpc; /** - * Class Evaluation + * Class Application * @package Datto\JsonRpc\Exception * - * The JSON-RPC 2.0 specifications allow you to define your own error objects! - * You can use this to communicate any issues that arise during the evaluation - * of a request. + * The JSON-RPC 2.0 specifications allows each application that evaluates a user + * request to define its own custom error codes: * * @link http://www.jsonrpc.org/specification#error_object + * + * You can use this flexibility to communicate any issues that arise while + * your application is evaluating a user request. + * + * However: + * + * If one or more of the user-supplied arguments are invalid, then you should + * report the issue through an "Argument" exception instead. */ -class Evaluation extends Exception implements JsonRpc\Exception +class Application extends JsonRpc\Exception { /** * @param string $message @@ -46,39 +52,44 @@ class Evaluation extends Exception implements JsonRpc\Exception * * @param int $code * Integer identifying the type of error that occurred. As the author of - * your API, you are free to define the error codes that you find useful - * for your application. + * a server-side application, you are free to define any error codes + * that you find useful for your application. * * Please be aware that the error codes in the range from -32768 to -32000, - * inclusive, have special meanings under the JSON-RPC 2.0 specification! - * These error codes have already been taken, so they cannot also be used - * as application-defined error codes. You can safely use any integer value - * from outside the reserved range. + * inclusive, have special meanings under the JSON-RPC 2.0 specification: + * These error codes have already been taken, so they cannot be redefined + * as application-defined error codes! However, you can safely use any + * integer from outside this reserved range. + * + * @param null|boolean|integer|float|string|array $data + * An optional primitive value that contains additional information about + * the error.You're free to define the format of this data (e.g. you could + * supply an array with detailed error information). Alternatively, you may + * omit this field by providing a null value. */ - public function __construct($message = '', $code = 1) + public function __construct($message, $code, $data = null) { if (!self::isValidCode($code)) { $code = 1; } - if (!self::isValidMessage($message)) { - $message = ''; - } - - parent::__construct($message, $code); + parent::__construct($message, $code, $data); } /** + * Determines whether a value can be used as an application-defined error + * code. + * * @param int $code * Integer identifying the type of error that occurred. As the author of - * your API, you are free to define the error codes that you find useful - * for your application. + * a server-side application, you are free to define any error codes + * that you find useful for your application. * * Please be aware that the error codes in the range from -32768 to -32000, - * inclusive, have special meanings under the JSON-RPC 2.0 specification! - * These error codes have already been taken, so they cannot also be used - * as application-defined error codes. You can safely use any integer value - * from outside the reserved range. + * inclusive, have special meanings under the JSON-RPC 2.0 specification: + * These error codes have already been taken, so they cannot be redefined + * as application-defined error codes! However, you can safely use any + * integer from outside this reserved range. * * @return bool * Returns true iff the value can be used as an application-defined @@ -88,18 +99,4 @@ private static function isValidCode($code) { return is_int($code) && (($code < -32768) || (-32000 < $code)); } - - /** - * @param string $message - * Short description of the error that occurred. This message SHOULD - * be limited to a single, concise sentence. - * - * @return bool - * Returns true iff the value can be used as an application-defined - * error message. - */ - private static function isValidMessage($message) - { - return is_string($message); - } } diff --git a/src/Exception/Argument.php b/src/Exception/Argument.php index 89dd19e..bec2c2e 100644 --- a/src/Exception/Argument.php +++ b/src/Exception/Argument.php @@ -24,10 +24,9 @@ namespace Datto\JsonRpc\Exception; -use Exception; use Datto\JsonRpc; -class Argument extends Exception implements JsonRpc\Exception +class Argument extends JsonRpc\Exception { public function __construct() { diff --git a/src/Exception/Implementation.php b/src/Exception/Implementation.php new file mode 100644 index 0000000..22bbc6d --- /dev/null +++ b/src/Exception/Implementation.php @@ -0,0 +1,90 @@ +. + * + * @author Spencer Mortensen + * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0 + * @copyright 2015 Datto, Inc. + */ + +namespace Datto\JsonRpc\Exception; + +use Datto\JsonRpc; + +/** + * Class Implementation + * @package Datto\JsonRpc\Exception + * + * The JSON-RPC 2.0 specifications allows each codebase that implements + * the specifications to define its own custom error codes: + * + * @link http://www.jsonrpc.org/specification#error_object + * + * You can use this flexibility to communicate any issues that arise while + * your JSON-RPC 2.0 implementation is processing a request. + * + * However: + * + * If the method cannot be called (e.g. if the method doesn't exist, or is a + * private method), then you should report the issue through the "Method" + * exception instead. + * + * If the method exists, but the user-supplied arguments are incompatible with + * the method's type signature, or one or more of the arguments is invalid, + * then you should report the issue through the "Argument" exception. + * + * Finally, if the issue did not arise within your codebase, but instead arose + * within the application-specific library code that actually evaluated the + * request, then you should report the issue through an "Application" exception. + */ +class Implementation extends JsonRpc\Exception +{ + /** + * @param int $code + * Integer identifying the type of error that occurred. As the author of a + * JSON-RPC 2.0 implementation, you are free to define any error code that + * you find useful for your implementation. However, you MUST choose your + * error codes from within the range from -32000 to -32099, inclusive. + * + * @param null|boolean|integer|float|string|array $data + * An optional primitive value that contains additional information about + * the error.You're free to define the format of this data (e.g. you could + * supply an array with detailed error information). Alternatively, you may + * omit this field by providing a null value. + */ + public function __construct($code, $data = null) + { + if (!self::isValidCode($code)) { + $code = -32099; + } + + parent::__construct('Server error', $code, $data); + } + + /** + * @param int $code + * + * @return bool + * Returns true iff the value can be used as an implementation-defined + * error code. + */ + private static function isValidCode($code) + { + return is_int($code) && (-32000 <= $code) && ($code <= -32099); + } +} diff --git a/src/Exception/Method.php b/src/Exception/Method.php index f687637..903d0e0 100644 --- a/src/Exception/Method.php +++ b/src/Exception/Method.php @@ -24,10 +24,9 @@ namespace Datto\JsonRpc\Exception; -use Exception; use Datto\JsonRpc; -class Method extends Exception implements JsonRpc\Exception +class Method extends JsonRpc\Exception { public function __construct() { diff --git a/src/Server.php b/src/Server.php index 10fc81c..b34d6e7 100644 --- a/src/Server.php +++ b/src/Server.php @@ -204,7 +204,11 @@ private function processQuery($id, $method, $arguments) $result = $this->evaluator->evaluate($method, $arguments); return self::response($id, $result); } catch (Exception $exception) { - return self::error($id, $exception->getCode(), $exception->getMessage()); + $code = $exception->getCode(); + $message = $exception->getMessage(); + $data = $exception->getData(); + + return self::error($id, $code, $message, $data); } } @@ -262,18 +266,28 @@ private static function errorRequest() * @param string $message * Concise description of the error (ideally a single sentence). * + * @param null|boolean|integer|float|string|array $data + * An optional primitive value that contains additional information about + * the error. + * * @return array * Returns an error object. */ - private static function error($id, $code, $message) + private static function error($id, $code, $message, $data = null) { + $error = array( + 'code' => $code, + 'message' => $message + ); + + if ($data !== null) { + $error['data'] = $data; + } + return array( 'jsonrpc' => self::VERSION, 'id' => $id, - 'error' => array( - 'code' => $code, - 'message' => $message - ) + 'error' => $error ); } diff --git a/tests/Api.php b/tests/Api.php index 546e090..4043ebb 100644 --- a/tests/Api.php +++ b/tests/Api.php @@ -31,11 +31,22 @@ class Api implements Evaluator { public function evaluate($method, $arguments) { - if ($method === 'subtract') { - return self::subtract($arguments); - } + switch ($method) { + case 'subtract': + return self::subtract($arguments); + + case 'implementation error': + return self::implementationError($arguments); + + case 'application error': + return self::applicationError($arguments); + + case 'invalid error': + return self::invalidError(); - throw new Exception\Method(); + default: + throw new Exception\Method(); + } } private static function subtract($arguments) @@ -53,4 +64,21 @@ private static function subtract($arguments) return $a - $b; } + + private static function implementationError($arguments) + { + throw new Exception\Implementation(-32099, @$arguments[0]); + } + + private static function applicationError($arguments) + { + throw new Exception\Application("Application error", 1, @$arguments[0]); + } + + private static function invalidError() + { + $invalid = new \StdClass(); + + throw new Exception\Application($invalid, $invalid, $invalid); + } } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 18b4900..600ee27 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -31,45 +31,45 @@ class ServerTest extends PHPUnit_Framework_TestCase { public function testArgumentsPositionalA() { - $input = '{"jsonrpc": "2.0", "method": "subtract", "params": [3, 2], "id": 1}'; + $input = '{"jsonrpc": "2.0", "id": 1, "method": "subtract", "params": [3, 2]}'; - $output = '{"jsonrpc": "2.0", "result": 1, "id": 1}'; + $output = '{"jsonrpc": "2.0", "id": 1, "result": 1}'; $this->compare($input, $output); } public function testArgumentsPositionalB() { - $input = '{"jsonrpc": "2.0", "method": "subtract", "params": [2, 3], "id": 1}'; + $input = '{"jsonrpc": "2.0", "id": 1, "method": "subtract", "params": [2, 3]}'; - $output = '{"jsonrpc": "2.0", "result": -1, "id": 1}'; + $output = '{"jsonrpc": "2.0", "id": 1, "result": -1}'; $this->compare($input, $output); } public function testArgumentsNamedA() { - $input = '{"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 3, "subtrahend": 2}, "id": 1}'; + $input = '{"jsonrpc": "2.0", "id": 1, "method": "subtract", "params": {"minuend": 3, "subtrahend": 2}}'; - $output = '{"jsonrpc": "2.0", "result": 1, "id": 1}'; + $output = '{"jsonrpc": "2.0", "id": 1, "result": 1}'; $this->compare($input, $output); } public function testArgumentsInvalid() { - $input = '{"jsonrpc": "2.0", "method": "subtract", "params": [], "id": 1}'; + $input = '{"jsonrpc": "2.0", "id": 1, "method": "subtract", "params": []}'; - $output = '{"jsonrpc": "2.0", "error": {"code": -32602, "message": "Invalid params"}, "id": "1"}'; + $output = '{"jsonrpc": "2.0", "id": 1, "error": {"code": -32602, "message": "Invalid params"}}'; $this->compare($input, $output); } public function testArgumentsNamedB() { - $input = '{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 2, "minuend": 3}, "id": 1}'; + $input = '{"jsonrpc": "2.0", "id": 1, "method": "subtract", "params": {"subtrahend": 2, "minuend": 3}}'; - $output = '{"jsonrpc": "2.0", "result": 1, "id": 1}'; + $output = '{"jsonrpc": "2.0", "id": 1, "result": 1}'; $this->compare($input, $output); } @@ -94,9 +94,9 @@ public function testNotification() public function testUndefinedMethod() { - $input ='{"jsonrpc": "2.0", "method": "undefined", "id": "1"}'; + $input ='{"jsonrpc": "2.0", "id": 1, "method": "undefined"}'; - $output = '{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}'; + $output = '{"jsonrpc": "2.0", "id": 1, "error": {"code": -32601, "message": "Method not found"}}'; $this->compare($input, $output); } @@ -105,7 +105,7 @@ public function testInvalidJson() { $input = '{"jsonrpc": "2.0", "method": "foobar", "params": "bar", "baz]'; - $output = '{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}'; + $output = '{"jsonrpc": "2.0", "id": null, "error": {"code": -32700, "message": "Parse error"}}'; $this->compare($input, $output); } @@ -114,7 +114,7 @@ public function testInvalidMethod() { $input = '{"jsonrpc": "2.0", "method": 1, "params": [1, 2]}'; - $output = '{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}'; + $output = '{"jsonrpc": "2.0", "id": null, "error": {"code": -32600, "message": "Invalid Request"}}'; $this->compare($input, $output); } @@ -123,7 +123,52 @@ public function testInvalidParams() { $input = '{"jsonrpc": "2.0", "method": "foobar", "params": "bar"}'; - $output = '{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}'; + $output = '{"jsonrpc": "2.0", "id": null, "error": {"code": -32600, "message": "Invalid Request"}}'; + + $this->compare($input, $output); + } + + public function testImplementationError() + { + $input = '{"jsonrpc": "2.0", "id": 1, "method": "implementation error"}'; + + $output = '{"jsonrpc": "2.0", "id": 1, "error": {"code": -32099, "message": "Server error"}}'; + + $this->compare($input, $output); + } + + public function testImplementationErrorData() + { + $input = '{"jsonrpc": "2.0", "id": 1, "method": "implementation error", "params": ["details"]}'; + + $output = '{"jsonrpc": "2.0", "id": 1, "error": {"code": -32099, "message": "Server error", "data": "details"}}'; + + $this->compare($input, $output); + } + + public function testApplicationError() + { + $input = '{"jsonrpc": "2.0", "id": 1, "method": "application error"}'; + + $output = '{"jsonrpc": "2.0", "id": 1, "error": {"code": 1, "message": "Application error"}}'; + + $this->compare($input, $output); + } + + public function testApplicationErrorData() + { + $input = '{"jsonrpc": "2.0", "id": 1, "method": "application error", "params": ["details"]}'; + + $output = '{"jsonrpc": "2.0", "id": 1, "error": {"code": 1, "message": "Application error", "data": "details"}}'; + + $this->compare($input, $output); + } + + public function testInvalidError() + { + $input = '{"jsonrpc": "2.0", "id": 1, "method": "invalid error"}'; + + $output = '{"jsonrpc": "2.0", "id": 1, "error": {"code": 1, "message": ""}}'; $this->compare($input, $output); } @@ -132,7 +177,7 @@ public function testInvalidId() { $input = '{"jsonrpc": "2.0", "method": "foobar", "params": [1, 2], "id": [1]}'; - $output = '{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}'; + $output = '{"jsonrpc": "2.0", "id": null, "error": {"code": -32600, "message": "Invalid Request"}}'; $this->compare($input, $output); } @@ -144,7 +189,7 @@ public function testBatchInvalidJson() {"jsonrpc": "2.0", "method" ]'; - $output = '{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}'; + $output = '{"jsonrpc": "2.0", "id": null, "error": {"code": -32700, "message": "Parse error"}}'; $this->compare($input, $output); } @@ -154,7 +199,7 @@ public function testBatchEmpty() $input = '[ ]'; - $output = '{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}'; + $output = '{"jsonrpc": "2.0", "id": null, "error": {"code": -32600, "message": "Invalid Request"}}'; $this->compare($input, $output); } @@ -166,7 +211,7 @@ public function testBatchInvalidElement() ]'; $output = '[ - {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null} + {"jsonrpc": "2.0", "id": null, "error": {"code": -32600, "message": "Invalid Request"}} ]'; $this->compare($input, $output); @@ -181,9 +226,9 @@ public function testBatchInvalidElements() ]'; $output = '[ - {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}, - {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}, - {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null} + {"jsonrpc": "2.0", "id": null, "error": {"code": -32600, "message": "Invalid Request"}}, + {"jsonrpc": "2.0", "id": null, "error": {"code": -32600, "message": "Invalid Request"}}, + {"jsonrpc": "2.0", "id": null, "error": {"code": -32600, "message": "Invalid Request"}} ]'; $this->compare($input, $output); @@ -199,9 +244,9 @@ public function testBatch() ]'; $output = '[ - {"jsonrpc": "2.0", "result": 2, "id": "1"}, - {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}, - {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"} + {"jsonrpc": "2.0", "id": "1", "result": 2}, + {"jsonrpc": "2.0", "id": null, "error": {"code": -32600, "message": "Invalid Request"}}, + {"jsonrpc": "2.0", "id": "5", "error": {"code": -32601, "message": "Method not found"}} ]'; $this->compare($input, $output); @@ -227,6 +272,6 @@ private function compare($input, $expectedJsonOutput) $expectedOutput = json_decode($expectedJsonOutput, true); $actualOutput = json_decode($actualJsonOutput, true); - $this->assertEquals($expectedOutput, $actualOutput); + $this->assertSame($expectedOutput, $actualOutput); } }