can handle basic requests #1

Merged
cerys merged 16 commits from dev into master 6 months ago
  1. 4
      .gitignore
  2. 3
      README.md
  3. 53
      composer.json
  4. 1756
      composer.lock
  5. 10
      phpunit.xml
  6. 46
      src/Attributes/QueryBuilderQuery.php
  7. 20
      src/Attributes/QueryBuilderRequiredField.php
  8. 88
      src/Core/DeegraphServer.php
  9. 21
      src/DataStructures/QueryResponseWrapper.php
  10. 23
      src/DataStructures/ServerInfo.php
  11. 13
      src/Enumerators/DeegraphEqualityOperator.php
  12. 11
      src/Enumerators/DeegraphLogicalOperators.php
  13. 12
      src/Enumerators/DeegraphNumericalComparitor.php
  14. 17
      src/Exceptions/QueryBuilderConflictingFieldAlreadyExistsException.php
  15. 17
      src/Exceptions/QueryBuilderInvalidDeegraphPathException.php
  16. 17
      src/Exceptions/QueryBuilderInvalidInputException.php
  17. 17
      src/Exceptions/QueryBuilderRequiredFieldIsNotSetException.php
  18. 24
      src/QueryBuilder/QueryBuilder.php
  19. 30
      src/QueryBuilder/QueryBuilderTrait.php
  20. 84
      src/QueryBuilder/QueryBuilders/InsertQuery.php
  21. 114
      src/QueryBuilder/QueryBuilders/PutQuery.php
  22. 61
      src/QueryBuilder/QueryBuilders/SelectQuery.php
  23. 32
      tests/QueryBuilderPutTest.php
  24. 3
      tests/bootstrap.php

4
.gitignore

@ -0,0 +1,4 @@
.idea/
composer.phar
vendor/
*.cache

3
README.md

@ -1,3 +1,4 @@
# DeegraphPHP
Documentation is available at https://wiki.darksparrow.uk/en/Packages/PHP/DeegraphPHP.
- Package is available at https://packagist.org/packages/darksparrow/deegraph-php
- Documentation is available at https://wiki.darksparrow.uk/en/Packages/PHP/DeegraphPHP.

53
composer.json

@ -1,20 +1,33 @@
{
"name": "darksparrow/deegraph-php",
"description": "Deegraph Interactions Wrapper",
"type": "library",
"license": "MPL-2.0",
"authors": [
{
"name": "Cerys Lewis",
"email": "cerys@darksparrow.uk",
"homepage": "https://ceryslewis.dev",
"role": "Lead Developer"
}
],
"support": {
"email": "cerys@darksparrow.uk"
},
"require": {
"php": ">=8.1"
}
}
{
"name": "darksparrow/deegraph-php",
"description": "Deegraph Interactions Wrapper",
"type": "library",
"license": "MPL-2.0",
"authors": [
{
"name": "Cerys Lewis",
"email": "cerys@darksparrow.uk",
"homepage": "https://ceryslewis.dev",
"role": "Lead Developer"
}
],
"minimum-stability": "dev",
"support": {
"email": "cerys@darksparrow.uk"
},
"autoload": {
"psr-4": {
"Darksparrow\\DeegraphPHP\\": "src/"
}
},
"require": {
"php": ">=8.1",
"ext-curl": "*"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
},
"scripts": {
"test": "phpunit"
}
}

1756
composer.lock

File diff suppressed because it is too large

10
phpunit.xml

@ -0,0 +1,10 @@
<phpunit
colors="true"
bootstrap="tests/bootstrap.php"
>
<testsuites>
<testsuite name="DeegraphPHP Tests">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>

46
src/Attributes/QueryBuilderQuery.php

@ -0,0 +1,46 @@
<?php
namespace Darksparrow\DeegraphPHP\Attributes;
use Darksparrow\DeegraphPHP\QueryBuilder\QueryBuilders\InsertQuery;
use Darksparrow\DeegraphPHP\QueryBuilder\QueryBuilders\PutQuery;
use Darksparrow\DeegraphPHP\QueryBuilder\QueryBuilders\SelectQuery;
use PhpParser\Node\Attribute;
use PhpParser\Node\Name;
use ReflectionClass;
#[\Attribute] class QueryBuilderQuery extends Attribute
{
public function __construct()
{
parent::__construct(new Name("QueryBuilderQuery", []), [], []);
}
public function ValidateValues(
InsertQuery|PutQuery|SelectQuery $target
): void
{
$nameBase = "Darksparrow\\DeegraphPHP\\Attributes";
$reflection = new ReflectionClass($target);
$properties = $reflection->getProperties();
foreach ($properties as $property)
{
$attributes = $property->getAttributes();
$propertyName = $property->getName();
$propertyValue = $property->getValue($target);
if(sizeof($attributes) == 0) continue;
foreach ($attributes as $attribute)
{
if($attribute->getName() == "$nameBase\\QueryBuilderRequiredField")
{
if($propertyValue == "")
throw new \Exception();
}
}
}
}
}

20
src/Attributes/QueryBuilderRequiredField.php

@ -0,0 +1,20 @@
<?php
namespace Darksparrow\DeegraphPHP\Attributes;
use PhpParser\Node\Attribute;
use PhpParser\Node\Name;
use ReflectionClass;
#[\Attribute] class QueryBuilderRequiredField extends Attribute
{
public bool $Required;
public function __construct(
bool $required = false
)
{
$this->Required = $required;
parent::__construct(new Name("QueryBuilderRequiredField", []), [], []);
}
}

88
src/Core/DeegraphServer.php

@ -0,0 +1,88 @@
<?php
namespace Darksparrow\DeegraphPHP\Core;
use Darksparrow\DeegraphPHP\DataStructures\QueryResponseWrapper;
use Darksparrow\DeegraphPHP\DataStructures\ServerInfo;
use Darksparrow\DeegraphPHP\QueryBuilder\QueryBuilders\InsertQuery;
use Darksparrow\DeegraphPHP\QueryBuilder\QueryBuilders\PutQuery;
use Darksparrow\DeegraphPHP\QueryBuilder\QueryBuilders\SelectQuery;
class DeegraphServer
{
private string $ServerDomain;
private int $Port;
private string $Token;
private string $Actor;
private bool $AllowSelfSignedCerts;
public function __construct(
string $token,
string $actor,
string $server = "localhost",
int $port = 8088,
bool $allowSelfSignedCerts = false)
{
$this->ServerDomain = $server;
$this->Port = $port;
$this->Token = $token;
$this->Actor = $actor;
$this->AllowSelfSignedCerts = $allowSelfSignedCerts;
}
/**
* Runs an API request against the Deegraph Server, and returns back information about the Deegraph Server.
* @return ServerInfo
*/
public function ServerInfo(): ServerInfo
{
$response = $this->RunRawRequest(endpoint: "/api/v1/@server_info");
return ServerInfo::FromAPIResponse(response: $response);
}
private function RunRawRequest(string $endpoint, string $method = "GET", string $body = null): string
{
$ch = curl_init("https://{$this->ServerDomain}:{$this->Port}{$endpoint}");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
"Authorization: Bearer {$this->Token}",
"X-Auxilium-Actor: {$this->Actor}",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($method == "POST") {
curl_setopt($ch, CURLOPT_POST, 1);
}
if($this->AllowSelfSignedCerts)
{
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
}
if ($method == "POST" || $method == "PUT") {
if ($body != null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
}
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
/**
* @param InsertQuery|PutQuery|SelectQuery $query Takes in a Query Builder object.
* @return QueryResponseWrapper
*/
public function RunQuery(InsertQuery|PutQuery|SelectQuery $query): QueryResponseWrapper
{
$response = $this->RunRawRequest(
endpoint: "/api/v1/@query",
method: "POST",
body: $query
);
return QueryResponseWrapper::FromAPIResponse($response);
}
}

21
src/DataStructures/QueryResponseWrapper.php

@ -0,0 +1,21 @@
<?php
namespace Darksparrow\DeegraphPHP\DataStructures;
class QueryResponseWrapper
{
public array $Rows;
public string $RowFormat;
public static function FromAPIResponse(string $response): QueryResponseWrapper
{
$temp = json_decode($response, true);
$builder = new QueryResponseWrapper();
$builder->Rows = $temp["@rows"];
$builder->RowFormat = $temp["@row_format"];
return $builder;
}
}

23
src/DataStructures/ServerInfo.php

@ -0,0 +1,23 @@
<?php
namespace Darksparrow\DeegraphPHP\DataStructures;
class ServerInfo
{
public string $InstanceID;
public string $InstanceFQDN;
public array $PublicKeys;
public static function FromAPIResponse(string $response): ServerInfo
{
$temp = json_decode($response, true);
$builder = new ServerInfo();
$builder->InstanceID = $temp["@instance_id"];
$builder->InstanceFQDN = $temp["@instance_fqdn"];
$builder->PublicKeys = $temp["@public_keys"];
return $builder;
}
}

13
src/Enumerators/DeegraphEqualityOperator.php

@ -0,0 +1,13 @@
<?php
namespace Darksparrow\DeegraphPHP\Enumerators;
enum DeegraphEqualityOperator: string
{
case EQUALS = "=";
case IDENTICAL = "==";
case IS = "===";
case DIFFERENT = "!=";
case ISNT = "ISNT";
}

11
src/Enumerators/DeegraphLogicalOperators.php

@ -0,0 +1,11 @@
<?php
namespace Darksparrow\DeegraphPHP\Enumerators;
enum DeegraphLogicalOperators: string
{
case NOT = "!";
case AND = "&&";
case OR = "||";
case XOR = "^|";
}

12
src/Enumerators/DeegraphNumericalComparitor.php

@ -0,0 +1,12 @@
<?php
namespace Darksparrow\DeegraphPHP\Enumerators;
enum DeegraphNumericalComparitor: string
{
case LESS_THAN = "<";
case MORE_THAN = ">";
case LESS_THAN_OR_EQUAL_TO = "<=";
case MORE_THAN_OR_EQUAL_TO = ">=";
}

17
src/Exceptions/QueryBuilderConflictingFieldAlreadyExistsException.php

@ -0,0 +1,17 @@
<?php
namespace Darksparrow\DeegraphPHP\Exceptions;
use Exception;
class QueryBuilderConflictingFieldAlreadyExistsException extends Exception
{
public function __construct(
string $message = "You have tried to set a field which would nullify a field you've previously set.",
int $code = 422
) {
parent::__construct($message, $code);
$this->message = "$message";
$this->code = $code;
}
}

17
src/Exceptions/QueryBuilderInvalidDeegraphPathException.php

@ -0,0 +1,17 @@
<?php
namespace Darksparrow\DeegraphPHP\Exceptions;
use Exception;
class QueryBuilderInvalidDeegraphPathException extends Exception
{
public function __construct(
string $message = "The Deegraph path you have entered is not valid.",
int $code = 422
) {
parent::__construct($message, $code);
$this->message = "$message";
$this->code = $code;
}
}

17
src/Exceptions/QueryBuilderInvalidInputException.php

@ -0,0 +1,17 @@
<?php
namespace Darksparrow\DeegraphPHP\Exceptions;
use Exception;
class QueryBuilderInvalidInputException extends Exception
{
public function __construct(
string $message = "The value you have entered doesn't pass regex validation.",
int $code = 422
) {
parent::__construct($message, $code);
$this->message = "$message";
$this->code = $code;
}
}

17
src/Exceptions/QueryBuilderRequiredFieldIsNotSetException.php

@ -0,0 +1,17 @@
<?php
namespace Darksparrow\DeegraphPHP\Exceptions;
use Exception;
class QueryBuilderRequiredFieldIsNotSetException extends Exception
{
public function __construct(
string $message = "A field that is required for this operation has not been set.",
int $code = 422
) {
parent::__construct($message, $code);
$this->message = "$message";
$this->code = $code;
}
}

24
src/QueryBuilder/QueryBuilder.php

@ -0,0 +1,24 @@
<?php
namespace Darksparrow\DeegraphPHP\QueryBuilder;
use Darksparrow\DeegraphPHP\QueryBuilder\QueryBuilders\InsertQuery;
use Darksparrow\DeegraphPHP\QueryBuilder\QueryBuilders\PutQuery;
use Darksparrow\DeegraphPHP\QueryBuilder\QueryBuilders\SelectQuery;
class QueryBuilder
{
public static function Insert(): InsertQuery
{
return new InsertQuery();
}
public static function Put(): PutQuery
{
return new PutQuery();
}
public static function Select(): SelectQuery
{
return new SelectQuery();
}
}

30
src/QueryBuilder/QueryBuilderTrait.php

@ -0,0 +1,30 @@
<?php
namespace Darksparrow\DeegraphPHP\QueryBuilder;
use Darksparrow\DeegraphPHP\Exceptions\QueryBuilderConflictingFieldAlreadyExistsException;
use Darksparrow\DeegraphPHP\Exceptions\QueryBuilderException;
use Darksparrow\DeegraphPHP\Exceptions\QueryBuilderInvalidInputException;
trait QueryBuilderTrait
{
protected function RegexValidate(string $target, string $pattern): string
{
if (!preg_match(pattern: $pattern, subject: $target))
throw new QueryBuilderInvalidInputException();
return $target;
}
protected function EnsureNotSet(string $target): void
{
if ($target != "")
throw new QueryBuilderConflictingFieldAlreadyExistsException();
}
protected function ValidateDeegraphPath(string $target): string
{
if (!preg_match(pattern: "(^(\{[0-9a-f]{8}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{12}\})$)|(^\{[0-9a-f]{8}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{12}\}(\/([a-z][a-z0-9]*|[0-9]+|#|\*))+$)|(^(([a-z][a-z0-9]*|[0-9]+|#|\*))(\/([a-z][a-z0-9]*|[0-9]+|#|\*))*$)", subject: $target))
throw new QueryBuilderInvalidInputException();
return $target;
}
}

84
src/QueryBuilder/QueryBuilders/InsertQuery.php

@ -0,0 +1,84 @@
<?php
namespace Darksparrow\DeegraphPHP\QueryBuilder\QueryBuilders;
use Darksparrow\DeegraphPHP\Attributes\QueryBuilderRequiredField;
use Darksparrow\DeegraphPHP\Attributes\QueryBuilderQuery;
use Darksparrow\DeegraphPHP\QueryBuilder\QueryBuilderTrait;
#[QueryBuilderQuery]
final class InsertQuery
{
use QueryBuilderTrait;
#[QueryBuilderRequiredField]
protected string $RelativePath = "";
protected array $Keys = [];
protected array $Schemas = [];
protected string $Values = "";
protected bool $Duplicate = false;
protected bool $Replace = false;
public function __construct()
{
}
public function __toString(): string
{
$instance = new QueryBuilderQuery();
$instance->ValidateValues($this);
$builder = "INSERT INTO $this->RelativePath";
if(sizeof($this->Keys) > 0) $builder .= "KEYS " . implode(separator: ", ", array: $this->Keys);
if(sizeof($this->Schemas) > 0) $builder .= "SCHEMAS " . implode(separator: ", ", array: $this->Schemas);
if($this->Values != "") $builder .= "VALUES $this->Values";
if($this->Duplicate) $builder .= "DUPLICATE";
if($this->Replace) $builder .= "REPLACE";
return $builder;
}
public function RelativePath(string $relativePath): InsertQuery
{
self::ValidateDeegraphPath(target: $relativePath);
$this->RelativePath = $relativePath;
return $this;
}
public function Keys(string $keys): InsertQuery
{
$this->Keys = $keys;
return $this;
}
public function Schemas(string $schemas): InsertQuery
{
$this->Schemas = $schemas;
return $this;
}
public function Values(string $values): InsertQuery
{
$this->Values .= $values;
return $this;
}
public function Duplicate(): InsertQuery
{
$this->Duplicate = true;
return $this;
}
public function Replace(): InsertQuery
{
$this->Replace = true;
return $this;
}
}

114
src/QueryBuilder/QueryBuilders/PutQuery.php

@ -0,0 +1,114 @@
<?php
namespace Darksparrow\DeegraphPHP\QueryBuilder\QueryBuilders;
use Darksparrow\DeegraphPHP\Attributes\QueryBuilderRequiredField;
use Darksparrow\DeegraphPHP\Exceptions\QueryBuilderConflictingFieldAlreadyExistsException;
use Darksparrow\DeegraphPHP\Exceptions\QueryBuilderInvalidInputException;
use Darksparrow\DeegraphPHP\Exceptions\QueryBuilderRequiredFieldIsNotSetException;
use Darksparrow\DeegraphPHP\QueryBuilder\QueryBuilderTrait;
final class PutQuery
{
use QueryBuilderTrait;
protected string $PutWhat = "";
protected string $PutAt = "";
protected string $PutInto = "";
protected bool $Safe = false;
public function __construct()
{
}
/**
* @throws QueryBuilderRequiredFieldIsNotSetException
*/
public function __toString(): string
{
$builder = "PUT";
if($this->PutWhat != "") $builder .= " $this->PutWhat";
if($this->PutAt != "") $builder .= " $this->PutAt";
elseif ($this->PutInto != "") $builder .= " $this->PutInto";
else throw new QueryBuilderRequiredFieldIsNotSetException();
if($this->Safe) $builder .= " SAFE";
return $builder;
}
/**
* @throws QueryBuilderInvalidInputException
*/
public function Schema(string $uri): PutQuery
{
$this->PutWhat = self::RegexValidate(
target: "SCHEMA \"$uri\"",
pattern: "/SCHEMA \".+\"/"
);
return $this;
}
/**
* @throws QueryBuilderInvalidInputException
*/
public function URI(string $uri): PutQuery
{
$this->PutWhat = self::RegexValidate(
target: "URI \"$uri\"",
pattern: "/URI \".+\"/"
);
return $this;
}
/**
* @throws QueryBuilderInvalidInputException
*/
public function DataURI(string $mimeType, string $data): PutQuery
{
$this->PutWhat = self::RegexValidate(
target: "URI \"data:$mimeType;$data\"",
pattern: "/URI \"data:[a-zA-Z0-9]/[a-zA-Z0-9];.+\"/"
);
return $this;
}
/**
* @throws QueryBuilderInvalidInputException
* @throws QueryBuilderConflictingFieldAlreadyExistsException
*/
public function At(string $node, string $uwu): PutQuery
{
self::EnsureNotSet($this->PutInto);
$this->PutAt = self::RegexValidate(
target: 'AT {' . $node . '}/' . $uwu,
pattern: "/AT {[a-zA-Z0-9\-]+}\/[a-zA-Z0-9]+/"
);
return $this;
}
/**
* @throws QueryBuilderInvalidInputException
* @throws QueryBuilderConflictingFieldAlreadyExistsException
*/
public function Into(string $relativePath, string $propertyName): PutQuery
{
self::EnsureNotSet($this->PutAt);
$this->PutInto = self::RegexValidate(
target: "INTO \"$relativePath\" AS \"$propertyName\"",
pattern: "/INTO \"[a-zA-Z0-9\-]+\" AS \"[a-zA-Z0-9]+\"/"
);
return $this;
}
public function Safe(): PutQuery
{
$this->Safe = true;
return $this;
}
}

61
src/QueryBuilder/QueryBuilders/SelectQuery.php

@ -0,0 +1,61 @@
<?php
namespace Darksparrow\DeegraphPHP\QueryBuilder\QueryBuilders;
use Darksparrow\DeegraphPHP\Attributes\QueryBuilderQuery;
use Darksparrow\DeegraphPHP\Attributes\QueryBuilderRequiredField;
use Darksparrow\DeegraphPHP\Enumerators\DeegraphEqualityOperator;
use Darksparrow\DeegraphPHP\Exceptions\QueryBuilderRequiredFieldIsNotSetException;
use Darksparrow\DeegraphPHP\QueryBuilder\QueryBuilderTrait;
#[QueryBuilderQuery]
final class SelectQuery
{
use QueryBuilderTrait;
#[QueryBuilderRequiredField]
protected array $RelativePaths = [];
protected string $From = "";
protected string $Where = "";
protected string $InstanceOf = "";
public function __construct()
{
}
/**
* @throws QueryBuilderRequiredFieldIsNotSetException
*/
public function __toString(): string
{
$builder = "SELECT ";
if(sizeof($this->RelativePaths)) $builder .= " " . implode(separator: ", ", array: $this->RelativePaths);
if($this->From != "") $builder .= " FROM $this->From";
if($this->Where != "") $builder .= " WHERE $this->Where";
if($this->InstanceOf != "") $builder .= " INSTANCEOF $this->InstanceOf";
return $builder;
}
public function RelativePaths(array $relativePaths): SelectQuery
{
$this->RelativePaths = $relativePaths;
return $this;
}
public function From(string $target): SelectQuery
{
$this->From = "" . $target;
return $this;
}
public function Where(string $target, DeegraphEqualityOperator $operator, string $value): SelectQuery
{
$this->Where = "" . $target . " " . $operator->value . " " . $value;
return $this;
}
public function InstanceOf(string $target): SelectQuery
{
$this->InstanceOf = "" . $target;
return $this;
}
}

32
tests/QueryBuilderPutTest.php

@ -0,0 +1,32 @@
<?php
use Darksparrow\DeegraphPHP\Exceptions\QueryBuilderConflictingFieldAlreadyExistsException;
use Darksparrow\DeegraphPHP\QueryBuilder\QueryBuilder;
use PHPUnit\Framework\TestCase;
final class QueryBuilderPutTest extends TestCase
{
public function testPutURIAtSafeWithValidData()
{
$queryBuilder = new QueryBuilder();
$query = $queryBuilder->Put()
->URI("https://schemas.auxiliumsoftware.co.uk/v1/collection.json")
->At(node: "970334ed-1f4f-465a-94d7-923a99698786", uwu: "todos")
->Safe();
self::assertEquals(
expected: 'PUT URI "https://schemas.auxiliumsoftware.co.uk/v1/collection.json" AT {970334ed-1f4f-465a-94d7-923a99698786}/todos SAFE',
actual: $query
);
}
public function testPutWithBothThings()
{
$this->expectException(QueryBuilderConflictingFieldAlreadyExistsException::class);
$queryBuilder = new QueryBuilder();
$query = $queryBuilder->Put()
->URI("https://schemas.auxiliumsoftware.co.uk/v1/collection.json")
->At(node: "970334ed-1f4f-465a-94d7-923a99698786", uwu: "todos")
->Into(relativePath: "Relative Path", propertyName: "OwO")
->Safe();
}
}

3
tests/bootstrap.php

@ -0,0 +1,3 @@
<?php
namespace tests;
Loading…
Cancel
Save