- First commit

This commit is contained in:
Dave Mc Nicoll committed 2024-10-14 15:28:35 +00:00
commit f5317f906d
9 files changed
+484

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Lean\Api\Attribute;
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_CLASS)]
class ContextField
{
public function __construct(
public string $description = "",
public bool $mandatory = false,
public ?int $minLength = null,
public ?int $maxLength = null,
public ?string $regexFormat = null,
public ?string $example = null,
) {}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace Lean\Api;
trait DescriptorTrait
{
protected function displayValue(mixed $value) : string
{
if (is_null($value)) {
return "NULL";
}
elseif (is_bool($value)) {
return $value ? "TRUE" : "FALSE";
}
elseif ($value instanceof \UnitEnum) {
return $value::class . "::" . $value->name;
}
return $value;
}
protected function yesOrNo(bool $toggle) : string
{
return sprintf("<span style='color:%s'>%s</span>", $toggle ? 'green' : '#ac1b1b', $toggle ? 'oui' : 'non');
}
protected function length(int|null $length) : string
{
return sprintf("<span style='color:%s'>%s</span>", $length ? 'black' : 'gray', $length ?? "non-défini");
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
namespace Lean\Api;
use Lean\Factory\HttpFactory;
use Notes\ObjectReflection;
use Notes\Route\Attribute\Method\Route;
use Ulmus\Attribute\Obj\Table;
use Ulmus\Attribute\Property\Field;
use Ulmus\SearchRequest\Attribute\SearchParameter;
use Ulmus\SearchRequest\Attribute\SearchRequestParameter;
class EntityDescriptor
{
use DescriptorTrait;
public string $entityTitle = <<<HTML
<div style="margin-left:15px">
<h4 class='entity-name'>%s</h4>
<div class='description'>%s</div>
<hr style="margin-top:15px">
<h5>Champs</h5>
<ol class='fields'>%s</ol>
<hr style="margin-top:15px;">
<h4 class='request-name'>%s</h4>
<h5>Requêtes</h5>
<div class='description' style="margin-bottom:10px">%s</div>
<ol class='requests'>%s</ol>
</div>
HTML;
public string $entityField = <<<HTML
<li class="odd-even">
<div style="display:flex; justify-content: space-between">
<div><strong style="font-family:monospace">$%s</strong> <span style='margin-left:15px'>%s</span></div>
</div>
<div class="field-desc" style="margin-top:10px;;background:#fff;padding:5px;font-size:0.9em">
<div style="padding:5px"><u>Champ SQL</u> : %s</div>
<div style="padding:5px"><u>Attribut</u> : %s</div>
<div style="padding:5px"><u>Type</u> : %s</div>
<div style="padding:5px"><u>Nullable</u> %s</div>
<div style="padding:5px"><u>Taille</u> : %s</div>
<div style="padding:5px"><u>Lecture seule</u> : %s</div>
</div>
</li>
HTML;
public string $searchRequestField = <<<HTML
<li class="odd-even">
<div style="display:flex; justify-content: space-between">
<div><strong style="font-family:monospace">$%s</strong> <span style='margin-left:15px'>%s</span></div>
</div>
<div class="field-desc" style="margin-top:10px;;background:#fff;padding:5px;font-size:0.9em">
<div style="padding:5px"><u>Paramètre de requête (GET)</u> : %s</div>
<div style="padding:5px"><u>Attribut</u> : %s</div>
<div style="padding:5px"><u>Type</u> : %s</div>
<div style="padding:5px"><u>Nullable</u> : %s</div>
<div style="padding:5px"><u>Valeur par défault</u> : %s</div>
</div>
</li>
HTML;
public function __construct() {}
/**
* @return Returns HTML describing entity fields
*/
public function describe(...$entities) : string
{
$html = "";
foreach($entities as $entity) {
$propertyHtml = $searchRequestHtml = "";
$entityName = is_object($entity) ? $entity::class : $entity;
$reflector = new ObjectReflection($entityName);
$table = $reflector->reflectClass()->getAttribute(Table::class);
foreach($reflector->reflectProperties() as $property) {
$field = $property->getAttribute(Field::class);
if ($field) {
$types = $property->getTypes();
$propertyHtml .= sprintf(
$this->entityField,
$property->name,
$field->object->description,
$field->object->name ?: $property->name,
$field->tag,
$field->object->type ?? implode(' | ', array_map(fn($e) => $e->type, $types)),
$this->yesOrNo($property->allowsNull()),
$this->length($field->object->length ?? null),
$this->yesOrNo($field->object->readonly),
);
}
}
$searchRequestReflector = new ObjectReflection($entityName::searchRequest()::class);
$searchRequestParameter = $searchRequestReflector->reflectClass()->getAttribute(SearchRequestParameter::class);
foreach($searchRequestReflector->reflectProperties() as $property) {
$field = $property->getAttribute(SearchParameter::class);
if ($field) {
$types = $property->getTypes();
$searchRequestHtml .= sprintf(
$this->searchRequestField,
$property->name,
$field->object->description,
$field->object->parameter ? implode(', ', $field->object->getParameters()) : $property->name,
$field->tag,
implode(' | ', array_map(fn($e) => $e->type, $types)),
$this->yesOrNo($property->allowsNull()),
$this->displayValue($property->value ?? "<i>aucune</i>")
);
}
}
$html .= sprintf($this->entityTitle, $reflector->reflectClass()->getClassName(), $table->object->description ?? null, $propertyHtml, $reflector->reflectClass()->getClassName() . "::searchRequest()", $searchRequestParameter->object->description ?? "", $searchRequestHtml);
}
return $html;
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace Lean\Api;
use Lean\Api\Attribute\ContextField;
use Notes\ObjectReflection;
class FormDescriptor
{
use DescriptorTrait;
public string $formTitle = <<<HTML
<div style="margin-left:15px">
<h4 style="background:#e0ecf7" class='form-name'>%s</h4>
<div class='description'>%s</div>
<hr style="margin-top:15px">
<h5>Champs</h5>
<ol class='fields'>%s</ol>
</div>
HTML;
public string $formField = <<<HTML
<li class="odd-even">
<div style="display:flex; justify-content: space-between">
<div><strong style="font-family:monospace">$%s</strong> <span style='margin-left:15px'>%s</span></div>
</div>
<div class="field-desc" style="margin-top:10px;;background:#fff;padding:5px;font-size:0.9em">
<div style="padding:5px"><u>Variable POST / champ JSON</u> : %s</div>
<div style="padding:5px"><u>Type</u> : %s</div>
<div style="padding:5px"><u>Nullable</u> : %s</div>
<div style="padding:5px"><u>Pattern regex</u> : %s</div>
<div style="padding:5px"><u>Taille min.</u> : %s</div>
<div style="padding:5px"><u>Taille max.</u> : %s</div>
<div style="padding:5px"><u>Exemple</u> : %s</div>
</div>
</li>
HTML;
/**
* @return Returns HTML describing given controller's routes
*/
public function describe(...$forms) : string
{
$html = "";
foreach($forms as $form) {
$propertyHtml = "";
$entityName = is_object($form) ? $form::class : $form;
$reflector = new ObjectReflection($entityName);
$context = $reflector->reflectClass()->getAttribute(ContextField::class);
foreach($reflector->reflectProperties() as $property) {
$field = $property->getAttribute(ContextField::class);
if ($field) {
$types = $property->getTypes();
$propertyHtml .= sprintf(
$this->formField,
$property->name,
$field->object->description,
$property->name,
$field->object->type ?? implode(' | ', array_map(fn($e) => $e->type, $types)),
$this->yesOrNo($property->allowsNull()),
$field->object->regexFormat ?? "aucun",
$field->object->minLength ?? "aucune",
$field->object->maxLength ?? "aucune",
$field->object->example
);
}
}
$html .= sprintf($this->formTitle, $reflector->reflectClass()->getClassName(), $context->object->description ?? null, $propertyHtml, );
}
return $html;
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace Lean\Api;
use League\CommonMark\CommonMarkConverter;
use Notes\Attribute\Ignore;
use Psr\Http\Message\ResponseInterface;
trait LeanApiTrait
{
#[Ignore]
public function renderMarkdown(string $filepath, array $entities = [], array $forms = [], int $code = 200, array $headers = []) : ResponseInterface
{
if ( ! class_exists(CommonMarkConverter::class)) {
throw new \BadFunctionCallException("League\CommonMark seems to be missing, please install dependency before trying to render Markdown content");
}
$markdown = ( new CommonMarkConverter() )->convert(file_get_contents($filepath))->getContent();
if (str_contains($markdown, '{route:descriptor}'))
{
$describe = (new RouteDescriptor($this))->describe();
$markdown = str_replace('{route:descriptor}', $describe, $markdown);
}
if (str_contains($markdown, '{entity:descriptor}'))
{
$describe = (new EntityDescriptor())->describe(... $entities);
$markdown = str_replace('{entity:descriptor}', $describe, $markdown);
}
if (str_contains($markdown, '{form:descriptor}'))
{
$describe = (new FormDescriptor())->describe(... $forms);
$markdown = str_replace('{form:descriptor}', $describe, $markdown);
}
return $this->renderView("lean/layout/docs", get_defined_vars());
}
#[Ignore]
protected function output(\JsonSerializable|array $data, int $count) {
return $this->renderJson([
'data' => $data,
'count' => $count,
]);
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace Lean\Api\Middleware;
use Laminas\Diactoros\Response\JsonResponse;
use Lean\Factory\HttpFactory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class ApiRenderer implements MiddlewareInterface {
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
{
try {
$response = $handler->handle($request);
}
catch(\Exception $ex) {
if ( ! getenv('DEBUG') ) {
return HttpFactory::createJsonResponse([
'status' => 'failed',
'message' => $ex->getMessage(),
'ts' => time(),
], 400);
}
else {
throw $ex;
}
}
if ($response instanceof JsonResponse) {
$payload = $response->getPayload();
# For now, we match only response having a 'data' field
if (isset($payload['data'])) {
return HttpFactory::createJsonResponse([
'status' => 'success',
'ts' => time(),
] + $payload, 200);
}
}
return $response;
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace Lean\Api;
use Lean\Factory\HttpFactory;
use Notes\ObjectReflection;
use Notes\Route\Attribute\Method\Route;
use function CSSLSJ\Reprise\Api\View\{ _, lang, url, route, form };
class RouteDescriptor
{
public string $routeLine = <<<HTML
<li>
<span><a href='%s' title="%s" style='font-family:monospace;font-size:.85em'>%s</a> - %s</span>
<span style='color:#ac1b1b'>%s</span>
<small style="color:#374300">%s</small>
</li>
HTML;
public function __construct(
public object $controller,
) {}
/**
* @return Returns HTML describing given controller's routes
*/
public function describe() : string
{
$html = "";
$reflector = new ObjectReflection($this->controller::class);
$attribute = $reflector->reflectClass()->getAttribute(\Notes\Route\Attribute\Object\Route::class);
$base = $attribute ? $attribute->object->base : "";
foreach($reflector->reflectMethods() as $method) {
foreach(array_reverse($method->getAttributes(Route::class)) as $routeAttribute) {
$route = $routeAttribute->object;
$path = rtrim($route->route, '/');
$cleaned = $this->cleanRouteFromRegex($base.$path);
$url = url($cleaned);
$html .= sprintf($this->routeLine, $url, $base.$path, $cleaned, $route->description, implode(', ', (array)$route->method), $route->name );
}
}
return sprintf('<ul>%s</ul>', $html);
}
protected function cleanRouteFromRegex(string $route) : string
{
$paths = explode('/', $route);
foreach($paths as &$path) {
list($newPath, ) = explode(':', $path);
$path = $newPath === $path ? $path : "$newPath}";
}
return implode('/', $paths);
}
}