Skip to content

Commit

Permalink
add request parser
Browse files Browse the repository at this point in the history
  • Loading branch information
Baptouuuu committed Feb 12, 2023
1 parent 9a72f76 commit 56bde91
Show file tree
Hide file tree
Showing 9 changed files with 648 additions and 1 deletion.
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
"issues": "http://github.com/innmind/http-parser/issues"
},
"require": {
"php": "~8.1"
"php": "~8.1",
"innmind/http": "^6.0.1",
"innmind/immutable": "^4.10",
"innmind/time-continuum": "^3.2"
},
"autoload": {
"psr-4": {
Expand Down
43 changes: 43 additions & 0 deletions src/Request/Buffer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php
declare(strict_types = 1);

namespace Innmind\HttpParser\Request;

use Innmind\HttpParser\Request\Buffer\{
State,
Uninitialized,
};
use Innmind\TimeContinuum\Clock;
use Innmind\Http\Message\Request;
use Innmind\Immutable\{
Maybe,
Str,
};

final class Buffer
{
private State $state;

private function __construct(State $state)
{
$this->state = $state;
}

public static function new(Clock $clock): self
{
return new self(Uninitialized::new($clock));
}

public function add(Str $chunk): self
{
return new self($this->state->add($chunk));
}

/**
* @return Maybe<Request>
*/
public function finish(): Maybe
{
return $this->state->finish();
}
}
72 changes: 72 additions & 0 deletions src/Request/Buffer/Body.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
declare(strict_types = 1);

namespace Innmind\HttpParser\Request\Buffer;

use Innmind\Http\{
Message\Request,
Message\Method,
ProtocolVersion,
Headers,
};
use Innmind\Filesystem\File\Content;
use Innmind\Url\Url;
use Innmind\Immutable\{
Maybe,
Str,
};

final class Body implements State
{
private Method $method;
private Url $url;
private ProtocolVersion $protocol;
private Headers $headers;
private Str $body;

private function __construct(
Method $method,
Url $url,
ProtocolVersion $protocol,
Headers $headers,
Str $body,
) {
$this->method = $method;
$this->url = $url;
$this->protocol = $protocol;
$this->headers = $headers;
$this->body = $body;
}

public static function new(
Method $method,
Url $url,
ProtocolVersion $protocol,
Headers $headers,
): self {
return new self($method, $url, $protocol, $headers, Str::of(''));
}

public function add(Str $chunk): self
{
return new self(
$this->method,
$this->url,
$this->protocol,
$this->headers,
$this->body->append($chunk->toString()),
);
}

public function finish(): Maybe
{
/** @var Maybe<Request> */
return Maybe::just(new Request\Request(
$this->url,
$this->method,
$this->protocol,
$this->headers,
Content\Lines::ofContent($this->body->toString()),
));
}
}
24 changes: 24 additions & 0 deletions src/Request/Buffer/Failure.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
declare(strict_types = 1);

namespace Innmind\HttpParser\Request\Buffer;

use Innmind\Http\Message\Request;
use Innmind\Immutable\{
Maybe,
Str,
};

final class Failure implements State
{
public function add(Str $chunk): self
{
return new $this;
}

public function finish(): Maybe
{
/** @var Maybe<Request> */
return Maybe::nothing();
}
}
183 changes: 183 additions & 0 deletions src/Request/Buffer/Headers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php
declare(strict_types = 1);

namespace Innmind\HttpParser\Request\Buffer;

use Innmind\Http\{
Message\Request,
Message\Method,
ProtocolVersion,
Header,
Headers as Container,
Factory\Header\TryFactory,
};
use Innmind\Url\Url;
use Innmind\Immutable\{
Maybe,
Str,
Sequence,
Map,
};

final class Headers implements State
{
private TryFactory $factory;
private Method $method;
private Url $url;
private ProtocolVersion $protocol;
private Str $buffer;
/** @var Sequence<Str> */
private Sequence $headers;

/**
* @param Sequence<Str> $headers
*/
private function __construct(
TryFactory $factory,
Method $method,
Url $url,
ProtocolVersion $protocol,
Str $buffer,
Sequence $headers,
) {
$this->factory = $factory;
$this->method = $method;
$this->url = $url;
$this->protocol = $protocol;
$this->buffer = $buffer;
$this->headers = $headers;
}

public static function new(
TryFactory $factory,
Method $method,
Url $url,
ProtocolVersion $protocol,
): self {
return new self(
$factory,
$method,
$url,
$protocol,
Str::of('', 'ASCII'),
Sequence::of(),
);
}

public function add(Str $chunk): State
{
$buffer = $this->buffer->append($chunk->toString());

return match ($buffer->contains("\n")) {
true => $this->parse($buffer),
false => new self(
$this->factory,
$this->method,
$this->url,
$this->protocol,
$buffer,
$this->headers,
),
};
}

public function finish(): Maybe
{
if (!$this->buffer->empty()) {
/** @var Maybe<Request> */
return Maybe::nothing();
}

/** @var Maybe<Request> */
return $this
->headers()
->map(fn($headers) => new Request\Request(
$this->url,
$this->method,
$this->protocol,
$headers,
));
}

private function parse(Str $buffer): State
{
// by adding an empty chunk at the end will recursively parse the
// headers while there is a new line in the buffer
return $buffer
->split("\n")
->match(
fn($header, $rest) => match ($header->empty()) {
true => $this->body(Str::of("\n")->join($rest->map(
static fn($part) => $part->toString(),
))),
false => new self(
$this->factory,
$this->method,
$this->url,
$this->protocol,
Str::of("\n")->join($rest->map(
static fn($part) => $part->toString(),
)),
($this->headers)($header),
),
},
fn() => $this->body(),
)
->add(Str::of(''));
}

/**
* @return Maybe<Container>
*/
private function headers(): Maybe
{
/**
* @psalm-suppress NamedArgumentNotAllowed
* Technically as header name can contain any octet between 0 and 127
* except control ones, the regexp below is a bit more restrictive than
* that by only accepting letters, numbers, '-', '_' and '.'
* @see https://www.rfc-editor.org/rfc/rfc2616#section-4.2
*/
return $this
->headers
->map(static fn($header) => $header->rightTrim("\r"))
->map(static fn($header) => $header->capture('~^(?<name>[a-zA-Z0-9\-\_\.]+): (?<value>.*)$~'))
->map(fn($captured) => $this->createHeader($captured))
->match(
static fn($first, $rest) => Maybe::all($first, ...$rest->toList())->map(
static fn(Header ...$headers) => Container::of(...$headers),
),
static fn() => Maybe::just(Container::of()),
);
}

/**
* @param Map<int|string, Str> $info
*
* @return Maybe<Header>
*/
private function createHeader(Map $info): Maybe
{
return Maybe::all($info->get('name'), $info->get('value'))->map(
fn(Str $name, Str $value) => ($this->factory)($name, $value),
);
}

private function body(Str $buffer = null): State
{
$buffer ??= Str::of('');

return $this
->headers()
->map(fn($headers) => Body::new(
$this->method,
$this->url,
$this->protocol,
$headers,
))
->match(
static fn($body) => $body->add($buffer),
static fn() => new Failure,
);
}
}
20 changes: 20 additions & 0 deletions src/Request/Buffer/State.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php
declare(strict_types = 1);

namespace Innmind\HttpParser\Request\Buffer;

use Innmind\Http\Message\Request;
use Innmind\Immutable\{
Maybe,
Str,
};

interface State
{
public function add(Str $chunk): self;

/**
* @return Maybe<Request>
*/
public function finish(): Maybe;
}
Loading

0 comments on commit 56bde91

Please sign in to comment.