Skip to content

Commit

Permalink
Docs: add testing section, finalize responses section, finish create-…
Browse files Browse the repository at this point in the history
…api section
  • Loading branch information
pionl committed May 1, 2022
1 parent e3d235b commit 75db051
Show file tree
Hide file tree
Showing 10 changed files with 407 additions and 58 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ composer require wrkflow/php-api-sdk-builder

> This library is still in its early stages. But the main concepts will probably remain same.
This package is for those who wants to build wants to consume external APIs with type strict code in mind.

## Features

![img](https://img.shields.io/badge/PHPStan-8-blue)
Expand All @@ -15,7 +17,7 @@ composer require wrkflow/php-api-sdk-builder
- ✅ Uses PSR packages you already use for HTTP/S communication
- 🏆 Forcing type strict implementation for input (request options) and output (`Response`)
- 🎗 Encouraging `Data transfer objects`
- 🎭 Re-usable headers using objects
- 🎭 Re-usable and configurable headers using objects

[📖 Read the documentation](https://php-sdk-builder.wrk-flow.com)

Expand Down
27 changes: 0 additions & 27 deletions docs/content/en/architecture/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,31 +134,4 @@ class UnitAvailabilityEntity
}
```

## Concerns (utils)

### WorksWithJson

Trait that allows you to access values from an array with type safe manner.

```php
/**
* @param array<string, mixed> $data
*/
public function getInt(array $data, string $key): ?int;

/**
* @param array<string, mixed> $data
*/
public function getFloat(array $data, string $key): ?float;

/**
* @param array<string, mixed> $data
*/
public function getBool(array $data, string $key): ?bool;

public function floatVal(mixed $value): ?float;

public function intVal(mixed $value): ?int;

public function boolVal(mixed $value): ?bool;
```
8 changes: 5 additions & 3 deletions docs/content/en/architecture/headers.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
title: Headers
category: Architecture
position: 15
fullscreen: true
---

> Use `YourApi\Headers` namespace
Expand All @@ -14,9 +15,10 @@ can return a map of headers 'key' => 'header' or 'key' => 'headers'.

**Available classes (fell free to PR more):**

- JsonContentTypeHeaders
- AcceptsJsonHeaders
- JsonHeaders (sets the content type and accepts json headers)
- `new JsonContentTypeHeaders()`
- `new AcceptsJsonHeaders()`
- `new JsonHeaders()` (sets the content type and accepts json headers)
- `BearerTokenAuthorizationHeader(string $token)`

Each header class can chain other headers (check JsonHeaders file).

Expand Down
53 changes: 39 additions & 14 deletions docs/content/en/architecture/responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ You are responsible to validate the:
- build the data and set it in response (can raise an exception).
- build an error entity if there is an error

Currently, we are not throwing exceptions. Use `isSuccesfull` to determine if there is an error.
Currently, we are not throwing exceptions. Use `isSuccesfull` to determine if there is an error. This allows
to work with the response with any state and adds more flexibility.

<alert>

Expand All @@ -26,32 +27,56 @@ This can probably change. Maybe raising exception is always the best option.

## Base implementation

1. Override **__construct** and try to build the response
1. Override **__construct** and try to build the response (parse json / xml / etc).
2. Create `private bool $isSuccessful = false;` and set it to true if the response is successful.
3. Create properties and entities for your data.
3. Implement `public function isSuccessful(): bool`
4. Create properties and entities for your data. Use **getters** instead of properties

## Json responses
### Consumer functions

To help parse JSON responses extend `AbstractJsonResponse` that will try to parse response to json and check if keys are present in an array.
These functions are provided for the consumer:

- `isSuccessful: bool` - Indicates if the response is successfully and the data can be accessed.
- `getResponse(): ResponseInterface` - Returns underlying response.

## JSON response

To help parse JSON responses extend `AbstractJsonResponse` that will try to parse response to json and check if keys are
present in an array.

- Raises `InvalidJsonResponseException` if the response is not json.
- Raises `InvalidJsonResponseException` if the of json value is invalid.
- Will not raise exception if any of desired keys is missing.
- Use `$this->hasKeys(json: $json, keys: [...]): bool` to check if the array contains given keys.
- Use `toArray` to get json data. Will raise exception if the data is not valid.

### AbstractJsonItemsResponse
### Consumer functions

These functions are provided for the consumer:

- `toArray(): array` - Returns the json. Will raise exception if the data is not valid.

### Implementation

- Implement `parseJson(array $json): bool;` to parse the json. Return false if not valid. **I recommend to create
properties and getters instead using toArray function**.
- You can use `$this->hasKeys(json: $json, keys: [...]): bool` to check if the array contains given keys.
- You can use [WorksWithJson](/architecture/utils#workswithjson) to easily get values from json with proper type.

## JSON response with items array

> For step by step implementation check [Start building / create response](/start-building/create-response)
When you want to provide a way to easily access array items in the json using transformer and base root keys validation
Use this if you want to provide a way to easily access array items in the json using transformer and base root keys
validation.

### Consumer functions

You can use:
These functions are provided for the consumer

- `getRawItems` - Returns raw items array
- `items` - Will return
- `getRawItems` - Returns raw items array.
- `items` - Will return transformed data.
- `loopItems` - Will provide a way to loop transformed items using closure.

#### Implementation
### Implementation

```php
/**
Expand All @@ -75,7 +100,7 @@ abstract protected function requiredRootKeys(): array;
abstract protected function itemsKey(): ?string;
```

Then implement `items` / `loopItems`. These methods are only to force you to set correct typehints.
Then implement `items` / `loopItems`. These methods are only to force you to set correct typehints.

```php
/**
Expand Down
201 changes: 201 additions & 0 deletions docs/content/en/architecture/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
---
title: Testing
category: Architecture
subTitle: Tools for testing.
position: 16
---

We do like unit test. We have provided basic TestCase using `Mockery` and `PHPUnit`.

## ResponseTestCase

Test case that is designed for testing responses.

Creates `SDKContainerFactoryContract` mock under `$this->container`.

### createTransformerMockViaContainer

Creates a mock of given transformer class and tells the container to return if it should be created.

### createJsonResponse

Returns mock for response interface that returns json array.

### Example

```php
class UnitsAvailabilitiesResponseTest extends ResponseTestCase
{
private JsonToUnitAvailabilityEntity|MockInterface $transformer;

protected function setUp(): void
{
parent::setUp();

$this->transformer = $this->createTransformerMockViaContainer(
JsonToUnitAvailabilityEntity::class
);
}


public function testValidData(): void
{
$firstItem = [
'ID' => 22918,
'isAvailable' => true,
'availabilityStatus' => [
[
'day' => '2016-11-21',
'status' => 'A',
],
],
];
$firstItemEntity = new UnitAvailabilityEntity(
22918,
true,
[]
);

$this->transformer->shouldReceive('transform')
->twice()
->with($firstItem)
->andReturn($firstItemEntity);

$secondItem = [
'ID' => 22919,
'isAvailable' => false,
'availabilityStatus' => [],
];
$secondItemEntity = new UnitAvailabilityEntity(
22919,
false,
[]
);

$this->transformer->shouldReceive('transform')
->twice()
->with($secondItem)
->andReturn($secondItemEntity);

$response = $this->createResponse([
'from' => '2016-11-21',
'to' => '2016-11-30',
'items' => [
$firstItem,
$secondItem,
],
]);

$foundFirstEntity = false;
$foundSecondEntity = false;

$this->assertEquals(true, $response->isSuccessful());
$this->assertEquals([$firstItemEntity, $secondItemEntity], $response->items());
$this->assertEquals(true, $response->loopItems(
function (UnitAvailabilityEntity $entity) use (
$firstItemEntity, &$foundFirstEntity, $secondItemEntity, &$foundSecondEntity
) {
if ($entity === $firstItemEntity) {
$foundFirstEntity = true;
}
if ($entity === $secondItemEntity) {
$foundSecondEntity = true;
}
})
);

$this->assertTrue($foundFirstEntity, 'loopItems should return correct entity');
$this->assertTrue($foundSecondEntity, 'loopItems should return correct entity');
}

public function testInvalidDataGetTo(): void
{
$response = $this->createResponse([
'test' => 'nothing',
]);

$this->expectErrorMessage('must not be accessed before initialization');
$this->assertEquals(false, $response->to);
}

/**
* @throws InvalidJsonResponseException
*/
protected function createResponse(array $responseData): UnitsAvailabilitiesResponse
{
return new UnitsAvailabilitiesResponse(
$this->createJsonResponse($responseData),
$this->container
);
}
}
```

## EndpointTestCase

Testcase that prepares basic API mocking:

- Builds `$this->uri : Uri` from with `https://localhost/test'` value.
- Mocks `$this->apiFactory : ApiFactory`
- Mocks `$this->api : AbstractApi` and returns `$this->uri` as a base url and `$this->apiFatory` for `factory()` calls.

### expectPost

Allows to expect a `post` call on api object. Provides a closures to validate the arguments:

```php
/**
* @param string $expectedUri expected url from the base url
* @param Closure(mixed):bool|null $assertBody Checks the body. Return false if not valid.
* @param Closure(mixed):bool|null $assertHeaders Checks the headers. Return false if not valid.
*/
protected function expectPost(
string $expectedUri,
?Closure $assertBody = null,
?Closure $assertHeaders = null,
): ExpectationInterface;
```

### expectMakeResponse

Tells the container to return given response when the functions calls `container->makeResponse`.

```php
protected function expectMakeResponse(
string $expectedClass,
ExpectationInterface $expectedResponse
): MockInterface;
```

### Example

```php
class UnitsAvailabilitiesEndpointTest extends EndpointTestCase
{
public function testGet(): void
{
$options = new GetAvailabilityOptions(
ids: [1],
from: '2022-01-01',
to: '2022-01-05'
);

$response = $this->expectPost(
self::BASE_URI . '/unitsavailabilities',
function (GetAvailabilityOptions $givenOptions) use ($options) {
return $options === $givenOptions;
}
);

$expectedResponse = $this->expectMakeResponse(
UnitsAvailabilitiesResponse::class,
$response
);

$endpoint = new UnitsAvailabilitiesEndpoint($this->api);
$result = $endpoint->get($options);

$this->assertSame($expectedResponse, $result);
}
}
```
37 changes: 37 additions & 0 deletions docs/content/en/architecture/utils.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: Utils
category: Architecture
subTitle: Tools for testing.
position: 17
---

## Concerns

Traits that helps you de-duplicate code.

### WorksWithJson

Trait that allows you to access values from an array with type safe manner.

```php
/**
* @param array<string, mixed> $data
*/
public function getInt(array $data, string $key): ?int;

/**
* @param array<string, mixed> $data
*/
public function getFloat(array $data, string $key): ?float;

/**
* @param array<string, mixed> $data
*/
public function getBool(array $data, string $key): ?bool;

public function floatVal(mixed $value): ?float;

public function intVal(mixed $value): ?int;

public function boolVal(mixed $value): ?bool;
```
Loading

0 comments on commit 75db051

Please sign in to comment.