diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fe1e45..32ce4ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,33 @@ ## [Unreleased] +### Added + +- `Innmind\Framework\Http\Route` +- `Innmind\Framework\Http\Route\Reference` +- `Innmind\Framework\Application::mapRoute()` +- `Innmind\Framework\Application::routes(class-string)` +- `Innmind\Framework\Application::recoverRouteError()` + ### Changed - Requires `innmind/foundation:~1.9` - Requires `innmind/di:~3.0` - `Innmind\Framework\Application::route()` callable must now return a `Innmind\Router\Component` +- `Innmind\Framework\Application::route()` callable first parameter now is a `Innmint\Router\Pipe` +- `Innmind\Framework\Application::route()` first parameter must now be expressed via a component inside the callable +- `Innminf\Framework\Application::notFoundRequestHandler()` callable must now return an `Innmind\Immutable\Attempt` +- `Innminf\Framework\Application::notFoundRequestHandler()` has been renamed `::routeNotFound()` ### Removed - The ability to use `string`s to reference services - `Innmind\Framework\Http\Service` +- `Innmind\Framework\Http\To` +- `Innmind\Framework\Http\Routes` +- `Innmind\Framework\Application::appendRoutes()` +- `Innmind\Framework\Application::mapRequestHandler()` +- `Innmind\Framework\Http\RequestHandler` ### Fixed diff --git a/composer.json b/composer.json index 41de291..dc083da 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "php": "~8.2", "innmind/foundation": "~1.9", "innmind/di": "~3.0", - "innmind/router": "^5.0.1" + "innmind/router": "~5.2" }, "autoload": { "psr-4": { diff --git a/fixtures/server.php b/fixtures/server.php index d650585..5fca289 100644 --- a/fixtures/server.php +++ b/fixtures/server.php @@ -6,25 +6,19 @@ use Innmind\Framework\{ Application, Main\Async\Http, - Http\Routes, -}; -use Innmind\Router\{ - Method, - Endpoint, - Handle, - Respond, }; +use Innmind\Router\Respond; use Innmind\Http\Response\StatusCode; -use Innmind\Immutable\Attempt; new class extends Http { protected function configure(Application $app): Application { - return $app->appendRoutes(static fn($routes) => $routes->add( - Method::get() - ->pipe(Endpoint::of('/hello')) + return $app->route( + static fn($pipe) => $pipe + ->get() + ->endpoint('/hello') ->pipe(Respond::with(StatusCode::ok)), - )); + ); } }; diff --git a/src/Application.php b/src/Application.php index 1bf8918..03b6290 100644 --- a/src/Application.php +++ b/src/Application.php @@ -3,10 +3,6 @@ namespace Innmind\Framework; -use Innmind\Framework\Http\{ - Routes, - RequestHandler, -}; use Innmind\OperatingSystem\OperatingSystem; use Innmind\CLI\{ Environment as CliEnv, @@ -16,7 +12,10 @@ Container, Service, }; -use Innmind\Router\Component; +use Innmind\Router\{ + Component, + Pipe, +}; use Innmind\Http\{ ServerRequest, Response, @@ -147,50 +146,71 @@ public function mapCommand(callable $map): self /** * @psalm-mutation-free * - * @param literal-string $pattern - * @param callable(Container, OperatingSystem, Environment): Component $handle + * @param Http\Route\Reference|callable(Pipe, Container, OperatingSystem, Environment): Component $handle + * + * @return self + */ + public function route(Http\Route\Reference|callable $handle): self + { + if ($handle instanceof Http\Route\Reference) { + $handle = $handle->route(); + } + + return new self($this->app->route($handle)); + } + + /** + * @psalm-mutation-free + * + * @param class-string $routes * * @return self */ - public function route(string $pattern, callable $handle): self + public function routes(string $routes): self { - return new self($this->app->route($pattern, $handle)); + $self = $this; + + foreach ($routes::cases() as $route) { + $self = $self->route($route); + } + + return $self; } /** * @psalm-mutation-free * - * @param callable(Routes, Container, OperatingSystem, Environment): Routes $append + * @param callable(Component, Container): Component $map * * @return self */ - public function appendRoutes(callable $append): self + public function mapRoute(callable $map): self { - return new self($this->app->appendRoutes($append)); + return new self($this->app->mapRoute($map)); } /** * @psalm-mutation-free * - * @param callable(RequestHandler, Container, OperatingSystem, Environment): RequestHandler $map + * @param callable(ServerRequest, Container, OperatingSystem, Environment): Attempt $handle * * @return self */ - public function mapRequestHandler(callable $map): self + public function routeNotFound(callable $handle): self { - return new self($this->app->mapRequestHandler($map)); + return new self($this->app->routeNotFound($handle)); } /** * @psalm-mutation-free * - * @param callable(ServerRequest, Container, OperatingSystem, Environment): Response $handle + * @param callable(ServerRequest, \Throwable, Container): Attempt $recover * * @return self */ - public function notFoundRequestHandler(callable $handle): self + public function recoverRouteError(callable $recover): self { - return new self($this->app->notFoundRequestHandler($handle)); + return new self($this->app->recoverRouteError($recover)); } /** diff --git a/src/Application/Async/Http.php b/src/Application/Async/Http.php index 4d08e46..1ee4f1c 100644 --- a/src/Application/Async/Http.php +++ b/src/Application/Async/Http.php @@ -6,9 +6,7 @@ use Innmind\Framework\{ Environment, Application\Implementation, - Http\Routes, Http\Router, - Http\RequestHandler, }; use Innmind\CLI\{ Environment as CliEnv, @@ -22,8 +20,8 @@ Service, }; use Innmind\Router\{ - Method, - Endpoint, + Component, + Pipe, }; use Innmind\Http\{ ServerRequest, @@ -33,6 +31,7 @@ Maybe, Sequence, Attempt, + SideEffect, }; /** @@ -47,17 +46,19 @@ final class Http implements Implementation * * @param \Closure(OperatingSystem, Environment): array{OperatingSystem, Environment} $map * @param \Closure(OperatingSystem, Environment): Builder $container - * @param Sequence $routes - * @param \Closure(RequestHandler, Container, OperatingSystem, Environment): RequestHandler $mapRequestHandler - * @param Maybe $notFound + * @param Sequence> $routes + * @param \Closure(Component, Container): Component $mapRoute + * @param Maybe> $notFound + * @param \Closure(ServerRequest, \Throwable, Container): Attempt $recover */ private function __construct( private OperatingSystem $os, private \Closure $map, private \Closure $container, private Sequence $routes, - private \Closure $mapRequestHandler, + private \Closure $mapRoute, private Maybe $notFound, + private \Closure $recover, ) { } @@ -66,16 +67,17 @@ private function __construct( */ public static function of(OperatingSystem $os): self { - /** @var Maybe */ + /** @var Maybe> */ $notFound = Maybe::nothing(); return new self( $os, static fn(OperatingSystem $os, Environment $env) => [$os, $env], static fn() => Builder::new(), - Sequence::of(), - static fn(RequestHandler $handler) => $handler, + Sequence::lazyStartingWith(), + static fn(Component $component) => $component, $notFound, + static fn(ServerRequest $request, \Throwable $e) => Attempt::error($e), ); } @@ -97,8 +99,9 @@ static function(OperatingSystem $os, Environment $env) use ($previous, $map): ar }, $this->container, $this->routes, - $this->mapRequestHandler, + $this->mapRoute, $this->notFound, + $this->recover, ); } @@ -120,8 +123,9 @@ static function(OperatingSystem $os, Environment $env) use ($previous, $map): ar }, $this->container, $this->routes, - $this->mapRequestHandler, + $this->mapRoute, $this->notFound, + $this->recover, ); } @@ -141,8 +145,9 @@ public function service(Service $name, callable $definition): self static fn($service) => $definition($service, $os, $env), ), $this->routes, - $this->mapRequestHandler, + $this->mapRoute, $this->notFound, + $this->recover, ); } @@ -168,38 +173,16 @@ public function mapCommand(callable $map): self * @psalm-mutation-free */ #[\Override] - public function route(string $pattern, callable $handle): self + public function route(callable $handle): self { - /** - * @psalm-suppress PossiblyUndefinedArrayOffset Todo better typing - * @var literal-string $path - */ - [$method, $path] = \explode(' ', $pattern, 2); - - $method = match ($method) { - 'get', 'GET' => Method::get(), - 'post', 'POST' => Method::post(), - 'put', 'PUT' => Method::put(), - 'patch', 'PATCH' => Method::patch(), - 'delete', 'DELETE' => Method::delete(), - 'options', 'OPTIONS' => Method::options(), - 'trace', 'TRACE' => Method::trace(), - 'connect', 'CONNECT' => Method::connect(), - 'head', 'HEAD' => Method::head(), - 'link', 'LINK' => Method::link(), - 'unlink', 'UNLINK' => Method::unlink(), - }; - - /** - * @psalm-suppress MixedArgumentTypeCoercion - * @psalm-suppress InvalidArgument - */ - return $this->appendRoutes( - static fn($routes, $container, $os, $env) => $routes->add( - $method - ->pipe(Endpoint::of($path)) - ->pipe($handle($container, $os, $env)), - ), + return new self( + $this->os, + $this->map, + $this->container, + ($this->routes)($handle), + $this->mapRoute, + $this->notFound, + $this->recover, ); } @@ -207,15 +190,21 @@ public function route(string $pattern, callable $handle): self * @psalm-mutation-free */ #[\Override] - public function appendRoutes(callable $append): self + public function mapRoute(callable $map): self { + $previous = $this->mapRoute; + return new self( $this->os, $this->map, $this->container, - ($this->routes)($append), - $this->mapRequestHandler, + $this->routes, + static fn($component, $get) => $map( + $previous($component, $get), + $get, + ), $this->notFound, + $this->recover, ); } @@ -223,27 +212,16 @@ public function appendRoutes(callable $append): self * @psalm-mutation-free */ #[\Override] - public function mapRequestHandler(callable $map): self + public function routeNotFound(callable $handle): self { - $previous = $this->mapRequestHandler; - return new self( $this->os, $this->map, $this->container, $this->routes, - static fn( - RequestHandler $handler, - Container $container, - OperatingSystem $os, - Environment $env, - ) => $map( - $previous($handler, $container, $os, $env), - $container, - $os, - $env, - ), - $this->notFound, + $this->mapRoute, + Maybe::just($handle), + $this->recover, ); } @@ -251,15 +229,20 @@ public function mapRequestHandler(callable $map): self * @psalm-mutation-free */ #[\Override] - public function notFoundRequestHandler(callable $handle): self + public function recoverRouteError(callable $recover): self { + $previous = $this->recover; + return new self( $this->os, $this->map, $this->container, $this->routes, - $this->mapRequestHandler, - Maybe::just($handle), + $this->mapRoute, + $this->notFound, + static fn($request, $e, $container) => $previous($request, $e, $container)->recover( + static fn($e) => $recover($request, $e, $container), + ), ); } @@ -270,7 +253,8 @@ public function run($input) $container = $this->container; $routes = $this->routes; $notFound = $this->notFound; - $mapRequestHandler = $this->mapRequestHandler; + $mapRoute = $this->mapRoute; + $recover = $this->recover; $run = Commands::of(Serve::of( $this->os, @@ -279,20 +263,16 @@ static function(ServerRequest $request, OperatingSystem $os) use ( $container, $routes, $notFound, - $mapRequestHandler, + $mapRoute, + $recover, ): Response { $env = Environment::http($request->environment()); [$os, $env] = $map($os, $env); $container = $container($os, $env)->build(); - $routes = Sequence::lazyStartingWith($routes) - ->flatMap(static fn($routes) => $routes) - ->map(static fn($provide) => $provide( - Routes::lazy(), - $container, - $os, - $env, - )) - ->flatMap(static fn($routes) => $routes->toSequence()); + $pipe = Pipe::new(); + $routes = $routes + ->map(static fn($handle) => $handle($pipe, $container, $os, $env)) + ->map(static fn($component) => $mapRoute($component, $container)); $router = new Router( $routes, $notFound->map( @@ -303,10 +283,10 @@ static function(ServerRequest $request, OperatingSystem $os) use ( $env, ), ), + static fn($request, $e) => $recover($request, $e, $container), ); - $handle = $mapRequestHandler($router, $container, $os, $env); - return $handle($request); + return $router($request); }, )); diff --git a/src/Application/Cli.php b/src/Application/Cli.php index 63574a2..350ea25 100644 --- a/src/Application/Cli.php +++ b/src/Application/Cli.php @@ -158,7 +158,7 @@ public function mapCommand(callable $map): self * @psalm-mutation-free */ #[\Override] - public function route(string $pattern, callable $handle): self + public function route(callable $handle): self { return $this; } @@ -167,7 +167,7 @@ public function route(string $pattern, callable $handle): self * @psalm-mutation-free */ #[\Override] - public function appendRoutes(callable $append): self + public function mapRoute(callable $map): self { return $this; } @@ -176,7 +176,7 @@ public function appendRoutes(callable $append): self * @psalm-mutation-free */ #[\Override] - public function mapRequestHandler(callable $map): self + public function routeNotFound(callable $handle): self { return $this; } @@ -185,7 +185,7 @@ public function mapRequestHandler(callable $map): self * @psalm-mutation-free */ #[\Override] - public function notFoundRequestHandler(callable $handle): self + public function recoverRouteError(callable $recover): self { return $this; } diff --git a/src/Application/Http.php b/src/Application/Http.php index 656cb94..bbe3131 100644 --- a/src/Application/Http.php +++ b/src/Application/Http.php @@ -5,9 +5,7 @@ use Innmind\Framework\{ Environment, - Http\Routes, Http\Router, - Http\RequestHandler, }; use Innmind\OperatingSystem\OperatingSystem; use Innmind\DI\{ @@ -16,8 +14,8 @@ Service, }; use Innmind\Router\{ - Method, - Endpoint, + Component, + Pipe, }; use Innmind\Http\{ ServerRequest, @@ -26,6 +24,8 @@ use Innmind\Immutable\{ Maybe, Sequence, + SideEffect, + Attempt, }; /** @@ -38,17 +38,19 @@ final class Http implements Implementation * @psalm-mutation-free * * @param \Closure(OperatingSystem, Environment): Builder $container - * @param Sequence $routes - * @param \Closure(RequestHandler, Container, OperatingSystem, Environment): RequestHandler $mapRequestHandler - * @param Maybe $notFound + * @param Sequence> $routes + * @param \Closure(Component, Container): Component $mapRoute + * @param Maybe> $notFound + * @param \Closure(ServerRequest, \Throwable, Container): Attempt $recover */ private function __construct( private OperatingSystem $os, private Environment $env, private \Closure $container, private Sequence $routes, - private \Closure $mapRequestHandler, + private \Closure $mapRoute, private Maybe $notFound, + private \Closure $recover, ) { } @@ -57,16 +59,17 @@ private function __construct( */ public static function of(OperatingSystem $os, Environment $env): self { - /** @var Maybe */ + /** @var Maybe> */ $notFound = Maybe::nothing(); return new self( $os, $env, static fn() => Builder::new(), - Sequence::of(), - static fn(RequestHandler $handler) => $handler, + Sequence::lazyStartingWith(), + static fn(Component $component) => $component, $notFound, + static fn(ServerRequest $request, \Throwable $e) => Attempt::error($e), ); } @@ -82,8 +85,9 @@ public function mapEnvironment(callable $map): self $map($this->env, $this->os), $this->container, $this->routes, - $this->mapRequestHandler, + $this->mapRoute, $this->notFound, + $this->recover, ); } @@ -99,8 +103,9 @@ public function mapOperatingSystem(callable $map): self $this->env, $this->container, $this->routes, - $this->mapRequestHandler, + $this->mapRoute, $this->notFound, + $this->recover, ); } @@ -120,8 +125,9 @@ public function service(Service $name, callable $definition): self static fn($service) => $definition($service, $os, $env), ), $this->routes, - $this->mapRequestHandler, + $this->mapRoute, $this->notFound, + $this->recover, ); } @@ -147,38 +153,16 @@ public function mapCommand(callable $map): self * @psalm-mutation-free */ #[\Override] - public function route(string $pattern, callable $handle): self + public function route(callable $handle): self { - /** - * @psalm-suppress PossiblyUndefinedArrayOffset Todo better typing - * @var literal-string $path - */ - [$method, $path] = \explode(' ', $pattern, 2); - - $method = match ($method) { - 'get', 'GET' => Method::get(), - 'post', 'POST' => Method::post(), - 'put', 'PUT' => Method::put(), - 'patch', 'PATCH' => Method::patch(), - 'delete', 'DELETE' => Method::delete(), - 'options', 'OPTIONS' => Method::options(), - 'trace', 'TRACE' => Method::trace(), - 'connect', 'CONNECT' => Method::connect(), - 'head', 'HEAD' => Method::head(), - 'link', 'LINK' => Method::link(), - 'unlink', 'UNLINK' => Method::unlink(), - }; - - /** - * @psalm-suppress MixedArgumentTypeCoercion - * @psalm-suppress InvalidArgument - */ - return $this->appendRoutes( - static fn($routes, $container, $os, $env) => $routes->add( - $method - ->pipe(Endpoint::of($path)) - ->pipe($handle($container, $os, $env)), - ), + return new self( + $this->os, + $this->env, + $this->container, + ($this->routes)($handle), + $this->mapRoute, + $this->notFound, + $this->recover, ); } @@ -186,15 +170,21 @@ public function route(string $pattern, callable $handle): self * @psalm-mutation-free */ #[\Override] - public function appendRoutes(callable $append): self + public function mapRoute(callable $map): self { + $previous = $this->mapRoute; + return new self( $this->os, $this->env, $this->container, - ($this->routes)($append), - $this->mapRequestHandler, + $this->routes, + static fn($component, $get) => $map( + $previous($component, $get), + $get, + ), $this->notFound, + $this->recover, ); } @@ -202,27 +192,16 @@ public function appendRoutes(callable $append): self * @psalm-mutation-free */ #[\Override] - public function mapRequestHandler(callable $map): self + public function routeNotFound(callable $handle): self { - $previous = $this->mapRequestHandler; - return new self( $this->os, $this->env, $this->container, $this->routes, - static fn( - RequestHandler $handler, - Container $container, - OperatingSystem $os, - Environment $env, - ) => $map( - $previous($handler, $container, $os, $env), - $container, - $os, - $env, - ), - $this->notFound, + $this->mapRoute, + Maybe::just($handle), + $this->recover, ); } @@ -230,15 +209,20 @@ public function mapRequestHandler(callable $map): self * @psalm-mutation-free */ #[\Override] - public function notFoundRequestHandler(callable $handle): self + public function recoverRouteError(callable $recover): self { + $previous = $this->recover; + return new self( $this->os, $this->env, $this->container, $this->routes, - $this->mapRequestHandler, - Maybe::just($handle), + $this->mapRoute, + $this->notFound, + static fn($request, $e, $container) => $previous($request, $e, $container)->recover( + static fn($e) => $recover($request, $e, $container), + ), ); } @@ -248,15 +232,13 @@ public function run($input) $container = ($this->container)($this->os, $this->env)->build(); $os = $this->os; $env = $this->env; - $routes = Sequence::lazyStartingWith($this->routes) - ->flatMap(static fn($routes) => $routes) - ->map(static fn($provide) => $provide( - Routes::lazy(), - $container, - $os, - $env, - )) - ->flatMap(static fn($routes) => $routes->toSequence()); + $mapRoute = $this->mapRoute; + $recover = $this->recover; + $pipe = Pipe::new(); + $routes = $this + ->routes + ->map(static fn($handle) => $handle($pipe, $container, $os, $env)) + ->map(static fn($component) => $mapRoute($component, $container)); $router = new Router( $routes, $this->notFound->map( @@ -267,9 +249,9 @@ public function run($input) $env, ), ), + static fn($request, $e) => $recover($request, $e, $container), ); - $handle = ($this->mapRequestHandler)($router, $container, $this->os, $this->env); - return $handle($input); + return $router($input); } } diff --git a/src/Application/Implementation.php b/src/Application/Implementation.php index 58644f0..0137121 100644 --- a/src/Application/Implementation.php +++ b/src/Application/Implementation.php @@ -3,11 +3,7 @@ namespace Innmind\Framework\Application; -use Innmind\Framework\{ - Environment, - Http\Routes, - Http\RequestHandler, -}; +use Innmind\Framework\Environment; use Innmind\OperatingSystem\OperatingSystem; use Innmind\CLI\{ Environment as CliEnv, @@ -17,7 +13,10 @@ Container, Service, }; -use Innmind\Router\Component; +use Innmind\Router\{ + Component, + Pipe, +}; use Innmind\Http\{ ServerRequest, Response, @@ -82,39 +81,38 @@ public function mapCommand(callable $map): self; /** * @psalm-mutation-free * - * @param literal-string $pattern - * @param callable(Container, OperatingSystem, Environment): Component $handle + * @param callable(Pipe, Container, OperatingSystem, Environment): Component $handle * * @return self */ - public function route(string $pattern, callable $handle): self; + public function route(callable $handle): self; /** * @psalm-mutation-free * - * @param callable(Routes, Container, OperatingSystem, Environment): Routes $append + * @param callable(Component, Container): Component $map * * @return self */ - public function appendRoutes(callable $append): self; + public function mapRoute(callable $map): self; /** * @psalm-mutation-free * - * @param callable(RequestHandler, Container, OperatingSystem, Environment): RequestHandler $map + * @param callable(ServerRequest, Container, OperatingSystem, Environment): Attempt $handle * * @return self */ - public function mapRequestHandler(callable $map): self; + public function routeNotFound(callable $handle): self; /** * @psalm-mutation-free * - * @param callable(ServerRequest, Container, OperatingSystem, Environment): Response $handle + * @param callable(ServerRequest, \Throwable, Container): Attempt $recover * * @return self */ - public function notFoundRequestHandler(callable $handle): self; + public function recoverRouteError(callable $recover): self; /** * @param I $input diff --git a/src/Http/RequestHandler.php b/src/Http/RequestHandler.php deleted file mode 100644 index f9a2fd7..0000000 --- a/src/Http/RequestHandler.php +++ /dev/null @@ -1,14 +0,0 @@ -)> $handler + * + * @return callable(Pipe, Container): Component + */ + public static function get( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->get() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler + * + * @return callable(Pipe, Container): Component + */ + public static function post( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->post() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler + * + * @return callable(Pipe, Container): Component + */ + public static function put( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->put() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler + * + * @return callable(Pipe, Container): Component + */ + public static function patch( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->patch() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler + * + * @return callable(Pipe, Container): Component + */ + public static function delete( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->delete() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler + * + * @return callable(Pipe, Container): Component + */ + public static function options( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->options() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler + * + * @return callable(Pipe, Container): Component + */ + public static function trace( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->trace() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler + * + * @return callable(Pipe, Container): Component + */ + public static function connect( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->connect() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler + * + * @return callable(Pipe, Container): Component + */ + public static function head( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->head() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler + * + * @return callable(Pipe, Container): Component + */ + public static function link( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->link() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } + + /** + * @psalm-pure + * + * @param literal-string|Template|Alias $endpoint + * @param Service)> $handler + * + * @return callable(Pipe, Container): Component + */ + public static function unlink( + string|Template|Alias $endpoint, + Service $handler, + ): callable { + return static fn(Pipe $pipe, Container $get) => $pipe + ->endpoint($endpoint) + ->unlink() + ->spread() + ->handle(Proxy::of( + static fn() => $get($handler), + )); + } +} diff --git a/src/Http/Route/Reference.php b/src/Http/Route/Reference.php new file mode 100644 index 0000000..a25e4e3 --- /dev/null +++ b/src/Http/Route/Reference.php @@ -0,0 +1,25 @@ + + */ + public function route(): callable; +} diff --git a/src/Http/Router.php b/src/Http/Router.php index 8700952..d1c3ba9 100644 --- a/src/Http/Router.php +++ b/src/Http/Router.php @@ -25,30 +25,36 @@ /** * @internal */ -final class Router implements RequestHandler +final class Router { /** * @param Sequence> $routes - * @param Maybe<\Closure(ServerRequest): Response> $notFound + * @param Maybe<\Closure(ServerRequest): Attempt> $notFound + * @param \Closure(ServerRequest, \Throwable): Attempt $recover */ public function __construct( private Sequence $routes, private Maybe $notFound, + private \Closure $recover, ) { } - #[\Override] public function __invoke(ServerRequest $request): Response { + $recover = $this->recover; + /** * @psalm-suppress MixedArgumentTypeCoercion */ $route = Route::of( Any::from($this->routes) ->otherwise(Respond::withHttpErrors()) + ->otherwise(static fn($e) => Handle::via( + static fn($request) => $recover($request, $e), + )) ->or(Handle::via( fn($request, SideEffect $_) => $this->notFound->match( - static fn($handle) => Attempt::result($handle($request)), + static fn($handle) => $handle($request), static fn() => Attempt::result(Response::of( StatusCode::notFound, $request->protocolVersion(), diff --git a/src/Http/Routes.php b/src/Http/Routes.php deleted file mode 100644 index abb6a94..0000000 --- a/src/Http/Routes.php +++ /dev/null @@ -1,58 +0,0 @@ -> $routes - */ - private function __construct(private Sequence $routes) - { - } - - /** - * @psalm-pure - */ - public static function lazy(): self - { - return new self(Sequence::lazyStartingWith()); - } - - /** - * @param Component $route - */ - public function add(Component $route): self - { - return new self(($this->routes)($route)); - } - - /** - * @param Sequence> $routes - */ - public function append(Sequence $routes): self - { - return new self($this->routes->append($routes)); - } - - /** - * @internal - * - * @return Sequence> - */ - public function toSequence(): Sequence - { - return $this->routes; - } -} diff --git a/src/Http/To.php b/src/Http/To.php deleted file mode 100644 index ba1a100..0000000 --- a/src/Http/To.php +++ /dev/null @@ -1,50 +0,0 @@ -, Response> - */ - public function __invoke( - Container $container, - OperatingSystem $os, - Environment $env, - ): Component { - $service = $this->service; - - /** - * @psalm-suppress MissingClosureReturnType - * @psalm-suppress MixedArgumentTypeCoercion - * @psalm-suppress InvalidFunctionCall - * @todo fix non lazy call - */ - return Handle::of( - $container($service), - ); - } - - public static function service(Service $service): self - { - return new self($service); - } -} diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index d48067f..d273e9d 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -9,8 +9,7 @@ Environment, Middleware\Optional, Middleware\LoadDotEnv, - Http\RequestHandler, - Http\To, + Http\Route, }; use Innmind\OperatingSystem\Factory; use Innmind\CLI\{ @@ -20,20 +19,13 @@ Console, }; use Innmind\DI\Service; -use Innmind\Router\{ - Endpoint, - Method as RouterMethod, - Handle, -}; use Innmind\Http\{ ServerRequest, Response, Method, Response\StatusCode, ProtocolVersion, - Header\ContentType, }; -use Innmind\MediaType\MediaType; use Innmind\Url\{ Url, Path, @@ -57,6 +49,20 @@ enum Services implements Service case serviceB; } +enum Routes implements Route\Reference +{ + case a; + case b; + + public function route(): callable + { + return match ($this) { + self::a => Route::get('/foo', Services::serviceA), + self::b => Route::get('/bar', Services::serviceB), + }; + } +} + class ApplicationTest extends TestCase { use BlackBox; @@ -850,24 +856,26 @@ public function testMatchRoutes(): BlackBox\Proof $responseB = Response::of(StatusCode::ok, $protocol); $app = Application::http(Factory::build(), Environment::test($variables)) - ->appendRoutes(fn($routes) => $routes->add( - RouterMethod::get() - ->pipe(Endpoint::of('/foo')) - ->pipe(Handle::via(function($request) use ($protocol, $responseA) { + ->route( + fn($pipe) => $pipe + ->get() + ->endpoint('/foo') + ->handle(function($request) use ($protocol, $responseA) { $this->assertSame($protocol, $request->protocolVersion()); return Attempt::result($responseA); - })), - )) - ->appendRoutes(fn($routes) => $routes->add( - RouterMethod::get() - ->pipe(Endpoint::of('/bar')) - ->pipe(Handle::via(function($request) use ($protocol, $responseB) { - $this->assertSame($protocol, $request->protocolVersion()); - - return Attempt::result($responseB); - })), - )); + }) + ->or( + $pipe + ->get() + ->endpoint('/bar') + ->handle(function($request) use ($protocol, $responseB) { + $this->assertSame($protocol, $request->protocolVersion()); + + return Attempt::result($responseB); + }), + ), + ); $response = $app->run(ServerRequest::of( Url::of('/foo'), @@ -905,16 +913,26 @@ public function testRouteShortDeclaration(): BlackBox\Proof $responseB = Response::of(StatusCode::ok, $protocol); $app = Application::http(Factory::build(), Environment::test($variables)) - ->route('GET /foo', fn() => Handle::via(function($request) use ($protocol, $responseA) { - $this->assertSame($protocol, $request->protocolVersion()); + ->route( + fn($pipe) => $pipe + ->get() + ->endpoint('/foo') + ->handle(function($request) use ($protocol, $responseA) { + $this->assertSame($protocol, $request->protocolVersion()); - return Attempt::result($responseA); - })) - ->route('GET /bar', fn() => Handle::via(function($request) use ($protocol, $responseB) { - $this->assertSame($protocol, $request->protocolVersion()); + return Attempt::result($responseA); + }), + ) + ->route( + fn($pipe) => $pipe + ->post() + ->endpoint('/bar') + ->handle(function($request) use ($protocol, $responseB) { + $this->assertSame($protocol, $request->protocolVersion()); - return Attempt::result($responseB); - })); + return Attempt::result($responseB); + }), + ); $response = $app->run(ServerRequest::of( Url::of('/foo'), @@ -926,7 +944,7 @@ public function testRouteShortDeclaration(): BlackBox\Proof $response = $app->run(ServerRequest::of( Url::of('/bar'), - Method::get, + Method::post, $protocol, )); @@ -951,7 +969,12 @@ public function testRouteToService(): BlackBox\Proof $expected = Response::of(StatusCode::ok, $protocol); $app = Application::http(Factory::build(), Environment::test($variables)) - ->route('GET /foo', To::service(Services::responseHandler)) + ->route( + static fn($pipe, $container) => $pipe + ->get() + ->endpoint('/foo') + ->handle($container(Services::responseHandler)), + ) ->service(Services::responseHandler, static fn() => new class($expected) { public function __construct(private $response) { @@ -973,11 +996,10 @@ public function __invoke() }); } - public function testMapRequestHandler(): BlackBox\Proof + public function testRouteToServiceShortcut(): BlackBox\Proof { return $this ->forAll( - FUrl::any(), Set::of(...Method::cases()), Set::of(...ProtocolVersion::cases()), Set::sequence( @@ -988,44 +1010,32 @@ public function testMapRequestHandler(): BlackBox\Proof ), )->between(0, 10), ) - ->prove(function($url, $method, $protocol, $variables) { + ->prove(function($method, $protocol, $variables) { + $expected = Response::of(StatusCode::ok, $protocol); + $app = Application::http(Factory::build(), Environment::test($variables)) - ->mapRequestHandler(static fn($inner) => new class($inner) implements RequestHandler { - public function __construct( - private $inner, - ) { + ->route(Route::{$method->name}( + '/foo', + Services::responseHandler, + )) + ->service(Services::responseHandler, static fn() => new class($expected) { + public function __construct(private $response) + { } - public function __invoke(ServerRequest $request): Response + public function __invoke() { - $response = ($this->inner)($request); - - return Response::of( - $response->statusCode(), - $response->protocolVersion(), - $response->headers()(ContentType::of(new MediaType( - 'application', - 'octet-stream', - ))), - ); + return Attempt::result($this->response); } }); $response = $app->run(ServerRequest::of( - $url, + Url::of('/foo'), $method, $protocol, )); - $this->assertSame(StatusCode::notFound, $response->statusCode()); - $this->assertSame($protocol, $response->protocolVersion()); - $this->assertSame( - 'Content-Type: application/octet-stream', - $response->headers()->get('content-type')->match( - static fn($header) => $header->toString(), - static fn() => null, - ), - ); + $this->assertSame($expected, $response); }); } @@ -1048,10 +1058,10 @@ public function testAllowToSpecifyHttpNotFoundRequestHandler(): BlackBox\Proof $expected = Response::of(StatusCode::ok, $protocol); $app = Application::http(Factory::build(), Environment::test($variables)) - ->notFoundRequestHandler(function($request) use ($protocol, $expected) { + ->routeNotFound(function($request) use ($protocol, $expected) { $this->assertSame($protocol, $request->protocolVersion()); - return $expected; + return Attempt::result($expected); }); $response = $app->run(ServerRequest::of( @@ -1079,16 +1089,20 @@ public function testMatchMethodAllowed(): BlackBox\Proof ) ->prove(function($protocol, $variables) { $app = Application::http(Factory::build(), Environment::test($variables)) - ->appendRoutes(static fn($routes) => $routes->add( - Endpoint::of('/foo') - ->pipe(RouterMethod::get()) - ->pipe(Handle::of(static fn($request) => Attempt::result( - Response::of( - StatusCode::ok, - $request->protocolVersion(), - ), - ))), - )); + ->route( + static fn($pipe) => $pipe + ->endpoint('/foo') + ->any( + $pipe + ->get() + ->handle(static fn($request) => Attempt::result( + Response::of( + StatusCode::ok, + $request->protocolVersion(), + ), + )), + ), + ); $response = $app->run(ServerRequest::of( Url::of('/foo'), @@ -1100,4 +1114,125 @@ public function testMatchMethodAllowed(): BlackBox\Proof $this->assertSame($protocol, $response->protocolVersion()); }); } + + public function testMapRoute(): BlackBox\Proof + { + return $this + ->forAll( + Set::of(...ProtocolVersion::cases()), + Set::sequence( + Set::compose( + static fn($key, $value) => [$key, $value], + Set::strings()->randomize(), + Set::strings(), + ), + )->between(0, 10), + ) + ->prove(function($protocol, $variables) { + $response = Response::of(StatusCode::ok, $protocol); + $expected = Response::of(StatusCode::ok, $protocol); + + $app = Application::http(Factory::build(), Environment::test($variables)) + ->route(Route::get( + '/foo', + Services::responseHandler, + )) + ->mapRoute(fn($component) => $component->map( + function($out) use ($response, $expected) { + $this->assertSame($out, $response); + + return $expected; + }, + )) + ->service(Services::responseHandler, static fn() => new class($response) { + public function __construct(private $response) + { + } + + public function __invoke() + { + return Attempt::result($this->response); + } + }); + + $response = $app->run(ServerRequest::of( + Url::of('/foo'), + Method::get, + $protocol, + )); + + $this->assertSame($expected, $response); + }); + } + + public function testRoutesAsEnumCases(): BlackBox\Proof + { + return $this + ->forAll( + Set::of(...ProtocolVersion::cases()), + Set::sequence( + Set::compose( + static fn($key, $value) => [$key, $value], + Set::strings()->randomize(), + Set::strings(), + ), + )->between(0, 10), + ) + ->prove(function($protocol, $variables) { + $responseA = Response::of(StatusCode::ok, $protocol); + $responseB = Response::of(StatusCode::ok, $protocol); + + $app = Application::http(Factory::build(), Environment::test($variables)) + ->service(Services::serviceA, static fn() => static fn() => Attempt::result($responseA)) + ->service(Services::serviceB, static fn() => static fn() => Attempt::result($responseB)) + ->routes(Routes::class); + + $response = $app->run(ServerRequest::of( + Url::of('/foo'), + Method::get, + $protocol, + )); + + $this->assertSame($responseA, $response); + + $response = $app->run(ServerRequest::of( + Url::of('/bar'), + Method::get, + $protocol, + )); + + $this->assertSame($responseB, $response); + }); + } + + public function testRecoverRouteError(): BlackBox\Proof + { + return $this + ->forAll( + Set::of(...ProtocolVersion::cases()), + Set::sequence( + Set::compose( + static fn($key, $value) => [$key, $value], + Set::strings()->randomize(), + Set::strings(), + ), + )->between(0, 10), + ) + ->prove(function($protocol, $variables) { + $expected = Response::of(StatusCode::ok, $protocol); + + $app = Application::http(Factory::build(), Environment::test($variables)) + ->service(Services::serviceA, static fn() => static fn() => Attempt::error(new \Exception)) + ->recoverRouteError(static fn() => Attempt::result($expected)) + ->routes(Routes::class); + + $response = $app->run(ServerRequest::of( + Url::of('/foo'), + Method::get, + $protocol, + )); + + $this->assertSame($expected, $response); + }); + } }