diff --git a/src/RpcActionAttribute.php b/src/RpcAction.php similarity index 89% rename from src/RpcActionAttribute.php rename to src/RpcAction.php index ab5e769..c1761dd 100644 --- a/src/RpcActionAttribute.php +++ b/src/RpcAction.php @@ -1,7 +1,7 @@ getMethods(); foreach($methodInfos as $methodInfo) { - $attrInfos = $methodInfo->getAttributes(RpcActionAttribute::class, ReflectionAttribute::IS_INSTANCEOF); + $attrInfos = $methodInfo->getAttributes(RpcAction::class, ReflectionAttribute::IS_INSTANCEOF); foreach($attrInfos as $attrInfo) { $handlerInfo = $attrInfo->newInstance(); diff --git a/src/RpcActionHandlerTrait.php b/src/RpcActionHandlerTrait.php index 21d870c..8d27ded 100644 --- a/src/RpcActionHandlerTrait.php +++ b/src/RpcActionHandlerTrait.php @@ -1,7 +1,7 @@ handler); } + + /** + * Invokes the handler and ensures required args are specified. + * + * @param array $args Arguments to invoke the handler with. + * @throws RuntimeException If any given data was invalid. + * @return mixed Result of the handler + */ + public function invokeHandler(array $args = []): mixed { + $handlerInfo = new ReflectionFunction($this->getHandler()); + if($handlerInfo->isVariadic()) + throw new RuntimeException('variadic'); + + $handlerArgs = []; + $handlerParams = $handlerInfo->getParameters(); + foreach($handlerParams as $paramInfo) { + $paramName = $paramInfo->getName(); + if(!array_key_exists($paramName, $args)) { + if($paramInfo->isOptional()) + continue; + + if(!$paramInfo->allowsNull()) + throw new RuntimeException('required'); + } + + $handlerArgs[$paramName] = $args[$paramName] ?? null; + } + + if(count($handlerArgs) !== count($args)) + throw new RuntimeException('params'); + + return $handlerInfo->invokeArgs($handlerArgs); + } } diff --git a/src/RpcProcedureAttribute.php b/src/RpcProcedure.php similarity index 77% rename from src/RpcProcedureAttribute.php rename to src/RpcProcedure.php index acc32cf..a3990a3 100644 --- a/src/RpcProcedureAttribute.php +++ b/src/RpcProcedure.php @@ -1,7 +1,7 @@ getHeaderLine('X-Aiwass-Verify'); if($userToken === '') { $response->setStatusCode(403); - return AiwassMsgPack::encode(['error' => 'aiwass:verify', 'message' => 'X-Aiwass-Verify header is missing.']); + return AiwassMsgPack::encode(['error' => 'aiwass:verify']); } $actInfo = $this->server->getActionInfo($action); if($actInfo === null) { $response->setStatusCode(404); - return AiwassMsgPack::encode(['error' => 'aiwass:unknown', 'message' => 'No action with that name exists.']); + return AiwassMsgPack::encode(['error' => 'aiwass:unknown']); } if($actInfo->isProcedure() !== $expectProcedure) { $response->setStatusCode(405); $response->setHeader('Allow', $actInfo->isProcedure() ? 'POST' : 'GET'); - return AiwassMsgPack::encode(['error' => 'aiwass:method', 'message' => 'This action cannot be called using this request method.']); + return AiwassMsgPack::encode(['error' => 'aiwass:method']); } if(!$this->server->getVerificationProvider()->verify($userToken, $expectProcedure, $action, $paramString)) - return AiwassMsgPack::encode(['error' => 'aiwass:verify', 'message' => 'Request token verification failed.']); + return AiwassMsgPack::encode(['error' => 'aiwass:verify']); - $handlerInfo = new ReflectionFunction($actInfo->getHandler()); - if($handlerInfo->isVariadic()) - return AiwassMsgPack::encode(['error' => 'aiwass:variadic', 'message' => 'Handler was declared as a variadic method and is thus impossible to be called.']); - - $handlerArgs = []; - - $handlerParams = $handlerInfo->getParameters(); - foreach($handlerParams as $paramInfo) { - $paramName = $paramInfo->getName(); - if(!array_key_exists($paramName, $params)) { - if($paramInfo->isOptional()) - continue; - - if(!$paramInfo->allowsNull()) - return AiwassMsgPack::encode(['error' => 'aiwass:param:required', 'message' => 'A required parameter is missing.', 'param' => $paramName]); - } - - $handlerArgs[$paramName] = $params[$paramName] ?? null; - } - - if(count($handlerArgs) !== count($params)) { + try { + return AiwassMsgPack::encode($actInfo->invokeHandler($params)); + } catch(RuntimeException $ex) { $response->setStatusCode(400); - return AiwassMsgPack::encode(['error' => 'aiwass:params', 'message' => 'Unsupported arguments were specified.']); + return AiwassMsgPack::encode(['error' => sprintf('aiwass:%s', $ex->getMessage())]); } - - return AiwassMsgPack::encode($handlerInfo->invokeArgs($handlerArgs)); } } diff --git a/tests/AttributesTest.php b/tests/AttributesTest.php new file mode 100644 index 0000000..2acf87b --- /dev/null +++ b/tests/AttributesTest.php @@ -0,0 +1,100 @@ +self->assertEquals('it is beans', $beans); + return 'query registered using action attribute'; + } + + #[RpcQuery('aiwass:test:query2')] + public static function staticQueryRegisteredUsingQueryAttribute(string $required, string $optional = 'the') { + self::$that->assertEquals('internet', $required); + self::$that->assertEquals('the', $optional); + return 'static query registered using query attribute'; + } + + #[RpcProcedure('aiwass:test:proc2')] + public function dynamicProcedureRegisteredUsingProcedureAttribute(string $optional = 'meow') { + $this->self->assertEquals('meow', $optional); + return 'dynamic procedure registered using procedure attribute'; + } + + #[RpcQuery('aiwass:test:query3')] + #[RpcProcedure('aiwass:test:proc3')] + public function multiple() { + return 'a dynamic method registered as both a query and a procedure'; + } + + public function hasNoAttr() { + return 'not an action handler'; + } + }; + + $server = RpcServer::createHmac(fn() => 'test'); + $server->register($handler); + + $this->assertNull($server->getActionInfo('aiwass:none')); + + $proc1 = $server->getActionInfo('aiwass:test:proc1'); + $this->assertTrue($proc1->isProcedure()); + $this->assertEquals('aiwass:test:proc1', $proc1->getName()); + $this->assertEquals('procedure registered using action attribute', $proc1->invokeHandler()); + + $proc2 = $server->getActionInfo('aiwass:test:proc2'); + $this->assertTrue($proc2->isProcedure()); + $this->assertEquals('aiwass:test:proc2', $proc2->getName()); + $this->assertEquals('dynamic procedure registered using procedure attribute', $proc2->invokeHandler()); + + $query1 = $server->getActionInfo('aiwass:test:query1'); + $this->assertFalse($query1->isProcedure()); + $this->assertEquals('aiwass:test:query1', $query1->getName()); + $this->assertEquals('query registered using action attribute', $query1->invokeHandler(['beans' => 'it is beans'])); + + $query2 = $server->getActionInfo('aiwass:test:query2'); + $this->assertFalse($query2->isProcedure()); + $this->assertEquals('aiwass:test:query2', $query2->getName()); + $this->assertEquals('static query registered using query attribute', $query2->invokeHandler(['required' => 'internet'])); + + $query3 = $server->getActionInfo('aiwass:test:query3'); + $proc3 = $server->getActionInfo('aiwass:test:proc3'); + $this->assertFalse($query3->isProcedure()); + $this->asserttrue($proc3->isProcedure()); + $this->assertEquals('aiwass:test:query3', $query3->getName()); + $this->assertEquals('aiwass:test:proc3', $proc3->getName()); + $this->assertEquals($query3->getHandler(), $proc3->getHandler()); + $this->assertEquals('a dynamic method registered as both a query and a procedure', $query3->invokeHandler()); + $this->assertEquals('a dynamic method registered as both a query and a procedure', $proc3->invokeHandler()); + + $this->expectException(RuntimeException::class); + $query1->invokeHandler(['notbeans' => 'it is not beans']); + } +}