diff --git a/CHANGELOG.md b/CHANGELOG.md index e571d23..7fe1e45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,16 @@ ## [Unreleased] +### Changed + +- Requires `innmind/foundation:~1.9` +- Requires `innmind/di:~3.0` +- `Innmind\Framework\Application::route()` callable must now return a `Innmind\Router\Component` + ### Removed - The ability to use `string`s to reference services +- `Innmind\Framework\Http\Service` ### Fixed diff --git a/composer.json b/composer.json index 0e3e6ec..41de291 100644 --- a/composer.json +++ b/composer.json @@ -16,14 +16,9 @@ }, "require": { "php": "~8.2", - "innmind/operating-system": "~4.1|~5.0", - "innmind/cli": "^3.1", - "innmind/immutable": "~5.2", - "innmind/di": "~2.1", - "innmind/url": "^4.1", - "innmind/filesystem": "~7.0", - "innmind/http-server": "~4.0", - "innmind/router": "~4.1" + "innmind/foundation": "~1.9", + "innmind/di": "~3.0", + "innmind/router": "^5.0.1" }, "autoload": { "psr-4": { @@ -39,11 +34,11 @@ "innmind/static-analysis": "^1.2.1", "innmind/black-box": "~6.5", "innmind/coding-standard": "~2.0", - "innmind/async-http-server": "~2.0|~3.0" + "innmind/async-http-server": "~4.0" }, "conflict": { "innmind/black-box": "<6.0|~7.0", - "innmind/async-http-server": "<2.0|~4.0" + "innmind/async-http-server": "<3.0|~5.0" }, "suggest": { "innmind/black-box": "For property based testing", diff --git a/fixtures/server.php b/fixtures/server.php index 28a4330..d650585 100644 --- a/fixtures/server.php +++ b/fixtures/server.php @@ -8,14 +8,23 @@ Main\Async\Http, Http\Routes, }; -use Innmind\Router\Route; +use Innmind\Router\{ + Method, + Endpoint, + Handle, + 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( - Route::literal('GET /hello'), + Method::get() + ->pipe(Endpoint::of('/hello')) + ->pipe(Respond::with(StatusCode::ok)), )); } }; diff --git a/src/Application.php b/src/Application.php index fcffeda..1bf8918 100644 --- a/src/Application.php +++ b/src/Application.php @@ -16,15 +16,19 @@ Container, Service, }; +use Innmind\Router\Component; use Innmind\Http\{ ServerRequest, Response, }; -use Innmind\Router\Route\Variables; +use Innmind\Immutable\{ + Attempt, + SideEffect, +}; /** * @template I of ServerRequest|CliEnv - * @template O of Response|CliEnv + * @template O of Response|Attempt */ final class Application { @@ -51,7 +55,7 @@ public static function http(OperatingSystem $os, Environment $env): self /** * @psalm-pure * - * @return self + * @return self> */ public static function cli(OperatingSystem $os, Environment $env): self { @@ -62,7 +66,7 @@ public static function cli(OperatingSystem $os, Environment $env): self * @psalm-pure * @experimental * - * @return self + * @return self> */ public static function asyncHttp(OperatingSystem $os): self { @@ -144,7 +148,7 @@ public function mapCommand(callable $map): self * @psalm-mutation-free * * @param literal-string $pattern - * @param callable(ServerRequest, Variables, Container, OperatingSystem, Environment): Response $handle + * @param callable(Container, OperatingSystem, Environment): Component $handle * * @return self */ @@ -194,7 +198,7 @@ public function notFoundRequestHandler(callable $handle): self * * @return O */ - public function run(CliEnv|ServerRequest $input): CliEnv|Response + public function run(CliEnv|ServerRequest $input): Attempt|Response { return $this->app->run($input); } diff --git a/src/Application/Async/Http.php b/src/Application/Async/Http.php index 9658bc7..4d08e46 100644 --- a/src/Application/Async/Http.php +++ b/src/Application/Async/Http.php @@ -21,22 +21,24 @@ Builder, Service, }; +use Innmind\Router\{ + Method, + Endpoint, +}; use Innmind\Http\{ ServerRequest, Response, }; -use Innmind\Router\{ - Route, -}; use Innmind\Immutable\{ Maybe, Sequence, + Attempt, }; /** * @experimental * @internal - * @implements Implementation + * @implements Implementation> */ final class Http implements Implementation { @@ -168,15 +170,35 @@ public function mapCommand(callable $map): self #[\Override] public function route(string $pattern, 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( - Route::literal($pattern)->handle(static fn($request, $variables) => $handle( - $request, - $variables, - $container, - $os, - $env, - )), + $method + ->pipe(Endpoint::of($path)) + ->pipe($handle($container, $os, $env)), ), ); } diff --git a/src/Application/Cli.php b/src/Application/Cli.php index c707a9b..63574a2 100644 --- a/src/Application/Cli.php +++ b/src/Application/Cli.php @@ -21,11 +21,12 @@ use Innmind\Immutable\{ Sequence, Str, + Attempt, }; /** * @internal - * @implements Implementation + * @implements Implementation> */ final class Cli implements Implementation { diff --git a/src/Application/Http.php b/src/Application/Http.php index 9508942..656cb94 100644 --- a/src/Application/Http.php +++ b/src/Application/Http.php @@ -15,11 +15,14 @@ Builder, Service, }; +use Innmind\Router\{ + Method, + Endpoint, +}; use Innmind\Http\{ ServerRequest, Response, }; -use Innmind\Router\Route; use Innmind\Immutable\{ Maybe, Sequence, @@ -146,15 +149,35 @@ public function mapCommand(callable $map): self #[\Override] public function route(string $pattern, 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( - Route::literal($pattern)->handle(static fn($request, $variables) => $handle( - $request, - $variables, - $container, - $os, - $env, - )), + $method + ->pipe(Endpoint::of($path)) + ->pipe($handle($container, $os, $env)), ), ); } diff --git a/src/Application/Implementation.php b/src/Application/Implementation.php index dec7e6f..58644f0 100644 --- a/src/Application/Implementation.php +++ b/src/Application/Implementation.php @@ -17,16 +17,20 @@ Container, Service, }; +use Innmind\Router\Component; use Innmind\Http\{ ServerRequest, Response, }; -use Innmind\Router\Route\Variables; +use Innmind\Immutable\{ + Attempt, + SideEffect, +}; /** * @internal * @template I of ServerRequest|CliEnv - * @template O of Response|CliEnv + * @template O of Response|Attempt */ interface Implementation { @@ -79,7 +83,7 @@ public function mapCommand(callable $map): self; * @psalm-mutation-free * * @param literal-string $pattern - * @param callable(ServerRequest, Variables, Container, OperatingSystem, Environment): Response $handle + * @param callable(Container, OperatingSystem, Environment): Component $handle * * @return self */ diff --git a/src/Cli/Command/Defer.php b/src/Cli/Command/Defer.php index 6438792..7d6418f 100644 --- a/src/Cli/Command/Defer.php +++ b/src/Cli/Command/Defer.php @@ -6,10 +6,12 @@ use Innmind\Framework\Environment; use Innmind\CLI\{ Command, + Command\Usage, Console, }; use Innmind\OperatingSystem\OperatingSystem; use Innmind\DI\Container; +use Innmind\Immutable\Attempt; /** * @internal @@ -32,7 +34,7 @@ public function __construct( } #[\Override] - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { // we map the command when running it instead of when loading it to // avoid loading the decorator multiple times for a same script as @@ -47,7 +49,7 @@ public function __invoke(Console $console): Console * @psalm-mutation-free */ #[\Override] - public function usage(): string + public function usage(): Usage { /** @psalm-suppress ImpureMethodCall */ return $this->command()->usage(); diff --git a/src/Http/Router.php b/src/Http/Router.php index 09b17f9..8700952 100644 --- a/src/Http/Router.php +++ b/src/Http/Router.php @@ -9,13 +9,17 @@ Response\StatusCode, }; use Innmind\Router\{ - Route, - Under, - RequestMatcher\RequestMatcher, + Component, + Router as Route, + Any, + Handle, + Respond, }; use Innmind\Immutable\{ Maybe, Sequence, + Attempt, + SideEffect, }; /** @@ -24,7 +28,7 @@ final class Router implements RequestHandler { /** - * @param Sequence $routes + * @param Sequence> $routes * @param Maybe<\Closure(ServerRequest): Response> $notFound */ public function __construct( @@ -36,18 +40,23 @@ public function __construct( #[\Override] public function __invoke(ServerRequest $request): Response { - $match = new RequestMatcher($this->routes); - $notFound = $this->notFound; + /** + * @psalm-suppress MixedArgumentTypeCoercion + */ + $route = Route::of( + Any::from($this->routes) + ->otherwise(Respond::withHttpErrors()) + ->or(Handle::via( + fn($request, SideEffect $_) => $this->notFound->match( + static fn($handle) => Attempt::result($handle($request)), + static fn() => Attempt::result(Response::of( + StatusCode::notFound, + $request->protocolVersion(), + )), + ), + )), + ); - return $match($request) - ->map(static fn($route) => $route->respondTo(...)) - ->otherwise(static fn() => $notFound) - ->match( - static fn($handle) => $handle($request), - static fn() => Response::of( - StatusCode::notFound, - $request->protocolVersion(), - ), - ); + return $route($request)->unwrap(); } } diff --git a/src/Http/Routes.php b/src/Http/Routes.php index 1bfe116..abb6a94 100644 --- a/src/Http/Routes.php +++ b/src/Http/Routes.php @@ -3,11 +3,12 @@ namespace Innmind\Framework\Http; -use Innmind\Router\{ - Route, - Under, +use Innmind\Router\Component; +use Innmind\Http\Response; +use Innmind\Immutable\{ + Sequence, + SideEffect, }; -use Innmind\Immutable\Sequence; /** * @psalm-immutable @@ -15,7 +16,7 @@ final class Routes { /** - * @param Sequence $routes + * @param Sequence> $routes */ private function __construct(private Sequence $routes) { @@ -29,13 +30,16 @@ public static function lazy(): self return new self(Sequence::lazyStartingWith()); } - public function add(Route|Under $route): self + /** + * @param Component $route + */ + public function add(Component $route): self { return new self(($this->routes)($route)); } /** - * @param Sequence $routes + * @param Sequence> $routes */ public function append(Sequence $routes): self { @@ -45,7 +49,7 @@ public function append(Sequence $routes): self /** * @internal * - * @return Sequence + * @return Sequence> */ public function toSequence(): Sequence { diff --git a/src/Http/Service.php b/src/Http/Service.php deleted file mode 100644 index 0a326c1..0000000 --- a/src/Http/Service.php +++ /dev/null @@ -1,37 +0,0 @@ -container)($this->service)($request, $variables); - } - - public static function of(Container $container, Ref $service): self - { - return new self($container, $service); - } -} diff --git a/src/Http/To.php b/src/Http/To.php index 2bd9c03..ba1a100 100644 --- a/src/Http/To.php +++ b/src/Http/To.php @@ -4,16 +4,17 @@ namespace Innmind\Framework\Http; use Innmind\Framework\Environment; -use Innmind\Http\{ - ServerRequest, - Response, -}; +use Innmind\Http\Response; use Innmind\DI\{ Container, Service, }; use Innmind\OperatingSystem\OperatingSystem; -use Innmind\Router\Route\Variables; +use Innmind\Router\{ + Component, + Handle, +}; +use Innmind\Immutable\Map; final class To { @@ -21,18 +22,25 @@ private function __construct(private Service $service) { } + /** + * @return Component, Response> + */ public function __invoke( - ServerRequest $request, - Variables $variables, Container $container, - ?OperatingSystem $os = null, // these arguments are not used, there here - ?Environment $env = null, // to satisfy Psalm when used in Framework::route() - ): Response { + OperatingSystem $os, + Environment $env, + ): Component { + $service = $this->service; + /** - * @psalm-suppress InvalidFunctionCall If it fails here then the service doesn't conform to the signature callable(ServerRequest, Variables): Response - * @var Response + * @psalm-suppress MissingClosureReturnType + * @psalm-suppress MixedArgumentTypeCoercion + * @psalm-suppress InvalidFunctionCall + * @todo fix non lazy call */ - return $container($this->service)($request, $variables); + return Handle::of( + $container($service), + ); } public static function service(Service $service): self diff --git a/src/Main/Async/Http.php b/src/Main/Async/Http.php index 2784211..d55c40b 100644 --- a/src/Main/Async/Http.php +++ b/src/Main/Async/Http.php @@ -9,6 +9,7 @@ Environment, }; use Innmind\OperatingSystem\OperatingSystem; +use Innmind\Immutable\Attempt; /** * @experimental @@ -16,19 +17,19 @@ abstract class Http extends Main { #[\Override] - protected function main(Environment $env, OperatingSystem $os): Environment + protected function main(Environment $env, OperatingSystem $os): Attempt { /** * @psalm-suppress InvalidReturnStatement Let the app crash in case of a misuse - * @var Environment + * @var Attempt */ return static::configure(Application::asyncHttp($os))->run($env); } /** - * @param Application $app + * @param Application> $app * - * @return Application + * @return Application> */ abstract protected function configure(Application $app): Application; } diff --git a/src/Main/Cli.php b/src/Main/Cli.php index abf678c..f41e7cd 100644 --- a/src/Main/Cli.php +++ b/src/Main/Cli.php @@ -12,23 +12,24 @@ Environment, }; use Innmind\OperatingSystem\OperatingSystem; +use Innmind\Immutable\Attempt; abstract class Cli extends Main { #[\Override] - protected function main(Environment $env, OperatingSystem $os): Environment + protected function main(Environment $env, OperatingSystem $os): Attempt { /** * @psalm-suppress InvalidReturnStatement Let the app crash in case of a misuse - * @var Environment + * @var Attempt */ return static::configure(Application::cli($os, AppEnv::of($env->variables())))->run($env); } /** - * @param Application $app + * @param Application> $app * - * @return Application + * @return Application> */ abstract protected function configure(Application $app): Application; } diff --git a/src/Middleware.php b/src/Middleware.php index eeb0e92..8303918 100644 --- a/src/Middleware.php +++ b/src/Middleware.php @@ -8,12 +8,13 @@ Response, }; use Innmind\CLI\Environment as CliEnv; +use Innmind\Immutable\Attempt; interface Middleware { /** * @template I of ServerRequest|CliEnv - * @template O of Response|CliEnv + * @template O of Response|Attempt * * @param Application $app * diff --git a/src/Middleware/LoadDotEnv.php b/src/Middleware/LoadDotEnv.php index 071b36f..2ba5373 100644 --- a/src/Middleware/LoadDotEnv.php +++ b/src/Middleware/LoadDotEnv.php @@ -34,7 +34,8 @@ public function __invoke(Application $app): Application static fn($env, $os) => $os ->filesystem() ->mount($folder) - ->get(Name::of('.env')) + ->maybe() + ->flatMap(static fn($adapter) => $adapter->get(Name::of('.env'))) ->keep(Instance::of(File::class)) ->match( static fn($file) => self::add($env, $file), diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index 7d5ee13..d48067f 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -16,12 +16,14 @@ use Innmind\CLI\{ Environment\InMemory, Command, + Command\Usage, Console, }; use Innmind\DI\Service; use Innmind\Router\{ - Route, - Under, + Endpoint, + Method as RouterMethod, + Handle, }; use Innmind\Http\{ ServerRequest, @@ -31,12 +33,15 @@ ProtocolVersion, Header\ContentType, }; +use Innmind\MediaType\MediaType; use Innmind\Url\{ Url, Path, }; -use Innmind\UrlTemplate\Template; -use Innmind\Immutable\Str; +use Innmind\Immutable\{ + Str, + Attempt, +}; use Innmind\BlackBox\{ PHPUnit\BlackBox, Set, @@ -80,7 +85,7 @@ public function testCliApplicationReturnsHelloWorldByDefault(): BlackBox\Proof $arguments, $variables, '/', - )); + ))->unwrap(); $this->assertSame(["Hello world\n"], $env->outputs()); $this->assertNull($env->exitCode()->match( @@ -122,7 +127,7 @@ public function testOrderOfMappingEnvironmentAndOperatingSystemIsKept(): BlackBo $arguments, $variables, '/', - )); + ))->unwrap(); $this->assertNull($env->exitCode()->match( static fn($exit) => $exit, @@ -148,7 +153,7 @@ public function testOrderOfMappingEnvironmentAndOperatingSystemIsKept(): BlackBo $arguments, $variables, '/', - )); + ))->unwrap(); $this->assertNull($env->exitCode()->match( static fn($exit) => $exit, @@ -174,14 +179,14 @@ public function testRunDefaultCommand(): BlackBox\Proof ->prove(function($inputs, $interactive, $variables) { $app = Application::cli(Factory::build(), Environment::test($variables)) ->command(static fn() => new class implements Command { - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { return $console->output(Str::of('my command output')); } - public function usage(): string + public function usage(): Usage { - return 'my-command'; + return Usage::parse('my-command'); } }); @@ -191,7 +196,7 @@ public function usage(): string ['script-name'], $variables, '/', - )); + ))->unwrap(); $this->assertSame(['my command output'], $env->outputs()); $this->assertNull($env->exitCode()->match( @@ -218,25 +223,25 @@ public function testRunSpecificCommand(): BlackBox\Proof ->prove(function($inputs, $interactive, $variables) { $app = Application::cli(Factory::build(), Environment::test($variables)) ->command(static fn() => new class implements Command { - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { return $console->output(Str::of('my command A output')); } - public function usage(): string + public function usage(): Usage { - return 'my-command-a'; + return Usage::parse('my-command-a'); } }) ->command(static fn() => new class implements Command { - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { return $console->output(Str::of('my command B output')); } - public function usage(): string + public function usage(): Usage { - return 'my-command-b'; + return Usage::parse('my-command-b'); } }); @@ -246,7 +251,7 @@ public function usage(): string ['script-name', 'my-command-a'], $variables, '/', - )); + ))->unwrap(); $this->assertSame(['my command A output'], $env->outputs()); $this->assertNull($env->exitCode()->match( @@ -260,7 +265,7 @@ public function usage(): string ['script-name', 'my-command-b'], $variables, '/', - )); + ))->unwrap(); $this->assertSame(['my command B output'], $env->outputs()); $this->assertNull($env->exitCode()->match( @@ -295,7 +300,7 @@ public function testServicesAreNotLoadedIfNotUsed(): BlackBox\Proof $arguments, $variables, '/', - )); + ))->unwrap(); $this->assertSame(["Hello world\n"], $env->outputs()); $this->assertNull($env->exitCode()->match( @@ -327,14 +332,14 @@ public function __construct( ) { } - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { return $console->output($this->output); } - public function usage(): string + public function usage(): Usage { - return 'my-command'; + return Usage::parse('my-command'); } }) ->service(Services::service, static fn() => Str::of('my command output')); @@ -345,7 +350,7 @@ public function usage(): string ['script-name'], $variables, '/', - )); + ))->unwrap(); $this->assertSame(['my command output'], $env->outputs()); $this->assertNull($env->exitCode()->match( @@ -377,14 +382,14 @@ public function __construct( ) { } - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { return $console->output($this->output); } - public function usage(): string + public function usage(): Usage { - return 'my-command'; + return Usage::parse('my-command'); } }) ->service(Services::serviceA, static fn($get) => Str::of('my command output')->append($get(Services::serviceB)->toString())) @@ -396,7 +401,7 @@ public function usage(): string ['script-name'], $variables, '/', - )); + ))->unwrap(); $this->assertSame(['my command output twice'], $env->outputs()); $this->assertNull($env->exitCode()->match( @@ -423,14 +428,14 @@ public function testUnusedCommandIsNotLoaded(): BlackBox\Proof ->prove(function($inputs, $interactive, $variables) { $app = Application::cli(Factory::build(), Environment::test($variables)) ->command(static fn() => new class implements Command { - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { return $console->output(Str::of('my command A output')); } - public function usage(): string + public function usage(): Usage { - return 'my-command-a'; + return Usage::parse('my-command-a'); } }) ->command(static fn() => new class implements Command { @@ -439,14 +444,14 @@ public function __construct() throw new \Exception; } - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { return $console->output(Str::of('my command B output')); } - public function usage(): string + public function usage(): Usage { - return 'my-command-b'; + return Usage::parse('my-command-b'); } }); @@ -456,7 +461,7 @@ public function usage(): string ['script-name', 'my-command-a'], $variables, '/', - )); + ))->unwrap(); $this->assertSame(['my command A output'], $env->outputs()); $this->assertNull($env->exitCode()->match( @@ -483,14 +488,14 @@ public function testDecoratingCommands(): BlackBox\Proof ->prove(function($inputs, $interactive, $variables) { $app = Application::cli(Factory::build(), Environment::test($variables)) ->command(static fn() => new class implements Command { - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { return $console->output(Str::of('my command output')); } - public function usage(): string + public function usage(): Usage { - return 'my-command'; + return Usage::parse('my-command'); } }) ->mapCommand(static fn($command) => new class($command) implements Command { @@ -499,12 +504,14 @@ public function __construct( ) { } - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { - return ($this->inner)($console)->output(Str::of('decorated')); + return ($this->inner)($console)->flatMap( + static fn($console) => $console->output(Str::of('decorated')), + ); } - public function usage(): string + public function usage(): Usage { return $this->inner->usage(); } @@ -516,7 +523,7 @@ public function usage(): string ['script-name'], $variables, '/', - )); + ))->unwrap(); $this->assertSame(['my command output', 'decorated'], $env->outputs()); $this->assertNull($env->exitCode()->match( @@ -545,25 +552,25 @@ public function testCommandDecoratorIsAppliedOnlyOnTheWishedOne(): BlackBox\Proo ++$testRuns; $app = Application::cli(Factory::build(), Environment::test($variables)) ->command(static fn() => new class implements Command { - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { return $console->output(Str::of('my command output A')); } - public function usage(): string + public function usage(): Usage { - return 'my-command-a'; + return Usage::parse('my-command-a'); } }) ->command(static fn() => new class implements Command { - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { return $console->output(Str::of('my command output B')); } - public function usage(): string + public function usage(): Usage { - return 'my-command-b'; + return Usage::parse('my-command-b'); } }) ->mapCommand(static fn($command) => new class($command) implements Command { @@ -575,12 +582,14 @@ public function __construct( $this->instances = ++$instances; } - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { - return ($this->inner)($console)->output(Str::of((string) $this->instances)); + return ($this->inner)($console)->flatMap( + fn($console) => $console->output(Str::of((string) $this->instances)), + ); } - public function usage(): string + public function usage(): Usage { return $this->inner->usage(); } @@ -592,7 +601,7 @@ public function usage(): string ['script-name', 'my-command-b'], $variables, '/', - )); + ))->unwrap(); $this->assertSame(['my command output B', (string) $testRuns], $env->outputs()); $this->assertNull($env->exitCode()->match( @@ -654,17 +663,17 @@ public function __construct( ) { } - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { return $console ->output(Str::of($this->env->get('foo'))) - ->output(Str::of($this->env->get('bar'))) - ->output(Str::of($this->env->get('baz'))); + ->flatMap(fn($console) => $console->output(Str::of($this->env->get('bar')))) + ->flatMap(fn($console) => $console->output(Str::of($this->env->get('baz')))); } - public function usage(): string + public function usage(): Usage { - return 'watev'; + return Usage::parse('watev'); } }); @@ -674,7 +683,7 @@ public function usage(): string ['script-name'], $variables, '/', - )); + ))->unwrap(); $this->assertSame(['bar', 'baz', 'foo'], $env->outputs()); $this->assertNull($env->exitCode()->match( @@ -715,14 +724,14 @@ public function __construct( ) { } - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { return $console->output(Str::of($this->env->get('foo'))); } - public function usage(): string + public function usage(): Usage { - return 'watev'; + return Usage::parse('watev'); } }); @@ -732,7 +741,7 @@ public function usage(): string ['script-name'], $variables, '/', - )); + ))->unwrap(); $this->assertSame(['bar'], $env->outputs()); $this->assertNull($env->exitCode()->match( @@ -765,16 +774,16 @@ public function __construct( ) { } - public function __invoke(Console $console): Console + public function __invoke(Console $console): Attempt { return $console ->output(Str::of($this->env->get('FOO'))) - ->output(Str::of($this->env->get('PASSWORD'))); + ->flatMap(fn($console) => $console->output(Str::of($this->env->get('PASSWORD')))); } - public function usage(): string + public function usage(): Usage { - return 'watev'; + return Usage::parse('watev'); } }); @@ -784,7 +793,7 @@ public function usage(): string ['script-name'], $variables, '/', - )); + ))->unwrap(); $this->assertSame(['bar', 'foo=" \n watev; bar!'], $env->outputs()); $this->assertNull($env->exitCode()->match( @@ -842,18 +851,22 @@ public function testMatchRoutes(): BlackBox\Proof $app = Application::http(Factory::build(), Environment::test($variables)) ->appendRoutes(fn($routes) => $routes->add( - Route::of(Method::get, Template::of('/foo'))->handle(function($request) use ($protocol, $responseA) { - $this->assertSame($protocol, $request->protocolVersion()); + RouterMethod::get() + ->pipe(Endpoint::of('/foo')) + ->pipe(Handle::via(function($request) use ($protocol, $responseA) { + $this->assertSame($protocol, $request->protocolVersion()); - return $responseA; - }), + return Attempt::result($responseA); + })), )) ->appendRoutes(fn($routes) => $routes->add( - Route::of(Method::get, Template::of('/bar'))->handle(function($request) use ($protocol, $responseB) { - $this->assertSame($protocol, $request->protocolVersion()); + RouterMethod::get() + ->pipe(Endpoint::of('/bar')) + ->pipe(Handle::via(function($request) use ($protocol, $responseB) { + $this->assertSame($protocol, $request->protocolVersion()); - return $responseB; - }), + return Attempt::result($responseB); + })), )); $response = $app->run(ServerRequest::of( @@ -892,16 +905,16 @@ public function testRouteShortDeclaration(): BlackBox\Proof $responseB = Response::of(StatusCode::ok, $protocol); $app = Application::http(Factory::build(), Environment::test($variables)) - ->route('GET /foo', function($request) use ($protocol, $responseA) { + ->route('GET /foo', fn() => Handle::via(function($request) use ($protocol, $responseA) { $this->assertSame($protocol, $request->protocolVersion()); - return $responseA; - }) - ->route('GET /bar', function($request) use ($protocol, $responseB) { + return Attempt::result($responseA); + })) + ->route('GET /bar', fn() => Handle::via(function($request) use ($protocol, $responseB) { $this->assertSame($protocol, $request->protocolVersion()); - return $responseB; - }); + return Attempt::result($responseB); + })); $response = $app->run(ServerRequest::of( Url::of('/foo'), @@ -946,7 +959,7 @@ public function __construct(private $response) public function __invoke() { - return $this->response; + return Attempt::result($this->response); } }); @@ -990,7 +1003,10 @@ public function __invoke(ServerRequest $request): Response return Response::of( $response->statusCode(), $response->protocolVersion(), - $response->headers()(ContentType::of('application', 'octet-stream')), + $response->headers()(ContentType::of(new MediaType( + 'application', + 'octet-stream', + ))), ); } }); @@ -1064,7 +1080,14 @@ public function testMatchMethodAllowed(): BlackBox\Proof ->prove(function($protocol, $variables) { $app = Application::http(Factory::build(), Environment::test($variables)) ->appendRoutes(static fn($routes) => $routes->add( - Under::of(Template::of('/foo'))->route(Method::get), + Endpoint::of('/foo') + ->pipe(RouterMethod::get()) + ->pipe(Handle::of(static fn($request) => Attempt::result( + Response::of( + StatusCode::ok, + $request->protocolVersion(), + ), + ))), )); $response = $app->run(ServerRequest::of( diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 9c17b99..5923313 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -35,7 +35,8 @@ public function setUp(): void ->execute( Command::foreground('php fixtures/server.php') ->withEnvironment('PATH', \getenv('PATH')), - ); + ) + ->unwrap(); } public function tearDown(): void @@ -44,7 +45,7 @@ public function tearDown(): void fn($pid) => $this->os->control()->processes()->kill( $pid, Signal::kill, - ), + )->unwrap(), static fn() => null, ); } @@ -54,8 +55,8 @@ public function testAsyncHttpServer() $started = $this ->server ->output() - ->chunks() - ->find(static fn($pair) => $pair[0]->startsWith('HTTP server ready!')); + ->map(static fn($chunk) => $chunk->data()) + ->find(static fn($chunk) => $chunk->startsWith('HTTP server ready!')); $this->assertTrue($started->match( static fn() => true,