Skip to content

Commit

Permalink
Enhanced decoding flexibility and mapping capability
Browse files Browse the repository at this point in the history
Introduced iterable as the return type for decoders to allow for more
flexible data structures, moving away from strict arrays. This allows
the use of generators for on-demand data processing. Added a
`MapperInterface`
for transforming responses into objects, facilitating object-oriented
handling of response data. Streamlined the creation of default decoders
by defining `ChainDecoder::default` to reduce redundancy and ensure
consistency across usage. A new `GenericMapper` allows custom mapping
functions, providing clients with customizable response handling
strategies.

Additionally, `Response` now implements `JsonSerializable`, offering a
standardized way to convert response data to JSON.
  • Loading branch information
jenky committed Jan 2, 2024
1 parent 088f6a6 commit d8dec9a
Show file tree
Hide file tree
Showing 13 changed files with 240 additions and 32 deletions.
4 changes: 2 additions & 2 deletions src/Contracts/DecoderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
interface DecoderInterface
{
/**
* Decode response body to native array type.
* Decode response body.
*
* @throws \Fansipan\Exception\NotDecodableException if decoder is unable to decode the response
*/
public function decode(ResponseInterface $response): array;
public function decode(ResponseInterface $response): iterable;
}
15 changes: 15 additions & 0 deletions src/Contracts/MapperInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Fansipan\Contracts;

use Psr\Http\Message\ResponseInterface;

interface MapperInterface extends DecoderInterface
{
/**
* Map the response to an object.
*/
public function map(ResponseInterface $response): ?object;
}
22 changes: 16 additions & 6 deletions src/Decoder/ChainDecoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,29 @@ public function __construct(iterable $decoders)
$this->decoders = $decoders;
}

/**
* @throws \Fansipan\Exception\NotDecodableException
*/
public function decode(ResponseInterface $response): array
public function decode(ResponseInterface $response): iterable
{
foreach ($this->decoders as $decoder) {
try {
return $decoder->decode($response);
yield from $decoder->decode($response);
} catch (NotDecodableException $e) {
continue;
}
}
}

/**
* Create default chain decoder.
*/
public static function default(): self
{
$decoders = static function () {
yield from [
new JsonDecoder(),
new XmlDecoder(),
];
};

throw NotDecodableException::create();
return new self($decoders());
}
}
2 changes: 1 addition & 1 deletion src/Decoder/JsonDecoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ final class JsonDecoder implements DecoderInterface
/**
* @throws \Fansipan\Exception\NotDecodableException
*/
public function decode(ResponseInterface $response): array
public function decode(ResponseInterface $response): iterable
{
if (! $this->supports($response)) {
throw NotDecodableException::create();
Expand Down
2 changes: 1 addition & 1 deletion src/Decoder/XmlDecoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ final class XmlDecoder implements DecoderInterface
/**
* @throws \Fansipan\Exception\NotDecodableException
*/
public function decode(ResponseInterface $response): array
public function decode(ResponseInterface $response): iterable
{
if (! $this->supports($response)) {
throw NotDecodableException::create();
Expand Down
59 changes: 59 additions & 0 deletions src/Mapper/GenericMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Fansipan\Mapper;

use Fansipan\Contracts\DecoderInterface;
use Fansipan\Contracts\MapperInterface;
use Fansipan\Decoder\ChainDecoder;
use Psr\Http\Message\ResponseInterface;

final class GenericMapper implements MapperInterface
{
/**
* @var callable(iterable): ?object
*/
private $onSuccess;

/**
* @var callable(iterable): ?object
*/
private $onFailure;

/**
* @var DecoderInterface
*/
private $decoder;

/**
* @param callable(iterable): ?object $onSuccess
* @param callable(iterable): ?object $onFailure
*/
public function __construct(
callable $onSuccess,
callable $onFailure,
?DecoderInterface $decoder = null
) {
$this->onSuccess = $onSuccess;
$this->onFailure = $onFailure;
$this->decoder = $decoder ?? ChainDecoder::default();
}

public function map(ResponseInterface $response): ?object
{
$status = $response->getStatusCode();
$decoded = $this->decoder->decode($response);

if ($status >= 200 && $status < 300) {
return ($this->onSuccess)($decoded);
} else {
return ($this->onFailure)($decoded);
}
}

public function decode(ResponseInterface $response): iterable
{
return $this->decoder->decode($response);
}
}
11 changes: 1 addition & 10 deletions src/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
use Fansipan\Contracts\DecoderInterface;
use Fansipan\Contracts\PayloadInterface;
use Fansipan\Decoder\ChainDecoder;
use Fansipan\Decoder\JsonDecoder;
use Fansipan\Decoder\XmlDecoder;

abstract class Request
{
Expand Down Expand Up @@ -128,13 +126,6 @@ private function createPayload(): PayloadInterface
*/
public function decoder(): DecoderInterface
{
$decoders = static function () {
yield from [
new JsonDecoder(),
new XmlDecoder(),
];
};

return new ChainDecoder($decoders());
return ChainDecoder::default();
}
}
34 changes: 24 additions & 10 deletions src/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@

use Closure;
use Fansipan\Contracts\DecoderInterface;
use Fansipan\Contracts\MapperInterface;
use Fansipan\Exception\HttpException;
use LogicException;
use Psr\Http\Message\ResponseInterface;

final class Response implements \ArrayAccess, \Stringable
final class Response implements \ArrayAccess, \JsonSerializable, \Stringable
{
use Traits\Macroable;

/**
* @var \Psr\Http\Message\ResponseInterface
* @var ResponseInterface
*/
private $response;

/**
* @var null|\Fansipan\Contracts\DecoderInterface
* @var null|DecoderInterface
*/
private $decoder;

Expand Down Expand Up @@ -53,20 +54,30 @@ public function body(): string
public function data(): array
{
if (! $this->decoded) {
$this->decoded = $this->decode();
$this->decoded = Util::iteratorToArray($this->decode());
}

return $this->decoded;
}

/**
* Decode the response body.
*
* @return array<array-key, mixed>
* Get the decoded body of the response as an object.
*/
public function object(): ?object
{
if (! $this->decoder instanceof MapperInterface) {
return null;
}

return $this->decoder->map($this->response);
}

/**
* Get the decoded body the response.
*
* @throws \Fansipan\Exception\NotDecodableException
*/
private function decode(): array
public function decode(): iterable
{
if (! $this->decoder instanceof DecoderInterface) {
return [];
Expand Down Expand Up @@ -285,10 +296,13 @@ public function offsetUnset($offset): void
throw new LogicException('Response data may not be mutated using array access.');
}

public function jsonSerialize(): mixed
{
return \json_encode($this->data());
}

/**
* Get the body of the response.
*
* @return string
*/
public function __toString()
{
Expand Down
8 changes: 8 additions & 0 deletions src/Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,12 @@ public static function request(Request $request, ?string $baseUri = null): Reque
->createStream((string) $request->body())
);
}

/**
* Convert iterator to array.
*/
public static function iteratorToArray(iterable $data): array
{
return $data instanceof \Traversable ? \iterator_to_array($data) : (array) $data;
}
}
30 changes: 30 additions & 0 deletions tests/MapperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Fansipan\Tests;

use Fansipan\GenericConnector;
use Fansipan\Mock\MockClient;
use Fansipan\Mock\MockResponse;
use Fansipan\Tests\Services\JsonPlaceholder\GetUserRequest;
use Fansipan\Tests\Services\JsonPlaceholder\User;

final class MapperTest extends TestCase
{
public function test_mapper(): void
{
$client = new MockClient([
MockResponse::fixture(__DIR__.'/fixtures/user.json'),
]);

$connector = (new GenericConnector())->withClient($client);
$response = $connector->send(new GetUserRequest(1));

$user = $response->object();

$this->assertInstanceOf(User::class, $user);
$this->assertSame(1, $user->id);
$this->assertSame('Leanne Graham', $user->name);
}
}
2 changes: 0 additions & 2 deletions tests/Services/HTTPBin/GetXmlRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@

final class GetXmlRequest extends Request
{
protected $connector = Connector::class;

public function endpoint(): string
{
return '/xml';
Expand Down
37 changes: 37 additions & 0 deletions tests/Services/JsonPlaceholder/GetUserRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Fansipan\Tests\Services\JsonPlaceholder;

use Fansipan\Contracts\DecoderInterface;
use Fansipan\Mapper\GenericMapper;
use Fansipan\Request;
use Fansipan\Util;

final class GetUserRequest extends Request
{
private $id;

public function __construct(int $id)
{
$this->id = $id;
}

public function endpoint(): string
{
return 'https://jsonplaceholder.typicode.com/users/'.$this->id;
}

public function decoder(): DecoderInterface
{
return new GenericMapper(
static function (iterable $data) {
return User::fromArray(Util::iteratorToArray($data));
},
static function (iterable $data) {
return User::fromArray(Util::iteratorToArray($data));
}
);
}
}
46 changes: 46 additions & 0 deletions tests/Services/JsonPlaceholder/User.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Fansipan\Tests\Services\JsonPlaceholder;

final class User
{
/**
* @var int
*/
public $id;

/**
* @var string
*/
public $name;

/**
* @var string
*/
public $email;

/**
* @var string
*/
public $phone;

/**
* @var string
*/
public $website;

public static function fromArray(array $data): self
{
$self = new self();

$self->id = $data['id'] ?? 0;
$self->name = $data['name'] ?? '';
$self->email = $data['email'] ?? '';
$self->phone = $data['phone'] ?? '';
$self->website = $data['website'] ?? '';

return $self;
}
}

0 comments on commit d8dec9a

Please sign in to comment.