Skip to content

Commit

Permalink
Added package config file and moved all parameters there (#12)
Browse files Browse the repository at this point in the history
* Added package config file and moved all parameters there

* Improved docs
  • Loading branch information
DimanKuskov authored and sspat committed Jan 24, 2020
1 parent 89e9769 commit 30f5b5d
Show file tree
Hide file tree
Showing 12 changed files with 299 additions and 134 deletions.
66 changes: 26 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,53 +24,39 @@ OnMoon\OpenApiServerBundle\OpenApiServerBundle::class => ['all' => true],
```
to array in `config/bundles.php`.

## Confirugation

You can configure the bundle by adding the following parameters to your `/config/services.xml`
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="openapi.generated.code.language.level">7.4.0</parameter>
<parameter key="openapi.generated.code.dir.permissions">0755</parameter>
<parameter key="openapi.generated.code.root.namespace">App\Generated</parameter>
<parameter key="openapi.generated.code.root.path">%kernel.project_dir%/src/Generated</parameter>
<parameter key="openapi.generated.code.media.type">application/json</parameter>
</parameters>
</container>
```
`openapi.generated.code.language.level` - minimum PHP version the generated code should be compatible with

`openapi.generated.code.dir.permissions` - permissions for the generated directories

`openapi.generated.code.root.namespace` - root namespace for the generated code

`openapi.generated.code.root.path` - absolute path to the directory where the code will be generated
## Usage

`openapi.generated.code.media.type` - media type from the specification files to use for generating request and response DTOs
You can configure the bundle by adding the following parameters to your `/config/packages/open_api_server.yaml`

## Usage
```yaml
open_api_server:
# root_name_space: App\Generated # NameSpace for DTOs and Api Interfaces
## We will try to derive path for generated files from namespace. If you do not want them to be
## stored in App namespace or if you App namespace is not in %kernel.project_dir%/src/, then you
## can specify path manually:
# root_path: %kernel.project_dir%/src/Generated
# language_level: 7.4.0 # minimum PHP version the generated code should be compatible with
# generated_dir_permissions: 0755 # permissions for the generated directories
specs:
petstore:
path: '../spec/petstore.yaml' # path to OpenApi specification
# type: yaml # Specification format, either yaml or json. Extension is used if omitted
name_space: PetStore # NameSpace for generated DTOs and Interfaces
media_type: 'application/json' # media type from the specification files to use for generating request and response DTOs
```
Add your OpenApi specifications to the application routes configuration file:
Add your OpenApi specifications to the application routes configuration file using standard `resource` keyword
with `open_api` type:

```yaml
first-api:
resource: '../spec/first.yaml'
type: openapi-yaml
prefix: '/first'
name_prefix: 'first_'

second-api:
resource: '../spec/second.yaml'
type: openapi-yaml
prefix: '/second'
name_prefix: 'second_'
petstore-api:
resource: 'petstore' # This should be same as in specs section of bundle config
type: open_api
# prefix: '/api' # Add this standard parameter to add base path to all paths in api
# name_prefix: 'petstore_' # This will add prefix to route names
```

Generate the server code: `php bin/console openapi:generate-code`
Generate the server code: `php bin/console open-api:generate-code`

Now you can implement the service interfaces generated by the previous command and put the code that should
handle the api calls there.
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"symfony/dependency-injection": "4.4.*",
"symfony/http-kernel": "4.4.*",
"symfony/property-access": "4.4.*",
"symfony/property-info": "4.4.*",
"symfony/psr-http-message-bridge": "^2.0",
"symfony/routing": "4.4.*",
"symfony/serializer": "4.4.*",
Expand Down
4 changes: 2 additions & 2 deletions src/CodeGenerator/Naming/DefaultNamingStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ public function isAllowedPhpPropertyName(string $name): bool
return ! preg_match('/^\d/', $name) && preg_match('/^[A-Za-z0-9_]+$/i', $name);
}

public function getInterfaceFQCN(string $apiTitle, string $operationId) : string
public function getInterfaceFQCN(string $apiNameSpace, string $operationId) : string
{
return $this->buildNamespace(
$this->rootNamespace,
GenerateApiCodeCommand::APIS_NAMESPACE,
$this->stringToNamespace($apiTitle),
$apiNameSpace,
$this->stringToNamespace($operationId),
$this->stringToNamespace($operationId) . GenerateApiCodeCommand::SERVICE_SUFFIX,
);
Expand Down
2 changes: 1 addition & 1 deletion src/CodeGenerator/Naming/NamingStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

interface NamingStrategy
{
public function getInterfaceFQCN(string $apiTitle, string $operationId) : string;
public function getInterfaceFQCN(string $apiNameSpace, string $operationId) : string;

public function stringToNamespace(string $text) : string;

Expand Down
49 changes: 20 additions & 29 deletions src/Command/GenerateApiCodeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use OnMoon\OpenApiServerBundle\CodeGenerator\ServiceSubscriber\ServiceSubscriberFactory;
use OnMoon\OpenApiServerBundle\Exception\CannotGenerateCodeForOperation;
use OnMoon\OpenApiServerBundle\Router\RouteLoader;
use OnMoon\OpenApiServerBundle\Specification\Specification;
use OnMoon\OpenApiServerBundle\Specification\SpecificationLoader;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
Expand Down Expand Up @@ -44,9 +46,9 @@ class GenerateApiCodeCommand extends Command
private ServiceSubscriberFactory $serviceSubscriberFactory;
private FileWriter $fileWriter;
private RouterInterface $router;
private SpecificationLoader $loader;
private string $rootNamespace;
private string $rootPath;
private string $mediaType;

/**
* @param NamingStrategy $namingStrategy
Expand All @@ -56,9 +58,9 @@ class GenerateApiCodeCommand extends Command
* @param ServiceSubscriberFactory $serviceSubscriberFactory
* @param FileWriter $fileWriter
* @param RouterInterface $router
* @param SpecificationLoader $loader
* @param string $rootNamespace
* @param string $rootPath
* @param string $mediaType
* @param string|null $name
*/
public function __construct(
Expand All @@ -69,9 +71,9 @@ public function __construct(
ServiceSubscriberFactory $serviceSubscriberFactory,
FileWriter $fileWriter,
RouterInterface $router,
SpecificationLoader $loader,
string $rootNamespace,
string $rootPath,
string $mediaType,
?string $name = null
) {
$this->namingStrategy = $namingStrategy;
Expand All @@ -83,13 +85,13 @@ public function __construct(
$this->router = $router;
$this->rootNamespace = $rootNamespace;
$this->rootPath = $rootPath;
$this->mediaType = $mediaType;
$this->loader = $loader;

parent::__construct($name);
}

/** @var string */
protected static $defaultName = 'openapi:generate-code';
protected static $defaultName = 'open-api:generate-code';

protected function execute(InputInterface $input, OutputInterface $output) : ?int
{
Expand All @@ -98,14 +100,15 @@ protected function execute(InputInterface $input, OutputInterface $output) : ?in
/** @var GeneratedClass[] $serviceInterfaces */
$serviceInterfaces = [];

foreach ($this->findSpecifications() as $specificationFile=>$parsedSpecification) {
/** @var OpenApi $parsedSpecification */
foreach ($this->loader->list() as $specificationName=>$specification) {
$parsedSpecification = $this->loader->load($specificationName);

if ($parsedSpecification->paths === null) {
continue;
}

$apiName = $this->namingStrategy->stringToNamespace($parsedSpecification->info->title);
$apiName = $specification->getNameSpace();
$specMediaType = $specification->getMediaType();
$apiNamespace = $this->namingStrategy->buildNamespace($this->rootNamespace, self::APIS_NAMESPACE, $apiName);
$apiPath = $this->namingStrategy->buildPath($this->rootPath, self::APIS_NAMESPACE, $apiName);

Expand All @@ -115,14 +118,14 @@ protected function execute(InputInterface $input, OutputInterface $output) : ?in
$summary = $operation->summary;

$operationName = $this->namingStrategy->stringToNamespace($operationId);
$operationNamesapce = $this->namingStrategy->buildNamespace($apiNamespace, $operationName);
$operationNamespace = $this->namingStrategy->buildNamespace($apiNamespace, $operationName);
$operationPath = $this->namingStrategy->buildPath($apiPath, $operationName);

if ($operationId === null) {
throw CannotGenerateCodeForOperation::becauseNoOperationIdSpecified(
$url,
$method,
$specificationFile->getFilePath()
$specification->getPath()
);
}

Expand All @@ -138,15 +141,15 @@ protected function execute(InputInterface $input, OutputInterface $output) : ?in

if ($requestBody !== null &&
$requestBody->content !== null &&
array_key_exists($this->mediaType, $requestBody->content)
array_key_exists($specMediaType, $requestBody->content)
) {
$mediaType = $requestBody->content[$this->mediaType];
$mediaType = $requestBody->content[$specMediaType];

if ($mediaType->schema instanceof Schema) {
$schema = $mediaType->schema;

$dtoNamespace = $this->namingStrategy->buildNamespace(
$operationNamesapce,
$operationNamespace,
self::DTO_NAMESPACE,
self::REQUEST_SUFFIX
);
Expand Down Expand Up @@ -179,15 +182,15 @@ protected function execute(InputInterface $input, OutputInterface $output) : ?in
if ($responses !== null &&
$responses[200] !== null &&
$responses[200]->content !== null &&
array_key_exists($this->mediaType, $responses[200]->content)
array_key_exists($specMediaType, $responses[200]->content)
) {
$mediaType = $responses[200]->content[$this->mediaType];
$mediaType = $responses[200]->content[$specMediaType];

if ($mediaType->schema instanceof Schema) {
$schema = $mediaType->schema;

$dtoNamespace = $this->namingStrategy->buildNamespace(
$operationNamesapce,
$operationNamespace,
self::DTO_NAMESPACE,
self::RESPONSE_SUFFIX
);
Expand Down Expand Up @@ -220,7 +223,7 @@ protected function execute(InputInterface $input, OutputInterface $output) : ?in
// Root dto generation

$dtoNamespace = $this->namingStrategy->buildNamespace(
$operationNamesapce,
$operationNamespace,
self::DTO_NAMESPACE,
self::REQUEST_SUFFIX
);
Expand Down Expand Up @@ -334,16 +337,4 @@ private function generateDtoGraph(
);
}

private function findSpecifications() {
$specs = [];
foreach ($this->router->getRouteCollection()->all() as $route) {
if($path = $route->getOption(RouteLoader::OPENAPI_SPEC_PATH)) {
if(!isset($specs[$path])) {
$specs[$path] = $route->getOption(RouteLoader::OPENAPI_SPEC);
}
}
}

return $specs;
}
}
28 changes: 16 additions & 12 deletions src/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use League\OpenAPIValidation\PSR7\OperationAddress;
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
use Nyholm\Psr7\Factory\Psr17Factory;
use OnMoon\OpenApiServerBundle\Specification\SpecificationLoader;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
Expand All @@ -27,24 +28,27 @@
class ApiController
{
private ?ApiLoader $apiLoader = null;
private DtoSerializer $serializer;

public function setApiLoader(
ApiLoader $loader,
DtoSerializer $serializer
) {
public function setApiLoader(ApiLoader $loader) {
$this->apiLoader = $loader;
$this->serializer = $serializer;
}

public function handle(Request $request, RouterInterface $router, NamingStrategy $namingStrategy) {
public function handle(
Request $request,
RouterInterface $router,
NamingStrategy $namingStrategy,
DtoSerializer $serializer,
SpecificationLoader $loader
) {
$routeName = $request->attributes->get('_route');
$route = $router->getRouteCollection()->get($routeName);

$path = $route->getOption(RouteLoader::OPENAPI_PATH);
$method = $route->getOption(RouteLoader::OPENAPI_METHOD);
/** @var OpenApi $spec $spec */
$spec = $route->getOption(RouteLoader::OPENAPI_SPEC);
$specName = $route->getOption(RouteLoader::OPENAPI_SPEC);
$nameSpace = $loader->get($specName)->getNameSpace();
$spec = $loader->load($specName);

$operationId = $spec->paths[$path]->{$method}->operationId;

$psr17Factory = new Psr17Factory();
Expand All @@ -65,7 +69,7 @@ public function handle(Request $request, RouterInterface $router, NamingStrategy
throw ApiCallFailed::becauseApiLoaderNotFound();
}

$apiInterface = $namingStrategy->getInterfaceFQCN($spec->info->title, $operationId);
$apiInterface = $namingStrategy->getInterfaceFQCN($nameSpace, $operationId);
$methodName = $namingStrategy->stringToMethodName($operationId);

$service = $this->apiLoader->get($apiInterface);
Expand All @@ -82,14 +86,14 @@ public function handle(Request $request, RouterInterface $router, NamingStrategy
$service->setClientIp($request->getClientIp());
}

$requestDto = $this->serializer->createRequestDto($request, $route, $apiInterface, $methodName);
$requestDto = $serializer->createRequestDto($request, $route, $apiInterface, $methodName);
$responseDto = $requestDto ? $service->{$methodName}($requestDto) : $service->{$methodName}();

$response = new JsonResponse();
$response->setEncodingOptions(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);

if (is_object($responseDto)) {
$response->setContent($this->serializer->createResponse($responseDto));
$response->setContent($serializer->createResponse($responseDto));
}

return $response;
Expand Down
43 changes: 43 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php


namespace OnMoon\OpenApiServerBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{

/**
* @inheritDoc
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('open_api_server');

$treeBuilder->getRootNode()
->children()
->scalarNode('root_path')->end()
->scalarNode('root_name_space')->defaultValue('App\Generated')->cannotBeEmpty()->end()
->scalarNode('language_level')->defaultValue('7.4.0')->cannotBeEmpty()->end()
->scalarNode('generated_dir_permissions')->defaultValue('0755')->cannotBeEmpty()->end()
->arrayNode('specs')
->arrayPrototype()
->children()
->scalarNode('path')->isRequired()->cannotBeEmpty()->end()
->enumNode('type')->values(['yaml','json'])->end()
->scalarNode('name_space')->isRequired()->cannotBeEmpty()->end()
->enumNode('media_type')
->values(['application/json'])
->isRequired()
->cannotBeEmpty()
->end()
->end()
->end()
->end()
;

return $treeBuilder;
}
}
Loading

0 comments on commit 30f5b5d

Please sign in to comment.