Skip to content

Commit

Permalink
[Slim4] Add Data Mocker middleware (OpenAPITools#4978)
Browse files Browse the repository at this point in the history
* [Slim4] Store response schemas

* [Slim4] Add Data Mocker middleware

* [Slim4] Enhance Slim router

* [Slim4] Enhance config

* [Slim4] Fix data format key in object mocking

* [Slim4] Add tests for Data Mocker middleware

* [Slim4] Add Mock feature documentation

* [Slim4] Refresh samples
  • Loading branch information
ybelenko authored and MikailBag committed Mar 23, 2020
1 parent 1710446 commit 6b6410c
Show file tree
Hide file tree
Showing 13 changed files with 2,058 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ public void processOpts() {
additionalProperties.put("interfacesSrcPath", "./" + toSrcPath(interfacesPackage, srcBasePath));
additionalProperties.put("interfacesTestPath", "./" + toSrcPath(interfacesPackage, testBasePath));

// external docs folder
additionalProperties.put("docsBasePath", "./" + docsBasePath);

if (additionalProperties.containsKey(PSR7_IMPLEMENTATION)) {
this.setPsr7Implementation((String) additionalProperties.get(PSR7_IMPLEMENTATION));
}
Expand Down Expand Up @@ -150,6 +153,9 @@ public void processOpts() {
supportingFiles.add(new SupportingFile("openapi_data_mocker_interface.mustache", toSrcPath(mockPackage, srcBasePath), toInterfaceName("OpenApiDataMocker") + ".php"));
supportingFiles.add(new SupportingFile("openapi_data_mocker.mustache", toSrcPath(mockPackage, srcBasePath), "OpenApiDataMocker.php"));
supportingFiles.add(new SupportingFile("openapi_data_mocker_test.mustache", toSrcPath(mockPackage, testBasePath), "OpenApiDataMockerTest.php"));
supportingFiles.add(new SupportingFile("openapi_data_mocker_middleware.mustache", toSrcPath(mockPackage, srcBasePath), "OpenApiDataMockerMiddleware.php"));
supportingFiles.add(new SupportingFile("openapi_data_mocker_middleware_test.mustache", toSrcPath(mockPackage, testBasePath), "OpenApiDataMockerMiddlewareTest.php"));
supportingFiles.add(new SupportingFile("mock_server.mustache", docsBasePath, "MockServer.md"));

// traits of ported utils
supportingFiles.add(new SupportingFile("string_utils_trait.mustache", toSrcPath(utilsPackage, srcBasePath), toTraitName("StringUtils") + ".php"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Command | Target
`$ composer test` | All tests
`$ composer test-apis` | Apis tests
`$ composer test-models` | Models tests
`$ composer test-mock` | Mock feature tests
`$ composer test-utils` | Utils tests

#### Config

Expand Down Expand Up @@ -110,6 +112,8 @@ Switch on option in `./index.php`:
+++ $app->addErrorMiddleware(true, true, true);
```

## [Mock Server Documentation]({{docsBasePath}}/MockServer.md)

{{#generateApiDocs}}
## API Endpoints

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ use Dyorg\TokenAuthentication;
use Dyorg\TokenAuthentication\TokenSearch;
use Psr\Http\Message\ServerRequestInterface;
use {{invokerPackage}}\Middleware\JsonBodyParserMiddleware;
use {{mockPackage}}\OpenApiDataMocker;
use {{mockPackage}}\OpenApiDataMockerMiddleware;
use Exception;

/**
Expand Down Expand Up @@ -69,6 +71,15 @@ class SlimRouter
'classname' => '{{classname}}',
'userClassname' => '{{userClassname}}',
'operationId' => '{{operationId}}',
'responses' => [
{{#responses}}
'{{#isDefault}}default{{/isDefault}}{{^isDefault}}{{code}}{{/isDefault}}' => [
'code' => {{code}},
'message' => '{{message}}',
'jsonSchema' => '{{{jsonSchema}}}',
],
{{/responses}}
],
'authMethods' => [
{{#hasAuthMethods}}
{{#authMethods}}
Expand Down Expand Up @@ -161,12 +172,13 @@ class SlimRouter
};
{{/hasAuthMethods}}

$userOptions = null;
if ($settings instanceof ContainerInterface && $settings->has('tokenAuthenticationOptions')) {
$userOptions = $settings->get('tokenAuthenticationOptions');
} elseif (is_array($settings) && isset($settings['tokenAuthenticationOptions'])) {
$userOptions = $settings['tokenAuthenticationOptions'];
}
$userOptions = $this->getSetting($settings, 'tokenAuthenticationOptions', null);

// mocker options
$mockerOptions = $this->getSetting($settings, 'mockerOptions', null);
$dataMocker = $mockerOptions['dataMocker'] ?? new OpenApiDataMocker();
$getMockResponseCallback = $mockerOptions['getMockResponseCallback'] ?? null;
$mockAfterCallback = $mockerOptions['afterCallback'] ?? null;

foreach ($this->operations as $operation) {
$callback = function ($request, $response, $arguments) use ($operation) {
Expand Down Expand Up @@ -235,6 +247,10 @@ class SlimRouter
}
{{/hasAuthMethods}}

if (is_callable($getMockResponseCallback)) {
$middlewares[] = new OpenApiDataMockerMiddleware($dataMocker, $operation['responses'], $getMockResponseCallback, $mockAfterCallback);
}

$this->addRoute(
[$operation['httpMethod']],
"{$operation['basePathWithoutHost']}{$operation['path']}",
Expand All @@ -261,6 +277,26 @@ class SlimRouter
return array_merge($userOptions, $staticOptions);
}

/**
* Returns app setting by name.
*
* @param ContainerInterface|array $settings Either a ContainerInterface or an associative array of app settings
* @param string $settingName Setting name
* @param mixed $default Default setting value.
*
* @return mixed
*/
private function getSetting($settings, $settingName, $default = null)
{
if ($settings instanceof ContainerInterface && $settings->has($settingName)) {
return $settings->get($settingName);
} elseif (is_array($settings) && array_key_exists($settingName, $settings)) {
return $settings[$settingName];
}

return $default;
}

/**
* Add route with multiple methods
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
require_once __DIR__ . '/vendor/autoload.php';

use {{invokerPackage}}\SlimRouter;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use {{mockPackage}}\OpenApiDataMocker;
{{/apiInfo}}

$config = [];
Expand Down Expand Up @@ -51,6 +54,35 @@ $config['tokenAuthenticationOptions'] = [
// 'error' => null,
];

/**
* Mocker Middleware options.
*/
$config['mockerOptions'] = [
// 'dataMocker' => new OpenApiDataMocker(),

// 'getMockResponseCallback' => function (ServerRequestInterface $request, array $responses) {
// // check if client clearly asks for mocked response
// if (
// $request->hasHeader('X-{{invokerPackage}}-Mock')
// && $request->getHeader('X-{{invokerPackage}}-Mock')[0] === 'ping'
// ) {
// if (array_key_exists('default', $responses)) {
// return $responses['default'];
// }

// // return first response
// return $responses[array_key_first($responses)];
// }

// return false;
// },

// 'afterCallback' => function ($request, $response) {
// // mark mocked response to distinguish real and fake responses
// return $response->withHeader('X-{{invokerPackage}}-Mock', 'pong');
// },
];

$router = new SlimRouter($config);
$app = $router->getSlimApp();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# {{packageName}} - PHP Slim 4 Server library for {{appName}}

## Mock Server Documentation

### Mocker Options
To enable mock server uncomment these lines in `index.php` config file:

```php
/**
* Mocker Middleware options.
*/
$config['mockerOptions'] = [
'dataMocker' => new OpenApiDataMocker(),

'getMockResponseCallback' => function (ServerRequestInterface $request, array $responses) {
// check if client clearly asks for mocked response
if (
$request->hasHeader('X-{{invokerPackage}}-Mock')
&& $request->getHeader('X-{{invokerPackage}}-Mock')[0] === 'ping'
) {
if (array_key_exists('default', $responses)) {
return $responses['default'];
}

// return first response
return $responses[array_key_first($responses)];
}

return false;
},

'afterCallback' => function ($request, $response) {
// mark mocked response to distinguish real and fake responses
return $response->withHeader('X-{{invokerPackage}}-Mock', 'pong');
},
];
```

* `dataMocker` is mocker class instance. To create custom data mocker extend `{{mockPackage}}\{{interfaceNamePrefix}}OpenApiDataMocker{{interfaceNameSuffix}}`.
* `getMockResponseCallback` is callback before mock data generation. Above example shows how to enable mock feature for only requests with `{{X-{{invokerPackage}}}}-mock: ping` HTTP header. Adjust requests filtering to fit your project requirements. This function must return single response schema from `$responses` array parameter. **Mock feature is disabled when callback returns anything beside array.**
* `afterCallback` is callback executed after mock data generation. Most obvious use case is append specific HTTP headers to distinguish real and fake responses. **This function must always return response instance.**

### Supported features

All data types supported except specific string formats: `email`, `uuid`, `password` which are poorly implemented.

#### Data Types Support

| Data Type | Data Format | Supported |
|:---------:|:-----------:|:------------------:|
| `integer` | `int32` | :white_check_mark: |
| `integer` | `int64` | :white_check_mark: |
| `number` | `float` | :white_check_mark: |
| `number` | `double` | |
| `string` | `byte` | :white_check_mark: |
| `string` | `binary` | :white_check_mark: |
| `boolean` | | :white_check_mark: |
| `string` | `date` | :white_check_mark: |
| `string` | `date-time` | :white_check_mark: |
| `string` | `password` | :white_check_mark: |
| `string` | `email` | :white_check_mark: |
| `string` | `uuid` | :white_check_mark: |

#### Data Options Support

| Data Type | Option | Supported |
|:-----------:|:----------------------:|:------------------:|
| `string` | `minLength` | :white_check_mark: |
| `string` | `maxLength` | :white_check_mark: |
| `string` | `enum` | :white_check_mark: |
| `string` | `pattern` | |
| `integer` | `minimum` | :white_check_mark: |
| `integer` | `maximum` | :white_check_mark: |
| `integer` | `exclusiveMinimum` | :white_check_mark: |
| `integer` | `exclusiveMaximum` | :white_check_mark: |
| `number` | `minimum` | :white_check_mark: |
| `number` | `maximum` | :white_check_mark: |
| `number` | `exclusiveMinimum` | :white_check_mark: |
| `number` | `exclusiveMaximum` | :white_check_mark: |
| `array` | `items` | :white_check_mark: |
| `array` | `additionalItems` | |
| `array` | `minItems` | :white_check_mark: |
| `array` | `maxItems` | :white_check_mark: |
| `array` | `uniqueItems` | |
| `object` | `properties` | :white_check_mark: |
| `object` | `maxProperties` | |
| `object` | `minProperties` | |
| `object` | `patternProperties` | |
| `object` | `additionalProperties` | |
| `object` | `required` | |
| `*` | `$ref` | :white_check_mark: |
| `*` | `allOf` | |
| `*` | `anyOf` | |
| `*` | `oneOf` | |
| `*` | `not` | |

### Known Limitations

Avoid circular refs in your schema. Schema below can cause infinite loop and `Out of Memory` PHP error:
```yml
# ModelA has reference to ModelB while ModelB has reference to ModelA.
# Mock server will produce huge nested JSON example and ended with `Out of Memory` error.
definitions:
ModelA:
type: object
properties:
model_b:
$ref: '#/definitions/ModelB'
ModelB:
type: array
items:
$ref: '#/definitions/ModelA'
```

Don't ref scalar types, because generator will not produce models which mock server can find. So schema below will cause error:
```yml
# generated build contains only `OuterComposite` model class which referenced to not existed `OuterNumber`, `OuterString`, `OuterBoolean` classes
# mock server cannot mock `OuterComposite` model and throws exception
definitions:
OuterComposite:
type: object
properties:
my_number:
$ref: '#/definitions/OuterNumber'
my_string:
$ref: '#/definitions/OuterString'
my_boolean:
$ref: '#/definitions/OuterBoolean'
OuterNumber:
type: number
OuterString:
type: string
OuterBoolean:
type: boolean
```
Loading

0 comments on commit 6b6410c

Please sign in to comment.