Added scoping.
This commit is contained in:
parent
25e0441c1c
commit
d6819a29fe
10 changed files with 191 additions and 96 deletions
|
@ -1,18 +1,18 @@
|
||||||
<?php
|
<?php
|
||||||
// IRpcActionHandler.php
|
// IRpcActionHandler.php
|
||||||
// Created: 2024-08-15
|
// Created: 2024-08-15
|
||||||
// Updated: 2024-08-15
|
// Updated: 2024-08-16
|
||||||
|
|
||||||
namespace Aiwass;
|
namespace Aiwass;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the interface for RpcServer::register().
|
* Provides the interface for IRpcServer::register().
|
||||||
*/
|
*/
|
||||||
interface IRpcActionHandler {
|
interface IRpcActionHandler {
|
||||||
/**
|
/**
|
||||||
* Registers axctions on a given RpcServer instance.
|
* Registers axctions on a given IRpcServer instance.
|
||||||
*
|
*
|
||||||
* @param RpcServer $server Target RPC server.
|
* @param IRpcServer $server Target RPC server.
|
||||||
*/
|
*/
|
||||||
public function registerRpcActions(RpcServer $server): void;
|
public function registerRpcActions(IRpcServer $server): void;
|
||||||
}
|
}
|
||||||
|
|
68
src/IRpcServer.php
Normal file
68
src/IRpcServer.php
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
// IRpcServer.php
|
||||||
|
// Created: 2024-08-16
|
||||||
|
// Updated: 2024-08-16
|
||||||
|
|
||||||
|
namespace Aiwass;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a common interface for RPC servers.
|
||||||
|
*/
|
||||||
|
interface IRpcServer {
|
||||||
|
/**
|
||||||
|
* Creates a proxy for this RPC server with a specified namespace.
|
||||||
|
*
|
||||||
|
* @param string $prefix Prefix to apply to the scoped RPC server.
|
||||||
|
* @return IRpcServer A scoped RPC server instance.
|
||||||
|
*/
|
||||||
|
function scopeTo(string $prefix): IRpcServer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a handler class.
|
||||||
|
*
|
||||||
|
* @param IRpcActionHandler $handler Handler to register.
|
||||||
|
*/
|
||||||
|
function register(IRpcActionHandler $handler): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an action.
|
||||||
|
*
|
||||||
|
* @param bool $isProcedure true if the action is a procedure (HTTP POST), false if it is a query (HTTP GET).
|
||||||
|
* @param string $name Unique name of the action.
|
||||||
|
* @param callable $handler Handler for the action.
|
||||||
|
* @throws RuntimeException If a handler with the same name is already registered.
|
||||||
|
* @throws InvalidArgumentException If $handler is not a callable type.
|
||||||
|
*/
|
||||||
|
function registerAction(bool $isProcedure, string $name, $handler): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a query action (HTTP GET).
|
||||||
|
*
|
||||||
|
* @param string $name Unique name of the action.
|
||||||
|
* @param callable $handler Handler for the action.
|
||||||
|
* @throws RuntimeException If a handler with the same name is already registered.
|
||||||
|
* @throws InvalidArgumentException If $handler is not a callable type.
|
||||||
|
*/
|
||||||
|
function registerQueryAction(string $name, $handler): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a procedure action (HTTP POST).
|
||||||
|
*
|
||||||
|
* @param string $name Unique name of the action.
|
||||||
|
* @param callable $handler Handler for the action.
|
||||||
|
* @throws RuntimeException If a handler with the same name is already registered.
|
||||||
|
* @throws InvalidArgumentException If $handler is not a callable type.
|
||||||
|
*/
|
||||||
|
function registerProcedureAction(string $name, $handler): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves information about an action.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the action.
|
||||||
|
* @return ?RpcActionInfo An object containing information about the action, or null if it does not exist.
|
||||||
|
*/
|
||||||
|
function getActionInfo(string $name): ?RpcActionInfo;
|
||||||
|
}
|
|
@ -42,12 +42,12 @@ class RpcAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads attributes from methods in a IRpcActionHandler instance and registers them to a given RpcServer instance.
|
* Reads attributes from methods in a IRpcActionHandler instance and registers them to a given IRpcServer instance.
|
||||||
*
|
*
|
||||||
* @param RpcServer $server RPC server instance.
|
* @param IRpcServer $server RPC server instance.
|
||||||
* @param IRpcActionHandler $handler Handler instance.
|
* @param IRpcActionHandler $handler Handler instance.
|
||||||
*/
|
*/
|
||||||
public static function register(RpcServer $server, IRpcActionHandler $handler): void {
|
public static function register(IRpcServer $server, IRpcActionHandler $handler): void {
|
||||||
$objectInfo = new ReflectionObject($handler);
|
$objectInfo = new ReflectionObject($handler);
|
||||||
$methodInfos = $objectInfo->getMethods();
|
$methodInfos = $objectInfo->getMethods();
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ namespace Aiwass;
|
||||||
* For more advanced use, everything can be use'd separately and RpcActionAttribute::register called manually.
|
* For more advanced use, everything can be use'd separately and RpcActionAttribute::register called manually.
|
||||||
*/
|
*/
|
||||||
trait RpcActionHandlerTrait {
|
trait RpcActionHandlerTrait {
|
||||||
public function registerRpcActions(RpcServer $server): void {
|
public function registerRpcActions(IRpcServer $server): void {
|
||||||
RpcAction::register($server, $this);
|
RpcAction::register($server, $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,11 @@ use Index\Http\Routing\{HttpGet,HttpPost,RouteHandler};
|
||||||
*/
|
*/
|
||||||
class RpcRouteHandler extends RouteHandler {
|
class RpcRouteHandler extends RouteHandler {
|
||||||
/**
|
/**
|
||||||
* @param RpcServer $server An RPC server instance.
|
* @param IRpcServer $server An RPC server instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private RpcServer $server
|
private IVerificationProvider $verification,
|
||||||
|
private IRpcServer $server
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -73,7 +74,7 @@ class RpcRouteHandler extends RouteHandler {
|
||||||
return AiwassMsgPack::encode(['error' => 'aiwass:method']);
|
return AiwassMsgPack::encode(['error' => 'aiwass:method']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!$this->server->getVerificationProvider()->verify($userToken, $expectProcedure, $action, $paramString))
|
if(!$this->verification->verify($userToken, $expectProcedure, $action, $paramString))
|
||||||
return AiwassMsgPack::encode(['error' => 'aiwass:verify']);
|
return AiwassMsgPack::encode(['error' => 'aiwass:verify']);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// RpcServer.php
|
// RpcServer.php
|
||||||
// Created: 2024-08-13
|
// Created: 2024-08-13
|
||||||
// Updated: 2024-08-15
|
// Updated: 2024-08-16
|
||||||
|
|
||||||
namespace Aiwass;
|
namespace Aiwass;
|
||||||
|
|
||||||
|
@ -11,53 +11,16 @@ use RuntimeException;
|
||||||
/**
|
/**
|
||||||
* Implements an RPC server.
|
* Implements an RPC server.
|
||||||
*/
|
*/
|
||||||
class RpcServer {
|
class RpcServer implements IRpcServer {
|
||||||
|
use RpcServerTrait;
|
||||||
|
|
||||||
/** @var array<string, RpcActionInfo> */
|
/** @var array<string, RpcActionInfo> */
|
||||||
private array $actions = [];
|
private array $actions = [];
|
||||||
|
|
||||||
/**
|
public function scopeTo(string $prefix): IRpcServer {
|
||||||
* @param IVerificationProvider $verify A verification provider.
|
return new RpcServerScoped($this, $prefix);
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
private IVerificationProvider $verify
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves this verification provider for this server.
|
|
||||||
*
|
|
||||||
* @return IVerificationProvider Verification provider implementation.
|
|
||||||
*/
|
|
||||||
public function getVerificationProvider(): IVerificationProvider {
|
|
||||||
return $this->verify;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an RPC route handler.
|
|
||||||
*
|
|
||||||
* @return RpcRouteHandler RPC route handler.
|
|
||||||
*/
|
|
||||||
public function createRouteHandler(): RpcRouteHandler {
|
|
||||||
return new RpcRouteHandler($this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a handler class.
|
|
||||||
*
|
|
||||||
* @param IRpcActionHandler $handler Handler to register.
|
|
||||||
*/
|
|
||||||
public function register(IRpcActionHandler $handler): void {
|
|
||||||
$handler->registerRpcActions($this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers an action.
|
|
||||||
*
|
|
||||||
* @param bool $isProcedure true if the action is a procedure (HTTP POST), false if it is a query (HTTP GET).
|
|
||||||
* @param string $name Unique name of the action.
|
|
||||||
* @param callable $handler Handler for the action.
|
|
||||||
* @throws RuntimeException If a handler with the same name is already registered.
|
|
||||||
* @throws InvalidArgumentException If $handler is not a callable type.
|
|
||||||
*/
|
|
||||||
public function registerAction(bool $isProcedure, string $name, $handler): void {
|
public function registerAction(bool $isProcedure, string $name, $handler): void {
|
||||||
if(array_key_exists($name, $this->actions))
|
if(array_key_exists($name, $this->actions))
|
||||||
throw new RuntimeException('an action with that name has already been registered');
|
throw new RuntimeException('an action with that name has already been registered');
|
||||||
|
@ -65,47 +28,7 @@ class RpcServer {
|
||||||
$this->actions[$name] = new RpcActionInfo($isProcedure, $name, $handler);
|
$this->actions[$name] = new RpcActionInfo($isProcedure, $name, $handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a query action (HTTP GET).
|
|
||||||
*
|
|
||||||
* @param string $name Unique name of the action.
|
|
||||||
* @param callable $handler Handler for the action.
|
|
||||||
* @throws RuntimeException If a handler with the same name is already registered.
|
|
||||||
* @throws InvalidArgumentException If $handler is not a callable type.
|
|
||||||
*/
|
|
||||||
public function registerQueryAction(string $name, $handler): void {
|
|
||||||
$this->registerAction(false, $name, $handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a procedure action (HTTP POST).
|
|
||||||
*
|
|
||||||
* @param string $name Unique name of the action.
|
|
||||||
* @param callable $handler Handler for the action.
|
|
||||||
* @throws RuntimeException If a handler with the same name is already registered.
|
|
||||||
* @throws InvalidArgumentException If $handler is not a callable type.
|
|
||||||
*/
|
|
||||||
public function registerProcedureAction(string $name, $handler): void {
|
|
||||||
$this->registerAction(true, $name, $handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves information about an action.
|
|
||||||
*
|
|
||||||
* @param string $name Name of the action.
|
|
||||||
* @return ?RpcActionInfo An object containing information about the action, or null if it does not exist.
|
|
||||||
*/
|
|
||||||
public function getActionInfo(string $name): ?RpcActionInfo {
|
public function getActionInfo(string $name): ?RpcActionInfo {
|
||||||
return $this->actions[$name] ?? null;
|
return $this->actions[$name] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an RPC server with a Hmac verification provider.
|
|
||||||
*
|
|
||||||
* @param callable(): string $getSecretKey A method that returns the secret key to use.
|
|
||||||
* @return RpcServer The RPC server.
|
|
||||||
*/
|
|
||||||
public static function createHmac($getSecretKey): RpcServer {
|
|
||||||
return new RpcServer(new HmacVerificationProvider($getSecretKey));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
30
src/RpcServerScoped.php
Normal file
30
src/RpcServerScoped.php
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
// RpcServerScoped.php
|
||||||
|
// Created: 2024-08-16
|
||||||
|
// Updated: 2024-08-16
|
||||||
|
|
||||||
|
namespace Aiwass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a scoped RPC server implementation.
|
||||||
|
*/
|
||||||
|
class RpcServerScoped implements IRpcServer {
|
||||||
|
use RpcServerTrait;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private IRpcServer $base,
|
||||||
|
private string $prefix
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function scopeTo(string $prefix): IRpcServer {
|
||||||
|
return new RpcServerScoped($this->base, $this->prefix . $prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerAction(bool $isProcedure, string $name, $handler): void {
|
||||||
|
$this->base->registerAction($isProcedure, $this->prefix . $name, $handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getActionInfo(string $name): ?RpcActionInfo {
|
||||||
|
return $this->base->getActionInfo($this->prefix . $name);
|
||||||
|
}
|
||||||
|
}
|
23
src/RpcServerTrait.php
Normal file
23
src/RpcServerTrait.php
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
// RpcServerTrait.php
|
||||||
|
// Created: 2024-08-16
|
||||||
|
// Updated: 2024-08-16
|
||||||
|
|
||||||
|
namespace Aiwass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides implementations for IRpcServer methods that are likely going to be identical across implementations.
|
||||||
|
*/
|
||||||
|
trait RpcServerTrait {
|
||||||
|
public function register(IRpcActionHandler $handler): void {
|
||||||
|
$handler->registerRpcActions($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerQueryAction(string $name, $handler): void {
|
||||||
|
$this->registerAction(false, $name, $handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerProcedureAction(string $name, $handler): void {
|
||||||
|
$this->registerAction(true, $name, $handler);
|
||||||
|
}
|
||||||
|
}
|
|
@ -59,33 +59,39 @@ final class AttributesTest extends TestCase {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$server = RpcServer::createHmac(fn() => 'test');
|
$server = new RpcServer;
|
||||||
$server->register($handler);
|
$server->register($handler);
|
||||||
|
|
||||||
$this->assertNull($server->getActionInfo('aiwass:none'));
|
$this->assertNull($server->getActionInfo('aiwass:none'));
|
||||||
|
|
||||||
$proc1 = $server->getActionInfo('aiwass:test:proc1');
|
$proc1 = $server->getActionInfo('aiwass:test:proc1');
|
||||||
|
$this->assertNotNull($proc1);
|
||||||
$this->assertTrue($proc1->isProcedure());
|
$this->assertTrue($proc1->isProcedure());
|
||||||
$this->assertEquals('aiwass:test:proc1', $proc1->getName());
|
$this->assertEquals('aiwass:test:proc1', $proc1->getName());
|
||||||
$this->assertEquals('procedure registered using action attribute', $proc1->invokeHandler());
|
$this->assertEquals('procedure registered using action attribute', $proc1->invokeHandler());
|
||||||
|
|
||||||
$proc2 = $server->getActionInfo('aiwass:test:proc2');
|
$proc2 = $server->getActionInfo('aiwass:test:proc2');
|
||||||
|
$this->assertNotNull($proc2);
|
||||||
$this->assertTrue($proc2->isProcedure());
|
$this->assertTrue($proc2->isProcedure());
|
||||||
$this->assertEquals('aiwass:test:proc2', $proc2->getName());
|
$this->assertEquals('aiwass:test:proc2', $proc2->getName());
|
||||||
$this->assertEquals('dynamic procedure registered using procedure attribute', $proc2->invokeHandler());
|
$this->assertEquals('dynamic procedure registered using procedure attribute', $proc2->invokeHandler());
|
||||||
|
|
||||||
$query1 = $server->getActionInfo('aiwass:test:query1');
|
$query1 = $server->getActionInfo('aiwass:test:query1');
|
||||||
|
$this->assertNotNull($query1);
|
||||||
$this->assertFalse($query1->isProcedure());
|
$this->assertFalse($query1->isProcedure());
|
||||||
$this->assertEquals('aiwass:test:query1', $query1->getName());
|
$this->assertEquals('aiwass:test:query1', $query1->getName());
|
||||||
$this->assertEquals('query registered using action attribute', $query1->invokeHandler(['beans' => 'it is beans']));
|
$this->assertEquals('query registered using action attribute', $query1->invokeHandler(['beans' => 'it is beans']));
|
||||||
|
|
||||||
$query2 = $server->getActionInfo('aiwass:test:query2');
|
$query2 = $server->getActionInfo('aiwass:test:query2');
|
||||||
|
$this->assertNotNull($query2);
|
||||||
$this->assertFalse($query2->isProcedure());
|
$this->assertFalse($query2->isProcedure());
|
||||||
$this->assertEquals('aiwass:test:query2', $query2->getName());
|
$this->assertEquals('aiwass:test:query2', $query2->getName());
|
||||||
$this->assertEquals('static query registered using query attribute', $query2->invokeHandler(['required' => 'internet']));
|
$this->assertEquals('static query registered using query attribute', $query2->invokeHandler(['required' => 'internet']));
|
||||||
|
|
||||||
$query3 = $server->getActionInfo('aiwass:test:query3');
|
$query3 = $server->getActionInfo('aiwass:test:query3');
|
||||||
$proc3 = $server->getActionInfo('aiwass:test:proc3');
|
$proc3 = $server->getActionInfo('aiwass:test:proc3');
|
||||||
|
$this->assertNotNull($query3);
|
||||||
|
$this->assertNotNull($proc3);
|
||||||
$this->assertFalse($query3->isProcedure());
|
$this->assertFalse($query3->isProcedure());
|
||||||
$this->asserttrue($proc3->isProcedure());
|
$this->asserttrue($proc3->isProcedure());
|
||||||
$this->assertEquals('aiwass:test:query3', $query3->getName());
|
$this->assertEquals('aiwass:test:query3', $query3->getName());
|
||||||
|
@ -94,6 +100,8 @@ final class AttributesTest extends TestCase {
|
||||||
$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', $query3->invokeHandler());
|
||||||
$this->assertEquals('a dynamic method registered as both a query and a procedure', $proc3->invokeHandler());
|
$this->assertEquals('a dynamic method registered as both a query and a procedure', $proc3->invokeHandler());
|
||||||
|
|
||||||
|
$this->assertNull($server->getActionInfo('doesnotexist'));
|
||||||
|
|
||||||
$this->expectException(RuntimeException::class);
|
$this->expectException(RuntimeException::class);
|
||||||
$query1->invokeHandler(['notbeans' => 'it is not beans']);
|
$query1->invokeHandler(['notbeans' => 'it is not beans']);
|
||||||
}
|
}
|
||||||
|
|
42
tests/ScopedServerTest.php
Normal file
42
tests/ScopedServerTest.php
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
// ScopedServerTest.php
|
||||||
|
// Created: 2024-08-16
|
||||||
|
// Updated: 2024-08-16
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use PHPUnit\Framework\Attributes\{CoversClass,UsesClass};
|
||||||
|
use Aiwass\{RpcServer,RpcServerScoped};
|
||||||
|
|
||||||
|
#[CoversClass(RpcServerScoped::class)]
|
||||||
|
#[UsesClass(RpcServer::class)]
|
||||||
|
final class ScopedServerTest extends TestCase {
|
||||||
|
public function testServerScoping(): void {
|
||||||
|
$base = new RpcServer;
|
||||||
|
$base->registerQueryAction('test', fn() => 'test');
|
||||||
|
|
||||||
|
$scopedToBeans = $base->scopeTo('beans:');
|
||||||
|
$scopedToBeans->registerQueryAction('test', fn() => 'test in beans');
|
||||||
|
|
||||||
|
$scopedToGarf = $base->scopeTo('garf:');
|
||||||
|
$scopedToGarfield = $scopedToGarf->scopeTo('ield:');
|
||||||
|
$scopedToGarfield->registerQueryAction('test', fn() => 'test in garfield');
|
||||||
|
|
||||||
|
$baseTest = $base->getActionInfo('test');
|
||||||
|
$this->assertNotNull($baseTest);
|
||||||
|
$this->assertEquals('test', $baseTest->getName());
|
||||||
|
|
||||||
|
$baseBeansTest = $base->getActionInfo('beans:test');
|
||||||
|
$scopedBeansTest = $scopedToBeans->getActionInfo('test');
|
||||||
|
$this->assertNotNull($baseBeansTest);
|
||||||
|
$this->assertSame($baseBeansTest, $scopedBeansTest);
|
||||||
|
$this->assertEquals('beans:test', $scopedBeansTest->getName());
|
||||||
|
|
||||||
|
$baseGarfieldTest = $base->getActionInfo('garf:ield:test');
|
||||||
|
$scopedGarfieldTest = $scopedToGarfield->getActionInfo('test');
|
||||||
|
$this->assertNotNull($baseGarfieldTest);
|
||||||
|
$this->assertSame($baseGarfieldTest, $scopedGarfieldTest);
|
||||||
|
$this->assertEquals('garf:ield:test', $scopedGarfieldTest->getName());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue