<?php
declare(strict_types=1);
namespace frdlweb\Api\Rpc;
use frdlweb\Api\Rpc\DiscoverMethod;
use frdlweb\Api\Rpc\MethodDiscoverableInterface;
use frdlweb\Api\Rpc\SchemaLoader;
use LogicException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use TypeError;
use UMA\JsonRpc\Error;
use UMA\JsonRpc\Request;
use UMA\JsonRpc\Response;
use UMA\JsonRpc\Internal\Assert;
use UMA\JsonRpc\Internal\Input;
use UMA\JsonRpc\Internal\MiddlewareStack;
use UMA\JsonRpc\Internal\Validator;
use UMA\JsonRpc\Procedure;
use Opis\JsonSchema\Validator as OpisValidator;
class Server /* extends \UMA\JsonRpc\Server */
{
protected $config = [];
/**
* @var ContainerInterface
*/
protected $container;
/**
* @var string[]
*/
protected $methods;
/**
* @var string[]
*/
protected $middlewares;
/**
* @var int|null
*/
protected $batchLimit;
public function __construct(ContainerInterface $container, int $batchLimit = null, array $config = null, bool $discovery = true)
{
if(!is_array($config)){
$config = [];
}
$this->config = array_merge([
'schemaLoaderPrefix' => '',
'schemaLoaderDirs' => [],
// 'schemaCacheDir' => __DIR__.\DIRECTORY_SEPARATOR.'schema-store'.\DIRECTORY_SEPARATOR,
'schemaCacheDir' => sys_get_temp_dir() . \DIRECTORY_SEPARATOR . get_current_user(). \DIRECTORY_SEPARATOR . 'json-schema-store' . \DIRECTORY_SEPARATOR,
'discovery' => $discovery,
'meta' => [
'openrpc' => '1.0.0-rc1',
"info" => [
"title" => "JSON-RPC Server",
"description" =>"This the RPC-part of an Frdlweb API Server definition https://look-up.webfan3.de/?goto=oid%3A1.3.6.1.4.1.37553.8.1.8.1.13878",
"version" => "1.0.0",
],
'servers' => [
[
'name' => 'Webfan Homepagesystem RPC API',
'summary' => 'Webfan Homepagesystem RPC API methods description',
'description' => 'This is the RPC part of an implementation of the Frdlweb API Specification (1.3.6.1.4.1.37553.8.1.8.1.13878)',
'url' => 'https://'.$_SERVER['SERVER_NAME'].'/software-center/modules-api/rpc/0.0.2/',
]
],
'methods' => [],
'components' => [
'links' => [],
'contentDescriptors' => [],
'schemas' => [],
'examples' => [],
],
],
], $config);
/*
parent::__construct($container, $batchLimit);
*/
$this->container = $container;
$this->batchLimit = $batchLimit;
$this->methods = [];
$this->middlewares = [];
if(true === $this->config['discovery']){
/* $this->setDiscovery(DiscoverMethod::class, [$this, 'discoveryFactory']); */
$callable = [$this, 'discoveryFactory'];
$this->setDiscovery(DiscoverMethod::class,static function(ContainerInterface $c) use($callable){
return call_user_func_array($callable, func_get_args());
});
}
}
public function discoveryFactory(ContainerInterface $c) : MethodDiscoverableInterface{
$DiscoverMethod = new DiscoverMethod($this);
$DiscoverMethod->config(null, $this->config['meta']);
return $DiscoverMethod;
}
public function setDiscovery($serviceId, callable $factory){
if(!$this->getContainer()->has( $serviceId)){
if(
$this->container instanceof \Di\CompiledContainer
|| 'CompiledContainer' === basename(get_class($this->container))
) {
$this->getContainer()->set( $serviceId, call_user_func_array($factory, [$this->container]));
}elseif($factory instanceof \closure || 'ContainerBuilder' === basename(get_class($this->container)) ){
$this->getContainer()->set( $serviceId, $factory);
}else{
//$this->getContainer()->set( $serviceId, $factory);
$this->getContainer()->set( $serviceId, call_user_func_array($factory, [$this->container]));
}
}
$this->set('rpc.discover', $serviceId);
/**/
return $this;
}
public function getMethodDefinitions(){
return $this->methods;
}
public function getContainer():ContainerInterface{
return $this->container;
}
public function getConfig(){
return $this->config;
}
public function set(string $method, string $serviceId): Server
{
if (!$this->container->has($serviceId)) {
throw new LogicException("Cannot find service '$serviceId' in the container");
}
$this->methods[$method] = $serviceId;
return $this;
}
public function attach(string $serviceId): Server
{
if (!$this->container->has($serviceId)) {
throw new LogicException("Cannot find service '$serviceId' in the container");
}
$this->middlewares[$serviceId] = null;
return $this;
}
/**
* @throws TypeError
*/
public function run(string $raw): ?string
{
$input = Input::fromString($raw, true);
if (!$input->parsable()) {
return self::end(Error::parsing());
}
if ($input->isArray()) {
if ($this->tooManyBatchRequests($input)) {
return self::end(Error::tooManyBatchRequests($this->batchLimit));
}
return $this->batch($input);
}
return $this->single($input);
}
protected function batch(Input $input): ?string
{
\assert($input->isArray());
$responses = [];
foreach ($input->data() as $request) {
$pseudoInput = Input::fromSafeData($request);
if (null !== $response = $this->single($pseudoInput)) {
$responses[] = $response;
}
}
return empty($responses) ?
null : \sprintf('[%s]', \implode(',', $responses));
}
/**
* @throws TypeError
*/
protected function single(Input $input): ?string
{
if (!$input->isRpcRequest()) {
return self::end(Error::invalidRequest());
}
$request = new Request($input);
if (!\array_key_exists($request->method(), $this->methods)) {
return self::end(Error::unknownMethod($request->id()), $request);
}
try {
$procedure = Assert::isProcedure(
$this->container->get($this->methods[$request->method()])
);
} catch (ContainerExceptionInterface | NotFoundExceptionInterface $e) {
return self::end(Error::internal($request->id()), $request);
}
if (!Validator::validate($procedure->getSpec(), $request->params())) {
return self::end(Error::invalidParams($request->id()), $request);
}
$stack = MiddlewareStack::compose(
$procedure,
...\array_map(function(string $serviceId) {
return $this->container->get($serviceId);
}, \array_keys($this->middlewares))
);
return self::end($stack($request), $request, $procedure, $this);
}
protected function tooManyBatchRequests(Input $input): bool
{
\assert($input->isArray());
return \is_int($this->batchLimit) && $this->batchLimit < \count($input->data());
}
protected static function end(Response $response, Request $request = null, Procedure $procedure = null, Server $Server = null): ?string
{
if( $procedure && true !== $response instanceof Error && $procedure instanceof MethodDiscoverableInterface){
$spec = $procedure->getResultSpec();
$result = json_decode(json_encode($response));
if (!self::validateResponse($validation, $spec, $result->result, $Server)) {
$ea=$validation->getFirstError()->errorArgs();
return self::end(new Error($request->id(), 'Invalid result '.print_r($ea,true), $result->result), $request);
}
}
//$spec = $this->container->get($this->methods[$request->method()])->getResultSpec();
//if (true !== $response instanceof Error && !Validator::validate( $this->container->get($this->methods[$request->method()])->getResultSpec(), $request->params())) {
// return self::end(Error::invalidParams($request->id()), $request);
// }
return $request instanceof Request && null === $request->id() ?
null : \json_encode($response);
}
public static function validateResponse(&$validation = null, \stdClass $schema, $data, Server $Server = null): bool
{
\assert(false !== \json_encode($data));
if(null!==$Server){
$config =$Server->getConfig();
}else{
$config = [
'schemaLoaderPrefix' => 'https://json-schema.org',
'schemaLoaderDirs' => [],
'schemaCacheDir' =>sys_get_temp_dir() . \DIRECTORY_SEPARATOR . get_current_user(). \DIRECTORY_SEPARATOR . 'json-schema-store' . \DIRECTORY_SEPARATOR,
];
}
$validation = (new OpisValidator)
->setLoader(new SchemaLoader($config['schemaLoaderPrefix'],
$config['schemaLoaderDirs'],
$config['schemaCacheDir']))
->dataValidation($data, $schema);
return $validation->isValid();
}
}
|