Skip to content

Commit

Permalink
Merge a366845 into 7eb28cd
Browse files Browse the repository at this point in the history
  • Loading branch information
mabar committed Aug 24, 2018
2 parents 7eb28cd + a366845 commit 929b3c3
Show file tree
Hide file tree
Showing 18 changed files with 441 additions and 74 deletions.
77 changes: 48 additions & 29 deletions .docs/README.md
Expand Up @@ -22,7 +22,7 @@

## Installation

Simpliest way to register this core API library is via [Nette\DI\CompilerExtension](https://api.nette.org/2.4/Nette.DI.CompilerExtension.html).
Simplest way to register this core API library is via [Nette\DI\CompilerExtension](https://api.nette.org/2.4/Nette.DI.CompilerExtension.html).

```
composer require apitte/core
Expand Down Expand Up @@ -106,21 +106,21 @@ At the end, open your browser and locate to `localhost/<api-project>/hello/world

## Annotations

| Annotation | Target | Attributes | Description |
|----------------------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
| `@Controller` | Class | none | Mark as as type `controller`. |
| `@ControllerId` | Class | value=`{a-z, A-Z, 0-9, _}` | Prefix all children methods ids with `id`. |
| `@ControllerPath` | Class | value=`{a-z, A-Z, 0-9, -_/}` | Prefix all children methods paths with `path`. |
| `@GroupId` | Class | value=`{a-z, A-Z, 0-9, _}` | Prefix all children methods ids with `id`. Can be set only on abstract class. |
| `@GroupPath` | Class | value=`{a-z, A-Z, 0-9, -_/}` | Prefix all children methods paths with `path`. Can be set only on abstract class. |
| `@Id` | Method | value=`{a-z, A-Z, 0-9, _}` | Set `id` to target method. |
| `@Method` | Method | GET, POST, PUT, OPTION, DELETE, HEAD | Set `method` to target method. |
| `@Negotiations` | Method | `@Negotiation` | Group annotation for `@Negotiation`. |
| `@Negotiation` | Method | `suffix={string}`, `default={true/false}`, `renderer={string}` | Define negotiation mode to target method. |
| `@Path` | Method | `value={a-z, A-Z, 0-9, -_/{}}` | Set `path` to target method. A.k.a. URL path. |
| `@RequestParameters` | Method | `@RequestParameter` | Group annotation for `@RequestParameter`. |
| `@RequestParameter` | Method | `name={string}`, `type={int/string/float/bool}`, `description={string}`, `in={path/query}`, `required={true/false}`, `deprecated={true/false}`, `allowEmpty={true/false}` | Define dynamic typed parameter. |
| `@Tag` | Method | `name={string}`, `value={mixed}` | Add `tag` to target method. |
| Annotation | Target | Attributes | Description |
|----------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
| `@Controller` | Class | none | Mark as as type `controller`. |
| `@ControllerId` | Class | value=`{a-z, A-Z, 0-9, _}` | Prefix all children methods ids with `id`. |
| `@ControllerPath` | Class | value=`{a-z, A-Z, 0-9, -_/}` | Prefix all children methods paths with `path`. |
| `@GroupId` | Class | value=`{a-z, A-Z, 0-9, _}` | Prefix all children methods ids with `id`. Can be set only on abstract class. |
| `@GroupPath` | Class | value=`{a-z, A-Z, 0-9, -_/}` | Prefix all children methods paths with `path`. Can be set only on abstract class. |
| `@Id` | Method | value=`{a-z, A-Z, 0-9, _}` | Set `id` to target method. |
| `@Method` | Method | GET, POST, PUT, OPTION, DELETE, HEAD | Set `method` to target method. |
| `@Negotiations` | Method | `@Negotiation` | Group annotation for `@Negotiation`. |
| `@Negotiation` | Method | `suffix={string}`, `default={true/false}`, `renderer={string}` | Define negotiation mode to target method. |
| `@Path` | Method | `value={a-z, A-Z, 0-9, -_/{}}` | Set `path` to target method. A.k.a. URL path. |
| `@RequestParameters` | Method | `@RequestParameter` | Group annotation for `@RequestParameter`. |
| `@RequestParameter` | Method | `name={string}`, `type={scalar/string/int/float/bool/datetime}`, `description={string}`, `in={path/query}`, `required={true/false}`, `deprecated={true/false}`, `allowEmpty={true/false}` | Define dynamic typed parameter. |
| `@Tag` | Method | `name={string}`, `value={mixed}` | Add `tag` to target method. |

## Decorators

Expand Down Expand Up @@ -150,7 +150,7 @@ Another available plugins are:

- [`apitte\debug`](https://github.com/apitte/debug) - adds debugging tools for developing
- [`apitte\middlewares`](https://github.com/apitte/middlewares) - adds support for middlewares, depends on [`contributte\middlewares`](https://github.com/contributte/middlewares)
- [`apitte\negotiation`](https://github.com/apitte/negotiation) - adds support for varient content negotiations (.json, .debug, .csv, etc.)
- [`apitte\negotiation`](https://github.com/apitte/negotiation) - adds support for variant content negotiations (.json, .debug, .csv, etc.)
- [`apitte\openapi`](https://github.com/apitte/openapi) - adds support for openapi and swagger
- [`apitte\events`](https://github.com/apitte/events) - [WIP] - adds support for symfony/event-dispatcher (which is ported into nette via [`contributte\event-dispatcher`](https://github.com/contributte/event-dispatcher))

Expand All @@ -168,8 +168,8 @@ Each **decorator** should be registered with tag `apitte.core.decorator`.

Each decorator should provide `type` attribute:

- `handle.before` - called before controller method is trigged (after endpoint is matched in router)
- `handle.after` - called after controller method is trigged (after logic in controller)
- `handle.before` - called before controller method is triggered (after endpoint is matched in router)
- `handle.after` - called after controller method is triggered (after logic in controller)
- `dispatcher.exception` - called if exception has been occurred

Also you should define a priority for better sorting. Default is 10.
Expand Down Expand Up @@ -207,12 +207,12 @@ api:
Apitte\Core\DI\Plugin\CoreMappingPlugin:
types: []
request:
validator:
validator: Apitte\Core\Mapping\Validator\NullValidator
```

#### Types

This plugin allows you to define new annotations.
This plugin allows you to define annotation @RequestParameter which validates and converts data type of GET parameters.

```php
/**
Expand All @@ -229,22 +229,42 @@ public function detail(ApiRequest $request)
}
```

It converts request parameters to defined types. By default, you can use `int`, `float`, `string`. Or defined
more types in neon.
Available data types are `scalar`, `string`, `int`, `float`, `bool` and `datetime`.

- `scalar`
- Tries to automatically convert value to boolean, int or float.
- Returns string, if conversion is not possible.
- `string`
- Simply returns given value.
- `int`
- Converts value to int.
- Could overflow to float if value is bigger than PHP could handle. If it is your case then replace `IntegerTypeMapper` with `StringTypeMapper`
- `float`
- Converts value to float.
- Accepts values which have decimals divided by comma `,` or dot `.`
- `bool`
- Converts `'true'` and `'1'` to `true`
- and `false` and `0` to `false`
- `datetime`
- Converts value to DateTimeImmutable.
- Each of the data types could return null if @RequestParameter(allowEmpty=true)
- If conversion is not possible so API returns HTTP 400

You can override these types by your own implementation.

```yaml
api:
plugins:
Apitte\Core\DI\Plugin\CoreMappingPlugin:
types:
scalar: Apitte\Core\Mapping\Parameter\ScalarTypeMapper
string: Apitte\Core\Mapping\Parameter\StringTypeMapper
int: Apitte\Core\Mapping\Parameter\IntegerTypeMapper
float: Apitte\Core\Mapping\Parameter\FloatTypeMapper
string: Apitte\Core\Mapping\Parameter\StringTypeMapper
special: App\MySpecialType
bool: Apitte\Core\Mapping\Parameter\BooleanTypeMapper
datetime: Apitte\Core\Mapping\Parameter\DateTimeTypeMapper
```

Don't forget to register default one, because filling of `types` overrides default types.

#### Entity

##### RequestMapper
Expand All @@ -270,7 +290,7 @@ final class UserFilter extends BasicEntity
}
```

And some endoint with `@RequestMapper` annotation. There's a method `ApiRequest::getEntity()`, it gets
And some endpoint with `@RequestMapper` annotation. There's a method `ApiRequest::getEntity()`, it gets
the entity from request attributes. So simple, right?

```php
Expand All @@ -293,7 +313,6 @@ pick the validator you want to.
api:
plugins:
Apitte\Core\DI\Plugin\CoreMappingPlugin:
types: []
request:
# By default
validator: Apitte\Core\Mapping\Validator\NullValidator
Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -119,7 +119,7 @@ As you can see, the architecture is ultra simple. `ApiRequest` & `ApiResponse` w

<a href="https://github.com/tlapnet"><img width="200" src="https://cdn.rawgit.com/f3l1x/xsource/2463efb7/assets/tlapdev.png"></a>

The development is sponsored by [Tlapnet](http://www.tlapnet.cz) and a lot of coffeees. Thank you guys! :+1:
The development is sponsored by [Tlapnet](http://www.tlapnet.cz) and a lot of coffees. Thank you guys! :+1:

-----

Expand Down
13 changes: 8 additions & 5 deletions src/DI/Plugin/CoreMappingPlugin.php
Expand Up @@ -6,9 +6,11 @@
use Apitte\Core\Decorator\RequestEntityDecorator;
use Apitte\Core\Decorator\RequestParametersDecorator;
use Apitte\Core\DI\ApiExtension;
use Apitte\Core\Exception\Logical\InvalidStateException;
use Apitte\Core\Mapping\Parameter\BooleanTypeMapper;
use Apitte\Core\Mapping\Parameter\DateTimeTypeMapper;
use Apitte\Core\Mapping\Parameter\FloatTypeMapper;
use Apitte\Core\Mapping\Parameter\IntegerTypeMapper;
use Apitte\Core\Mapping\Parameter\ScalarTypeMapper;
use Apitte\Core\Mapping\Parameter\StringTypeMapper;
use Apitte\Core\Mapping\RequestEntityMapping;
use Apitte\Core\Mapping\RequestParameterMapping;
Expand All @@ -23,9 +25,12 @@ class CoreMappingPlugin extends AbstractPlugin
/** @var mixed[] */
protected $defaults = [
'types' => [
'scalar' => ScalarTypeMapper::class,
'string' => StringTypeMapper::class,
'int' => IntegerTypeMapper::class,
'float' => FloatTypeMapper::class,
'string' => StringTypeMapper::class,
'bool' => BooleanTypeMapper::class,
'datetime' => DateTimeTypeMapper::class,
],
'request' => [
'validator' => NullValidator::class,
Expand All @@ -44,9 +49,7 @@ public function __construct(PluginCompiler $compiler)
public function loadPluginConfiguration(): void
{
$builder = $this->getContainerBuilder();
$config = $this->getConfig();

if (empty($config['types'])) throw new InvalidStateException('No mapping types provided');
$config = $this->setupConfig($this->defaults, $this->getConfig());

$builder->addDefinition($this->prefix('request.parameters.decorator'))
->setFactory(RequestParametersDecorator::class)
Expand Down
38 changes: 38 additions & 0 deletions src/Mapping/Parameter/BooleanTypeMapper.php
@@ -0,0 +1,38 @@
<?php declare(strict_types = 1);

namespace Apitte\Core\Mapping\Parameter;

use Apitte\Core\Exception\Logical\InvalidArgumentException;
use Apitte\Core\Schema\EndpointParameter;

class BooleanTypeMapper implements ITypeMapper
{

/**
* @param mixed $value
*/
public function normalize($value, EndpointParameter $parameter): ?bool
{
if ($value === null || $value === '') {
return null;
}

if (is_bool($value)) {
return $value;
}

if ($value === '1' || $value === 'true') {
return true;
}

if ($value === '0' || $value === 'false') {
return false;
}

throw new InvalidArgumentException(sprintf(
'Parameter "%s" should be of type boolean. Pass "true" or "1" for true and "false" or "0" for false.',
$parameter->getName()
));
}

}
41 changes: 41 additions & 0 deletions src/Mapping/Parameter/DateTimeTypeMapper.php
@@ -0,0 +1,41 @@
<?php declare(strict_types = 1);

namespace Apitte\Core\Mapping\Parameter;

use Apitte\Core\Exception\Logical\InvalidArgumentException;
use Apitte\Core\Schema\EndpointParameter;
use DateTimeImmutable;
use TypeError;

class DateTimeTypeMapper implements ITypeMapper
{

/**
* @param mixed $value
*/
public function normalize($value, EndpointParameter $parameter): ?DateTimeImmutable
{
if ($value === null || $value === '') {
return null;
}

try {
$value = DateTimeImmutable::createFromFormat(DATE_ATOM, $value);
} catch (TypeError $e) {
throw new InvalidArgumentException(sprintf(
'Parameter "%s" should be of type datetime in format ISO 8601 (Y-m-d\TH:i:sP).',
$parameter->getName()
));
}

if ($value !== false) {
return $value;
}

throw new InvalidArgumentException(sprintf(
'Parameter "%s" should be of type datetime in format ISO 8601 (Y-m-d\TH:i:sP).',
$parameter->getName()
));
}

}
22 changes: 18 additions & 4 deletions src/Mapping/Parameter/FloatTypeMapper.php
Expand Up @@ -2,19 +2,33 @@

namespace Apitte\Core\Mapping\Parameter;

use Apitte\Core\Exception\Logical\InvalidArgumentException;
use Apitte\Core\Schema\EndpointParameter;

class FloatTypeMapper implements ITypeMapper
{

/**
* @param mixed $value
*/
public function normalize($value): ?float
public function normalize($value, EndpointParameter $parameter): ?float
{
if ($value === null) {
return $value;
if ($value === null || $value === '') {
return null;
}

if (is_string($value)) {
$value = str_replace(',', '.', $value); // Accept also comma as decimal separator
}

if (is_float($value) || is_int($value) || (is_string($value) && preg_match('#^-?[0-9]*[.]?[0-9]+\z#', $value))) {
return (float) $value;
}

return (float) $value;
throw new InvalidArgumentException(sprintf(
'Parameter "%s" should be of type float or integer.',
$parameter->getName()
));
}

}
4 changes: 3 additions & 1 deletion src/Mapping/Parameter/ITypeMapper.php
Expand Up @@ -2,13 +2,15 @@

namespace Apitte\Core\Mapping\Parameter;

use Apitte\Core\Schema\EndpointParameter;

interface ITypeMapper
{

/**
* @param mixed $value
* @return mixed
*/
public function normalize($value);
public function normalize($value, EndpointParameter $parameter);

}
18 changes: 14 additions & 4 deletions src/Mapping/Parameter/IntegerTypeMapper.php
Expand Up @@ -2,19 +2,29 @@

namespace Apitte\Core\Mapping\Parameter;

use Apitte\Core\Exception\Logical\InvalidArgumentException;
use Apitte\Core\Schema\EndpointParameter;

class IntegerTypeMapper implements ITypeMapper
{

/**
* @param mixed $value
*/
public function normalize($value): ?int
public function normalize($value, EndpointParameter $parameter): ?int
{
if ($value === null) {
return $value;
if ($value === null || $value === '') {
return null;
}

if (is_int($value) || (is_string($value) && preg_match('#^-?[0-9]+\z#', $value))) {
return (int) $value;
}

return (int) $value;
throw new InvalidArgumentException(sprintf(
'Parameter "%s" should be of type integer.',
$parameter->getName()
));
}

}

0 comments on commit 929b3c3

Please sign in to comment.