- First commit
This commit is contained in:
commit
f5317f906d
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020 Dave Mc Nicoll
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"name": "mcnd/lean-api",
|
||||
"description": "Tooling to facilitate an API subset for Lean's application",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Dave Mc Nicoll",
|
||||
"email": "info@mcnd.ca"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"php-di/php-di": "dev-master",
|
||||
"ext-json": "*",
|
||||
"mcnd/lean": "dev-master"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://git.mcnd.ca/mcndave/lean.git"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Lean\\Api\\": "src/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-install-cmd": [
|
||||
"Lean\\Composer::postInstall"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"Lean\\Composer::postUpdate"
|
||||
]
|
||||
},
|
||||
"extra" : {
|
||||
"lean" : {
|
||||
"autoload": {
|
||||
"definitions" : [
|
||||
],
|
||||
"config": [
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
) {}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue