Skip to content

Commit

Permalink
Merge pull request #236 from Sammyjo20/feature/v2-stream-body
Browse files Browse the repository at this point in the history
Feature | HasStreamBody
  • Loading branch information
Sammyjo20 authored Jun 15, 2023
2 parents d7c3e14 + 655bac5 commit e6d3017
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 0 deletions.
5 changes: 5 additions & 0 deletions phpstan.baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,8 @@ parameters:
message: "#^Parameter \\#2 \\$ of callable callable\\(\\$this, TValue\\)\\: void expects TValue\\, array\\|float\\|int\\|string\\|false\\|null given.$#"
count: 2
path: src/Repositories/Body/StringBodyRepository.php

-
message: "#^Parameter \\#2 \\$ of callable callable\\(\\$this, TValue\\)\\: void expects TValue\\, array\\|float\\|int\\|string\\|false\\|null given.$#"
count: 2
path: src/Repositories/Body/StreamBodyRepository.php
2 changes: 2 additions & 0 deletions src/Http/Senders/GuzzleSender.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Saloon\Repositories\Body\FormBodyRepository;
use Saloon\Repositories\Body\JsonBodyRepository;
use Saloon\Contracts\Response as ResponseContract;
use Saloon\Repositories\Body\StreamBodyRepository;
use Saloon\Repositories\Body\StringBodyRepository;
use Saloon\Exceptions\Request\FatalRequestException;
use Saloon\Repositories\Body\MultipartBodyRepository;
Expand Down Expand Up @@ -190,6 +191,7 @@ protected function createRequestOptions(PendingRequest $pendingRequest): array
$body instanceof MultipartBodyRepository => $requestOptions[RequestOptions::MULTIPART] = $body->toArray(),
$body instanceof FormBodyRepository => $requestOptions[RequestOptions::FORM_PARAMS] = $body->all(),
$body instanceof StringBodyRepository => $requestOptions[RequestOptions::BODY] = $body->all(),
$body instanceof StreamBodyRepository => $requestOptions[RequestOptions::BODY] = $body->all(),
default => $requestOptions[RequestOptions::BODY] = (string)$body,
};

Expand Down
118 changes: 118 additions & 0 deletions src/Repositories/Body/StreamBodyRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace Saloon\Repositories\Body;

use GuzzleHttp\Psr7\Utils;
use InvalidArgumentException;
use Saloon\Traits\Conditionable;
use Psr\Http\Message\StreamInterface;
use Saloon\Contracts\Body\BodyRepository;

class StreamBodyRepository implements BodyRepository
{
use Conditionable;

/**
* The stream body
*
* @var StreamInterface|null
*/
protected ?StreamInterface $stream = null;

/**
* Constructor
*
* @param StreamInterface|resource|null $value
*/
public function __construct(mixed $value = null)
{
$this->set($value);
}

/**
* Set a value inside the repository
*
* @param StreamInterface|resource|null $value
* @return $this
*/
public function set(mixed $value): static
{
if (isset($value) && ! $value instanceof StreamInterface && ! is_resource($value)) {
throw new InvalidArgumentException('The value must a resource or be an instance of ' . StreamInterface::class);
}

if (is_resource($value)) {
$value = Utils::streamFor($value);
}

$this->stream = $value;

return $this;
}

/**
* Retrieve the stream from the repository
*
* @return StreamInterface|null
*/
public function all(): ?StreamInterface
{
return $this->stream;
}

/**
* Retrieve the stream from the repository
*
* Alias of "all" method.
*
* @return StreamInterface|null
*/
public function get(): ?StreamInterface
{
return $this->all();
}

/**
* Determine if the repository is empty
*
* @return bool
*/
public function isEmpty(): bool
{
return is_null($this->stream);
}

/**
* Determine if the repository is not empty
*
* @return bool
*/
public function isNotEmpty(): bool
{
return ! $this->isEmpty();
}

/**
* Get the contents of the stream as a string
*
* @return string
*/
public function __toString(): string
{
$stream = &$this->stream;

if (is_null($stream)) {
return '';
}

$contents = $stream->getContents();

if ($stream->isSeekable()) {
$stream->rewind();
}

return $contents;
}
}
40 changes: 40 additions & 0 deletions src/Traits/Body/HasStreamBody.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Saloon\Traits\Body;

use Psr\Http\Message\StreamInterface;
use Saloon\Repositories\Body\StreamBodyRepository;

trait HasStreamBody
{
use ChecksForHasBody;

/**
* Body Repository
*
* @var \Saloon\Repositories\Body\StreamBodyRepository
*/
protected StreamBodyRepository $body;

/**
* Retrieve the data repository
*
* @return \Saloon\Repositories\Body\StreamBodyRepository
*/
public function body(): StreamBodyRepository
{
return $this->body ??= new StreamBodyRepository($this->defaultBody());
}

/**
* Default body
*
* @return StreamInterface|resource|null
*/
protected function defaultBody(): mixed
{
return null;
}
}
36 changes: 36 additions & 0 deletions tests/Feature/Body/HasStreamBodyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

use Saloon\Http\Faking\MockResponse;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\RequestInterface;
use GuzzleHttp\Promise\FulfilledPromise;
use Saloon\Tests\Fixtures\Connectors\TestConnector;
use Saloon\Tests\Fixtures\Requests\HasStreamBodyRequest;

test('the default body is loaded', function () {
$request = new HasStreamBodyRequest;

expect($request->body()->all())->toBeInstanceOf(StreamInterface::class);
expect($request->body()->get())->toBeInstanceOf(StreamInterface::class);
expect((string)$request->body())->toEqual('Howdy, Partner');
});

test('the guzzle sender properly sends it', function () {
$connector = new TestConnector;
$request = new HasStreamBodyRequest;

$request->headers()->add('Content-Type', 'application/custom');

$connector->sender()->addMiddleware(function (callable $handler) use ($request) {
return function (RequestInterface $guzzleRequest, array $options) use ($request) {
expect($guzzleRequest->getHeader('Content-Type'))->toEqual(['application/custom']);
expect((string)$guzzleRequest->getBody())->toEqual('Howdy, Partner');

return new FulfilledPromise(MockResponse::make()->getPsrResponse());
};
});

$connector->send($request);
});
43 changes: 43 additions & 0 deletions tests/Fixtures/Requests/HasStreamBodyRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Saloon\Tests\Fixtures\Requests;

use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Contracts\Body\HasBody;
use Saloon\Traits\Body\HasStreamBody;

class HasStreamBodyRequest extends Request implements HasBody
{
use HasStreamBody;

/**
* Define the method that the request will use.
*
* @var Method
*/
protected Method $method = Method::GET;

/**
* Define the endpoint for the request.
*
* @return string
*/
public function resolveEndpoint(): string
{
return '/user';
}

protected function defaultBody(): mixed
{
$temp = fopen('php://memory', 'rw');

fwrite($temp, 'Howdy, Partner');

rewind($temp);

return $temp;
}
}
114 changes: 114 additions & 0 deletions tests/Unit/Body/StreamBodyRepositoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

declare(strict_types=1);

use GuzzleHttp\Psr7\Utils;
use Saloon\Repositories\Body\StreamBodyRepository;

test('the store is empty by default', function () {
$body = new StreamBodyRepository();

expect($body->all())->toBeNull();
});


test('the store can have a default stream provided', function () {
$resource = tmpfile();

$body = new StreamBodyRepository($resource);

expect($body->all())->toEqual(Utils::streamFor($resource));
});

test('you can set it', function () {
$resourceA = fopen('php://memory', 'rw+');
fwrite($resourceA, 'Howdy');

$resourceB = fopen('php://memory', 'rw+');
fwrite($resourceB, 'Yeehaw');

$body = new StreamBodyRepository($resourceA);

$body->set($resourceB);

expect($body->all())->toEqual(Utils::streamFor($resourceB));
});

test('you can conditionally set on the store', function () {
$body = new StreamBodyRepository();

$resourceA = fopen('php://memory', 'rw+');
fwrite($resourceA, 'Howdy');

$resourceB = fopen('php://memory', 'rw+');
fwrite($resourceB, 'Yeehaw');

$body->when(true, fn (StreamBodyRepository $body) => $body->set($resourceA));
$body->when(false, fn (StreamBodyRepository $body) => $body->set($resourceB));

expect($body->all())->toEqual(Utils::streamFor($resourceA));
});

test('you can check if the store is empty or not', function () {
$body = new StreamBodyRepository();

expect($body->isEmpty())->toBeTrue();
expect($body->isNotEmpty())->toBeFalse();

$body->set(tmpfile());

expect($body->isEmpty())->toBeFalse();
expect($body->isNotEmpty())->toBeTrue();
});

test('it will throw an exception if the value is not a resource or StreamInterface when instantiating', function (mixed $value) {
$this->expectException(InvalidArgumentException::class);

new StreamBodyRepository($value);
})->with([
fn () => 'Howdy',
fn () => 123,
fn () => [],
fn () => false,
]);

test('it will throw an exception if the value is not a resource or StreamInterface when setting', function (mixed $value) {
$this->expectException(InvalidArgumentException::class);

new StreamBodyRepository($value);
})->with([
fn () => 'Howdy',
fn () => 123,
fn () => [],
fn () => false,
]);

test('it allows null values', function () {
$body = new StreamBodyRepository(null);

expect($body->all())->toBeNull();
expect($body->isEmpty())->toBeTrue();

$body->set(null);

expect($body->all())->toBeNull();
expect($body->isEmpty())->toBeTrue();
});

test('the stream is rewound after converting to a string', function () {
$resource = fopen('php://memory', 'rw+');
fwrite($resource, 'Howdy');
rewind($resource);

$body = new StreamBodyRepository($resource);
$stream = $body->get();

expect((string)$body)->toEqual('Howdy');
expect($stream->getContents())->toEqual('Howdy');
});

test('if the contents of the body is null then an empty string is returned', function () {
$body = new StreamBodyRepository();

expect((string)$body)->toEqual('');
});

0 comments on commit e6d3017

Please sign in to comment.