Skip to content

Commit

Permalink
feat: Adds default status header based on HTTP code. Upgrade php vers…
Browse files Browse the repository at this point in the history
…ion to 8.2. (#21)
  • Loading branch information
gustavofreze authored Feb 4, 2024
1 parent 1605548 commit 690b675
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 42 deletions.
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,16 @@
}
},
"require": {
"php": "^8.1||^8.2",
"php": "^8.2",
"tiny-blocks/serializer": "^2.0",
"psr/http-message": "^1.1",
"ext-mbstring": "*"
},
"require-dev": {
"infection/infection": "^0.27",
"phpmd/phpmd": "^2.13",
"phpmd/phpmd": "^2.15",
"phpunit/phpunit": "^9.6",
"squizlabs/php_codesniffer": "^3.7"
"squizlabs/php_codesniffer": "^3.8"
},
"suggest": {
"ext-mbstring": "Provides multibyte-specific string functions that help us deal with multibyte encodings in PHP."
Expand Down
16 changes: 10 additions & 6 deletions src/HttpHeaders.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,20 @@ final class HttpHeaders
{
private array $values = [];

private function __construct()
public static function build(): HttpHeaders
{
return new HttpHeaders();
}

public static function build(): HttpHeaders
public function addFromCode(HttpCode $code): HttpHeaders
{
return new HttpHeaders();
$template = 'HTTP/1.1 %s';
$this->values['Status'][] = sprintf($template, $code->message());

return $this;
}

public function add(Header $header): HttpHeaders
public function addFromContentType(Header $header): HttpHeaders
{
$this->values[$header->key()][] = $header->value();

Expand All @@ -34,9 +38,9 @@ public function getHeader(string $key): array
return $this->values[$key] ?? [];
}

public function hasHeaders(): bool
public function hasNoHeaders(): bool
{
return !empty($this->values);
return empty($this->values);
}

public function hasHeader(string $key): bool
Expand Down
26 changes: 26 additions & 0 deletions src/HttpResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
*/
final class HttpResponse
{
# Successful (200 – 299)

public static function ok(mixed $data, ?HttpHeaders $headers = null): ResponseInterface
{
return Response::from(code: HttpCode::OK, data: $data, headers: $headers);
Expand All @@ -31,4 +33,28 @@ public static function noContent(?HttpHeaders $headers = null): ResponseInterfac
{
return Response::from(code: HttpCode::NO_CONTENT, data: null, headers: $headers);
}

# Client error (400 – 499)

public static function badRequest(mixed $data, ?HttpHeaders $headers = null): ResponseInterface
{
return Response::from(code: HttpCode::BAD_REQUEST, data: $data, headers: $headers);
}

public static function notFound(mixed $data, ?HttpHeaders $headers = null): ResponseInterface
{
return Response::from(code: HttpCode::NOT_FOUND, data: $data, headers: $headers);
}

public static function conflict(mixed $data, ?HttpHeaders $headers = null): ResponseInterface
{
return Response::from(code: HttpCode::CONFLICT, data: $data, headers: $headers);
}

# Server error (500 – 599)

public static function internalServerError(mixed $data, ?HttpHeaders $headers = null): ResponseInterface
{
return Response::from(code: HttpCode::INTERNAL_SERVER_ERROR, data: $data, headers: $headers);
}
}
10 changes: 10 additions & 0 deletions src/Internal/Header.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@

interface Header
{
/**
* Get the key of the header.
*
* @return string The key of the header.
*/
public function key(): string;

/**
* Get the value of the header.
*
* @return string The value of the header.
*/
public function value(): string;
}
15 changes: 7 additions & 8 deletions src/Internal/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,18 @@
use TinyBlocks\Http\Internal\Exceptions\BadMethodCall;
use TinyBlocks\Http\Internal\Stream\StreamFactory;

final class Response implements ResponseInterface
final readonly class Response implements ResponseInterface
{
private function __construct(
private readonly HttpCode $code,
private readonly StreamInterface $body,
private readonly HttpHeaders $headers
) {
private function __construct(private HttpCode $code, private StreamInterface $body, private HttpHeaders $headers)
{
}

public static function from(HttpCode $code, mixed $data, ?HttpHeaders $headers): ResponseInterface
{
if (is_null($headers) || !$headers->hasHeaders()) {
$headers = HttpHeaders::build()->add(header: HttpContentType::APPLICATION_JSON);
if (is_null($headers) || $headers->hasNoHeaders()) {
$headers = HttpHeaders::build()
->addFromCode(code: $code)
->addFromContentType(header: HttpContentType::APPLICATION_JSON);
}

return new Response(code: $code, body: StreamFactory::from(data: $data), headers: $headers);
Expand Down
10 changes: 5 additions & 5 deletions src/Internal/Stream/StreamMetaData.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

namespace TinyBlocks\Http\Internal\Stream;

final class StreamMetaData
final readonly class StreamMetaData
{
public function __construct(
private readonly string $uri,
private readonly string $mode,
private readonly bool $seekable,
private readonly string $streamType
private string $uri,
private string $mode,
private bool $seekable,
private string $streamType
) {
}

Expand Down
79 changes: 68 additions & 11 deletions tests/HttpResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,6 @@

class HttpResponseTest extends TestCase
{
private array $defaultHeader;

protected function setUp(): void
{
$this->defaultHeader = ['Content-Type' => [HttpContentType::APPLICATION_JSON->value]];
}

/**
* @dataProvider providerData
*/
Expand All @@ -26,7 +19,7 @@ public function testResponseOk(mixed $data, mixed $expected): void
self::assertEquals($expected, $response->getBody()->getContents());
self::assertEquals(HttpCode::OK->value, $response->getStatusCode());
self::assertEquals(HttpCode::OK->message(), $response->getReasonPhrase());
self::assertEquals($this->defaultHeader, $response->getHeaders());
self::assertEquals($this->defaultHeaderFrom(code: HttpCode::OK), $response->getHeaders());
}

/**
Expand All @@ -40,7 +33,7 @@ public function testResponseCreated(mixed $data, mixed $expected): void
self::assertEquals($expected, $response->getBody()->getContents());
self::assertEquals(HttpCode::CREATED->value, $response->getStatusCode());
self::assertEquals(HttpCode::CREATED->message(), $response->getReasonPhrase());
self::assertEquals($this->defaultHeader, $response->getHeaders());
self::assertEquals($this->defaultHeaderFrom(code: HttpCode::CREATED), $response->getHeaders());
}

/**
Expand All @@ -54,7 +47,7 @@ public function testResponseAccepted(mixed $data, mixed $expected): void
self::assertEquals($expected, $response->getBody()->getContents());
self::assertEquals(HttpCode::ACCEPTED->value, $response->getStatusCode());
self::assertEquals(HttpCode::ACCEPTED->message(), $response->getReasonPhrase());
self::assertEquals($this->defaultHeader, $response->getHeaders());
self::assertEquals($this->defaultHeaderFrom(code: HttpCode::ACCEPTED), $response->getHeaders());
}

public function testResponseNoContent(): void
Expand All @@ -65,7 +58,63 @@ public function testResponseNoContent(): void
self::assertEquals('', $response->getBody()->getContents());
self::assertEquals(HttpCode::NO_CONTENT->value, $response->getStatusCode());
self::assertEquals(HttpCode::NO_CONTENT->message(), $response->getReasonPhrase());
self::assertEquals($this->defaultHeader, $response->getHeaders());
self::assertEquals($this->defaultHeaderFrom(code: HttpCode::NO_CONTENT), $response->getHeaders());
}

/**
* @dataProvider providerData
*/
public function testResponseBadRequest(mixed $data, mixed $expected): void
{
$response = HttpResponse::badRequest(data: $data);

self::assertEquals($expected, $response->getBody()->__toString());
self::assertEquals($expected, $response->getBody()->getContents());
self::assertEquals(HttpCode::BAD_REQUEST->value, $response->getStatusCode());
self::assertEquals(HttpCode::BAD_REQUEST->message(), $response->getReasonPhrase());
self::assertEquals($this->defaultHeaderFrom(code: HttpCode::BAD_REQUEST), $response->getHeaders());
}

/**
* @dataProvider providerData
*/
public function testResponseNotFound(mixed $data, mixed $expected): void
{
$response = HttpResponse::notFound(data: $data);

self::assertEquals($expected, $response->getBody()->__toString());
self::assertEquals($expected, $response->getBody()->getContents());
self::assertEquals(HttpCode::NOT_FOUND->value, $response->getStatusCode());
self::assertEquals(HttpCode::NOT_FOUND->message(), $response->getReasonPhrase());
self::assertEquals($this->defaultHeaderFrom(code: HttpCode::NOT_FOUND), $response->getHeaders());
}

/**
* @dataProvider providerData
*/
public function testResponseConflict(mixed $data, mixed $expected): void
{
$response = HttpResponse::conflict(data: $data);

self::assertEquals($expected, $response->getBody()->__toString());
self::assertEquals($expected, $response->getBody()->getContents());
self::assertEquals(HttpCode::CONFLICT->value, $response->getStatusCode());
self::assertEquals(HttpCode::CONFLICT->message(), $response->getReasonPhrase());
self::assertEquals($this->defaultHeaderFrom(code: HttpCode::CONFLICT), $response->getHeaders());
}

/**
* @dataProvider providerData
*/
public function testResponseInternalServerError(mixed $data, mixed $expected): void
{
$response = HttpResponse::internalServerError(data: $data);

self::assertEquals($expected, $response->getBody()->__toString());
self::assertEquals($expected, $response->getBody()->getContents());
self::assertEquals(HttpCode::INTERNAL_SERVER_ERROR->value, $response->getStatusCode());
self::assertEquals(HttpCode::INTERNAL_SERVER_ERROR->message(), $response->getReasonPhrase());
self::assertEquals($this->defaultHeaderFrom(code: HttpCode::INTERNAL_SERVER_ERROR), $response->getHeaders());
}

public function providerData(): array
Expand Down Expand Up @@ -97,4 +146,12 @@ public function providerData(): array
]
];
}

private function defaultHeaderFrom(HttpCode $code): array
{
return [
'Status' => [sprintf('HTTP/1.1 %s', $code->message())],
'Content-Type' => [HttpContentType::APPLICATION_JSON->value]
];
}
}
6 changes: 3 additions & 3 deletions tests/Internal/HeadersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class HeadersTest extends TestCase
{
public function testAddAndGetValues(): void
{
$headers = HttpHeaders::build()->add(header: HttpContentType::APPLICATION_JSON);
$headers = HttpHeaders::build()->addFromContentType(header: HttpContentType::APPLICATION_JSON);
$expected = ['Content-Type' => [HttpContentType::APPLICATION_JSON->value]];

self::assertEquals($expected, $headers->toArray());
Expand All @@ -19,8 +19,8 @@ public function testAddAndGetValues(): void
public function testAddAndGetUniqueValues(): void
{
$headers = HttpHeaders::build()
->add(header: HttpContentType::TEXT_HTML)
->add(header: HttpContentType::APPLICATION_PDF);
->addFromContentType(header: HttpContentType::TEXT_HTML)
->addFromContentType(header: HttpContentType::APPLICATION_PDF);
$expected = ['Content-Type' => [HttpContentType::APPLICATION_PDF->value]];

self::assertEquals($expected, $headers->toArray());
Expand Down
11 changes: 7 additions & 4 deletions tests/Internal/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ class ResponseTest extends TestCase
public function testDefaultHeaders(): void
{
$response = Response::from(code: HttpCode::OK, data: [], headers: null);
$expected = ['Content-Type' => [HttpContentType::APPLICATION_JSON->value]];
$expected = [
'Status' => [sprintf('HTTP/1.1 %s', HttpCode::OK->message())],
'Content-Type' => [HttpContentType::APPLICATION_JSON->value]
];

self::assertEquals($expected, $response->getHeaders());
}
Expand All @@ -28,7 +31,7 @@ public function testGetProtocolVersion(): void

public function testGetHeaders(): void
{
$headers = HttpHeaders::build()->add(header: HttpContentType::APPLICATION_JSON);
$headers = HttpHeaders::build()->addFromContentType(header: HttpContentType::APPLICATION_JSON);
$response = Response::from(code: HttpCode::OK, data: [], headers: $headers);
$expected = [HttpContentType::APPLICATION_JSON->value];

Expand All @@ -38,7 +41,7 @@ public function testGetHeaders(): void

public function testHasHeader(): void
{
$headers = HttpHeaders::build()->add(header: HttpContentType::TEXT_PLAIN);
$headers = HttpHeaders::build()->addFromContentType(header: HttpContentType::TEXT_PLAIN);
$response = Response::from(code: HttpCode::OK, data: [], headers: $headers);
$expected = [HttpContentType::TEXT_PLAIN->value];

Expand All @@ -48,7 +51,7 @@ public function testHasHeader(): void

public function testGetHeaderLine(): void
{
$headers = HttpHeaders::build()->add(header: HttpContentType::APPLICATION_JSON);
$headers = HttpHeaders::build()->addFromContentType(header: HttpContentType::APPLICATION_JSON);
$response = Response::from(code: HttpCode::OK, data: [], headers: $headers);

self::assertEquals(HttpContentType::APPLICATION_JSON->value, $response->getHeaderLine(name: 'Content-Type'));
Expand Down
4 changes: 2 additions & 2 deletions tests/Mock/Xyz.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

namespace TinyBlocks\Http\Mock;

final class Xyz
final readonly class Xyz
{
public function __construct(public readonly int $value)
public function __construct(public int $value)
{
}
}

0 comments on commit 690b675

Please sign in to comment.