Add cli client, test stubs, fixes and improvements
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
/vendor/
|
||||
/.phpunit.*
|
||||
|
174
bin/paramcli
Executable file
174
bin/paramcli
Executable file
@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once __DIR__."/../vendor/autoload.php";
|
||||
|
||||
$opts = (object)[
|
||||
'help' => false,
|
||||
'server' => '127.0.0.1:8000',
|
||||
'args' => [],
|
||||
'json' => false,
|
||||
'from' => null,
|
||||
'until' => null,
|
||||
];
|
||||
|
||||
function parse_datetime(string $timestamp): ?DateTime {
|
||||
if (strtotime($timestamp)) {
|
||||
return new DateTime($timestamp);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parse_options() {
|
||||
global $argc,$argv,$opts;
|
||||
$o = getopt("hs:f:u:j",["help","server:","from:","until:","json"],$optind);
|
||||
foreach ($o as $opt=>$val) {
|
||||
switch ($opt) {
|
||||
case 'h':
|
||||
case 'help':
|
||||
$opts->help = true;
|
||||
break;
|
||||
case 's':
|
||||
case 'server':
|
||||
$opts->server = $val;
|
||||
break;
|
||||
case 'f':
|
||||
case 'from':
|
||||
$opts->from = parse_datetime($val);
|
||||
break;
|
||||
case 'u':
|
||||
case 'until':
|
||||
$opts->until = parse_datetime($val);
|
||||
break;
|
||||
case 'j':
|
||||
case 'json':
|
||||
$opts->json = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$opts->args = array_slice($argv,$optind);
|
||||
}
|
||||
|
||||
function print_help() {
|
||||
global $argv;
|
||||
printf("Syntax:\n %s [options] {action} [args...]\n\n", basename($argv[0]));
|
||||
printf("Options:\n");
|
||||
printf(" -h,--help Show this help\n");
|
||||
printf(" -s,--server SERVER Specify the server (ip:port) to use\n");
|
||||
printf(" -f,--from DATETIME Set the validity start timestamp\n");
|
||||
printf(" -u,--until DATETIME Set the validity end timestamp\n");
|
||||
printf(" -j,--json Assume json encoded values with set\n");
|
||||
printf("\nActions:\n");
|
||||
printf(" set {collection} {key}={value}*\n Set the keys to value, using the --from and --until values. Multiple\n key-value pairs can be specified. Use --json to pass values as json.\n");
|
||||
printf(" get {collection} [{when}]\n Fetch the current values, or the values at {when}.\n");
|
||||
printf(" show {collection}\n Show keys and all values, allowing to delete values.\n");
|
||||
printf(" delete {collection} {key|id}*\n Delete a key or a value from a collection\n");
|
||||
printf(" purge {collection}\n Delete an entire collection\n");
|
||||
}
|
||||
|
||||
function exit_error(string $msg, int $code=1): never {
|
||||
fwrite(STDERR, $msg."\n");
|
||||
exit($code);
|
||||
}
|
||||
|
||||
parse_options();
|
||||
|
||||
if ($opts->help) {
|
||||
print_help();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$http = new React\Http\Browser();
|
||||
|
||||
function http_get(string $url, array $headers) {
|
||||
global $opts,$http;
|
||||
return $http->get("http://{$opts->server}/{$url}", $headers);
|
||||
}
|
||||
|
||||
function http_post(string $url, array $headers, string $body) {
|
||||
global $opts,$http;
|
||||
return $http->post("http://{$opts->server}/{$url}", $headers, $body);
|
||||
}
|
||||
|
||||
function action_get(object $opts) {
|
||||
$collection = array_shift($opts->args);
|
||||
if (!$collection) {
|
||||
exit_error("Missing collection. Expected: get {collection}");
|
||||
}
|
||||
http_get($collection, [])->then(
|
||||
function ($response) {
|
||||
echo $response->getBody()->getContents();
|
||||
},
|
||||
function ($error) {
|
||||
echo $error->getMessage()."\n";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function action_show(object $opts) {
|
||||
$collection = array_shift($opts->args);
|
||||
if (!$collection) {
|
||||
exit_error("Missing collection. Expected: get {collection}");
|
||||
}
|
||||
http_get($collection."/all", [])->then(
|
||||
function ($response) {
|
||||
echo $response->getBody()->getContents();
|
||||
},
|
||||
function ($error) {
|
||||
echo $error->getMessage()."\n";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function action_set(object $opts) {
|
||||
$collection = array_shift($opts->args);
|
||||
if (!$collection) {
|
||||
exit_error("Missing collection. Expected: get {collection}");
|
||||
}
|
||||
$body = [];
|
||||
while ($kvpair = array_shift($opts->args)) {
|
||||
$op = [];
|
||||
if (!str_contains($kvpair,"=")) {
|
||||
exit_error("Invalid key-value pair: {$kvpair}");
|
||||
}
|
||||
[$key,$value] = explode("=", $kvpair, 2);
|
||||
if (ctype_digit($key)) {
|
||||
$op['id'] = $key;
|
||||
} else {
|
||||
$op['key'] = $key;
|
||||
}
|
||||
$op['value'] = $opts->json ? json_decode($value) : $value;
|
||||
if ($opts->from || $opts->until) {
|
||||
$vfrom = $opts->from ? [ 'from' => $opts->from->format('Y-m-d H:i:s P') ] : null;
|
||||
$vuntil = $opts->until ? [ 'until' => $opts->until->format('Y-m-d H:i:s P') ] : null;
|
||||
$op['validity'] = [ ...$vfrom, ...$vuntil ];
|
||||
}
|
||||
$body[$key] = $op;
|
||||
}
|
||||
http_post($collection, [ 'content-type'=>'application/json' ], json_encode($body))->then(
|
||||
function ($response) {
|
||||
echo $response->getBody()->getContents();
|
||||
},
|
||||
function ($error) {
|
||||
echo $error->getMessage()."\n";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$action = array_shift($opts->args);
|
||||
|
||||
switch ($action) {
|
||||
|
||||
case 'get':
|
||||
action_get($opts);
|
||||
break;
|
||||
|
||||
case 'show':
|
||||
action_show($opts);
|
||||
break;
|
||||
|
||||
case 'set':
|
||||
action_set($opts);
|
||||
break;
|
||||
}
|
||||
|
26
phpunit.xml
Normal file
26
phpunit.xml
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.3/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
executionOrder="depends,defects"
|
||||
shortenArraysForExportThreshold="10"
|
||||
requireCoverageMetadata="true"
|
||||
beStrictAboutCoverageMetadata="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
displayDetailsOnPhpunitDeprecations="true"
|
||||
failOnPhpunitDeprecation="true"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true">
|
||||
<testsuites>
|
||||
<testsuite name="default">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<source ignoreIndirectDeprecations="true" restrictNotices="true" restrictWarnings="true">
|
||||
<include>
|
||||
<directory>src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
@ -72,9 +72,11 @@ class Daemon
|
||||
$this->opCollectionSet($request, $collectionName);
|
||||
return Response::json(true);
|
||||
}
|
||||
break;
|
||||
throw new \Exception("Invalid request method");
|
||||
|
||||
case 'all':
|
||||
break;
|
||||
$result = $this->opCollectionGetAll($collectionName);
|
||||
return Response::json($result);
|
||||
}
|
||||
|
||||
return Response::json([ 'error'=>"Not Found" ])->withStatus(404);
|
||||
@ -93,27 +95,41 @@ class Daemon
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function opCollectionGetAll(string $collectionName): array
|
||||
{
|
||||
try {
|
||||
$collection = $this->db->getCollection($collectionName);
|
||||
$result = $collection->getAllValues();
|
||||
}
|
||||
catch (\Throwable $t) {
|
||||
throw new \Exception("No such collection");
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function opCollectionSet(ServerRequestInterface $request, string $collectionName): void
|
||||
{
|
||||
|
||||
try {
|
||||
$collection = $this->db->getCollection($collectionName);
|
||||
}
|
||||
catch (\Throwable $t) {
|
||||
$collection = $this->db->createCollection($collectionName);
|
||||
$collection = $this->db->getOrCreateCollection($collectionName);
|
||||
|
||||
$body = json_decode($request->getBody()->getContents());
|
||||
|
||||
// Allow passing a single value in a request, as well as array of values.
|
||||
if (isset($body->key)) {
|
||||
$body = [ $body ];
|
||||
}
|
||||
|
||||
$body = (array)json_decode($request->getBody()->getContents());
|
||||
|
||||
foreach ($body as $change) {
|
||||
foreach ((array)$body as $change) {
|
||||
|
||||
echo "Applying change (collection={$collectionName}): ".json_encode($change)."\n";
|
||||
$id = $change->id??null;
|
||||
$name = $change->name;
|
||||
$name = $change->key;
|
||||
$value = $change->value;
|
||||
$validFrom = isset($change->validity->from) ? new DateTime($change->validity->from) : null;
|
||||
$validUntil = isset($change->validity->until) ? new DateTime($change->validity->until) : null;
|
||||
|
||||
$collection->setValue($name, $value, $validFrom, $validUntil, $id);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -16,14 +16,22 @@ class Collection
|
||||
|
||||
public function setValue(string $key, mixed $value, ?DateTime $validFrom = null, ?DateTime $validUntil = null, ?int $updateId = null)
|
||||
{
|
||||
// echo "setValue: {$key} = ".json_encode($value)."\n";
|
||||
$this->loadKeys();
|
||||
if (!array_key_exists($key, $this->keys)) {
|
||||
//echo "setValue: new key, inserting to get new key id (cid={$this->id}, name={$key})\n";
|
||||
try {
|
||||
$this->db->prepare("INSERT INTO keys (collection_id,name) VALUES (:cid,:name)")
|
||||
->execute([ 'cid' => $this->id, 'name' => $key ]);
|
||||
}
|
||||
catch (\PDOException $e) {
|
||||
echo $e."\n";
|
||||
}
|
||||
$id = $this->db->lastInsertId('keys');
|
||||
} else {
|
||||
$id = $this->keys[$key];
|
||||
}
|
||||
//echo "key id={$id}\n";
|
||||
|
||||
$from = $validFrom ? $validFrom->format('Y-m-d H:i:s P') : null;
|
||||
$until = $validUntil ? $validUntil->format('Y-m-d H:i:s P') : null;
|
||||
@ -47,6 +55,7 @@ class Collection
|
||||
$existQuery = $this->db->prepare("SELECT * FROM vals WHERE key_id=:id AND valid_from IS NULL AND valid_until IS NULL");
|
||||
$existQuery->execute([ 'id' => $id ]);
|
||||
}
|
||||
|
||||
if ($row = $existQuery->fetch()) {
|
||||
$vid = $row['id'];
|
||||
$this->db->prepare("UPDATE vals SET value=:value, valid_from=:from, valid_until=:until WHERE id=:vid")
|
||||
@ -82,11 +91,13 @@ class Collection
|
||||
"SELECT * FROM vals WHERE ".
|
||||
"key_id=:id AND (".
|
||||
"(valid_from IS NULL) OR (datetime(valid_from) < datetime(:now)) AND ".
|
||||
"(valid_until IS NULL) OR (datetime(valid_until) > datetime(:now)))");
|
||||
"(valid_until IS NULL) OR (datetime(valid_until) > datetime(:now))) ".
|
||||
"ORDER BY valid_from ASC");
|
||||
$valQuery->execute([ "id" => $id, "now" => $when->format('Y-m-d H:i:sP') ]);
|
||||
$last = null;
|
||||
while ($row = $valQuery->fetch(PDO::FETCH_ASSOC)) $last = $row;
|
||||
$resolved[$key] = json_decode($last['value']);
|
||||
if ($last)
|
||||
$resolved[$key] = json_decode($last['value']);
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
@ -100,7 +111,7 @@ class Collection
|
||||
foreach ($this->keys as $key=>$id) {
|
||||
if ($onlyKeys !== null && !in_array($key, $onlyKeys))
|
||||
continue;
|
||||
$valQuery = $this->db->prepare("SELECT * FROM vals WHERE key_id=:id");
|
||||
$valQuery = $this->db->prepare("SELECT * FROM vals WHERE key_id=:id ORDER BY valid_from ASC");
|
||||
$valQuery->execute([ "id" => $id]);
|
||||
$values[$key] = [];
|
||||
while ($row = $valQuery->fetch(PDO::FETCH_ASSOC)) {
|
||||
|
@ -22,9 +22,11 @@ class Database
|
||||
"CREATE TABLE IF NOT EXISTS keys (".
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT,".
|
||||
"collection_id INTEGER NOT NULL,".
|
||||
"name VARCHAR UNIQUE NOT NULL,".
|
||||
"name VARCHAR NOT NULL,".
|
||||
"FOREIGN KEY(collection_id) REFERENCES collections(id)".
|
||||
")");
|
||||
$this->pdo->exec(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS keyname ON keys(collection_id,name)");
|
||||
$this->pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS vals (".
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT,".
|
||||
@ -38,6 +40,24 @@ class Database
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS valkey ON vals(key_id,valid_from,valid_until)");
|
||||
}
|
||||
|
||||
public function getOrCreateCollection(string $name): Collection
|
||||
{
|
||||
$query = $this->pdo->prepare("SELECT * FROM collections WHERE name=:name");
|
||||
$query->execute([ 'name' => $name ]);
|
||||
$row = $query->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
$query = $this->pdo->prepare("INSERT INTO collections (name) VALUES (:name)");
|
||||
$query->execute([ 'name' => $name ]);
|
||||
$id = $this->pdo->lastInsertId('collections');
|
||||
|
||||
return new Collection($this->pdo, intval($id));
|
||||
}
|
||||
|
||||
$id = $row['id'];
|
||||
return new Collection($this->pdo, $id);
|
||||
}
|
||||
|
||||
public function getCollection(string $name): Collection
|
||||
{
|
||||
$query = $this->pdo->prepare("SELECT * FROM collections WHERE name=:name");
|
||||
|
36
tests/DaemonTest.php
Normal file
36
tests/DaemonTest.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\ParamDb;
|
||||
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
|
||||
#[CoversClass(Daemon::class)]
|
||||
class DaemonTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testHandlingGetRequest()
|
||||
{
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
public function testHandlingSetRequest()
|
||||
{
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
public function testHandlingGetAllRequest()
|
||||
{
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
public function testHandlingDeleteRequest()
|
||||
{
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
public function testHandlingPurgeRequest()
|
||||
{
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
}
|
31
tests/Storage/CollectionTest.php
Normal file
31
tests/Storage/CollectionTest.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\ParamDb\Storage;
|
||||
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
|
||||
#[CoversClass(Collection::class)]
|
||||
class CollectionTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testInsertingValues()
|
||||
{
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
public function testResolvingValues()
|
||||
{
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
public function testProperSelectionOrderValues()
|
||||
{
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
public function testDeletingValues()
|
||||
{
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
}
|
36
tests/Storage/DatabaseTest.php
Normal file
36
tests/Storage/DatabaseTest.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\ParamDb\Storage;
|
||||
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
|
||||
#[CoversClass(Database::class)]
|
||||
class DatabaseTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
|
||||
public function testOpeningDatabases()
|
||||
{
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
public function testCreatingCollections()
|
||||
{
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
public function testOpeningCollections()
|
||||
{
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
public function testPurgingCollection()
|
||||
{
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
public function testExceptionsIfCollectionNotExists()
|
||||
{
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
}
|
1
var/.gitignore
vendored
Normal file
1
var/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.db
|
Reference in New Issue
Block a user