From 4d7053a9cb53fece70ca90b369dcfe8ee9187fee Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Wed, 21 May 2025 18:36:19 +0000 Subject: [PATCH] - Lean's API WIP on forms and ApiRenderer --- composer.json | 3 +- meta/config.php | 21 +- meta/definitions/software.php | 24 +- meta/definitions/storage.php | 19 ++ meta/docs/debug.md | 21 ++ meta/docs/debug/errors.md | 21 ++ meta/docs/lean-api.md | 7 + meta/i18n/fr/lean.api.json | 4 +- src/ApplicationStrategy.php | 72 ++++++ src/Attribute/ContextField.php | 1 + src/Controller/Debug.php | 23 ++ src/Controller/Debug/Errors.php | 46 ++++ src/Entity/Error.php | 79 +++++++ src/Entity/User/UserPrivilegeInterface.php | 11 + src/Factory/DebugFormFactory.php | 39 ++++ src/Factory/DebugFormFactoryInterface.php | 21 ++ src/Form/Debug/ErrorContext.php | 45 ++++ src/Form/Debug/ErrorSave.php | 20 ++ src/Form/Save.php | 21 +- src/FormDescriptor.php | 28 ++- src/LeanApiTrait.php | 90 +++++++- src/Lib/ApiSearchRequestInterface.php | 13 ++ src/Lib/ControllerTrait.php | 13 ++ src/Lib/FormContext.php | 5 + src/Middleware/ApiRenderer.php | 57 ++++- src/RouteDescriptor.php | 50 ++++- view/lean-api/form_descriptor.phtml | 28 ++- view/lean-api/request_debugger.phtml | 241 +++++++++++++++++++++ view/lean-api/route_descriptor.phtml | 20 +- view/lean/layout/docs.phtml | 16 +- 30 files changed, 1003 insertions(+), 56 deletions(-) create mode 100644 meta/definitions/storage.php create mode 100644 meta/docs/debug.md create mode 100644 meta/docs/debug/errors.md create mode 100644 meta/docs/lean-api.md create mode 100644 src/ApplicationStrategy.php create mode 100644 src/Controller/Debug.php create mode 100644 src/Controller/Debug/Errors.php create mode 100644 src/Entity/Error.php create mode 100644 src/Entity/User/UserPrivilegeInterface.php create mode 100644 src/Factory/DebugFormFactory.php create mode 100644 src/Factory/DebugFormFactoryInterface.php create mode 100644 src/Form/Debug/ErrorContext.php create mode 100644 src/Form/Debug/ErrorSave.php create mode 100644 src/Lib/ApiSearchRequestInterface.php create mode 100644 src/Lib/ControllerTrait.php create mode 100644 src/Lib/FormContext.php create mode 100644 view/lean-api/request_debugger.phtml diff --git a/composer.json b/composer.json index 4aa66c5..f57d8e7 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,8 @@ "lean" : { "autoload": { "definitions" : [ - "meta/definitions/software.php" + "meta/definitions/software.php", + "meta/definitions/storage.php", ], "config": [ "meta/config.php" diff --git a/meta/config.php b/meta/config.php index d46d5fc..a723c71 100644 --- a/meta/config.php +++ b/meta/config.php @@ -1,9 +1,28 @@ [ 'autoload' => [ 'lean.api', ] ], -]; + + 'ulmus' => [ + 'connections' => [ + 'lean.api' => [ + 'adapter' => getenv("LEAN_API_ADAPTER") ?: "SQLite", + 'path' => getenv('PROJECT_PATH') . DIRECTORY_SEPARATOR . ( getenv("LEAN_API_PATH") ?: "var/lean-api.sqlite3" ), + 'pragma_begin' => array_merge( + explode(',', getenv("LEAN_API_PRAGMA_BEGIN") ?: "foreign_keys=ON,synchronous=NORMAL"), + explode(',', getenv('DEBUG') ? getenv("LEAN_API_PRAGMA_DEBUG_BEGIN") : "journal_mode=WAL") + ), + 'pragma_close' => array_merge( + explode(',', getenv("LEAN_API_PRAGMA_CLOSE") ?: "analysis_limit=500,optimize"), + explode(',', getenv('DEBUG') ? getenv("LEAN_API_PRAGMA_DEBUG_CLOSE") : "") + ), + ], + ] + ] +]; \ No newline at end of file diff --git a/meta/definitions/software.php b/meta/definitions/software.php index f90df3d..497042d 100644 --- a/meta/definitions/software.php +++ b/meta/definitions/software.php @@ -1,6 +1,8 @@ [ @@ -22,6 +24,13 @@ return [ ], ], + 'ulmus' => [ + 'entities' => [ 'Lean\\Api\\Entity' => implode(DIRECTORY_SEPARATOR, [ $path, 'src', 'Entity', '' ]) ], + 'adapters' => [ + DI\get('lean.api:storage'), + ] + ], + 'tell' => [ 'json' => [ [ @@ -31,10 +40,19 @@ return [ ] ], - /*'routes' => [ + 'routes' => [ 'Lean\\Api\\Controller' => implode(DIRECTORY_SEPARATOR, [ $path, "src", "Controller", "" ]), - ],*/ + ], ], + 'lean.api:storage' => function($c) { + $adapter = new ConnectionAdapter('lean.api', $c->get('config')['ulmus'], false); + $adapter->resolveConfiguration(); + + return $adapter; + }, + \Lean\Api\Factory\MessageFactoryInterface::class => DI\autowire(\Lean\Api\Lib\Message::class), + \League\Route\Strategy\ApplicationStrategy::class => DI\autowire(\Lean\Api\ApplicationStrategy::class), + \Lean\Api\Factory\DebugFormFactoryInterface::class => DI\autowire(\Lean\Api\Factory\DebugFormFactory::class), ]; \ No newline at end of file diff --git a/meta/definitions/storage.php b/meta/definitions/storage.php new file mode 100644 index 0000000..59ef76c --- /dev/null +++ b/meta/definitions/storage.php @@ -0,0 +1,19 @@ + function($c) { + $adapter = new ConnectionAdapter('lean.api', $c->get('config')['ulmus'], false); + $adapter->resolveConfiguration(); + + return $adapter; + }, +]; diff --git a/meta/docs/debug.md b/meta/docs/debug.md new file mode 100644 index 0000000..f127101 --- /dev/null +++ b/meta/docs/debug.md @@ -0,0 +1,21 @@ +# LEAN API + +## Debug tooling to help development within Lean's API + +[Documentation](../) / Debug API queries + +-------------------------------------------------------------------------------- + +### Routes + +{route:descriptor} + +{request:debugger} + +### Forms / actions + +{form:descriptor} + +### Entities + +{entity:descriptor} \ No newline at end of file diff --git a/meta/docs/debug/errors.md b/meta/docs/debug/errors.md new file mode 100644 index 0000000..9d2f81a --- /dev/null +++ b/meta/docs/debug/errors.md @@ -0,0 +1,21 @@ +# LEAN API + +## Debug tooling to help development within Lean's API + +[Lean API](../../debug) / Debug API queries + +-------------------------------------------------------------------------------- + +### Routes + +{route:descriptor} + +{request:debugger} + +### Forms / actions + +{form:descriptor} + +### Entities + +{entity:descriptor} \ No newline at end of file diff --git a/meta/docs/lean-api.md b/meta/docs/lean-api.md new file mode 100644 index 0000000..6877c23 --- /dev/null +++ b/meta/docs/lean-api.md @@ -0,0 +1,7 @@ +# Lean API + +## Lean's API internal debug API + +### Routes disponibles + +- [Debug / Errors](./debug/errors/documentation) diff --git a/meta/i18n/fr/lean.api.json b/meta/i18n/fr/lean.api.json index 857bd49..01ebd76 100644 --- a/meta/i18n/fr/lean.api.json +++ b/meta/i18n/fr/lean.api.json @@ -12,11 +12,11 @@ }, "form": { "error": { - "entity": "Une propriété $entity est nécessaire dans ce form" + "entity": "Une propriété $entity est nécessaire pour envoyer ce formulaire." }, "save": { "error": { - "save": "Une erreur est survenue en tenant de sauvegarder les données", + "entity": "Une erreur est survenue en tenant de sauvegarder les données pour l'entité {$entity}.", "pdo": "Une erreur est survenue : '{$error}'" }, "success": { diff --git a/src/ApplicationStrategy.php b/src/ApplicationStrategy.php new file mode 100644 index 0000000..33aeca1 --- /dev/null +++ b/src/ApplicationStrategy.php @@ -0,0 +1,72 @@ +container = $di; + } + + public function getThrowableHandler(): MiddlewareInterface + { + return new class implements MiddlewareInterface + { + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + try { + return $handler->handle($request); + } catch (\Throwable $ex) { + echo sprintf("%s in %s
%s
", $ex->getMessage(), $ex->getFile() . ":" . $ex->getLine(), $ex->getTraceAsString()); + exit(); + } + } + }; + } + + public function getNotFoundDecorator(NotFoundException $exception): MiddlewareInterface + { + return new class($this->httpFactory) implements MiddlewareInterface { + + public function __construct( + protected HttpFactoryInterface $httpFactory, + ) {} + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + if (php_sapi_name() !== 'cli' and ! defined('STDIN')) { + return $this->throw404($request); + } + + return $handler->handle($request); + } + + public function throw404(ServerRequestInterface $request) : ResponseInterface + { + return $this->httpFactory->createJsonResponse([ + 'status' => 'error', + 'message' => "Not Found", + 'ts' => time(), + ], 404); + } + }; + } +} \ No newline at end of file diff --git a/src/Attribute/ContextField.php b/src/Attribute/ContextField.php index 0b04e15..1e67e25 100644 --- a/src/Attribute/ContextField.php +++ b/src/Attribute/ContextField.php @@ -12,5 +12,6 @@ class ContextField public ?int $maxLength = null, public ?string $regexFormat = null, public ?string $example = null, + public bool $hidden = false, ) {} } \ No newline at end of file diff --git a/src/Controller/Debug.php b/src/Controller/Debug.php new file mode 100644 index 0000000..8b2c538 --- /dev/null +++ b/src/Controller/Debug.php @@ -0,0 +1,23 @@ +renderMarkdown(getenv("LEAN_API_PROJECT_PATH") . "/meta/docs/lean-api.md", [ Entity\Error::class, ], [ Form\Debug\ErrorContext::class ]); + } +} diff --git a/src/Controller/Debug/Errors.php b/src/Controller/Debug/Errors.php new file mode 100644 index 0000000..75e6a7c --- /dev/null +++ b/src/Controller/Debug/Errors.php @@ -0,0 +1,46 @@ +renderMarkdown(getenv("LEAN_API_PROJECT_PATH") . "/meta/docs/debug/errors.md", [ Entity\Error::class, ], [ Form\Debug\ErrorContext::class ]); + } + + #[Route("/", name: "lean.api:errors-list", method: [ "GET" ], description: "List every errors received")] + public function list(ServerRequestInterface $request, array $arguments): ResponseInterface + { + $search = Entity\Error::searchRequest()->fromRequest($request); + $collection = Entity\Error::repository()->filterServerRequest($search)->loadAll()->iterate(function($e) { + unset($e->trace); + }); + + return $this->output($collection, $search->count); + } + + #[Route("/{id:\d+}", name: "lean.api:errors-single", method: [ "GET" ], description: "Get a single error from it's ID")] + public function single(ServerRequestInterface $request, array $arguments): ResponseInterface + { + $type = Entity\Error::class; + $request = $this->searchEntityFromRequest($request, $type); + $result = $request->getAttribute('lean.searchRequest'); + + return $this->output($result[$type]->entity, $result[$type]->search->count); + } + +} diff --git a/src/Entity/Error.php b/src/Entity/Error.php new file mode 100644 index 0000000..2dc2468 --- /dev/null +++ b/src/Entity/Error.php @@ -0,0 +1,79 @@ +message) > 255 ? mb_substr($this->message, 0, 255) . "[...]" : $this->message; + } + + public static function searchRequest(...$arguments) : SearchRequestInterface + { + return new #[SearchRequest\Attribute\SearchRequestParameter(Error::class)] class(...$arguments) extends SearchRequest\SearchRequest { + #[SearchRequest\Attribute\SearchWhere(source: SearchRequest\Attribute\PropertyValueSource::RequestAttribute)] + public int $id; + + #[SearchWhere(method: SearchMethodEnum::Like)] + public string $message; + + #[SearchOrderBy(parameter: "created_at",)] + public string $createdAt = SearchOrderBy::DESCENDING; + }; + } +} \ No newline at end of file diff --git a/src/Entity/User/UserPrivilegeInterface.php b/src/Entity/User/UserPrivilegeInterface.php new file mode 100644 index 0000000..6cf34a2 --- /dev/null +++ b/src/Entity/User/UserPrivilegeInterface.php @@ -0,0 +1,11 @@ +languageHandler, $this->messageFactory, $entity); + } + + public function errorSaveContext(? ServerRequestInterface $request = null, ? string $formName = null): FormContextInterface + { + return new Form\Debug\ErrorContext($request ?: $this->request, $formName); + } + + public function errorDelete(EntityInterface|Entity\Error $entity): FormInterface + { + return new Form\Debug\ErrorDelete($this->languageHandler, $this->messageFactory, $entity); + } + + public function errorDeleteContext(?ServerRequestInterface $request = null, ?string $formName = null): FormContextInterface + { + return new Lib\FormContext($request, $formName); + } +} \ No newline at end of file diff --git a/src/Factory/DebugFormFactoryInterface.php b/src/Factory/DebugFormFactoryInterface.php new file mode 100644 index 0000000..4ac1936 --- /dev/null +++ b/src/Factory/DebugFormFactoryInterface.php @@ -0,0 +1,21 @@ +getRequest()->getServerParams(); + + $this->url = ( ( 'on' === ( $server['HTTPS'] ?? false ) ) ? 'https' : 'http' ) . '://' . $server['HTTP_HOST'] . $server["REQUEST_URI"]; + + $this->httpMethod = MethodEnum::from($server['REQUEST_METHOD']); + + return parent::valid(); + } +} \ No newline at end of file diff --git a/src/Form/Debug/ErrorSave.php b/src/Form/Debug/ErrorSave.php new file mode 100644 index 0000000..03e937c --- /dev/null +++ b/src/Form/Debug/ErrorSave.php @@ -0,0 +1,20 @@ +languageHandler, $this->message); + + $this->contextClass = ErrorContext::class; + } +} diff --git a/src/Form/Save.php b/src/Form/Save.php index 3bd9eaa..67794d8 100644 --- a/src/Form/Save.php +++ b/src/Form/Save.php @@ -22,17 +22,17 @@ abstract class Save implements \Picea\Ui\Method\FormInterface { return $context->valid($this->getEntity()->isLoaded() ? $this->getEntity() : null); } - public function execute(FormContextInterface $context) : void + public function execute(FormContextInterface $context) : mixed { $entity = $this->getEntity(); - if ($entity->isLoaded() ) { - if (property_exists($entity, 'updatedAt')) { + if ($entity->isLoaded()) { + if (property_exists($entity, 'updatedAt') && $entity->repository()->generateDatasetDiff($entity) ) { $entity->updatedAt = new Datetime(); } } else { - if (property_exists($entity, 'createdAt')) { + if (property_exists($entity, 'createdAt') && empty($entity->createdAt)) { $entity->createdAt = new Datetime(); } } @@ -40,18 +40,22 @@ abstract class Save implements \Picea\Ui\Method\FormInterface { try { $this->assignContextToEntity($context); - if ( $entity::repository()->save($entity) ) { + if ( $saved = $entity::repository()->save($entity) ) { $context->pushMessage($this->message::generateSuccess( $this->lang('lean.api.form.save.success.entity') )); } else { - throw new \InvalidArgumentException($this->lang('lean.api.form.save.error.entity')); + $context->pushMessage($this->message::generateWarning( + $this->lang('lean.api.form.save.error.entity', [ 'entity' => $this->entity::class ]) + )); } } catch(\Throwable $ex) { - throw new \ErrorException($this->lang('lean.api.form.save.error.pdo', [ 'error' => $ex->getMessage() ])); + throw new \PDOException($this->lang('lean.api.form.save.error.pdo', [ 'error' => $ex->getMessage() ])); } + + return $saved; } protected function assignContextToEntity(FormContextInterface $context) : void @@ -60,13 +64,12 @@ abstract class Save implements \Picea\Ui\Method\FormInterface { foreach($entity::resolveEntity()->fieldList() as $key => $property) { $field = $property->getAttribute(Field::class)->object; - if (! $field->readonly || ! $entity->isLoaded()) { $apiField = $property->getAttribute(EntityField::class)->object ?? null; - if ($apiField) { $var = $apiField->field ?: $key; + if ( isset($context->{$var}) ) { if ($apiField->setterMethod) { # Use a setter method diff --git a/src/FormDescriptor.php b/src/FormDescriptor.php index b2306b1..fad6fda 100644 --- a/src/FormDescriptor.php +++ b/src/FormDescriptor.php @@ -15,7 +15,6 @@ class FormDescriptor foreach($forms as $form) { $fields = []; - $propertyHtml = ""; $formName = is_object($form) ? $form::class : $form; $reflector = new ObjectReflection($formName); @@ -24,17 +23,19 @@ class FormDescriptor foreach($reflector->reflectProperties() as $property) { $field = $property->getAttribute(ContextField::class); - if ($field) { + if ($field && ! $field->object->hidden ) { $types = $property->getTypes(); + $fields[] = [ 'name' => $property->name, 'description' => $field->object->description, - 'type' => $field->object->type ?? implode(' | ', array_map(fn($e) => $e->type, $types)), + 'type' => $field->object->type ?? $types, 'allowNulls' => $property->allowsNull(), - 'regexPattern' =>$field->object->regexFormat ?? "aucun", - 'minLength' =>$field->object->minLength ?? "aucune", - 'maxLength' =>$field->object->maxLength ?? "aucune", - 'example' =>$field->object->example, + 'regexPattern' => $field->object->regexFormat ?? null, + 'minLength' => $field->object->minLength ?? null, + 'maxLength' => $field->object->maxLength ?? null, + 'example' => $field->object->example ?? null, + 'values' => $this->generateExampleValues($types), ]; } } @@ -48,4 +49,17 @@ class FormDescriptor return $list; } + + protected function generateExampleValues(array $types) : string|array + { + $values = []; + + foreach($types as $type) { + if ( enum_exists($type->type) ) { + $values = array_merge($values, array_map(fn($e) => $e->value, $type->type::cases())); + } + } + + return $values; + } } \ No newline at end of file diff --git a/src/LeanApiTrait.php b/src/LeanApiTrait.php index 0bf08bb..c0f5100 100644 --- a/src/LeanApiTrait.php +++ b/src/LeanApiTrait.php @@ -2,14 +2,24 @@ namespace Lean\Api; +use DI\Attribute\Inject; use League\CommonMark\CommonMarkConverter; +use Lean\Api\Lib\ApiSearchRequestInterface; use Notes\Attribute\Ignore; +use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Ulmus\Entity\EntityInterface; +use Ulmus\EntityCollection; +use Ulmus\SearchRequest\SearchRequestInterface; trait LeanApiTrait { use DescriptorTrait; + #[Inject] + protected ContainerInterface $container; + #[Ignore] public function renderMarkdown(string $filepath, array $entities = [], array $forms = [], int $code = 200, array $headers = []) : ResponseInterface { @@ -22,10 +32,15 @@ trait LeanApiTrait if (str_contains($markdown, '{route:descriptor}')) { $markdown = str_replace('{route:descriptor}', $this->renderRawView('lean-api/route_descriptor', [ - 'routes' => (new RouteDescriptor($this, $this->picea->compiler->getExtensionFromToken('url')))->getRoutes() + 'routes' => (new RouteDescriptor($this, $this->picea->compiler->getExtensionFromToken('url'), $this->container))->getRoutes() ]), $markdown); } + if (str_contains($markdown, '{request:debugger}')) + { + $markdown = str_replace('{request:debugger}', $this->renderRawView('lean-api/request_debugger', []), $markdown); + } + if (str_contains($markdown, '{entity:descriptor}')) { $markdown = str_replace('{entity:descriptor}', $this->renderRawView('lean-api/entity_descriptor', [ @@ -44,10 +59,81 @@ trait LeanApiTrait } #[Ignore] - protected function output(\JsonSerializable|array $data, int $count) { + protected function output(\JsonSerializable|array $data, int $count) : ResponseInterface + { return $this->renderJson([ 'data' => $data, 'count' => $count, ]); } + + #[Ignore] + protected function outputSearchResult(ApiSearchRequestInterface $apiSearchRequest) : ResponseInterface + { + return $this->output($apiSearchRequest->getResult(), $apiSearchRequest->getSearch()->count); + } + + #[Ignore] + protected function searchEntityFromRequest(ServerRequestInterface $request, string $entityType, ? string $resultKey = null) : ServerRequestInterface + { + $search = $entityType::searchRequest()->fromRequest($request); + $entity = $entityType::repository()->filterServerRequest($search)->loadOne() ?? false; + + if (! $entity ) { + throw new \InvalidArgumentException(sprintf("L'entré pour l'entité demandé (%s) est introuvable avec le ou les arguments fournis '%s'", $entityType, json_encode($request->getAttributes()))); + } + + return $request->withAttribute("lean.searchRequest", + [ + $resultKey ?: $entityType => new class($search, $entity) implements ApiSearchRequestInterface { + public function __construct( + public readonly SearchRequestInterface $search, + public readonly EntityInterface $entity, + ) {} + + public function getSearch(): SearchRequestInterface + { + return $this->search; + } + + public function getResult(): EntityCollection|EntityInterface + { + return $this->entity; + } + } + ] + $request->getAttribute("lean.searchRequest", []) + ); + } + + #[Ignore] + protected function searchEntitiesFromRequest(ServerRequestInterface $request, string $entityType, ? string $resultKey = null) : ServerRequestInterface + { + $search = $entityType::searchRequest()->fromRequest($request); + $entity = $entityType::repository()->filterServerRequest($search)->loadAll(); + + if (! $entity ) { + throw new \InvalidArgumentException(sprintf("L'entré pour l'entité demandé (%s) est introuvable avec le ou les arguments fournis '%s'", $entityType, json_encode($request->getAttributes()))); + } + + return $request->withAttribute("lean.searchRequest", + [ + $resultKey ?: $entityType => new class($search, $entity) implements ApiSearchRequestInterface { + public function __construct( + public readonly SearchRequestInterface $search, + public readonly EntityCollection $collection, + ) {} + + public function getSearch(): SearchRequestInterface + { + return $this->search; + } + + public function getResult(): EntityCollection|EntityInterface + { + return $this->collection; + } + } + ] + $request->getAttribute("lean.searchRequest", []) + ); + } } \ No newline at end of file diff --git a/src/Lib/ApiSearchRequestInterface.php b/src/Lib/ApiSearchRequestInterface.php new file mode 100644 index 0000000..07031f3 --- /dev/null +++ b/src/Lib/ApiSearchRequestInterface.php @@ -0,0 +1,13 @@ +handle($request); } - catch(\Exception $ex) { - if ( ! getenv('DEBUG') ) { + catch(\Throwable $ex) { + $errorId = $this->saveError($request, $ex)->id; + + if ($this->awaitingJson($request)) { return HttpFactory::createJsonResponse([ 'status' => 'failed', - 'message' => $ex->getMessage(), 'ts' => time(), - ], 400); - } - else { - throw $ex; + 'data' => [ + # 'error_id' => $errorId, + 'message' => $ex->getMessage(), + 'file' => $ex->getFile(), + 'line' => $ex->getLine(), + 'code' => $ex->getCode(), + 'backtrace' => $ex->getTrace(), + ] + ], 500); } + + throw $ex; } if ($response instanceof JsonResponse) { @@ -43,4 +58,30 @@ class ApiRenderer implements MiddlewareInterface { return $response; } + + protected function saveError(ServerRequestInterface $request, \Throwable $exception) : Entity\Error + { + $request = $request->withMethod('PUT')->withParsedBody([]); + + $context = $this->debugFormFactory->errorSaveContext($request); + + $context->sets([ + 'message' => $exception->getMessage(), + 'code' => $exception->getCode(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => $exception->getTrace(), + ]); + + $form = $this->debugFormFactory->errorSave($entity = new Entity\Error()); + + new FormHandler($request, $form, $context); + + return $entity; + } + + protected function awaitingJson(ServerRequestInterface $request) : bool + { + return str_contains(strtolower($request->getHeaderLine('content-type')), 'json'); + } } diff --git a/src/RouteDescriptor.php b/src/RouteDescriptor.php index 2031254..ff0cd61 100644 --- a/src/RouteDescriptor.php +++ b/src/RouteDescriptor.php @@ -5,22 +5,27 @@ namespace Lean\Api; use Lean\Factory\HttpFactory; use Notes\ObjectReflection; use Notes\Route\Attribute\Method\Route; +use Notes\Security\SecurityHandler; use Picea\Extension\UrlExtension; +use Psr\Container\ContainerInterface; +use Taxus\Taxus; class RouteDescriptor { - public string $routeLine = << - %s - %s - %s - %s - -HTML; + protected SecurityHandler $securityHandler; + + protected Taxus $taxus; public function __construct( public object $controller, protected UrlExtension $urlExtension, - ) {} + ContainerInterface $container + ) { + if ($container->has(SecurityHandler::class) && $container->has(Taxus::class)) { + $this->securityHandler = $container->get(SecurityHandler::class); + $this->taxus = $container->get(Taxus::class); + } + } public function getRoutes() : array { @@ -47,6 +52,7 @@ HTML; 'description'=> $route->description, #'methods' =>implode(', ', (array)$route->method), 'methods' => (array) $route->method, + 'privileges' => $this->getPrivilegeFromRoute($method->name), ]; } } @@ -66,4 +72,32 @@ HTML; return implode('/', $paths); } + + /** + * @param string $method Which method are we inspecting + * @return false|array|null Returns NULL when securityHandler is undefined, FALSE when no security applies for this method and else an array of ['admin' => [ 'status' => true, 'description' => "' ], ...]. + */ + protected function getPrivilegeFromRoute(string $method) : null|false|array + { + if ( isset($this->securityHandler) ){ + $list = []; + + if ( $this->securityHandler->isLocked($this->controller::class, $method) ) { + foreach($this->taxus->list as $name => $definition) { + if ($definition[0]->testableArguments !== null) { + if ( $this->securityHandler->hasGrantPermission($this->controller::class, $method, ...$definition[0]->testableArguments) ) { + + } + } + } + } + else { + return false; + } + + return $list; + } + + return false; + } } \ No newline at end of file diff --git a/view/lean-api/form_descriptor.phtml b/view/lean-api/form_descriptor.phtml index 7681698..b2d1c97 100644 --- a/view/lean-api/form_descriptor.phtml +++ b/view/lean-api/form_descriptor.phtml @@ -6,8 +6,10 @@
{% foreach $forms as $name => $form %} -
-

{{ $name }}

+
+

+ {{ $name }} +

{{ $form['description'] }}

@@ -25,12 +27,24 @@
Variable POST / champ JSON : {{ $field['name'] }}
-
Type(s) : {{ $field['type'] }}
+
Type(s) : {{ implode(' | ', array_map(fn($e) => $e->type, $field['type'])) }}
Nullable : {{ yesOrNo($field['allowNulls']) }}
-
Pattern regex : {{ $field['regexPattern'] }}
-
Taille min. : {{ $field['minLength'] }}
-
Taille max. : {{ $field['maxLength'] }}
-
Exemple : {{ $field['example'] }}
+ {% if $field['regexPattern'] !== null %} +
Pattern regex : {{ $field['regexPattern'] }}
+ {% endif %} + {% if $field['minLength'] !== null %} +
Taille min. : {{ $field['minLength'] }}
+ {% endif %} + {% if $field['maxLength'] !== null %} +
Taille max. : {{ $field['maxLength'] }}
+ {% endif %} + {% if $field['example'] !== null || $field['values'] %} +
Exemple : {{ $field['example'] }}
+ {% endif %} + + {% if $field['values'] %} +
Valeurs possibles : [ {{= implode(', ', $field['values']) }} ]
+ {% endif %}
{% endforeach %} diff --git a/view/lean-api/request_debugger.phtml b/view/lean-api/request_debugger.phtml new file mode 100644 index 0000000..0675d5d --- /dev/null +++ b/view/lean-api/request_debugger.phtml @@ -0,0 +1,241 @@ +{% use Ulmus\Api\Common\MethodEnum %} + +{% language.set "lean.api.request.debugger" %} + +
+
+
+ {% ui.select "method", MethodEnum::generateArray(), post('method') %} +
+ +
+ {% ui:text "url" %} + +
+
+ +
+
{}
+
+ +
+
+

+
+        
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/view/lean-api/route_descriptor.phtml b/view/lean-api/route_descriptor.phtml index bc9fb28..06fed63 100644 --- a/view/lean-api/route_descriptor.phtml +++ b/view/lean-api/route_descriptor.phtml @@ -3,19 +3,33 @@
    {% foreach $routes as $route %} {% foreach $route['methods'] as $method %} -
  • +
  • {{ strtoupper($method) }} + {{ $route['cleaned'] }} - {{= $route['description'] }} - {{ $route['name'] }} + + + {{ $route['name'] }} +
    + + {% foreach ['admin', 'school' ] as $privilege %} + {{ $privilege }} + {% endforeach %} + +
  • {% or %} {% _ "none" %} {% endforeach %} {% endforeach %} -
\ No newline at end of file + + + \ No newline at end of file diff --git a/view/lean/layout/docs.phtml b/view/lean/layout/docs.phtml index b838b8b..267738a 100644 --- a/view/lean/layout/docs.phtml +++ b/view/lean/layout/docs.phtml @@ -16,6 +16,7 @@