Rename and a set of reworks.

- Aiwass -> RPCii
 - Swapped the meaning of "Procedure" and "Action"
 - IRpcActionHandler is now RpcHandler, abstract class no longer exists like Index
 - Anything suffixed by Trait is now suffixed by Common
 - All I prefixes on interfaces are gone
 - A PHP StreamWrapper implementation of HttpRequest added as a fallback for when cURL isnt installed
 - HttpRpcServer now hosts on /_rpcii instead of /_aiwass
 - Packagist package is now flashii/rpcii instead of flashwave/aiwass

That's probably all of it but I probably forgor.
This commit is contained in:
flash 2024-11-13 23:17:29 +00:00
parent cf6653ed46
commit 93ec139171
41 changed files with 782 additions and 641 deletions

View file

@ -1,29 +1,29 @@
# Aiwass
# RPCii (旧Aiwass)
Aiwass is an RPC client and server library.
RPCii is an RPC client and server library.
An Index compatible route handler is provided to make it really low effort to get a server going.
## Requirements and Dependencies
Aiwass currently targets **PHP 8.3** with the `msgpack` extension installed.
RPCii currently targets **PHP 8.3** with the `msgpack` extension installed.
## Versioning
Aiwass versioning will follows the [Semantic Versioning specification v2.0.0](https://semver.org/spec/v2.0.0.html).
RPCii versioning will follows the [Semantic Versioning specification v2.0.0](https://semver.org/spec/v2.0.0.html).
Changes to minimum required PHP version other major overhauls to Aiwass itself that break compatibility will be reasons for incrementing the major version.
Changes to minimum required PHP version other major overhauls to RPCii itself that break compatibility will be reasons for incrementing the major version.
Aiwass depends on Index, but its versioning depends on the minimum PHP version and should thus be fairly inconsequential.
RPCii depends on Index, but its versioning depends on the minimum PHP version and should thus be fairly inconsequential.
Previous major versions may be supported for a time with backports depending on what projects of mine still target older versions of PHP.
The version is stored in the root of the repository in a file called `VERSION` and can be read out within Aiwass using `Aiwass\Aiwass::version()`.
The version is stored in the root of the repository in a file called `VERSION` and can be read out within RPCii using `RPCii\RPCii::version()`.
## Contribution
By submitting code for inclusion in the main Aiwass source tree you agree to transfer ownership of the code to the project owner.
By submitting code for inclusion in the main RPCii source tree you agree to transfer ownership of the code to the project owner.
The contributor will still be attributed for the contributed code, unless they ask for this attribution to be removed.
This is to avoid intellectual property rights traps and drama that could lead to blackmail situations.
If you do not agree with these terms, you are free to fork off.
@ -31,4 +31,4 @@ If you do not agree with these terms, you are free to fork off.
## Licencing
Aiwass is available under the BSD 3-Clause Clear License, a full version of which is enclosed in the LICENCE file.
RPCii is available under the BSD 3-Clause Clear License, a full version of which is enclosed in the LICENCE file.

View file

@ -1 +1 @@
1.1.0
2.0.0

View file

@ -1,8 +1,8 @@
{
"name": "flashwave/aiwass",
"description": "Shared HTTP RPC client/server library.",
"name": "flashii/rpcii",
"description": "HTTP RPC client/server library.",
"type": "library",
"homepage": "https://railgun.sh/aiwass",
"homepage": "https://railgun.sh/rpcii",
"license": "bsd-3-clause-clear",
"require": {
"php": ">=8.3",
@ -10,8 +10,8 @@
"flashwave/index": "^0.2410"
},
"require-dev": {
"phpunit/phpunit": "^11.2",
"phpstan/phpstan": "^1.11"
"phpunit/phpunit": "^11.4",
"phpstan/phpstan": "^2.0"
},
"authors": [
{
@ -23,7 +23,7 @@
],
"autoload": {
"psr-4": {
"Aiwass\\": "src"
"RPCii\\": "src"
}
}
}

144
composer.lock generated
View file

@ -4,15 +4,15 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "8d01741caad2805272e619b4aa3c3c5e",
"content-hash": "f82764057cc610971b1823a40f95da29",
"packages": [
{
"name": "flashwave/index",
"version": "v0.2410.42339",
"version": "v0.2410.191603",
"source": {
"type": "git",
"url": "https://patchii.net/flash/index.git",
"reference": "d3e4d0985a1189d15fb8ed9eb105830c9dc38c4d"
"reference": "17cdb4d1c239241200d7e30968122a8cd8b26509"
},
"require": {
"ext-mbstring": "*",
@ -33,6 +33,14 @@
},
"type": "library",
"autoload": {
"files": [
"src/Cache/ArrayCache/_ndx.php",
"src/Cache/Memcached/_ndx.php",
"src/Cache/Valkey/_ndx.php",
"src/Db/MariaDb/_ndx.php",
"src/Db/NullDb/_ndx.php",
"src/Db/Sqlite/_ndx.php"
],
"psr-4": {
"Index\\": "src"
}
@ -51,7 +59,7 @@
],
"description": "Composer package for the common library for my projects.",
"homepage": "https://railgun.sh/index",
"time": "2024-10-04T23:39:32+00:00"
"time": "2024-10-19T16:04:17+00:00"
},
{
"name": "symfony/deprecation-contracts",
@ -122,16 +130,16 @@
},
{
"name": "symfony/mime",
"version": "v7.1.5",
"version": "v7.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff"
"reference": "caa1e521edb2650b8470918dfe51708c237f0598"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/711d2e167e8ce65b05aea6b258c449671cdd38ff",
"reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff",
"url": "https://api.github.com/repos/symfony/mime/zipball/caa1e521edb2650b8470918dfe51708c237f0598",
"reference": "caa1e521edb2650b8470918dfe51708c237f0598",
"shasum": ""
},
"require": {
@ -186,7 +194,7 @@
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v7.1.5"
"source": "https://github.com/symfony/mime/tree/v7.1.6"
},
"funding": [
{
@ -202,7 +210,7 @@
"type": "tidelift"
}
],
"time": "2024-09-20T08:28:38+00:00"
"time": "2024-10-25T15:11:02+00:00"
},
{
"name": "symfony/polyfill-ctype",
@ -673,16 +681,16 @@
},
{
"name": "twig/twig",
"version": "v3.14.0",
"version": "v3.14.2",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72"
"reference": "0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
"reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a",
"reference": "0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a",
"shasum": ""
},
"require": {
@ -736,7 +744,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.14.0"
"source": "https://github.com/twigphp/Twig/tree/v3.14.2"
},
"funding": [
{
@ -748,22 +756,22 @@
"type": "tidelift"
}
],
"time": "2024-09-09T17:55:12+00:00"
"time": "2024-11-07T12:36:22+00:00"
}
],
"packages-dev": [
{
"name": "myclabs/deep-copy",
"version": "1.12.0",
"version": "1.12.1",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c"
"reference": "123267b2c49fbf30d78a7b2d333f6be754b94845"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
"reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845",
"reference": "123267b2c49fbf30d78a7b2d333f6be754b94845",
"shasum": ""
},
"require": {
@ -802,7 +810,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.12.0"
"source": "https://github.com/myclabs/DeepCopy/tree/1.12.1"
},
"funding": [
{
@ -810,20 +818,20 @@
"type": "tidelift"
}
],
"time": "2024-06-12T14:39:25+00:00"
"time": "2024-11-08T17:47:46+00:00"
},
{
"name": "nikic/php-parser",
"version": "v5.3.0",
"version": "v5.3.1",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a"
"reference": "8eea230464783aa9671db8eea6f8c6ac5285794b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3abf7425cd284141dc5d8d14a9ee444de3345d1a",
"reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b",
"reference": "8eea230464783aa9671db8eea6f8c6ac5285794b",
"shasum": ""
},
"require": {
@ -866,9 +874,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.3.0"
"source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1"
},
"time": "2024-09-29T13:56:26+00:00"
"time": "2024-10-08T18:51:32+00:00"
},
{
"name": "phar-io/manifest",
@ -990,20 +998,20 @@
},
{
"name": "phpstan/phpstan",
"version": "1.12.5",
"version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17"
"reference": "ab4e9b4415a5fc9e4d27f7fe16c8bc9d067dcd6d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17",
"reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/ab4e9b4415a5fc9e4d27f7fe16c8bc9d067dcd6d",
"reference": "ab4e9b4415a5fc9e4d27f7fe16c8bc9d067dcd6d",
"shasum": ""
},
"require": {
"php": "^7.2|^8.0"
"php": "^7.4|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
@ -1044,39 +1052,39 @@
"type": "github"
}
],
"time": "2024-09-26T12:45:22+00:00"
"time": "2024-11-11T15:43:04+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "11.0.6",
"version": "11.0.7",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "ebdffc9e09585dafa71b9bffcdb0a229d4704c45"
"reference": "f7f08030e8811582cc459871d28d6f5a1a4d35ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ebdffc9e09585dafa71b9bffcdb0a229d4704c45",
"reference": "ebdffc9e09585dafa71b9bffcdb0a229d4704c45",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f7f08030e8811582cc459871d28d6f5a1a4d35ca",
"reference": "f7f08030e8811582cc459871d28d6f5a1a4d35ca",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
"nikic/php-parser": "^5.1.0",
"nikic/php-parser": "^5.3.1",
"php": ">=8.2",
"phpunit/php-file-iterator": "^5.0.1",
"phpunit/php-file-iterator": "^5.1.0",
"phpunit/php-text-template": "^4.0.1",
"sebastian/code-unit-reverse-lookup": "^4.0.1",
"sebastian/complexity": "^4.0.1",
"sebastian/environment": "^7.2.0",
"sebastian/lines-of-code": "^3.0.1",
"sebastian/version": "^5.0.1",
"sebastian/version": "^5.0.2",
"theseer/tokenizer": "^1.2.3"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
"phpunit/phpunit": "^11.4.1"
},
"suggest": {
"ext-pcov": "PHP extension that provides line coverage",
@ -1114,7 +1122,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.6"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.7"
},
"funding": [
{
@ -1122,7 +1130,7 @@
"type": "github"
}
],
"time": "2024-08-22T04:37:56+00:00"
"time": "2024-10-09T06:21:38+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -1371,16 +1379,16 @@
},
{
"name": "phpunit/phpunit",
"version": "11.3.6",
"version": "11.4.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b"
"reference": "e8e8ed1854de5d36c088ec1833beae40d2dedd76"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d62c45a19c665bb872c2a47023a0baf41a98bb2b",
"reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e8e8ed1854de5d36c088ec1833beae40d2dedd76",
"reference": "e8e8ed1854de5d36c088ec1833beae40d2dedd76",
"shasum": ""
},
"require": {
@ -1394,21 +1402,21 @@
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.2",
"phpunit/php-code-coverage": "^11.0.6",
"phpunit/php-code-coverage": "^11.0.7",
"phpunit/php-file-iterator": "^5.1.0",
"phpunit/php-invoker": "^5.0.1",
"phpunit/php-text-template": "^4.0.1",
"phpunit/php-timer": "^7.0.1",
"sebastian/cli-parser": "^3.0.2",
"sebastian/code-unit": "^3.0.1",
"sebastian/comparator": "^6.1.0",
"sebastian/comparator": "^6.1.1",
"sebastian/diff": "^6.0.2",
"sebastian/environment": "^7.2.0",
"sebastian/exporter": "^6.1.3",
"sebastian/global-state": "^7.0.2",
"sebastian/object-enumerator": "^6.0.1",
"sebastian/type": "^5.1.0",
"sebastian/version": "^5.0.1"
"sebastian/version": "^5.0.2"
},
"suggest": {
"ext-soap": "To be able to generate mocks based on WSDL files"
@ -1419,7 +1427,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "11.3-dev"
"dev-main": "11.4-dev"
}
},
"autoload": {
@ -1451,7 +1459,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.3.6"
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.4.3"
},
"funding": [
{
@ -1467,7 +1475,7 @@
"type": "tidelift"
}
],
"time": "2024-09-19T10:54:28+00:00"
"time": "2024-10-28T13:07:50+00:00"
},
{
"name": "sebastian/cli-parser",
@ -1641,16 +1649,16 @@
},
{
"name": "sebastian/comparator",
"version": "6.1.0",
"version": "6.2.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "fa37b9e2ca618cb051d71b60120952ee8ca8b03d"
"reference": "43d129d6a0f81c78bee378b46688293eb7ea3739"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa37b9e2ca618cb051d71b60120952ee8ca8b03d",
"reference": "fa37b9e2ca618cb051d71b60120952ee8ca8b03d",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/43d129d6a0f81c78bee378b46688293eb7ea3739",
"reference": "43d129d6a0f81c78bee378b46688293eb7ea3739",
"shasum": ""
},
"require": {
@ -1661,12 +1669,12 @@
"sebastian/exporter": "^6.0"
},
"require-dev": {
"phpunit/phpunit": "^11.3"
"phpunit/phpunit": "^11.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "6.1-dev"
"dev-main": "6.2-dev"
}
},
"autoload": {
@ -1706,7 +1714,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
"source": "https://github.com/sebastianbergmann/comparator/tree/6.1.0"
"source": "https://github.com/sebastianbergmann/comparator/tree/6.2.1"
},
"funding": [
{
@ -1714,7 +1722,7 @@
"type": "github"
}
],
"time": "2024-09-11T15:42:56+00:00"
"time": "2024-10-31T05:30:08+00:00"
},
{
"name": "sebastian/complexity",
@ -2340,16 +2348,16 @@
},
{
"name": "sebastian/version",
"version": "5.0.1",
"version": "5.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/version.git",
"reference": "45c9debb7d039ce9b97de2f749c2cf5832a06ac4"
"reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/version/zipball/45c9debb7d039ce9b97de2f749c2cf5832a06ac4",
"reference": "45c9debb7d039ce9b97de2f749c2cf5832a06ac4",
"url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874",
"reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874",
"shasum": ""
},
"require": {
@ -2382,7 +2390,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/version/issues",
"security": "https://github.com/sebastianbergmann/version/security/policy",
"source": "https://github.com/sebastianbergmann/version/tree/5.0.1"
"source": "https://github.com/sebastianbergmann/version/tree/5.0.2"
},
"funding": [
{
@ -2390,7 +2398,7 @@
"type": "github"
}
],
"time": "2024-07-03T05:13:08+00:00"
"time": "2024-10-09T05:16:32+00:00"
},
{
"name": "theseer/tokenizer",

View file

@ -3,17 +3,17 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://www.phpdoc.org"
xsi:noNamespaceSchemaLocation="data/xsd/phpdoc.xsd">
<title>Aiwass Documentation</title>
<title>RPCii Documentation</title>
<template name="default"/>
<paths>
<output>docs/html</output>
</paths>
<version number="0.1">
<version number="2.0">
<folder>latest</folder>
<api format="php">
<output>api</output>
<visibility>public</visibility>
<default-package-name>Aiwass</default-package-name>
<default-package-name>RPCii</default-package-name>
<source dsn=".">
<path>src</path>
</source>

View file

@ -3,5 +3,7 @@ parameters:
checkUninitializedProperties: true
checkImplicitMixed: true
checkBenevolentUnionTypes: true
treatPhpDocTypesAsCertain: false
paths:
- src
- tests

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.2/phpunit.xsd"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.4/phpunit.xsd"
colors="true"
executionOrder="depends,defects"
beStrictAboutOutputDuringTests="true"

View file

@ -1,9 +1,9 @@
<?php
// CurlHttpRequest.php
// Created: 2024-08-13
// Updated: 2024-10-05
// Updated: 2024-11-13
namespace Aiwass\Client;
namespace RPCii\Client;
use CurlHandle;
use RuntimeException;
@ -11,7 +11,7 @@ use RuntimeException;
/**
* Provides a cURL based HTTP request implementation.
*/
class CurlHttpRequest implements IHttpRequest {
class CurlHttpRequest implements HttpRequest {
private CurlHandle $handle;
private bool $isPost = false;
private ?string $url = null;
@ -36,15 +36,11 @@ class CurlHttpRequest implements IHttpRequest {
CURLOPT_MAXREDIRS => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_USERAGENT => 'Aiwass',
CURLOPT_USERAGENT => 'RPCii',
]);
}
public function __destruct() {
$this->close();
}
public function close(): void {
curl_close($this->handle);
}
@ -87,7 +83,6 @@ class CurlHttpRequest implements IHttpRequest {
if($response === false)
throw new RuntimeException(curl_error($this->handle));
$this->close();
if($response === true)
return '';

View file

@ -1,17 +1,16 @@
<?php
// IHttpRequest.php
// HttpRequest.php
// Created: 2024-08-13
// Updated: 2024-10-05
// Updated: 2024-11-13
namespace Aiwass\Client;
namespace RPCii\Client;
use RuntimeException;
use Index\Closeable;
/**
* Provides a common interface for making HTTP requests.
*/
interface IHttpRequest extends Closeable {
interface HttpRequest {
/**
* Sets whether this is a POST request or a GET request.
*

View file

@ -0,0 +1,73 @@
<?php
// HttpRpcClient.php
// Created: 2024-08-13
// Updated: 2024-11-13
namespace RPCii\Client;
use InvalidArgumentException;
use RPCii\{RpciiMsgPack,HmacVerificationProvider,VerificationProvider};
/**
* Implements an RPC client.
*/
class HttpRpcClient implements RpcClient {
/**
* @param string $url Base RPC url, up to the /_rpcii part.
* @param VerificationProvider $verify A verification provider.
* @param callable(): HttpRequest $createRequest Creates a HTTP request.
*/
public function __construct(
private string $url,
private VerificationProvider $verify,
private $createRequest
) {
if(!is_callable($createRequest))
throw new InvalidArgumentException('$createRequest must be a callable');
}
public function scopeTo(string $prefix): RpcClient {
return new ScopedRpcClient($this, $prefix);
}
/** @param array<string, mixed> $params */
private function callProcedure(bool $isAction, string $action, array $params): mixed {
$params = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
$request = ($this->createRequest)();
$request->setPost($isAction);
$request->setUrl(sprintf('%s/_rpcii/%s', $this->url, $action));
$request->setHeader('X-RPCii-Verify', $this->verify->sign($isAction, $action, $params));
$request->setParams($params);
return RpciiMsgPack::decode($request->execute());
}
public function query(string $action, array $params): mixed {
return $this->callProcedure(false, $action, $params);
}
public function action(string $action, array $params): mixed {
return $this->callProcedure(true, $action, $params);
}
/**
* Creates an RPC client with HMAC verification and a suitable HTTP request implementation.
*
* @param string $url Base RPC url, up to the /_rpcii part.
* @param callable(): string $getSecretKey A method that returns the secret key to use.
* @return HttpRpcClient An HTTP RPC client!
*/
public static function createHmac(string $url, $getSecretKey): HttpRpcClient {
if(extension_loaded('curl'))
$createRequest = fn() => new CurlHttpRequest;
else
$createRequest = fn() => new StreamHttpRequest;
return new HttpRpcClient(
$url,
new HmacVerificationProvider($getSecretKey),
$createRequest
);
}
}

View file

@ -1,37 +0,0 @@
<?php
// IRpcClient.php
// Created: 2024-08-16
// Updated: 2024-08-16
namespace Aiwass\Client;
/**
* Provides a common interface for RPC clients.
*/
interface IRpcClient {
/**
* Creates a proxy for this RPC client with a specified namespace.
*
* @param string $prefix Prefix to apply to the scoped RPC client.
* @return IRpcClient A scoped RPC client instance.
*/
function scopeTo(string $prefix): IRpcClient;
/**
* Makes an RPC query.
*
* @param string $action Name of the action.
* @param array<string, mixed> $params Parameters to query with.
* @return mixed Result of the query.
*/
function query(string $action, array $params): mixed;
/**
* Makes an RPC procedure call.
*
* @param string $action Name of the action.
* @param array<string, mixed> $params Parameters to query with.
* @return mixed Result of the procedure call.
*/
function procedure(string $action, array $params): mixed;
}

View file

@ -1,68 +1,37 @@
<?php
// RpcClient.php
// Created: 2024-08-13
// Updated: 2024-08-16
// Created: 2024-08-16
// Updated: 2024-11-13
namespace Aiwass\Client;
use InvalidArgumentException;
use Aiwass\{AiwassMsgPack,HmacVerificationProvider,IVerificationProvider};
namespace RPCii\Client;
/**
* Implemens an RPC client.
* Provides a common interface for RPC clients.
*/
class RpcClient implements IRpcClient {
interface RpcClient {
/**
* @param string $url Base RPC url, up to the /_aiwass part.
* @param IVerificationProvider $verify A verification provider.
* @param callable(): IHttpRequest $createRequest Creates a HTTP request.
*/
public function __construct(
private string $url,
private IVerificationProvider $verify,
private $createRequest
) {
if(!is_callable($createRequest))
throw new InvalidArgumentException('$createRequest must be a callable');
}
public function scopeTo(string $prefix): IRpcClient {
return new RpcClientScoped($this, $prefix);
}
/** @param array<string, mixed> $params */
private function callAction(bool $isProcedure, string $action, array $params): mixed {
$params = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
$request = ($this->createRequest)();
$request->setPost($isProcedure);
$request->setUrl(sprintf('%s/_aiwass/%s', $this->url, $action));
$request->setHeader('X-Aiwass-Verify', $this->verify->sign($isProcedure, $action, $params));
$request->setParams($params);
return AiwassMsgPack::decode($request->execute());
}
public function query(string $action, array $params): mixed {
return $this->callAction(false, $action, $params);
}
public function procedure(string $action, array $params): mixed {
return $this->callAction(true, $action, $params);
}
/**
* Creates an RPC client with HMAC verification and a suitable HTTP request implementation.
* Creates a proxy for this RPC client with a specified namespace.
*
* @param string $url Base RPC url, up to the /_aiwass part.
* @param callable(): string $getSecretKey A method that returns the secret key to use.
* @return RpcClient An RPC client!
* @param string $prefix Prefix to apply to the scoped RPC client.
* @return RpcClient A scoped RPC client instance.
*/
public static function createHmac(string $url, $getSecretKey): RpcClient {
return new RpcClient(
$url,
new HmacVerificationProvider($getSecretKey),
fn() => new CurlHttpRequest,
);
}
function scopeTo(string $prefix): RpcClient;
/**
* Makes an RPC query.
*
* @param string $procedure Name of the procedure.
* @param array<string, mixed> $params Parameters to query with.
* @return mixed Result of the query.
*/
function query(string $procedure, array $params): mixed;
/**
* Makes an RPC action call.
*
* @param string $procedure Name of the procedure.
* @param array<string, mixed> $params Parameters to query with.
* @return mixed Result of the procedure call.
*/
function action(string $procedure, array $params): mixed;
}

View file

@ -1,32 +0,0 @@
<?php
// RpcClientScoped.php
// Created: 2024-08-16
// Updated: 2024-08-16
namespace Aiwass\Client;
/**
* Provides a scoped interface to an underlying RPC client.
*/
class RpcClientScoped implements IRpcClient {
/**
* @param IRpcClient $base RPC client instance to use as a base.
* @param string $prefix Prefix to apply to action names passed.
*/
public function __construct(
private IRpcClient $base,
private string $prefix
) {}
public function scopeTo(string $prefix): IRpcClient {
return $this->base->scopeTo($this->prefix . $prefix);
}
public function query(string $action, array $params): mixed {
return $this->base->query($this->prefix . $action, $params);
}
public function procedure(string $action, array $params): mixed {
return $this->base->procedure($this->prefix . $action, $params);
}
}

View file

@ -0,0 +1,32 @@
<?php
// ScopedRpcClient.php
// Created: 2024-08-16
// Updated: 2024-11-13
namespace RPCii\Client;
/**
* Provides a scoped interface to an underlying RPC client.
*/
class ScopedRpcClient implements RpcClient {
/**
* @param RpcClient $base RPC client instance to use as a base.
* @param string $prefix Prefix to apply to procedure names passed.
*/
public function __construct(
private RpcClient $base,
private string $prefix
) {}
public function scopeTo(string $prefix): RpcClient {
return $this->base->scopeTo($this->prefix . $prefix);
}
public function query(string $procedure, array $params): mixed {
return $this->base->query($this->prefix . $procedure, $params);
}
public function action(string $procedure, array $params): mixed {
return $this->base->action($this->prefix . $procedure, $params);
}
}

View file

@ -0,0 +1,75 @@
<?php
// StreamHttpRequest.php
// Created: 2024-11-13
// Updated: 2024-11-13
namespace RPCii\Client;
use RuntimeException;
/**
* Provides a PHP Stream based HTTP request implementation.
*/
class StreamHttpRequest implements HttpRequest {
private bool $isPost = false;
private ?string $url = null;
/** @var string[] */
private array $headers = [];
private string $paramString = '';
public function setPost(bool $state): void {
$this->isPost = $state;
}
public function setUrl(string $url): void {
$this->url = $url;
}
public function setHeader(string $name, string $value): void {
$this->headers[] = sprintf('%s: %s', $name, $value);
}
public function setParams(string $paramString): void {
$this->paramString = $paramString;
}
public function execute(): string {
$url = $this->url;
if($url === null)
throw new RuntimeException('no target url specified');
$ctx = [];
if($this->isPost) {
$ctx['method'] = 'POST';
$this->setHeader('Content-Type', 'application/x-www-form-urlencoded');
$ctx['content'] = $this->paramString;
} else {
$ctx['method'] = 'GET';
$url .= str_contains($url, '?') ? '&' : '?';
$url .= $this->paramString;
}
$ctx['header'] = implode("\r\n", $this->headers);
$ctx = stream_context_create(['http' => $ctx]);
$handle = fopen($url, 'rb', context: $ctx);
if($handle === false)
throw new RuntimeException('failed to create HTTP request');
$response = stream_get_contents($handle);
if($response === false)
throw new RuntimeException('failed to read response body');
return $response;
}
/**
* Creates a new Stream HTTP Request instance.
*
* @return StreamHttpRequest File HTTP Request.
*/
public static function create(): StreamHttpRequest {
return new StreamHttpRequest;
}
}

View file

@ -1,9 +1,9 @@
<?php
// HmacVerificationProvider.php
// Created: 2024-08-13
// Updated: 2024-08-13
// Updated: 2024-11-13
namespace Aiwass;
namespace RPCii;
use InvalidArgumentException;
use Index\UriBase64;
@ -11,7 +11,7 @@ use Index\UriBase64;
/**
* Implements an HMAC verification provider.
*/
class HmacVerificationProvider implements IVerificationProvider {
class HmacVerificationProvider implements VerificationProvider {
/**
* Default list of allowed algorithms.
*
@ -49,15 +49,15 @@ class HmacVerificationProvider implements IVerificationProvider {
throw new InvalidArgumentException('$timeValidPlusMinusSeconds must be greater than zero');
}
public function sign(bool $isProcedure, string $action, string $paramString): string {
return $this->signWith($this->algos[0], $isProcedure, $action, $paramString);
public function sign(bool $isAction, string $action, string $paramString): string {
return $this->signWith($this->algos[0], $isAction, $action, $paramString);
}
private function signWith(string $algo, bool $isProcedure, string $action, string $paramString): string {
private function signWith(string $algo, bool $isAction, string $action, string $paramString): string {
$time = time();
$hash = $this->createHash($algo, $time, $isProcedure, $action, $paramString);
$hash = $this->createHash($algo, $time, $isAction, $action, $paramString);
return UriBase64::encode(AiwassMsgPack::encode([
return UriBase64::encode(RpciiMsgPack::encode([
'a' => $algo,
't' => $time,
'h' => $hash,
@ -67,14 +67,14 @@ class HmacVerificationProvider implements IVerificationProvider {
private function createHash(
string $algo,
int $timeStamp,
bool $isProcedure,
bool $isAction,
string $action,
string $paramString
): string {
$data = sprintf(
'^<=>%d<=>%s<=>%s<=>%s<=>$',
$timeStamp,
$isProcedure ? 'p' : 'q',
$isAction ? 'a' : 'q',
$action,
$paramString
);
@ -92,12 +92,12 @@ class HmacVerificationProvider implements IVerificationProvider {
return in_array($algo, $this->algos);
}
public function verify(string $userToken, bool $isProcedure, string $action, string $paramString): bool {
public function verify(string $userToken, bool $isAction, string $action, string $paramString): bool {
$userToken = UriBase64::decode($userToken);
if($userToken === false)
return false;
$userTokenInfo = AiwassMsgPack::decode($userToken);
$userTokenInfo = RpciiMsgPack::decode($userToken);
if(!is_array($userTokenInfo))
return false;
@ -114,7 +114,7 @@ class HmacVerificationProvider implements IVerificationProvider {
if(!is_string($userHash))
return false;
$realHash = $this->createHash($userAlgo, $userTime, $isProcedure, $action, $paramString);
$realHash = $this->createHash($userAlgo, $userTime, $isAction, $action, $paramString);
return hash_equals($realHash, $userHash);
}
}

View file

@ -1,22 +1,22 @@
<?php
// Aiwass.php
// RPCii.php
// Created: 2024-08-13
// Updated: 2024-08-13
// Updated: 2024-11-13
namespace Aiwass;
namespace RPCii;
/**
* Provides information about the Aiwass library.
* Provides information about the RPCii library.
*/
final class Aiwass {
final class RPCii {
public const PATH_SOURCE = __DIR__;
public const PATH_ROOT = self::PATH_SOURCE . DIRECTORY_SEPARATOR . '..';
public const PATH_VERSION = self::PATH_ROOT . DIRECTORY_SEPARATOR . 'VERSION';
/**
* Gets the current version of the Aiwass library.
* Gets the current version of the RPCii library.
*
* Reads the VERSION file in the root of the Aiwass directory.
* Reads the VERSION file in the root of the RPCii directory.
* Returns 0.0.0 if reading the file failed for any reason.
*
* @return string Current version string.

View file

@ -1,9 +1,9 @@
<?php
// AiwassMsgPack.php
// RpciiMsgPack.php
// Created: 2024-08-13
// Updated: 2024-08-13
// Updated: 2024-11-13
namespace Aiwass;
namespace RPCii;
use MessagePack;
@ -12,7 +12,7 @@ use MessagePack;
*
* @internal
*/
final class AiwassMsgPack {
final class RpciiMsgPack {
private static MessagePack $msgpack;
public static function encode(mixed $value): string {
@ -29,4 +29,4 @@ final class AiwassMsgPack {
}
}
AiwassMsgPack::init();
RpciiMsgPack::init();

View file

@ -0,0 +1,45 @@
<?php
// HttpRpcServer.php
// Created: 2024-08-13
// Updated: 2024-11-13
namespace RPCii\Server;
use InvalidArgumentException;
use RuntimeException;
use RPCii\VerificationProvider;
/**
* Implements an RPC server.
*/
class HttpRpcServer implements RpcServer {
use RpcServerCommon;
/** @var array<string, RpcProcedureInfo> */
private array $procedures = [];
/**
* Creates an RPC route handler.
*
* @param VerificationProvider $verification A verification provider.
* @return RpcRouteHandler RPC route handler.
*/
public function createRouteHandler(VerificationProvider $verification): RpcRouteHandler {
return new RpcRouteHandler($this, $verification);
}
public function scopeTo(string $prefix): RpcServer {
return new ScopedRpcServer($this, $prefix);
}
public function registerProcedure(bool $isAction, string $name, $handler): void {
if(array_key_exists($name, $this->procedures))
throw new RuntimeException('an procedure with that name has already been registered');
$this->procedures[$name] = new RpcProcedureInfo($isAction, $name, $handler);
}
public function getProcedureInfo(string $name): ?RpcProcedureInfo {
return $this->procedures[$name] ?? null;
}
}

View file

@ -1,18 +0,0 @@
<?php
// IRpcActionHandler.php
// Created: 2024-08-15
// Updated: 2024-08-16
namespace Aiwass\Server;
/**
* Provides the interface for IRpcServer::register().
*/
interface IRpcActionHandler {
/**
* Registers axctions on a given IRpcServer instance.
*
* @param IRpcServer $server Target RPC server.
*/
public function registerRpcActions(IRpcServer $server): void;
}

View file

@ -1,68 +0,0 @@
<?php
// IRpcServer.php
// Created: 2024-08-16
// Updated: 2024-08-16
namespace Aiwass\Server;
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

@ -1,67 +1,21 @@
<?php
// RpcAction.php
// Created: 2024-08-15
// Updated: 2024-08-16
// Updated: 2024-11-13
namespace Aiwass\Server;
namespace RPCii\Server;
use Attribute;
use ReflectionAttribute;
use ReflectionObject;
/**
* Provides base for attributes that mark methods in a class as RPC action handlers.
* Provides an attribute for marking methods in a class as an RPC Action procedure.
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class RpcAction {
class RpcAction extends RpcProcedure {
/**
* @param bool $isProcedure true if the action is a procedure, false if query.
* @param string $name Action name.
* @param string $name Procedure name.
*/
public function __construct(
private bool $isProcedure,
private string $name
) {}
/**
* Returns whether the action is a procedure.
*
* @return bool
*/
public function isProcedure(): bool {
return $this->isProcedure;
}
/**
* Returns the action name.
*
* @return string
*/
public function getName(): string {
return $this->name;
}
/**
* Reads attributes from methods in a IRpcActionHandler instance and registers them to a given IRpcServer instance.
*
* @param IRpcServer $server RPC server instance.
* @param IRpcActionHandler $handler Handler instance.
*/
public static function register(IRpcServer $server, IRpcActionHandler $handler): void {
$objectInfo = new ReflectionObject($handler);
$methodInfos = $objectInfo->getMethods();
foreach($methodInfos as $methodInfo) {
$attrInfos = $methodInfo->getAttributes(RpcAction::class, ReflectionAttribute::IS_INSTANCEOF);
foreach($attrInfos as $attrInfo) {
$handlerInfo = $attrInfo->newInstance();
$server->registerAction(
$handlerInfo->isProcedure(),
$handlerInfo->getName(),
$methodInfo->getClosure($methodInfo->isStatic() ? null : $handler)
);
}
}
public function __construct(string $name) {
parent::__construct(true, $name);
}
}

View file

@ -1,14 +0,0 @@
<?php
// RpcActionHandler.php
// Created: 2024-08-15
// Updated: 2024-08-16
namespace Aiwass\Server;
/**
* Provides an abstract class version of IRpcActionHandler that already includes the trait as well,
* letting you only have to use one use statement rather than two!
*/
abstract class RpcActionHandler implements IRpcActionHandler {
use RpcActionHandlerTrait;
}

View file

@ -1,16 +0,0 @@
<?php
// RpcActionHandlerTrait.php
// Created: 2024-08-15
// Updated: 2024-08-16
namespace Aiwass\Server;
/**
* Provides an implementation of IRpcActionHandler::registerRpcActions that uses the attributes.
* For more advanced use, everything can be use'd separately and RpcActionAttribute::register called manually.
*/
trait RpcActionHandlerTrait {
public function registerRpcActions(IRpcServer $server): void {
RpcAction::register($server, $this);
}
}

18
src/Server/RpcHandler.php Normal file
View file

@ -0,0 +1,18 @@
<?php
// RpcHandler.php
// Created: 2024-08-15
// Updated: 2024-11-13
namespace RPCii\Server;
/**
* Provides the interface for RpcServer::register().
*/
interface RpcHandler {
/**
* Registers procedures to a given RpcServer instance.
*
* @param RpcServer $server Target RPC server.
*/
function registerProcedures(RpcServer $server): void;
}

View file

@ -0,0 +1,16 @@
<?php
// RpcHandlerCommon.php
// Created: 2024-08-15
// Updated: 2024-11-13
namespace RPCii\Server;
/**
* Provides an implementation of RpcHandler::registerProcedures that uses the attributes.
* For more advanced use, everything can be use'd separately and RpcProcedure::register called manually.
*/
trait RpcHandlerCommon {
public function registerProcedures(RpcServer $server): void {
RpcProcedure::register($server, $this);
}
}

View file

@ -1,21 +1,67 @@
<?php
// RpcProcedure.php
// Created: 2024-08-15
// Updated: 2024-08-16
// Updated: 2024-11-13
namespace Aiwass\Server;
namespace RPCii\Server;
use Attribute;
use ReflectionAttribute;
use ReflectionObject;
/**
* Provides an attribute for marking methods in a class as an RPC Query action.
* Provides base for attributes that mark methods in a class as RPC procedure handlers.
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class RpcProcedure extends RpcAction {
class RpcProcedure {
/**
* @param bool $isAction true if the procedure is an action, false if query.
* @param string $name Action name.
*/
public function __construct(string $name) {
parent::__construct(true, $name);
public function __construct(
private bool $isAction,
private string $name
) {}
/**
* Returns whether the procedure is an action.
*
* @return bool
*/
public function isAction(): bool {
return $this->isAction;
}
/**
* Returns the procedure name.
*
* @return string
*/
public function getName(): string {
return $this->name;
}
/**
* Reads attributes from methods in a RpcHandler instance and registers them to a given RpcServer instance.
*
* @param RpcServer $server RPC server instance.
* @param RpcHandler $handler Handler instance.
*/
public static function register(RpcServer $server, RpcHandler $handler): void {
$objectInfo = new ReflectionObject($handler);
$methodInfos = $objectInfo->getMethods();
foreach($methodInfos as $methodInfo) {
$attrInfos = $methodInfo->getAttributes(RpcProcedure::class, ReflectionAttribute::IS_INSTANCEOF);
foreach($attrInfos as $attrInfo) {
$handlerInfo = $attrInfo->newInstance();
$server->registerProcedure(
$handlerInfo->isAction(),
$handlerInfo->getName(),
$methodInfo->getClosure($methodInfo->isStatic() ? null : $handler)
);
}
}
}
}

View file

@ -1,9 +1,9 @@
<?php
// RpcActionInfo.php
// RpcProcedureInfo.php
// Created: 2024-08-13
// Updated: 2024-08-16
// Updated: 2024-11-13
namespace Aiwass\Server;
namespace RPCii\Server;
use Closure;
use InvalidArgumentException;
@ -11,17 +11,17 @@ use ReflectionFunction;
use RuntimeException;
/**
* Provides information about an RPC action.
* Provides information about an RPC procedure.
*/
final class RpcActionInfo {
final class RpcProcedureInfo {
/**
* @param bool $isProcedure Whether this action is a procedure (true) or a query (false).
* @param string $name Name of the action.
* @param callable $handler Handler for the action.
* @param bool $isAction Whether this procedure is an action (true) or a query (false).
* @param string $name Name of the procedure.
* @param callable $handler Handler for the procedure.
* @throws InvalidArgumentException If $handler is not a callable type.
*/
public function __construct(
private bool $isProcedure,
private bool $isAction,
private string $name,
private $handler
) {
@ -32,27 +32,27 @@ final class RpcActionInfo {
}
/**
* Returns whether the action is a procedure.
* Returns whether the procedure is an action.
*
* @return bool true if the action is a procedure, false if it is a query.
* @return bool true if the procedure is an action, false if it is a query.
*/
public function isProcedure(): bool {
return $this->isProcedure;
public function isAction(): bool {
return $this->isAction;
}
/**
* Returns the name of this action.
* Returns the name of this procedure.
*
* @return string Name of the action.
* @return string Name of the procedure.
*/
public function getName(): string {
return $this->name;
}
/**
* Returns the handler for this action.
* Returns the handler for this procedure.
*
* @return Closure Handler for this action.
* @return Closure Handler for this procedure.
*/
public function getHandler(): Closure {
return Closure::fromCallable($this->handler);

View file

@ -1,19 +1,19 @@
<?php
// RpcQuery.php
// Created: 2024-08-15
// Updated: 2024-08-16
// Updated: 2024-11-13
namespace Aiwass\Server;
namespace RPCii\Server;
use Attribute;
/**
* Provides an attribute for marking methods in a class as an RPC Query action.
* Provides an attribute for marking methods in a class as an RPC Query procedure.
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class RpcQuery extends RpcAction {
class RpcQuery extends RpcProcedure {
/**
* @param string $name Action name.
* @param string $name Procedure name.
*/
public function __construct(string $name) {
parent::__construct(false, $name);

View file

@ -1,44 +1,44 @@
<?php
// RpcRouteHandler.php
// Created: 2024-08-13
// Updated: 2024-10-05
// Updated: 2024-11-13
namespace Aiwass\Server;
namespace RPCii\Server;
use Exception;
use RuntimeException;
use Aiwass\{AiwassMsgPack,IVerificationProvider};
use RPCii\{RpciiMsgPack,VerificationProvider};
use Index\Http\{HttpResponseBuilder,HttpRequest};
use Index\Http\FormHttpContent;
use Index\Http\Routing\{HttpGet,HttpPost,RouteHandler,RouteHandlerTrait};
/**
* Provides a router implementation for an Aiwass RPC server.
* Provides a router implementation for an RPCii RPC server.
*/
class RpcRouteHandler implements RouteHandler {
use RouteHandlerTrait;
/**
* @param IRpcServer $server An RPC server instance.
* @param IVerificationProvider $verification A verification provider instance.
* @param RpcServer $server An RPC server instance.
* @param VerificationProvider $verification A verification provider instance.
*/
public function __construct(
private IRpcServer $server,
private IVerificationProvider $verification
private RpcServer $server,
private VerificationProvider $verification
) {}
/**
* Handles an action request.
* Handles a procedure request.
*
* @param HttpResponseBuilder $response Response that it being built.
* @param HttpRequest $request Request that is being handled.
* @param string $action Name of the action specified in the URL.
* @param string $procedure Name of the procedure specified in the URL.
* @return int|string Response to the request.
*/
#[HttpGet('/_aiwass/([A-Za-z0-9\-_\.:]+)')]
#[HttpPost('/_aiwass/([A-Za-z0-9\-_\.:]+)')]
public function handleAction(HttpResponseBuilder $response, HttpRequest $request, string $action) {
if($action === '')
#[HttpGet('/_rpcii/([A-Za-z0-9\-_\.:]+)')]
#[HttpPost('/_rpcii/([A-Za-z0-9\-_\.:]+)')]
public function handleProcedure(HttpResponseBuilder $response, HttpRequest $request, string $procedure) {
if($procedure === '')
return 404;
if($request->getMethod() === 'POST') {
@ -46,11 +46,11 @@ class RpcRouteHandler implements RouteHandler {
if(!($content instanceof FormHttpContent))
return 400;
$expectProcedure = true;
$expectAction = true;
$paramString = $content->getParamString();
$params = $content->getParams();
} elseif($request->getMethod() === 'GET') {
$expectProcedure = false;
$expectAction = false;
$paramString = $request->getParamString();
$params = $request->getParams();
} else {
@ -60,32 +60,32 @@ class RpcRouteHandler implements RouteHandler {
$response->setContentType('application/vnd.msgpack');
$userToken = (string)$request->getHeaderLine('X-Aiwass-Verify');
$userToken = (string)$request->getHeaderLine('X-RPCii-Verify');
if($userToken === '') {
$response->setStatusCode(403);
return AiwassMsgPack::encode(['error' => 'aiwass:verify']);
return RpciiMsgPack::encode(['error' => 'rpcii:verify']);
}
$actInfo = $this->server->getActionInfo($action);
if($actInfo === null) {
$procInfo = $this->server->getProcedureInfo($procedure);
if($procInfo === null) {
$response->setStatusCode(404);
return AiwassMsgPack::encode(['error' => 'aiwass:unknown']);
return RpciiMsgPack::encode(['error' => 'rpcii:unknown']);
}
if($actInfo->isProcedure() !== $expectProcedure) {
if($procInfo->isAction() !== $expectAction) {
$response->setStatusCode(405);
$response->setHeader('Allow', $actInfo->isProcedure() ? 'POST' : 'GET');
return AiwassMsgPack::encode(['error' => 'aiwass:method']);
$response->setHeader('Allow', $procInfo->isAction() ? 'POST' : 'GET');
return RpciiMsgPack::encode(['error' => 'rpcii:method']);
}
if(!$this->verification->verify($userToken, $expectProcedure, $action, $paramString))
return AiwassMsgPack::encode(['error' => 'aiwass:verify']);
if(!$this->verification->verify($userToken, $expectAction, $procedure, $paramString))
return RpciiMsgPack::encode(['error' => 'rpcii:verify']);
try {
return AiwassMsgPack::encode($actInfo->invokeHandler($params));
return RpciiMsgPack::encode($procInfo->invokeHandler($params));
} catch(RuntimeException $ex) {
$response->setStatusCode(400);
return AiwassMsgPack::encode(['error' => sprintf('aiwass:%s', $ex->getMessage())]);
return RpciiMsgPack::encode(['error' => sprintf('rpcii:%s', $ex->getMessage())]);
}
}
}

View file

@ -1,45 +1,68 @@
<?php
// RpcServer.php
// Created: 2024-08-13
// Updated: 2024-08-16
// Created: 2024-08-16
// Updated: 2024-11-13
namespace Aiwass\Server;
namespace RPCii\Server;
use InvalidArgumentException;
use RuntimeException;
use Aiwass\IVerificationProvider;
use InvalidArgumentException;
/**
* Implements an RPC server.
* Provides a common interface for RPC servers.
*/
class RpcServer implements IRpcServer {
use RpcServerTrait;
/** @var array<string, RpcActionInfo> */
private array $actions = [];
interface RpcServer {
/**
* Creates a proxy for this RPC server with a specified namespace.
*
* @param string $prefix Prefix to apply to the scoped RPC server.
* @return RpcServer A scoped RPC server instance.
*/
function scopeTo(string $prefix): RpcServer;
/**
* Creates an RPC route handler.
* Registers a handler class.
*
* @param IVerificationProvider $verification A verification provider.
* @return RpcRouteHandler RPC route handler.
* @param RpcHandler $handler Handler to register.
*/
public function createRouteHandler(IVerificationProvider $verification): RpcRouteHandler {
return new RpcRouteHandler($this, $verification);
}
function register(RpcHandler $handler): void;
public function scopeTo(string $prefix): IRpcServer {
return new RpcServerScoped($this, $prefix);
}
/**
* Registers a procedure.
*
* @param bool $isAction true if the procedure is an action (HTTP POST), false if it is a query (HTTP GET).
* @param string $name Unique name of the procedure.
* @param callable $handler Handler for the procedure.
* @throws RuntimeException If a handler with the same name is already registered.
* @throws InvalidArgumentException If $handler is not a callable type.
*/
function registerProcedure(bool $isAction, string $name, $handler): void;
public function registerAction(bool $isProcedure, string $name, $handler): void {
if(array_key_exists($name, $this->actions))
throw new RuntimeException('an action with that name has already been registered');
/**
* Registers a query procedure (HTTP GET).
*
* @param string $name Unique name of the procedure.
* @param callable $handler Handler for the procedure.
* @throws RuntimeException If a handler with the same name is already registered.
* @throws InvalidArgumentException If $handler is not a callable type.
*/
function registerQueryProcedure(string $name, $handler): void;
$this->actions[$name] = new RpcActionInfo($isProcedure, $name, $handler);
}
/**
* Registers an action procedure (HTTP POST).
*
* @param string $name Unique name of the procedure.
* @param callable $handler Handler for the procedure.
* @throws RuntimeException If a handler with the same name is already registered.
* @throws InvalidArgumentException If $handler is not a callable type.
*/
function registerActionProcedure(string $name, $handler): void;
public function getActionInfo(string $name): ?RpcActionInfo {
return $this->actions[$name] ?? null;
}
/**
* Retrieves information about an procedure.
*
* @param string $name Name of the procedure.
* @return ?RpcProcedureInfo An object containing information about the procedure, or null if it does not exist.
*/
function getProcedureInfo(string $name): ?RpcProcedureInfo;
}

View file

@ -0,0 +1,23 @@
<?php
// RpcServerCommon.php
// Created: 2024-08-16
// Updated: 2024-11-13
namespace RPCii\Server;
/**
* Provides implementations for RpcServer methods that are likely going to be identical across implementations.
*/
trait RpcServerCommon {
public function register(RpcHandler $handler): void {
$handler->registerProcedures($this);
}
public function registerQueryProcedure(string $name, $handler): void {
$this->registerProcedure(false, $name, $handler);
}
public function registerActionProcedure(string $name, $handler): void {
$this->registerProcedure(true, $name, $handler);
}
}

View file

@ -1,34 +0,0 @@
<?php
// RpcServerScoped.php
// Created: 2024-08-16
// Updated: 2024-08-16
namespace Aiwass\Server;
/**
* Provides a scoped RPC server implementation.
*/
class RpcServerScoped implements IRpcServer {
use RpcServerTrait;
/**
* @param IRpcServer $base RPC server instance to use as a base.
* @param string $prefix Prefix to apply to action names passed.
*/
public function __construct(
private IRpcServer $base,
private string $prefix
) {}
public function scopeTo(string $prefix): IRpcServer {
return $this->base->scopeTo($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);
}
}

View file

@ -1,23 +0,0 @@
<?php
// RpcServerTrait.php
// Created: 2024-08-16
// Updated: 2024-08-16
namespace Aiwass\Server;
/**
* 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

@ -0,0 +1,34 @@
<?php
// ScopedRpcServer.php
// Created: 2024-08-16
// Updated: 2024-11-13
namespace RPCii\Server;
/**
* Provides a scoped RPC server implementation.
*/
class ScopedRpcServer implements RpcServer {
use RpcServerCommon;
/**
* @param RpcServer $base RPC server instance to use as a base.
* @param string $prefix Prefix to apply to action names passed.
*/
public function __construct(
private RpcServer $base,
private string $prefix
) {}
public function scopeTo(string $prefix): RpcServer {
return $this->base->scopeTo($this->prefix . $prefix);
}
public function registerProcedure(bool $isAction, string $name, $handler): void {
$this->base->registerProcedure($isAction, $this->prefix . $name, $handler);
}
public function getProcedureInfo(string $name): ?RpcProcedureInfo {
return $this->base->getProcedureInfo($this->prefix . $name);
}
}

View file

@ -1,24 +1,24 @@
<?php
// IVerificationProvider.php
// VerificationProvider.php
// Created: 2024-08-13
// Updated: 2024-08-13
// Updated: 2024-11-13
namespace Aiwass;
namespace RPCii;
/**
* Provides a common interface for verification providers for the route handler.
*/
interface IVerificationProvider {
interface VerificationProvider {
/**
* Creates a signature.
*
* @param bool $isProcedure true if the request is a query or false if it is a procedure call.
* @param bool $isAction true if the request is a query or false if it is an action.
* @param string $action Name of the action.
* @param string $paramString Query string value or body string value.
* @return string A token string for the given arguments.
*/
function sign(
bool $isProcedure,
bool $isAction,
string $action,
string $paramString
): string;
@ -27,14 +27,14 @@ interface IVerificationProvider {
* Verifies a signature.
*
* @param string $userToken A token provided by the user.
* @param bool $isProcedure true if the request is a query or false if it is a procedure call.
* @param bool $isAction true if the request is a query or false if it is a procedure call.
* @param string $action Name of the action.
* @param string $paramString Query string value or body string value.
* @return bool true if the token verified successfully.
*/
function verify(
string $userToken,
bool $isProcedure,
bool $isAction,
string $action,
string $paramString
): bool;

View file

@ -1,105 +1,107 @@
<?php
// AttributesTest.php
// Created: 2024-08-16
// Updated: 2024-08-16
// Updated: 2024-11-13
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\{CoversClass,UsesClass};
use Aiwass\Server\{RpcAction,RpcActionHandler,RpcProcedure,RpcQuery,RpcServer};
use RPCii\Server\{RpcAction,RpcHandler,RpcHandlerCommon,RpcProcedure,RpcQuery,HttpRpcServer};
#[CoversClass(RpcAction::class)]
#[CoversClass(RpcActionHandler::class)]
#[CoversClass(RpcHandler::class)]
#[CoversClass(RpcProcedure::class)]
#[CoversClass(RpcQuery::class)]
#[UsesClass(RpcServer::class)]
#[UsesClass(HttpRpcServer::class)]
final class AttributesTest extends TestCase {
public function testAttributes(): void {
$handler = new class($this) extends RpcActionHandler {
private static $that;
$handler = new class($this) implements RpcHandler {
use RpcHandlerCommon;
public function __construct(private $self) {
private static AttributesTest $that;
public function __construct(private AttributesTest $self) {
self::$that = $self;
}
#[RpcAction(true, 'aiwass:test:proc1')]
public function procedureRegisteredUsingActionAttribute() {
#[RpcProcedure(true, 'aiwass:test:proc1')]
public function procedureRegisteredUsingActionAttribute(): string {
return 'procedure registered using action attribute';
}
#[RpcAction(false, 'aiwass:test:query1')]
public function queryRegisteredUsingActionAttribute(string $beans) {
#[RpcProcedure(false, 'aiwass:test:query1')]
public function queryRegisteredUsingActionAttribute(string $beans): string {
$this->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') {
public static function staticQueryRegisteredUsingQueryAttribute(string $required, string $optional = 'the'): string {
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') {
#[RpcAction('aiwass:test:proc2')]
public function dynamicProcedureRegisteredUsingProcedureAttribute(string $optional = 'meow'): string {
$this->self->assertEquals('meow', $optional);
return 'dynamic procedure registered using procedure attribute';
}
#[RpcQuery('aiwass:test:query3')]
#[RpcProcedure('aiwass:test:proc3')]
public function multiple() {
#[RpcAction('aiwass:test:proc3')]
public function multiple(): string {
return 'a dynamic method registered as both a query and a procedure';
}
public function hasNoAttr() {
public function hasNoAttr(): string {
return 'not an action handler';
}
};
$server = new RpcServer;
$server = new HttpRpcServer;
$server->register($handler);
$this->assertNull($server->getActionInfo('aiwass:none'));
$this->assertNull($server->getProcedureInfo('aiwass:none'));
$proc1 = $server->getActionInfo('aiwass:test:proc1');
$this->assertNotNull($proc1);
$this->assertTrue($proc1->isProcedure());
$this->assertEquals('aiwass:test:proc1', $proc1->getName());
$this->assertEquals('procedure registered using action attribute', $proc1->invokeHandler());
$act1 = $server->getProcedureInfo('aiwass:test:proc1');
$this->assertNotNull($act1);
$this->assertTrue($act1->isAction());
$this->assertEquals('aiwass:test:proc1', $act1->getName());
$this->assertEquals('procedure registered using action attribute', $act1->invokeHandler());
$proc2 = $server->getActionInfo('aiwass:test:proc2');
$this->assertNotNull($proc2);
$this->assertTrue($proc2->isProcedure());
$this->assertEquals('aiwass:test:proc2', $proc2->getName());
$this->assertEquals('dynamic procedure registered using procedure attribute', $proc2->invokeHandler());
$act2 = $server->getProcedureInfo('aiwass:test:proc2');
$this->assertNotNull($act2);
$this->assertTrue($act2->isAction());
$this->assertEquals('aiwass:test:proc2', $act2->getName());
$this->assertEquals('dynamic procedure registered using procedure attribute', $act2->invokeHandler());
$query1 = $server->getActionInfo('aiwass:test:query1');
$query1 = $server->getProcedureInfo('aiwass:test:query1');
$this->assertNotNull($query1);
$this->assertFalse($query1->isProcedure());
$this->assertFalse($query1->isAction());
$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');
$query2 = $server->getProcedureInfo('aiwass:test:query2');
$this->assertNotNull($query2);
$this->assertFalse($query2->isProcedure());
$this->assertFalse($query2->isAction());
$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');
$query3 = $server->getProcedureInfo('aiwass:test:query3');
$proc3 = $server->getProcedureInfo('aiwass:test:proc3');
$this->assertNotNull($query3);
$this->assertNotNull($proc3);
$this->assertFalse($query3->isProcedure());
$this->asserttrue($proc3->isProcedure());
$this->assertFalse($query3->isAction());
$this->asserttrue($proc3->isAction());
$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->assertNull($server->getActionInfo('doesnotexist'));
$this->assertNull($server->getProcedureInfo('doesnotexist'));
$this->expectException(RuntimeException::class);
$query1->invokeHandler(['notbeans' => 'it is not beans']);

View file

@ -1,13 +1,13 @@
<?php
// CurlHttpTest.php
// Created: 2024-08-13
// Updated: 2024-08-16
// Updated: 2024-11-13
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use Aiwass\Client\CurlHttpRequest;
use RPCii\Client\CurlHttpRequest;
#[CoversClass(CurlHttpRequest::class)]
final class CurlHttpTest extends TestCase {
@ -19,13 +19,18 @@ final class CurlHttpTest extends TestCase {
$request->setParams('soap=beans');
$response = json_decode($request->execute(), true);
$this->assertTrue(array_key_exists('X-Test', $response['headers']));
$this->assertIsArray($response);
$this->assertArrayHasKey('headers', $response);
$this->assertIsArray($response['headers']);
$this->assertArrayHasKey('X-Test', $response['headers']);
$this->assertEquals('teste', $response['headers']['X-Test']);
$this->assertTrue(array_key_exists('alreadyhere', $response['args']));
$this->assertArrayHasKey('args', $response);
$this->assertIsArray($response['args']);
$this->assertArrayHasKey('alreadyhere', $response['args']);
$this->assertEquals('true', $response['args']['alreadyhere']);
$this->assertTrue(array_key_exists('soap', $response['args']));
$this->assertArrayHasKey('soap', $response['args']);
$this->assertEquals('beans', $response['args']['soap']);
}
@ -37,13 +42,18 @@ final class CurlHttpTest extends TestCase {
$request->setParams('windows=xp&macos=leopard');
$response = json_decode($request->execute(), true);
$this->assertTrue(array_key_exists('X-Meow', $response['headers']));
$this->assertIsArray($response);
$this->assertArrayHasKey('headers', $response);
$this->assertIsArray($response['headers']);
$this->assertArrayHasKey('X-Meow', $response['headers']);
$this->assertEquals('soap', $response['headers']['X-Meow']);
$this->assertTrue(array_key_exists('windows', $response['form']));
$this->assertArrayHasKey('form', $response);
$this->assertIsArray($response['form']);
$this->assertArrayHasKey('windows', $response['form']);
$this->assertEquals('xp', $response['form']['windows']);
$this->assertTrue(array_key_exists('macos', $response['form']));
$this->assertArrayHasKey('macos', $response['form']);
$this->assertEquals('leopard', $response['form']['macos']);
}
}

View file

@ -1,27 +1,27 @@
<?php
// HmacVerificationTest.php
// Created: 2024-08-13
// Updated: 2024-08-13
// Updated: 2024-11-13
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use Aiwass\HmacVerificationProvider;
use RPCii\HmacVerificationProvider;
#[CoversClass(HmacVerificationProvider::class)]
final class HmacVerificationTest extends TestCase {
public function testProcedureEnforce(): void {
public function testActionEnforce(): void {
$provider = new HmacVerificationProvider(fn() => 'meow');
$action = 'test';
$procedure = 'test';
$params = 'meow=cool&the=bean';
$queryToken = $provider->sign(false, $action, $params);
$this->assertTrue($provider->verify($queryToken, false, $action, $params));
$this->assertFalse($provider->verify($queryToken, true, $action, $params));
$queryToken = $provider->sign(false, $procedure, $params);
$this->assertTrue($provider->verify($queryToken, false, $procedure, $params));
$this->assertFalse($provider->verify($queryToken, true, $procedure, $params));
$procedureToken = $provider->sign(true, $action, $params);
$this->assertTrue($provider->verify($procedureToken, true, $action, $params));
$this->assertFalse($provider->verify($procedureToken, false, $action, $params));
$actionToken = $provider->sign(true, $procedure, $params);
$this->assertTrue($provider->verify($actionToken, true, $procedure, $params));
$this->assertFalse($provider->verify($actionToken, false, $procedure, $params));
}
}

View file

@ -1,40 +1,40 @@
<?php
// ScopedServerTest.php
// Created: 2024-08-16
// Updated: 2024-08-16
// Updated: 2024-11-13
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\{CoversClass,UsesClass};
use Aiwass\Server\{RpcServer,RpcServerScoped};
use RPCii\Server\{HttpRpcServer,ScopedRpcServer};
#[CoversClass(RpcServerScoped::class)]
#[UsesClass(RpcServer::class)]
#[CoversClass(ScopedRpcServer::class)]
#[UsesClass(HttpRpcServer::class)]
final class ScopedServerTest extends TestCase {
public function testServerScoping(): void {
$base = new RpcServer;
$base->registerQueryAction('test', fn() => 'test');
$base = new HttpRpcServer;
$base->registerQueryProcedure('test', fn() => 'test');
$scopedToBeans = $base->scopeTo('beans:');
$scopedToBeans->registerQueryAction('test', fn() => 'test in beans');
$scopedToBeans->registerQueryProcedure('test', fn() => 'test in beans');
$scopedToGarf = $base->scopeTo('garf:');
$scopedToGarfield = $scopedToGarf->scopeTo('ield:');
$scopedToGarfield->registerQueryAction('test', fn() => 'test in garfield');
$scopedToGarfield->registerQueryProcedure('test', fn() => 'test in garfield');
$baseTest = $base->getActionInfo('test');
$baseTest = $base->getProcedureInfo('test');
$this->assertNotNull($baseTest);
$this->assertEquals('test', $baseTest->getName());
$baseBeansTest = $base->getActionInfo('beans:test');
$scopedBeansTest = $scopedToBeans->getActionInfo('test');
$baseBeansTest = $base->getProcedureInfo('beans:test');
$scopedBeansTest = $scopedToBeans->getProcedureInfo('test');
$this->assertNotNull($baseBeansTest);
$this->assertSame($baseBeansTest, $scopedBeansTest);
$this->assertEquals('beans:test', $scopedBeansTest->getName());
$baseGarfieldTest = $base->getActionInfo('garf:ield:test');
$scopedGarfieldTest = $scopedToGarfield->getActionInfo('test');
$baseGarfieldTest = $base->getProcedureInfo('garf:ield:test');
$scopedGarfieldTest = $scopedToGarfield->getProcedureInfo('test');
$this->assertNotNull($baseGarfieldTest);
$this->assertSame($baseGarfieldTest, $scopedGarfieldTest);
$this->assertEquals('garf:ield:test', $scopedGarfieldTest->getName());

59
tests/StreamHttpTest.php Normal file
View file

@ -0,0 +1,59 @@
<?php
// StreamHttpTest.php
// Created: 2024-11-13
// Updated: 2024-11-13
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use RPCii\Client\StreamHttpRequest;
#[CoversClass(StreamHttpRequest::class)]
final class StreamHttpTest extends TestCase {
public function testGetRequest(): void {
$request = new StreamHttpRequest;
$request->setPost(false);
$request->setUrl('https://httpbin.org/get?alreadyhere=true');
$request->setHeader('X-Test', 'teste');
$request->setParams('soap=beans');
$response = json_decode($request->execute(), true);
$this->assertIsArray($response);
$this->assertArrayHasKey('headers', $response);
$this->assertIsArray($response['headers']);
$this->assertArrayHasKey('X-Test', $response['headers']);
$this->assertEquals('teste', $response['headers']['X-Test']);
$this->assertArrayHasKey('args', $response);
$this->assertIsArray($response['args']);
$this->assertArrayHasKey('alreadyhere', $response['args']);
$this->assertEquals('true', $response['args']['alreadyhere']);
$this->assertArrayHasKey('soap', $response['args']);
$this->assertEquals('beans', $response['args']['soap']);
}
public function testPostRequest(): void {
$request = new StreamHttpRequest;
$request->setPost(true);
$request->setUrl('https://httpbin.org/post');
$request->setHeader('X-Meow', 'soap');
$request->setParams('windows=xp&macos=leopard');
$response = json_decode($request->execute(), true);
$this->assertIsArray($response);
$this->assertArrayHasKey('headers', $response);
$this->assertIsArray($response['headers']);
$this->assertArrayHasKey('X-Meow', $response['headers']);
$this->assertEquals('soap', $response['headers']['X-Meow']);
$this->assertArrayHasKey('form', $response);
$this->assertIsArray($response['form']);
$this->assertArrayHasKey('windows', $response['form']);
$this->assertEquals('xp', $response['form']['windows']);
$this->assertArrayHasKey('macos', $response['form']);
$this->assertEquals('leopard', $response['form']['macos']);
}
}