diff --git a/CHANGELOG.md b/CHANGELOG.md index 35045af..f279ba3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Changed + +- Requires `innmind/foundation:~1.4` + ## 2.1.0 - 2024-03-10 ### Added diff --git a/composer.json b/composer.json index 999ef7b..22fc604 100644 --- a/composer.json +++ b/composer.json @@ -16,11 +16,7 @@ }, "require": { "php": "~8.2", - "innmind/http": "~7.0", - "innmind/immutable": "~5.2", - "innmind/time-continuum": "~3.2", - "innmind/stream": "~4.0", - "innmind/io": "~2.7" + "innmind/foundation": "~1.4" }, "autoload": { "psr-4": { diff --git a/src/Request/Frame/Body.php b/src/Request/Frame/Body.php index fb95d21..88aa8dc 100644 --- a/src/Request/Frame/Body.php +++ b/src/Request/Frame/Body.php @@ -8,11 +8,12 @@ Header\ContentLength, }; use Innmind\Filesystem\File\Content; -use Innmind\IO\Readable\Frame; +use Innmind\IO\Frame; use Innmind\Immutable\{ Str, Maybe, Sequence, + Pair, }; final class Body @@ -46,22 +47,32 @@ public static function of(Maybe $headers): Frame */ private static function bounded(int $length): Frame { - $accumulated = 0; - + /** + * @psalm-suppress MixedOperand + * @var Frame> + */ return match (\min($length, 8192)) { - $length => Frame\Chunk::of($length)->map(Sequence::of(...)), - default => Frame\Chunks::of(8192)->until( - 1, - static function($chunk) use (&$accumulated, $length) { - /** - * @psalm-suppress MixedOperand - * @psalm-suppress MixedAssignment - */ - $accumulated += $chunk->length(); - - return $accumulated >= $length; - }, - ), + $length => Frame::chunk($length)->strict()->map(Sequence::of(...)), + default => Frame::sequence( + Frame::chunk(8192)->loose(), + ) + ->map( + static fn($lines) => $lines + ->map(static fn($line) => $line->unwrap()) // todo find a way to not throw + ->map(static fn($line) => new Pair( + $line->length(), + $line, + )) + ->aggregate(static fn(Pair $a, Pair $b) => Sequence::of( + $a, + new Pair( + $a->key() + $b->key(), + $b->value(), + ), + )) + ->takeWhile(static fn($pair) => $pair->key() < $length) + ->map(static fn($pair) => $pair->value()), + ), }; } diff --git a/src/Request/Frame/Body/Unbounded.php b/src/Request/Frame/Body/Unbounded.php index fb91bf9..0459f52 100644 --- a/src/Request/Frame/Body/Unbounded.php +++ b/src/Request/Frame/Body/Unbounded.php @@ -3,98 +3,65 @@ namespace Innmind\HttpParser\Request\Frame\Body; -use Innmind\IO\{ - Readable\Frame, - Readable\Frame\Map, - Readable\Frame\Filter, - Readable\Frame\FlatMap, - Exception\FailedToLoadStream, -}; +use Innmind\IO\Frame; use Innmind\Immutable\{ Sequence, - Maybe, Str, + Monoid\Concat, }; /** * @internal - * @implements Frame> */ -final class Unbounded implements Frame +final class Unbounded { - private function __construct() - { - } - - #[\Override] - public function __invoke( - callable $read, - callable $readLine, - ): Maybe { - return Maybe::just(Sequence::lazy(function() use ($read) { - $buffer = Str::of(''); - - do { - $chunk = $read(8192)->match( - static fn($chunk) => $chunk, - static fn() => throw new FailedToLoadStream, - ); - - // use prepend to make sure to use the encoding of the chunk and - // not the default utf8 from the initial buffer - $tmpBuffer = $chunk - ->prepend($buffer) - ->takeEnd(4); - $endReached = $this->end($tmpBuffer); - - if ($endReached) { - $chunk = match ($chunk->endsWith("\n\n")) { - true => $chunk->dropEnd(2), - false => $chunk->dropEnd(4), - }; - } - - yield $chunk; - - $buffer = $tmpBuffer; - } while (!$endReached); - })); - } - - public static function new(): self - { - return new self; - } - - /** - * @psalm-mutation-free - */ - #[\Override] - public function filter(callable $predicate): Frame - { - return Filter::of($this, $predicate); - } - /** - * @psalm-mutation-free + * @return Frame> */ - #[\Override] - public function map(callable $map): Frame + public static function new(): Frame { - return Map::of($this, $map); + return Frame::sequence( + Frame::chunk(8192)->loose(), + ) + ->map( + static fn($chunks) => $chunks + ->map(static fn($chunk) => $chunk->unwrap()) // todo find a way to not throw + ->flatMap(static fn($chunk) => match ($chunk->empty()) { + true => Sequence::of($chunk), + false => $chunk->chunk(), + }) + ->windows(4) + ->via(self::bound(...)) + ->aggregate(static fn(Str $a, Str $b) => match (true) { + $a->length() < 8192 => Sequence::of($a->append($b)), + default => Sequence::of($a, $b), + }), + ); } /** - * @psalm-mutation-free + * @param Sequence> $chunks + * + * @return Sequence */ - #[\Override] - public function flatMap(callable $map): Frame + private static function bound(Sequence $chunks): Sequence { - return FlatMap::of($this, $map); - } + $end = Str::of(''); - private function end(Str $buffer): bool - { - return $buffer->empty() || $buffer->equals(Str::of("\r\n\r\n")) || $buffer->takeEnd(2)->equals(Str::of("\n\n")); + return $chunks + ->flatMap(static fn($chunk) => match (true) { + $chunk + ->fold(new Concat) + ->empty() => Sequence::of($end), + $chunk + ->fold(new Concat) + ->equals(Str::of("\r\n\r\n")) => Sequence::of($end), + $chunk + ->drop(2) + ->fold(new Concat) + ->equals(Str::of("\n\n")) => $chunk->take(2)->add($end), + default => $chunk->take(1), + }) + ->takeWhile(static fn($chunk) => $chunk !== $end); } } diff --git a/src/Request/Frame/FirstLine.php b/src/Request/Frame/FirstLine.php index 6847b8f..9a9ba06 100644 --- a/src/Request/Frame/FirstLine.php +++ b/src/Request/Frame/FirstLine.php @@ -8,7 +8,7 @@ ProtocolVersion, }; use Innmind\Url\Url; -use Innmind\IO\Readable\Frame; +use Innmind\IO\Frame; use Innmind\Immutable\{ Str, Maybe, @@ -22,7 +22,7 @@ final class FirstLine */ public static function new(): Frame { - return Frame\Line::new() + return Frame::line() ->map(static fn($line) => $line->trim()) ->map(static fn(Str $line) => $line->capture('~^(?[A-Z]+) (?.+) HTTP/(?1(\.[01])?)$~')) ->map(static function($parts) { diff --git a/src/Request/Frame/Headers.php b/src/Request/Frame/Headers.php index 8b4ed95..aa1992c 100644 --- a/src/Request/Frame/Headers.php +++ b/src/Request/Frame/Headers.php @@ -6,9 +6,9 @@ use Innmind\Http\{ Headers as Model, Header, - Factory\Header\TryFactory, + Factory\Header\Factory, }; -use Innmind\IO\Readable\Frame; +use Innmind\IO\Frame; use Innmind\Immutable\{ Str, Maybe, @@ -20,12 +20,16 @@ final class Headers /** * @return Frame> */ - public static function of(TryFactory $factory): Frame + public static function of(Factory $factory): Frame { - return Frame\Sequence::of( - Frame\Line::new()->map(static fn($line) => $line->trim()), + return Frame::sequence( + Frame::line()->map(static fn($line) => $line->trim()), ) - ->until(static fn($line) => $line->empty()) + ->map( + static fn($lines) => $lines + ->map(static fn($line) => $line->unwrap()) // todo find a way to not throw + ->takeWhile(static fn($line) => !$line->empty()), + ) ->map( static fn($lines) => $lines ->filter(static fn($line) => !$line->empty()) @@ -35,9 +39,9 @@ public static function of(TryFactory $factory): Frame } /** - * @return Maybe
+ * @return Maybe */ - private static function parse(Str $line, TryFactory $factory): Maybe + private static function parse(Str $line, Factory $factory): Maybe { $captured = $line->capture('~^(?[a-zA-Z0-9\-\_\.]+): (?.*)$~'); @@ -47,15 +51,16 @@ private static function parse(Str $line, TryFactory $factory): Maybe } /** - * @param Sequence> $headers + * @param Sequence> $headers * * @return Maybe */ private static function build(Sequence $headers): Maybe { - return $headers->match( - static fn($header, $rest) => Maybe::all($header, ...$rest->toList())->map(Model::of(...)), - static fn() => Maybe::just(Model::of()), - ); + return $headers + ->sink(Model::of()) + ->maybe(static fn($headers, $header) => $header->map( + $headers, + )); } } diff --git a/src/Request/Parse.php b/src/Request/Parse.php index 17e2fff..143ff8d 100644 --- a/src/Request/Parse.php +++ b/src/Request/Parse.php @@ -13,16 +13,15 @@ Method, ProtocolVersion, Headers as HttpHeaders, - Factory\Header\TryFactory, - Factory\Header\Factories, + Factory\Header\Factory, }; use Innmind\TimeContinuum\Clock; use Innmind\Filesystem\File\Content; use Innmind\Url\Url; use Innmind\IO\{ - Readable\Stream, - Readable\Frame, - Sockets\Client, + Streams\Stream\Read, + Sockets\Clients\Client, + Frame, }; use Innmind\Immutable\{ Maybe, @@ -31,9 +30,9 @@ final class Parse { - private TryFactory $factory; + private Factory $factory; - private function __construct(TryFactory $factory) + private function __construct(Factory $factory) { $this->factory = $factory; } @@ -41,9 +40,9 @@ private function __construct(TryFactory $factory) /** * @return Maybe */ - public function __invoke(Stream|Client $stream): Maybe + public function __invoke(Read|Client $stream): Maybe { - $frame = Frame\Composite::of( + $frame = Frame::compose( self::build(...), FirstLine::new(), Headers::of($this->factory)->flatMap( @@ -57,17 +56,18 @@ public function __invoke(Stream|Client $stream): Maybe ->toEncoding(Str\Encoding::ascii) ->frames($frame) ->one() + ->maybe() ->flatMap(static fn($request) => $request); } - public static function of(TryFactory $factory): self + public static function of(Factory $factory): self { return new self($factory); } public static function default(Clock $clock): self { - return new self(new TryFactory(Factories::default($clock))); + return new self(Factory::new($clock)); } /** diff --git a/src/ServerRequest/DecodeForm.php b/src/ServerRequest/DecodeForm.php index 3924963..fb4a1f3 100644 --- a/src/ServerRequest/DecodeForm.php +++ b/src/ServerRequest/DecodeForm.php @@ -7,9 +7,9 @@ ServerRequest, ServerRequest\Form, Header\ContentType, - Header\ContentTypeValue, }; use Innmind\Filesystem\File\Content; +use Innmind\MediaType\MediaType; final class DecodeForm { @@ -35,10 +35,10 @@ public static function of(): self } private function decode( - ContentTypeValue $header, + MediaType $header, ServerRequest $request, ): ServerRequest { - if ($header->type() !== 'application' || $header->subType() !== 'x-www-form-urlencoded') { + if ($header->topLevel() !== 'application' || $header->subType() !== 'x-www-form-urlencoded') { return $request; } diff --git a/tests/Request/ParseTest.php b/tests/Request/ParseTest.php index f2bf72c..9776d3c 100644 --- a/tests/Request/ParseTest.php +++ b/tests/Request/ParseTest.php @@ -4,18 +4,18 @@ namespace Tests\Innmind\HttpParser\Request; use Innmind\HttpParser\Request\Parse; -use Innmind\TimeContinuum\Earth\Clock; +use Innmind\TimeContinuum\Clock; use Innmind\Http\{ Request, Method, ProtocolVersion, - Factory\Header\TryFactory, - Factory\Header\Factories, + Factory\Header\Factory, }; use Innmind\IO\IO; -use Innmind\Url\Path; -use Innmind\Stream\Streams; -use Innmind\Immutable\Str; +use Innmind\Immutable\{ + Str, + Sequence, +}; use Innmind\BlackBox\PHPUnit\Framework\TestCase; class ParseTest extends TestCase @@ -24,15 +24,15 @@ class ParseTest extends TestCase public function setUp(): void { - $this->parse = Parse::of(new TryFactory(Factories::default(new Clock))); + $this->parse = Parse::of(Factory::new(Clock::live())); } public function testParseGet() { - $streams = Streams::fromAmbientAuthority(); - $io = IO::of($streams->watch()->waitForever(...)) - ->readable() - ->wrap($streams->readable()->open(Path::of('fixtures/get.txt'))); + $io = IO::fromAmbientAuthority() + ->streams() + ->acquire(\fopen('fixtures/get.txt', 'r')) + ->read(); $request = ($this->parse)($io)->match( static fn($request) => $request, @@ -92,19 +92,16 @@ public function testParseGetAllAtOnce() ->append("Accept-Encoding: gzip, deflate\r\n") ->append("Connection: keep-alive\r\n") ->append("\r\n"); - $streams = Streams::fromAmbientAuthority(); - $stream = $streams - ->temporary() - ->new() - ->write($content) - ->flatMap(static fn($stream) => $stream->rewind()) - ->match( - static fn($stream) => $stream, - static fn() => null, - ); - $io = IO::of($streams->watch()->waitForever(...)) - ->readable() - ->wrap($stream); + $tmp = \fopen('php://temp', 'w+'); + $io = IO::fromAmbientAuthority() + ->streams() + ->acquire($tmp); + $io + ->write() + ->sink(Sequence::of($content)) + ->unwrap(); + \fseek($tmp, 0); + $io = $io->read(); $request = ($this->parse)($io)->match( static fn($request) => $request, @@ -125,19 +122,16 @@ public function testParsePostWithoutABody() ->append("Content-Length: 0\r\n") ->append("Connection: close\r\n") ->append("\r\n"); - $streams = Streams::fromAmbientAuthority(); - $stream = $streams - ->temporary() - ->new() - ->write($content) - ->flatMap(static fn($stream) => $stream->rewind()) - ->match( - static fn($stream) => $stream, - static fn() => null, - ); - $io = IO::of($streams->watch()->waitForever(...)) - ->readable() - ->wrap($stream); + $tmp = \fopen('php://temp', 'w+'); + $io = IO::fromAmbientAuthority() + ->streams() + ->acquire($tmp); + $io + ->write() + ->sink(Sequence::of($content)) + ->unwrap(); + \fseek($tmp, 0); + $io = $io->read(); $request = ($this->parse)($io)->match( static fn($request) => $request, @@ -153,10 +147,10 @@ public function testParsePostWithoutABody() public function testParsePost() { - $streams = Streams::fromAmbientAuthority(); - $io = IO::of($streams->watch()->waitForever(...)) - ->readable() - ->wrap($streams->readable()->open(Path::of('fixtures/post.txt'))); + $io = IO::fromAmbientAuthority() + ->streams() + ->acquire(\fopen('fixtures/post.txt', 'r')) + ->read(); $request = ($this->parse)($io)->match( static fn($request) => $request, @@ -225,10 +219,10 @@ public function testParsePost() public function testParseUnboundedPost() { - $streams = Streams::fromAmbientAuthority(); - $io = IO::of($streams->watch()->waitForever(...)) - ->readable() - ->wrap($streams->readable()->open(Path::of('fixtures/unbounded-post.txt'))); + $io = IO::fromAmbientAuthority() + ->streams() + ->acquire(\fopen('fixtures/unbounded-post.txt', 'r')) + ->read(); $request = ($this->parse)($io)->match( static fn($request) => $request, @@ -300,20 +294,16 @@ public function testParseGetWithBackslashR() \r RAW; - $streams = Streams::fromAmbientAuthority(); - $io = IO::of($streams->watch()->waitForever(...)) - ->readable() - ->wrap( - $streams - ->temporary() - ->new() - ->write(Str::of($raw)) - ->flatMap(static fn($stream) => $stream->rewind()) - ->match( - static fn($stream) => $stream, - static fn() => null, - ), - ); + $tmp = \fopen('php://temp', 'w+'); + $io = IO::fromAmbientAuthority() + ->streams() + ->acquire($tmp); + $io + ->write() + ->sink(Sequence::of(Str::of($raw))) + ->unwrap(); + \fseek($tmp, 0); + $io = $io->read(); $request = ($this->parse)($io)->match( static fn($request) => $request, @@ -343,20 +333,16 @@ public function testParsePostWithBackslashR() \r RAW; - $streams = Streams::fromAmbientAuthority(); - $io = IO::of($streams->watch()->waitForever(...)) - ->readable() - ->wrap( - $streams - ->temporary() - ->new() - ->write(Str::of($raw)) - ->flatMap(static fn($stream) => $stream->rewind()) - ->match( - static fn($stream) => $stream, - static fn() => null, - ), - ); + $tmp = \fopen('php://temp', 'w+'); + $io = IO::fromAmbientAuthority() + ->streams() + ->acquire($tmp); + $io + ->write() + ->sink(Sequence::of(Str::of($raw))) + ->unwrap(); + \fseek($tmp, 0); + $io = $io->read(); $request = ($this->parse)($io)->match( static fn($request) => $request, @@ -390,22 +376,18 @@ public function testParsePostWithBackslashRWithChunkEndingWithBackslashR() \r RAW; - $streams = Streams::fromAmbientAuthority(); // first chunk ending in the middle of line between the headers and // the body - $io = IO::of($streams->watch()->waitForever(...)) - ->readable() - ->wrap( - $streams - ->temporary() - ->new() - ->write(Str::of($raw)) - ->flatMap(static fn($stream) => $stream->rewind()) - ->match( - static fn($stream) => $stream, - static fn() => null, - ), - ); + $tmp = \fopen('php://temp', 'w+'); + $io = IO::fromAmbientAuthority() + ->streams() + ->acquire($tmp); + $io + ->write() + ->sink(Sequence::of(Str::of($raw))) + ->unwrap(); + \fseek($tmp, 0); + $io = $io->read(); $request = ($this->parse)($io)->match( static fn($request) => $request, diff --git a/tests/ServerRequest/DecodeCookieTest.php b/tests/ServerRequest/DecodeCookieTest.php index 4cf7f26..aa208c7 100644 --- a/tests/ServerRequest/DecodeCookieTest.php +++ b/tests/ServerRequest/DecodeCookieTest.php @@ -8,10 +8,8 @@ ServerRequest\Transform, Request\Parse, }; -use Innmind\TimeContinuum\Earth\Clock; +use Innmind\TimeContinuum\Clock; use Innmind\IO\IO; -use Innmind\Stream\Streams; -use Innmind\Url\Path; use Innmind\Http\ServerRequest; use Innmind\BlackBox\PHPUnit\Framework\TestCase; @@ -21,15 +19,15 @@ class DecodeCookieTest extends TestCase public function setUp(): void { - $this->parse = Parse::default(new Clock); + $this->parse = Parse::default(Clock::live()); } public function testDecode() { - $streams = Streams::fromAmbientAuthority(); - $io = IO::of($streams->watch()->waitForever(...)) - ->readable() - ->wrap($streams->readable()->open(Path::of('fixtures/cookie.txt'))); + $io = IO::fromAmbientAuthority() + ->streams() + ->acquire(\fopen('fixtures/cookie.txt', 'r')) + ->read(); $request = ($this->parse)($io) ->map(Transform::of()) @@ -66,10 +64,10 @@ public function testDecode() public function testDecodeWhenNoCookieHeader() { - $streams = Streams::fromAmbientAuthority(); - $io = IO::of($streams->watch()->waitForever(...)) - ->readable() - ->wrap($streams->readable()->open(Path::of('fixtures/get.txt'))); + $io = IO::fromAmbientAuthority() + ->streams() + ->acquire(\fopen('fixtures/post.txt', 'r')) + ->read(); $request = ($this->parse)($io) ->map(Transform::of()) diff --git a/tests/ServerRequest/DecodeFormTest.php b/tests/ServerRequest/DecodeFormTest.php index bc93f37..7d03e3c 100644 --- a/tests/ServerRequest/DecodeFormTest.php +++ b/tests/ServerRequest/DecodeFormTest.php @@ -8,11 +8,9 @@ ServerRequest\Transform, Request\Parse, }; -use Innmind\TimeContinuum\Earth\Clock; +use Innmind\TimeContinuum\Clock; use Innmind\Http\ServerRequest; use Innmind\IO\IO; -use Innmind\Stream\Streams; -use Innmind\Url\Path; use Innmind\BlackBox\PHPUnit\Framework\TestCase; class DecodeFormTest extends TestCase @@ -21,15 +19,15 @@ class DecodeFormTest extends TestCase public function setUp(): void { - $this->parse = Parse::default(new Clock); + $this->parse = Parse::default(Clock::live()); } public function testDecode() { - $streams = Streams::fromAmbientAuthority(); - $io = IO::of($streams->watch()->waitForever(...)) - ->readable() - ->wrap($streams->readable()->open(Path::of('fixtures/post.txt'))); + $io = IO::fromAmbientAuthority() + ->streams() + ->acquire(\fopen('fixtures/post.txt', 'r')) + ->read(); $request = ($this->parse)($io) ->map(Transform::of()) @@ -66,10 +64,10 @@ public function testDecode() public function testDecodeEmptyBody() { - $streams = Streams::fromAmbientAuthority(); - $io = IO::of($streams->watch()->waitForever(...)) - ->readable() - ->wrap($streams->readable()->open(Path::of('fixtures/get.txt'))); + $io = IO::fromAmbientAuthority() + ->streams() + ->acquire(\fopen('fixtures/get.txt', 'r')) + ->read(); $request = ($this->parse)($io) ->map(Transform::of())