diff --git a/README.md b/README.md index 49f7464..5335e1d 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,49 @@ This yocLibrary enables your project to encode and decode JSON-RPC messages in P ### Serialization ```php +use YOCLIB\JSONRPC\JSONRPCException; +use YOCLIB\JSONRPC\Message; +$message = Message::createRequestMessageV1(123,'getInfo',['payments']); // Create request (version 1.0) +$message = Message::createNotificationMessageV1('notificationEvent',['payed']); // Create notification (version 1.0) +$message = Message::createResponseMessageV1(123,['payments'=>[]]); // Create response (version 1.0) + +$object = $message->toObject(); + +try{ + $json = Message::encodeJSON($object); +}catch(JSONRPCException $e){ + //Handle encoding exception +} ``` ### Deserialization ```php - +use YOCLIB\JSONRPC\JSONRPCException; +use YOCLIB\JSONRPC\Message; + +$json = file_get_contents('php://input'); // Get request body + +try{ + $object = Message::decodeJSON($json); +}catch(JSONRPCException $e){ + //Handle decoding exception +} + +if(Message::isBatch($object)){ + foreach($object AS $element){ + try{ + $message = Message::parse($element); + }catch(JSONRPCException $e){ + //Handle message exception + } + } +}else{ + try{ + $message = Message::parse($object); + }catch(JSONRPCException $e){ + //Handle message exception + } +} ``` \ No newline at end of file diff --git a/composer.json b/composer.json index 7483894..3f5029b 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "ext-json": "*" }, "require-dev": { - "phpunit/phpunit": "^7||^8||^9" + "phpunit/phpunit": "^7||^8||^9", + "phpunit/php-code-coverage": "^9.2" }, "autoload": { "psr-4": { @@ -33,4 +34,4 @@ "scripts": { "test": "phpunit" } -} \ No newline at end of file +} diff --git a/src/Message.php b/src/Message.php index aa27105..9bf7102 100644 --- a/src/Message.php +++ b/src/Message.php @@ -15,10 +15,10 @@ private function __construct(object $value){ } /** - * @return string + * @return object */ - public function toJSON(): string{ - return json_encode($this->value); + public function toObject(): object{ + return $this->value; } /** @@ -70,26 +70,51 @@ public static function createResponseMessageV1($id,$result=null,$error=null): Re ]); } + /** + * @param $object + * @return bool + */ + public static function isBatch($object): bool{ + return is_array($object); + } + + /** + * @param $object + * @return false|string + * @throws JSONRPCException + */ + public static function encodeJSON($object){ + try{ + return json_encode($object,JSON_THROW_ON_ERROR); + }catch(JsonException $e){ + throw new JSONRPCException('Failed to encode JSON.'); + } + } + /** * @param string $json - * @param bool $strictId - * @return Message[]|array|Message + * @return mixed * @throws JSONRPCException */ - public static function parse(string $json,bool $strictId=true){ + public static function decodeJSON(string $json){ try{ - $message = json_decode($json,false,512,JSON_THROW_ON_ERROR); + return json_decode($json,false,512,JSON_THROW_ON_ERROR); }catch(JsonException $e){ - throw new JSONRPCException('[V1] Failed to decode JSON.'); + throw new JSONRPCException('Failed to decode JSON.'); } - if(is_array($message)){ - $messages = []; - foreach($message AS $msg){ - $messages[] = self::handleMessage($msg,$strictId); - } - return $messages; + } + + /** + * @param $object + * @param bool $strictId + * @return Message + * @throws JSONRPCException + */ + public static function parseObject($object,bool $strictId=true){ + if(is_object($object)){ + return self::handleMessage($object,$strictId); } - return self::handleMessage($message,$strictId); + throw new JSONRPCException('A message MUST be a JSON object.'); } /** @@ -99,23 +124,38 @@ public static function parse(string $json,bool $strictId=true){ * @throws JSONRPCException */ private static function handleMessage($message,bool $strictId=true){ - if(isset($message['jsonrpc']) && $message['jsonrpc']==='2.0'){ - return self::handleMessageV2($message,$strictId); + if(property_exists($message,'jsonrpc')){ + if($message->jsonrpc==='2.0'){ + return self::handleMessageV2($message,$strictId); + } + throw new JSONRPCException('Unknown version "'.($message->jsonrpc).'".'); }else{ return self::handleMessageV1($message,$strictId); } } + /** + * @param $message + * @param bool $strictId + * @return null + * @throws JSONRPCException + */ private static function handleMessageV2($message,bool $strictId=true){ - return null; + if(self::isRequest($message)){ + return null; + }elseif(self::isResponse($message)){ + return null; + }else{ + throw new JSONRPCException('[V2] Unknown message type.'); + } } - private static function isRequestV1($message): bool{ + private static function isRequest($message): bool{ return property_exists($message,'method') || property_exists($message,'params'); } - private static function isResponseV1($message): bool{ + private static function isResponse($message): bool{ return property_exists($message,'result') || property_exists($message,'error'); } @@ -179,7 +219,7 @@ private static function validateErrorPropertyV1($message){ * @throws JSONRPCException */ private static function handleMessageV1($message,bool $strictId=true){ - if(self::isRequestV1($message)){ + if(self::isRequest($message)){ self::validateMethodPropertyV1($message); self::validateParamsPropertyV1($message); @@ -188,7 +228,7 @@ private static function handleMessageV1($message,bool $strictId=true){ }else{ return new NotificationMessage($message); } - }elseif(self::isResponseV1($message)){ + }elseif(self::isResponse($message)){ self::validateResultPropertyV1($message); self::validateErrorPropertyV1($message); if(!is_null($message->result) && !is_null($message->error)){ diff --git a/tests/MessageTest.php b/tests/MessageTest.php index 2bb9877..b78183a 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -8,15 +8,163 @@ class MessageTest extends TestCase{ + public function testDecodeEmptyJSON(){ + $this->expectException(JSONRPCException::class); + $this->expectExceptionMessage('Failed to decode JSON.'); + + Message::decodeJSON(''); + } + + /** + * @return void + * @throws JSONRPCException + */ + public function testDecodeJSONString(){ + $this->assertEquals('abc',Message::decodeJSON('"abc"')); + } + + /** + * @return void + * @throws JSONRPCException + */ + public function testDecodeJSONObject(){ + $this->assertEquals((object) [],Message::decodeJSON('{}')); + } + + /** + * @return void + * @throws JSONRPCException + */ + public function testDecodeJSONArray(){ + $this->assertEquals([],Message::decodeJSON('[]')); + } + + public function testIsBatch(){ + $this->assertTrue(Message::isBatch([])); + + $this->assertFalse(Message::isBatch('abc')); + $this->assertFalse(Message::isBatch(true)); + $this->assertFalse(Message::isBatch(false)); + $this->assertFalse(Message::isBatch(123)); + $this->assertFalse(Message::isBatch(123.456)); + $this->assertFalse(Message::isBatch((object) [])); + $this->assertFalse(Message::isBatch(null)); + } + + /** + * @return void + * @throws JSONRPCException + */ + public function testParseObjectString(){ + $this->expectException(JSONRPCException::class); + $this->expectExceptionMessage('A message MUST be a JSON object.'); + + Message::parseObject('abc'); + } + + /** + * @return void + * @throws JSONRPCException + */ + public function testParseObjectTrue(){ + $this->expectException(JSONRPCException::class); + $this->expectExceptionMessage('A message MUST be a JSON object.'); + + Message::parseObject(true); + } + + /** + * @return void + * @throws JSONRPCException + */ + public function testParseObjectFalse(){ + $this->expectException(JSONRPCException::class); + $this->expectExceptionMessage('A message MUST be a JSON object.'); + + Message::parseObject(false); + } + + /** + * @return void + * @throws JSONRPCException + */ + public function testParseObjectInteger(){ + $this->expectException(JSONRPCException::class); + $this->expectExceptionMessage('A message MUST be a JSON object.'); + + Message::parseObject(123); + } + + /** + * @return void + * @throws JSONRPCException + */ + public function testParseObjectFloat(){ + $this->expectException(JSONRPCException::class); + $this->expectExceptionMessage('A message MUST be a JSON object.'); + + Message::parseObject(123.456); + } + + /** + * @return void + * @throws JSONRPCException + */ + public function testParseObjectArray(){ + $this->expectException(JSONRPCException::class); + $this->expectExceptionMessage('A message MUST be a JSON object.'); + + Message::parseObject([]); + } + + /** + * @return void + * @throws JSONRPCException + */ + public function testParseEmptyObject(){ + $this->expectException(JSONRPCException::class); + $this->expectExceptionMessage('[V1] Unknown message type.'); + + Message::parseObject((object) []); + } + + /** + * @return void + * @throws JSONRPCException + */ + public function testParseVersion2(){ + $this->expectException(JSONRPCException::class); + $this->expectExceptionMessage('[V2] Unknown message type.'); + + Message::parseObject((object) [ + 'jsonrpc' => '2.0', + ]); + } + + /** + * @return void + * @throws JSONRPCException + */ + public function testParseUnknownVersion(){ + $this->expectException(JSONRPCException::class); + $this->expectExceptionMessage('Unknown version "1.5".'); + + Message::parseObject((object) [ + 'jsonrpc' => '1.5', + ]); + } + /** * @return void * @throws JSONRPCException */ public function testMessages(){ - $this->assertEquals('{"id":123,"method":"myMethod","params":[]}',Message::createRequestMessageV1(123,'myMethod')->toJSON()); - $this->assertEquals('{"id":null,"method":"myMethod","params":[]}',Message::createNotificationMessageV1('myMethod')->toJSON()); - $this->assertEquals('{"id":123,"result":"myResult","error":null}',Message::createResponseMessageV1(123,'myResult')->toJSON()); - $this->assertEquals('{"id":123,"result":null,"error":"myError"}',Message::createResponseMessageV1(123,null,'myError')->toJSON()); + $this->assertEquals((object) ["id"=>123,"method"=>"myMethod","params"=>[]],Message::createRequestMessageV1(123,'myMethod')->toObject()); + $this->assertEquals((object) ["id"=>123,"method"=>"myMethod","params"=>["a",1,false,12.34]],Message::createRequestMessageV1(123,'myMethod',['a',1,false,12.34])->toObject()); + $this->assertEquals((object) ["id"=>null,"method"=>"myMethod","params"=>[]],Message::createNotificationMessageV1('myMethod')->toObject()); + $this->assertEquals((object) ["id"=>null,"method"=>"myMethod","params"=>["b",0,true,34.12]],Message::createNotificationMessageV1('myMethod',['b',0,true,34.12])->toObject()); + $this->assertEquals((object) ["id"=>123,"result"=>"myResult","error"=>null],Message::createResponseMessageV1(123,'myResult')->toObject()); + $this->assertEquals((object) ["id"=>123,"result"=>null,"error"=>"myError"],Message::createResponseMessageV1(123,null,'myError')->toObject()); } } \ No newline at end of file