Added scoping.

This commit is contained in:
flash 2024-08-16 15:14:44 +00:00
parent 25e0441c1c
commit d6819a29fe
10 changed files with 191 additions and 96 deletions

View file

@ -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
View 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;
}

View file

@ -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();

View file

@ -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);
} }
} }

View file

@ -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 {

View file

@ -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
View 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
View 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);
}
}

View file

@ -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']);
} }

View 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());
}
}