diff --git a/README.md b/README.md
index 4d7eada7..25746009 100644
--- a/README.md
+++ b/README.md
@@ -23,40 +23,50 @@ SwaggerBake requires CakePHP4 and a few dependencies that will be automatically
composer require cnizzardini/cakephp-swagger-bake
```
-Add the plugin to `Application.php`:
+Run `bin/cake plugin load SwaggerBake` or manually load the plugin:
```php
-$this->addPlugin('SwaggerBake');
+# src/Application.php
+public function bootstrap(): void
+{
+ // other logic...
+ $this->addPlugin('SwaggerBake');
+}
```
-## Basic Usage
+## Setup
-Get going in just four easy steps:
+For standard applications that have not split their API into plugins, the automated setup should work. Otherwise
+use the manual setup.
-- Create a base swagger.yml file in `config\swagger.yml`. An example file is provided [here](assets/swagger.yml).
+### Automated Setup
-- Create a `config/swagger_bake.php` file. See the example file [here](assets/swagger_bake.php) for further
-explanation.
+Run `bin/cake swagger install`
-- Create a route for the SwaggerUI page in `config/routes.php`. See Extensibility for other ways to diplay Swagger.
+Create a route for the SwaggerUI page in `config/routes.php`, example:
```php
-$builder->connect('/api', ['controller' => 'Swagger', 'action' => 'index', 'plugin' => 'SwaggerBake']);
+$builder->connect('/your-api-path', ['controller' => 'Swagger', 'action' => 'index', 'plugin' => 'SwaggerBake']);
```
-- Use the `swagger bake` command to generate your swagger documentation.
+### Manual Setup
-```sh
-bin/cake swagger bake
-```
+- Create a base swagger.yml file in `config\swagger.yml`. An example file is provided [here](assets/swagger.yml).
-Using the above example you should now see your swagger documentation after browsing to http://your-project/api
+- Create a `config/swagger_bake.php` file. See the example file [here](assets/swagger_bake.php) for further
+explanation.
-### Hot Reload Swagger JSON
+- Create a route for the SwaggerUI page in `config/routes.php`. See Extensibility for other ways to diplay Swagger.
+
+```php
+$builder->connect('/your-api-path', ['controller' => 'Swagger', 'action' => 'index', 'plugin' => 'SwaggerBake']);
+```
-You can enable hot reloading. This setting re-generates swagger.json on each reload of Swagger UI. Simply set
-`hotReload` equal to `true` (using `Configure::read('debug')` is recommended) in your `config/swagger_bake.php` file.
+## Complete Setup
+If Hot Reload is enabled ([see config](assets/swagger_bake.php)) then you should be able to browse to the above
+route. Otherwise you must first run `bin/cake swagger bake` to generate your swagger documentation.
+
## Automatic Documentation
I built this library to reduce the need for annotations to build documentation. SwaggerBake will automatically
@@ -80,13 +90,14 @@ components > schemas > Exception as your Swagger documentations Exception schema
## Doc Blocks
SwaggerBake will parse your [DocBlocks](https://docs.phpdoc.org/latest/guides/docblocks.html) for information. The
-first line reads as the Path Summary and the second as the Path Description, `@see`, `@deprecated`, and `@throws` are
-also supported. Throw tags use the Exception classes HTTP status code. For instance, a `MethodNotAllowedException`
-displays as a 405 response in Swagger UI, while a standard PHP Exception displays as a 500 code.
+first line reads as the Operation Summary and the second as the Operation Description, `@see`, `@deprecated`, and
+`@throws` are also supported. Throw tags use the Exception classes HTTP status code. For instance, a
+`MethodNotAllowedException` displays as a 405 response in Swagger UI, while a standard PHP Exception displays as a 500
+code.
```php
/**
- * Swagger Path Summary
+ * Swagger Operation Summary
*
* This displays as the operations long description
*
diff --git a/assets/swagger_bake.php b/assets/swagger_bake.php
index 9bc4c89a..eaf5de1e 100644
--- a/assets/swagger_bake.php
+++ b/assets/swagger_bake.php
@@ -43,7 +43,7 @@
*/
return [
'SwaggerBake' => [
- 'prefix' => '/api',
+ 'prefix' => '/your-relative-api-url',
'yml' => '/config/swagger.yml',
'json' => '/webroot/swagger.json',
'webPath' => '/swagger.json',
diff --git a/src/Command/BakeCommand.php b/src/Command/BakeCommand.php
index 281ff6f9..38b3f0cf 100644
--- a/src/Command/BakeCommand.php
+++ b/src/Command/BakeCommand.php
@@ -9,6 +9,10 @@
use SwaggerBake\Lib\Configuration;
use SwaggerBake\Lib\Factory\SwaggerFactory;
+/**
+ * Class BakeCommand
+ * @package SwaggerBake\Command
+ */
class BakeCommand extends Command
{
/**
@@ -26,6 +30,10 @@ public function execute(Arguments $args, ConsoleIo $io)
$output = $config->getJson();
$swagger = (new SwaggerFactory())->create();
+ foreach ($swagger->getOperationsWithNoHttp20x() as $operation) {
+ triggerWarning('Operation ' . $operation->getOperationId() . ' does not have a HTTP 20x response');
+ }
+
$swagger->writeFile($output);
$io->out("Swagger File Created: $output");
diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php
new file mode 100644
index 00000000..3332caf0
--- /dev/null
+++ b/src/Command/InstallCommand.php
@@ -0,0 +1,78 @@
+hr();
+ $io->out("| SwaggerBake Install");
+ $io->hr();
+
+ $io->info('This will create, but not overwrite config/swagger.yml and config/swagger_bake.php');
+
+ $io->out(
+ 'If your API exists in a plugin or you have some other non-standard setup, please follow ' .
+ 'the manual installation steps.'
+ );
+
+ if (strtoupper($io->ask('Continue?', 'Y')) !== 'Y') {
+ return;
+ }
+
+ $assets = __DIR__ . DS . '..' . DS . '..' . DS . 'assets';
+ if (!dir($assets)) {
+ $io->error('Unable to locate assets directory, please install manually');
+ return;
+ }
+
+ if (file_exists(CONFIG . 'swagger.yml') || file_exists(CONFIG . 'swagger_bake.php')) {
+ $answer = $io->ask('The installer found existing SwaggerBake config files. Overwrite?', 'Y');
+ if (strtoupper($answer) !== 'Y') {
+ return;
+ }
+ }
+
+ if (!copy("$assets/swagger.yml", CONFIG . 'swagger.yml')) {
+ $io->error('Unable to copy swagger.yml, check permissions');
+ return;
+ }
+
+ if (!copy("$assets/swagger_bake.php", CONFIG . 'swagger_bake.php')) {
+ $io->error('Unable to copy swagger_bake.php, check permissions');
+ return;
+ }
+
+ $path = $io->ask('What is your relative API path (e.g. /api)');
+ if (!empty($path)) {
+ $contents = file_get_contents(CONFIG . 'swagger.yml');
+ $contents = str_replace('YOUR-SERVER-HERE', $path, $contents);
+ file_put_contents(CONFIG . 'swagger.yml', $contents);
+
+ $contents = file_get_contents(CONFIG . 'swagger_bake.php');
+ $contents = str_replace('/your-relative-api-url', $path, $contents);
+ file_put_contents(CONFIG . 'swagger_bake.php', $contents);
+ }
+
+ $io->success('Installation Complete!');
+
+ $io->out('Now just add a route in your config/routes.php for SwaggerUI and you\'re ready to go!');
+ }
+}
diff --git a/src/Command/ModelCommand.php b/src/Command/ModelCommand.php
index 8400df3d..6aef447f 100644
--- a/src/Command/ModelCommand.php
+++ b/src/Command/ModelCommand.php
@@ -14,6 +14,10 @@
use SwaggerBake\Lib\Utility\DataTypeConversion;
use SwaggerBake\Lib\Utility\ValidateConfiguration;
+/**
+ * Class ModelCommand
+ * @package SwaggerBake\Command
+ */
class ModelCommand extends Command
{
/**
diff --git a/src/Command/RouteCommand.php b/src/Command/RouteCommand.php
index 70131d3b..e82a8636 100644
--- a/src/Command/RouteCommand.php
+++ b/src/Command/RouteCommand.php
@@ -14,6 +14,10 @@
use SwaggerBake\Lib\Configuration;
use SwaggerBake\Lib\Utility\ValidateConfiguration;
+/**
+ * Class RouteCommand
+ * @package SwaggerBake\Command
+ */
class RouteCommand extends Command
{
/**
diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php
index e959496e..500c3f71 100644
--- a/src/Controller/AppController.php
+++ b/src/Controller/AppController.php
@@ -7,4 +7,9 @@
class AppController extends BaseController
{
+ public function initialize(): void
+ {
+ parent::initialize();
+ $this->loadComponent('Flash');
+ }
}
diff --git a/src/Controller/SwaggerController.php b/src/Controller/SwaggerController.php
index 2219ad2d..f0defafa 100644
--- a/src/Controller/SwaggerController.php
+++ b/src/Controller/SwaggerController.php
@@ -6,9 +6,13 @@
use Cake\Event\EventInterface;
use SwaggerBake\Lib\Configuration;
use SwaggerBake\Lib\Factory\SwaggerFactory;
+use SwaggerBake\Lib\Swagger;
class SwaggerController extends AppController
{
+ /** @var Swagger */
+ private $swagger;
+
/**
* @see https://book.cakephp.org/4/en/controllers.html#controller-callback-methods
* @param EventInterface $event
@@ -20,8 +24,8 @@ public function beforeFilter(EventInterface $event)
if ($config->getHotReload()) {
$output = $config->getJson();
- $swagger = (new SwaggerFactory())->create();
- $swagger->writeFile($output);
+ $this->swagger = (new SwaggerFactory())->create();
+ $this->swagger->writeFile($output);
}
}
@@ -32,6 +36,14 @@ public function beforeFilter(EventInterface $event)
*/
public function index()
{
+ foreach ($this->swagger->getOperationsWithNoHttp20x() as $operation) {
+ if (!isset($this->Flash)) {
+ triggerWarning('Operation ' . $operation->getOperationId() . ' does not have a HTTP 20x response');
+ continue;
+ }
+ $this->Flash->error('Operation ' . $operation->getOperationId() . ' does not have a HTTP 20x response');
+ }
+
$config = new Configuration();
$title = $config->getTitleFromYml();
$url = $config->getWebPath();
diff --git a/src/Lib/AbstractParameter.php b/src/Lib/AbstractParameter.php
deleted file mode 100644
index e2d86531..00000000
--- a/src/Lib/AbstractParameter.php
+++ /dev/null
@@ -1,64 +0,0 @@
-config = $config;
- $this->route = $route;
-
- $this->actionName = $this->route->getAction();
- $this->className = $this->route->getController() . 'Controller';
-
- $this->controller = $this->getControllerFromNamespaces($this->className);
- $instance = new $this->controller;
-
- $this->reflectionClass = new ReflectionClass($instance);
- $this->reflectionMethods = $this->reflectionClass->getMethods();
-
- $this->reader = new AnnotationReader();
- }
-
- protected function getMethods() : array
- {
- return array_filter($this->reflectionMethods, function ($method) {
- return $method->name == $this->actionName;
- });
- }
-
- private function getControllerFromNamespaces(string $className) : ?string
- {
- $namespaces = $this->config->getNamespaces();
-
- if (!isset($namespaces['controllers']) || !is_array($namespaces['controllers'])) {
- throw new SwaggerBakeRunTimeException(
- 'Invalid configuration, missing SwaggerBake.namespaces.controllers'
- );
- }
-
- foreach ($namespaces['controllers'] as $namespace) {
- $entity = $namespace . 'Controller\\' . $className;
- if (class_exists($entity, true)) {
- return $entity;
- }
- }
-
- return null;
- }
-}
\ No newline at end of file
diff --git a/src/Lib/Annotation/SwagEntityAttributeHandler.php b/src/Lib/Annotation/SwagEntityAttributeHandler.php
deleted file mode 100644
index 86a2c871..00000000
--- a/src/Lib/Annotation/SwagEntityAttributeHandler.php
+++ /dev/null
@@ -1,24 +0,0 @@
-setName($annotation->name)
- ->setDescription($annotation->description)
- ->setType($annotation->type)
- ->setReadOnly($annotation->readOnly)
- ->setWriteOnly($annotation->writeOnly)
- ->setRequired($annotation->required)
- ;
- }
-}
\ No newline at end of file
diff --git a/src/Lib/Annotation/SwagFormHandler.php b/src/Lib/Annotation/SwagFormHandler.php
deleted file mode 100644
index c69884a3..00000000
--- a/src/Lib/Annotation/SwagFormHandler.php
+++ /dev/null
@@ -1,22 +0,0 @@
-setDescription($annotation->description)
- ->setName($annotation->name)
- ->setType($annotation->type)
- ->setRequired($annotation->required)
- ;
- }
-}
\ No newline at end of file
diff --git a/src/Lib/Annotation/SwagHeaderHandler.php b/src/Lib/Annotation/SwagHeaderHandler.php
deleted file mode 100644
index 1eb2f44c..00000000
--- a/src/Lib/Annotation/SwagHeaderHandler.php
+++ /dev/null
@@ -1,26 +0,0 @@
-setName($annotation->name)
- ->setDescription($annotation->description)
- ->setAllowEmptyValue(false)
- ->setDeprecated(false)
- ->setRequired($annotation->required)
- ->setIn('header')
- ->setSchema((new Schema())->setType($annotation->type))
- ;
- }
-}
\ No newline at end of file
diff --git a/src/Lib/Annotation/SwagPaginatorHandler.php b/src/Lib/Annotation/SwagPaginatorHandler.php
deleted file mode 100644
index 02a55fe7..00000000
--- a/src/Lib/Annotation/SwagPaginatorHandler.php
+++ /dev/null
@@ -1,36 +0,0 @@
- 'integer',
- 'limit' => 'integer',
- 'sort' => 'string',
- 'direction' => 'string'
- ];
-
- $parameter = (new Parameter())
- ->setAllowEmptyValue(false)
- ->setDeprecated(false)
- ->setRequired(false)
- ->setIn('query');
-
- $return = [];
- foreach ($paginators as $name => $type) {
- $param = clone $parameter;
- $return[] = $param->setName($name)->setSchema((new Schema())->setType($type));
- }
- return $return;
- }
-}
\ No newline at end of file
diff --git a/src/Lib/Annotation/SwagQueryHandler.php b/src/Lib/Annotation/SwagQueryHandler.php
deleted file mode 100644
index 2bfb6ce9..00000000
--- a/src/Lib/Annotation/SwagQueryHandler.php
+++ /dev/null
@@ -1,22 +0,0 @@
-setName($annotation->name)
- ->setDescription($annotation->description)
- ->setAllowEmptyValue(false)
- ->setDeprecated(false)
- ->setRequired($annotation->required)
- ->setIn('query')
- ->setSchema((new Schema())->setType($annotation->type))
- ;
- }
-}
\ No newline at end of file
diff --git a/src/Lib/Annotation/SwagRequestBodyContentHandler.php b/src/Lib/Annotation/SwagRequestBodyContentHandler.php
deleted file mode 100644
index 599c2f2d..00000000
--- a/src/Lib/Annotation/SwagRequestBodyContentHandler.php
+++ /dev/null
@@ -1,20 +0,0 @@
-setMimeType($annotation->mimeType)
- ->setSchema($annotation->refEntity)
- ;
- }
-}
\ No newline at end of file
diff --git a/src/Lib/Annotation/SwagRequestBodyHandler.php b/src/Lib/Annotation/SwagRequestBodyHandler.php
deleted file mode 100644
index d86b97dd..00000000
--- a/src/Lib/Annotation/SwagRequestBodyHandler.php
+++ /dev/null
@@ -1,21 +0,0 @@
-setDescription($annotation->description)
- ->setRequired((bool) $annotation->required)
- ->setIgnoreCakeSchema((bool) $annotation)
- ;
- }
-}
\ No newline at end of file
diff --git a/src/Lib/Annotation/SwagResponseSchemaHandler.php b/src/Lib/Annotation/SwagResponseSchemaHandler.php
deleted file mode 100644
index 019a817c..00000000
--- a/src/Lib/Annotation/SwagResponseSchemaHandler.php
+++ /dev/null
@@ -1,32 +0,0 @@
-setCode(intval($annotation->httpCode))
- ->setDescription($annotation->description);
-
- if (empty($annotation->schemaFormat) && empty($annotation->mimeType)) {
- return $response;
- }
-
- return $response->pushContent(
- (new Content())
- ->setSchema($annotation->refEntity)
- ->setFormat($annotation->schemaFormat)
- ->setType($annotation->schemaType)
- ->setMimeType($annotation->mimeType)
- );
- }
-}
\ No newline at end of file
diff --git a/src/Lib/Annotation/SwagSecurityHandler.php b/src/Lib/Annotation/SwagSecurityHandler.php
deleted file mode 100644
index f30dd349..00000000
--- a/src/Lib/Annotation/SwagSecurityHandler.php
+++ /dev/null
@@ -1,20 +0,0 @@
-setName($annotation->name)
- ->setScopes($annotation->scopes)
- ;
- }
-}
\ No newline at end of file
diff --git a/src/Lib/AnnotationLoader.php b/src/Lib/AnnotationLoader.php
index 9f012cc6..96177cb2 100644
--- a/src/Lib/AnnotationLoader.php
+++ b/src/Lib/AnnotationLoader.php
@@ -5,6 +5,10 @@
use Doctrine\Common\Annotations\AnnotationRegistry;
use SwaggerBake\Lib\Annotation as SwagAnnotation;
+/**
+ * Class AnnotationLoader
+ * @package SwaggerBake\Lib
+ */
class AnnotationLoader
{
public static function load() : void
diff --git a/src/Lib/CakeModel.php b/src/Lib/CakeModel.php
index 45146506..96348c8e 100644
--- a/src/Lib/CakeModel.php
+++ b/src/Lib/CakeModel.php
@@ -6,12 +6,15 @@
use Cake\Datasource\EntityInterface;
use Cake\Database\Schema\TableSchema;
use Cake\Utility\Inflector;
+use SwaggerBake\Lib\Annotation\SwagEntity;
+use SwaggerBake\Lib\Decorator\EntityDecorator;
+use SwaggerBake\Lib\Decorator\PropertyDecorator;
use SwaggerBake\Lib\Exception\SwaggerBakeRunTimeException;
-use SwaggerBake\Lib\Model\ExpressiveAttribute;
-use SwaggerBake\Lib\Model\ExpressiveModel;
+use SwaggerBake\Lib\Utility\AnnotationUtility;
/**
* Class CakeModel
+ * @package SwaggerBake\Lib
*/
class CakeModel
{
@@ -32,9 +35,9 @@ public function __construct(CakeRoute $cakeRoute, Configuration $config)
}
/**
- * Gets an array of ExpressiveModel
+ * Gets an array of EntityDecorator
*
- * @return ExpressiveModel[]
+ * @return EntityDecorator[]
*/
public function getModels() : array
{
@@ -49,25 +52,23 @@ public function getModels() : array
foreach ($tables as $tableName) {
- if (!in_array($tableName, $tabularRoutes)) {
- continue;
- }
-
$className = Inflector::classify($tableName);
$entity = $this->getEntityFromNamespaces($className);
+
if (empty($entity)) {
continue;
}
+ if (!in_array($tableName, $tabularRoutes) && !$this->entityHasVisibility($entity)) {
+ continue;
+ }
+
$entityInstance = new $entity;
$schema = $collection->describe($tableName);
- $attributes = $this->getExpressiveAttributes($entityInstance, $schema);
-
- $expressiveModel = new ExpressiveModel();
- $expressiveModel->setName($className)->setAttributes($attributes);
+ $properties = $this->getPropertyDecorators($entityInstance, $schema);
- $return[] = $expressiveModel;
+ $return[] = (new EntityDecorator($entityInstance))->setProperties($properties);
}
return $return;
@@ -140,9 +141,9 @@ private function getTablesFromRoutes(array $routes) : array
/**
* @param EntityInterface $entity
* @param TableSchema $schema
- * @return ExpressiveAttribute[]
+ * @return PropertyDecorator[]
*/
- private function getExpressiveAttributes(EntityInterface $entity, TableSchema $schema) : array
+ private function getPropertyDecorators(EntityInterface $entity, TableSchema $schema) : array
{
$return = [];
@@ -157,14 +158,14 @@ private function getExpressiveAttributes(EntityInterface $entity, TableSchema $s
$vars = $schema->__debugInfo();
$default = isset($vars['columns'][$columnName]['default']) ? $vars['columns'][$columnName]['default'] : '';
- $expressiveAttribute = new ExpressiveAttribute();
- $expressiveAttribute
+ $PropertyDecorator = new PropertyDecorator();
+ $PropertyDecorator
->setName($columnName)
->setType($schema->getColumnType($columnName))
->setDefault($default)
->setIsPrimaryKey($this->isPrimaryKey($vars, $columnName))
;
- $return[] = $expressiveAttribute;
+ $return[] = $PropertyDecorator;
}
return $return;
@@ -183,4 +184,29 @@ private function isPrimaryKey(array $schemaDebugInfo, string $columnName) : bool
return in_array($columnName, $schemaDebugInfo['constraints']['primary']['columns']);
}
+
+ /**
+ * @param string $fqns
+ * @return bool
+ */
+ private function entityHasVisibility(string $fqns) : bool
+ {
+ if (empty($fqns)) {
+ return false;
+ }
+
+ $annotations = AnnotationUtility::getClassAnnotationsFromFqns($fqns);
+
+ $swagEntities = array_filter($annotations, function ($annotation) {
+ return $annotation instanceof SwagEntity;
+ });
+
+ if (empty($swagEntities)) {
+ return false;
+ }
+
+ $swagEntity = reset($swagEntities);
+
+ return $swagEntity->isVisible;
+ }
}
\ No newline at end of file
diff --git a/src/Lib/CakeRoute.php b/src/Lib/CakeRoute.php
index 23ccceab..37787856 100644
--- a/src/Lib/CakeRoute.php
+++ b/src/Lib/CakeRoute.php
@@ -2,14 +2,14 @@
namespace SwaggerBake\Lib;
-use SwaggerBake\Lib\Model\ExpressiveRoute;
+use SwaggerBake\Lib\Decorator\RouteDecorator;
use Cake\Routing\Route\Route;
use Cake\Routing\Router;
use InvalidArgumentException;
/**
* Class CakeRoute
- * Gets an array of routes matching a given route prefix
+ * @package SwaggerBake\Lib
*/
class CakeRoute
{
@@ -37,7 +37,7 @@ public function __construct(Router $router, Configuration $config)
/**
* Gets an array of Route
*
- * @return ExpressiveRoute[]
+ * @return RouteDecorator[]
*/
public function getRoutes() : array
{
@@ -52,7 +52,7 @@ public function getRoutes() : array
$routes = [];
foreach ($filteredRoutes as $route) {
- $routes[$route->getName()] = $this->createExpressiveRouteFromRoute($route);
+ $routes[$route->getName()] = new RouteDecorator($route);
}
ksort($routes);
@@ -60,29 +60,6 @@ public function getRoutes() : array
return $routes;
}
- /**
- * @param Route $route
- * @return ExpressiveRoute
- */
- private function createExpressiveRouteFromRoute(Route $route) : ExpressiveRoute
- {
- $defaults = (array) $route->defaults;
-
- $methods = $defaults['_method'];
- if (!is_array($defaults['_method'])) {
- $methods = explode(', ', $defaults['_method']);
- }
-
- return (new ExpressiveRoute())
- ->setPlugin($defaults['plugin'])
- ->setController($defaults['controller'])
- ->setName($route->getName())
- ->setAction($defaults['action'])
- ->setMethods($methods)
- ->setTemplate($route->template)
- ;
- }
-
/**
* @param Route $route
* @return bool
diff --git a/src/Lib/Configuration.php b/src/Lib/Configuration.php
index 85af5c8c..c1034202 100644
--- a/src/Lib/Configuration.php
+++ b/src/Lib/Configuration.php
@@ -6,6 +6,10 @@
use LogicException;
use Symfony\Component\Yaml\Yaml;
+/**
+ * Class Configuration
+ * @package SwaggerBake\Lib
+ */
class Configuration
{
/** @var array */
diff --git a/src/Lib/Decorator/EntityDecorator.php b/src/Lib/Decorator/EntityDecorator.php
new file mode 100644
index 00000000..81e003f5
--- /dev/null
+++ b/src/Lib/Decorator/EntityDecorator.php
@@ -0,0 +1,113 @@
+entity = $entity;
+ $this->fqns = get_class($entity);
+
+ try {
+ $this->name = (new ReflectionClass($entity))->getShortName();
+ } catch(ReflectionException $e) {
+ throw new SwaggerBakeRunTimeException('ReflectionException: ' . $e->getMessage());
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * @param string $name
+ * @return EntityDecorator
+ */
+ public function setName(string $name): EntityDecorator
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * @return PropertyDecorator[]
+ */
+ public function getProperties(): array
+ {
+ return $this->properties;
+ }
+
+ /**
+ * @param PropertyDecorator[] $properties
+ * @return EntityDecorator
+ */
+ public function setProperties(array $properties): EntityDecorator
+ {
+ $this->properties = $properties;
+ return $this;
+ }
+
+ /**
+ * @return Entity
+ */
+ public function getEntity(): Entity
+ {
+ return $this->entity;
+ }
+
+ /**
+ * @param Entity $entity
+ * @return EntityDecorator
+ */
+ public function setEntity(Entity $entity): EntityDecorator
+ {
+ $this->entity = $entity;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getFqns(): string
+ {
+ return $this->fqns;
+ }
+
+ /**
+ * @param string $fqns
+ * @return EntityDecorator
+ */
+ public function setFqns(string $fqns): EntityDecorator
+ {
+ $this->fqns = $fqns;
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/src/Lib/Model/ExpressiveAttribute.php b/src/Lib/Decorator/PropertyDecorator.php
similarity index 67%
rename from src/Lib/Model/ExpressiveAttribute.php
rename to src/Lib/Decorator/PropertyDecorator.php
index 569cdc04..a0eca4f7 100644
--- a/src/Lib/Model/ExpressiveAttribute.php
+++ b/src/Lib/Decorator/PropertyDecorator.php
@@ -1,10 +1,14 @@
name = $name;
return $this;
@@ -46,9 +50,9 @@ public function getType(): string
/**
* @param string $type
- * @return ExpressiveAttribute
+ * @return PropertyDecorator
*/
- public function setType(string $type): ExpressiveAttribute
+ public function setType(string $type): PropertyDecorator
{
$this->type = $type;
return $this;
@@ -64,9 +68,9 @@ public function getDefault(): string
/**
* @param string $default
- * @return ExpressiveAttribute
+ * @return PropertyDecorator
*/
- public function setDefault(string $default): ExpressiveAttribute
+ public function setDefault(string $default): PropertyDecorator
{
$this->default = $default;
return $this;
@@ -82,9 +86,9 @@ public function isPrimaryKey(): bool
/**
* @param bool $isPrimaryKey
- * @return ExpressiveAttribute
+ * @return PropertyDecorator
*/
- public function setIsPrimaryKey(bool $isPrimaryKey): ExpressiveAttribute
+ public function setIsPrimaryKey(bool $isPrimaryKey): PropertyDecorator
{
$this->isPrimaryKey = $isPrimaryKey;
return $this;
diff --git a/src/Lib/Model/ExpressiveRoute.php b/src/Lib/Decorator/RouteDecorator.php
similarity index 56%
rename from src/Lib/Model/ExpressiveRoute.php
rename to src/Lib/Decorator/RouteDecorator.php
index f013d588..fd8d7ea8 100644
--- a/src/Lib/Model/ExpressiveRoute.php
+++ b/src/Lib/Decorator/RouteDecorator.php
@@ -1,11 +1,20 @@
route;
+
+ $defaults = (array) $route->defaults;
+
+ $methods = $defaults['_method'];
+ if (!is_array($defaults['_method'])) {
+ $methods = explode(', ', $defaults['_method']);
+ }
+
+ $this
+ ->setTemplate($route->template)
+ ->setName($route->getName())
+ ->setPlugin($defaults['plugin'])
+ ->setController($defaults['controller'])
+ ->setAction($defaults['action'])
+ ->setMethods($methods)
+ ;
+ }
+
+ /**
+ * @return Route
+ */
+ public function getRoute(): Route
+ {
+ return $this->route;
+ }
+
+ /**
+ * @param Route $route
+ * @return RouteDecorator
+ */
+ public function setRoute(Route $route): RouteDecorator
+ {
+ $this->route = $route;
+ return $this;
+ }
+
/**
* @return string
*/
@@ -36,7 +84,7 @@ public function getName(): ?string
* @param $name
* @return $this
*/
- public function setName(string $name): ExpressiveRoute
+ public function setName(string $name): RouteDecorator
{
$this->name = $name;
return $this;
@@ -54,7 +102,7 @@ public function getPlugin(): ?string
* @param string|null $plugin
* @return $this
*/
- public function setPlugin(?string $plugin): ExpressiveRoute
+ public function setPlugin(?string $plugin): RouteDecorator
{
$this->plugin = $plugin;
return $this;
@@ -90,7 +138,7 @@ public function getAction(): ?string
* @param $action
* @return $this
*/
- public function setAction($action): ExpressiveRoute
+ public function setAction($action): RouteDecorator
{
$this->action = $action;
return $this;
@@ -108,9 +156,9 @@ public function getMethods(): array
* @param array $methods
* @return $this
*/
- public function setMethods(array $methods): ExpressiveRoute
+ public function setMethods(array $methods): RouteDecorator
{
- $this->methods = $methods;
+ $this->methods = array_map('strtoupper', $methods);
return $this;
}
@@ -126,7 +174,7 @@ public function getTemplate() : ?string
* @param $template
* @return $this
*/
- public function setTemplate(string $template): ExpressiveRoute
+ public function setTemplate(string $template): RouteDecorator
{
$this->template = $template;
return $this;
diff --git a/src/Lib/Exception/SwaggerBakeRunTimeException.php b/src/Lib/Exception/SwaggerBakeRunTimeException.php
index cea3a148..498a003d 100644
--- a/src/Lib/Exception/SwaggerBakeRunTimeException.php
+++ b/src/Lib/Exception/SwaggerBakeRunTimeException.php
@@ -5,6 +5,10 @@
use Cake\Core\Exception\Exception;
+/**
+ * Class SwaggerBakeRunTimeException
+ * @package SwaggerBake\Lib\Exception
+ */
class SwaggerBakeRunTimeException extends Exception
{
diff --git a/src/Lib/Factory/PathFactory.php b/src/Lib/Factory/PathFactory.php
deleted file mode 100644
index ecd8bc4e..00000000
--- a/src/Lib/Factory/PathFactory.php
+++ /dev/null
@@ -1,480 +0,0 @@
-config = $config;
- $this->route = $route;
- $this->prefix = $config->getPrefix();
- $this->dockBlock = $this->getDocBlock();
- }
-
- /**
- * Creates a Path and returns it
- *
- * @return Path|null
- */
- public function create() : ?Path
- {
- $path = new Path();
-
- if (empty($this->route->getMethods())) {
- return null;
- }
-
- if (!$this->isControllerVisible($this->route->getController())) {
- return null;
- }
-
- foreach ($this->route->getMethods() as $method) {
-
- $methodAnnotations = $this->getMethodAnnotations(
- $this->route->getController(),
- $this->route->getAction()
- );
-
- if (!$this->isMethodVisible($methodAnnotations)) {
- continue;
- }
-
- $path
- ->setType(strtolower($method))
- ->setPath($this->getPathName())
- ->setOperationId($this->route->getName())
- ->setSummary($this->dockBlock ? $this->dockBlock->getSummary() : '')
- ->setDescription($this->dockBlock ? $this->dockBlock->getDescription() : '')
- ->setTags([
- Inflector::humanize(Inflector::underscore($this->route->getController()))
- ])
- ->setParameters($this->getPathParameters())
- ->setDeprecated($this->isDeprecated())
- ;
-
- $path = $this->withDataTransferObject($path, $methodAnnotations);
- $path = $this->withResponses($path, $methodAnnotations);
- $path = $this->withRequestBody($path, $methodAnnotations);
- $path = $this->withExternalDoc($path);
- }
-
- return $path;
- }
-
- /**
- * Returns a route (e.g. /api/model/action)
- *
- * @return string
- */
- private function getPathName() : string
- {
- $pieces = array_map(
- function ($piece) {
- if (substr($piece, 0, 1) == ':') {
- return '{' . str_replace(':', '', $piece) . '}';
- }
- return $piece;
- },
- explode('/', $this->route->getTemplate())
- );
-
- if ($this->prefix == '/') {
- return implode('/', $pieces);
- }
-
- return substr(
- implode('/', $pieces),
- strlen($this->prefix)
- );
- }
-
- /**
- * Returns an array of Parameter
- *
- * @return Parameter[]
- */
- private function getPathParameters() : array
- {
- $return = [];
-
- $pieces = explode('/', $this->route->getTemplate());
- $results = array_filter($pieces, function ($piece) {
- return substr($piece, 0, 1) == ':' ? true : null;
- });
-
- if (empty($results)) {
- return $return;
- }
-
- foreach ($results as $result) {
-
- $schema = new Schema();
- $schema
- ->setType('string')
- ;
-
- $name = strtolower($result);
-
- if (substr($name, 0, 1) == ':') {
- $name = substr($name, 1);
- }
-
- $parameter = new Parameter();
- $parameter
- ->setName($name)
- ->setAllowEmptyValue(false)
- ->setDeprecated(false)
- ->setRequired(true)
- ->setIn('path')
- ->setSchema($schema)
- ;
- $return[] = $parameter;
- }
-
- return $return;
- }
-
- /**
- * Returns an array of Lib/Annotation objects that can be applied to methods
- *
- * @param string $className
- * @param string $method
- * @return array
- */
- private function getMethodAnnotations(string $className, string $method) : array
- {
- $className = $className . 'Controller';
- $controller = $this->getControllerFromNamespaces($className);
- return AnnotationUtility::getMethodAnnotations($controller, $method);
- }
-
- /**
- * Returns a Path after applying Data Transfer Objects from annotations argument
- *
- * @param Path $path
- * @param array $annotations
- * @return Path
- * @throws \ReflectionException
- */
- private function withDataTransferObject(Path $path, array $annotations) : Path
- {
- if (empty($annotations)) {
- return $path;
- }
-
- $dataTransferObjects = array_filter($annotations, function ($annotation) {
- return $annotation instanceof SwagAnnotation\SwagDto;
- });
-
- if (empty($dataTransferObjects)) {
- return $path;
- }
-
- $dto = reset($dataTransferObjects);
- $class = $dto->class;
-
- if (!class_exists($class)) {
- return $path;
- }
-
- $instance = (new ReflectionClass($class))->newInstanceWithoutConstructor();
- $properties = DocBlockUtility::getProperties($instance);
-
- if (empty($properties)) {
- return $path;
- }
-
- $filteredProperties = array_filter($properties, function ($property) use ($instance) {
- if (!isset($property->class) || $property->class != get_class($instance)) {
- return null;
- }
- return true;
- });
-
- $pathType = strtolower($path->getType());
- if ($pathType == 'post') {
- $requestBody = new RequestBody();
- $schema = (new Schema())->setType('object');
- }
-
- foreach ($filteredProperties as $name => $reflectionProperty) {
- $docBlock = DocBlockUtility::getPropertyDocBlock($reflectionProperty);
- $vars = $docBlock->getTagsByName('var');
- if (empty($vars)) {
- throw new LogicException('@var must be set for ' . $class . '::' . $name);
- }
- $var = reset($vars);
- $dataType = DocBlockUtility::getDocBlockConvertedVar($var);
-
- if ($pathType == 'get') {
- $path->pushParameter(
- (new Parameter())
- ->setName($name)
- ->setIn('query')
- ->setRequired(!empty($docBlock->getTagsByName('required')))
- ->setDescription($docBlock->getSummary())
- ->setSchema((new Schema())->setType($dataType))
- );
- } else if ($pathType == 'post' && isset($schema)) {
- $schema->pushProperty(
- (new SchemaProperty())
- ->setDescription($docBlock->getSummary())
- ->setName($name)
- ->setType($dataType)
- ->setRequired(!empty($docBlock->getTagsByName('required')))
- );
- }
- }
-
- if (isset($schema) && isset($requestBody)) {
- $content = (new Content())
- ->setMimeType('application/x-www-form-urlencoded')
- ->setSchema($schema);
-
- $path->setRequestBody(
- $requestBody->pushContent($content)
- );
- }
-
- return $path;
- }
-
- /**
- * Returns path with responses
- *
- * @param Path $path
- * @param array $annotations
- * @return Path
- */
- private function withResponses(Path $path, array $annotations) : Path
- {
- if (!empty($annotations)) {
- foreach ($annotations as $annotation) {
- if ($annotation instanceof SwagAnnotation\SwagResponseSchema) {
- $path->pushResponse((new SwagAnnotation\SwagResponseSchemaHandler())->getResponse($annotation));
- }
- }
- }
-
- if (!$this->dockBlock || !$this->dockBlock->hasTag('throws')) {
- return $path;
- }
-
- $throws = $this->dockBlock->getTagsByName('throws');
-
- foreach ($throws as $throw) {
- $exception = new ExceptionHandler($throw->getType()->__toString());
- $path->pushResponse(
- (new Response())->setCode($exception->getCode())->setDescription($exception->getMessage())
- );
- }
-
- return $path;
- }
-
- /**
- * Returns Path with request body
- *
- * @param Path $path
- * @param array $annotations
- * @return Path
- */
- private function withRequestBody(Path $path, array $annotations) : Path
- {
- if (!empty($path->getRequestBody())) {
- return $path;
- }
-
- if (empty($annotations)) {
- return $path;
- }
-
- $requestBody = new RequestBody();
-
- foreach ($annotations as $annotation) {
- if ($annotation instanceof SwagAnnotation\SwagRequestBody) {
- $requestBody = (new SwagAnnotation\SwagRequestBodyHandler())->getResponse($annotation);
- }
- }
-
- foreach ($annotations as $annotation) {
- if ($annotation instanceof SwagAnnotation\SwagRequestBodyContent) {
- $requestBody->pushContent(
- (new SwagAnnotation\SwagRequestBodyContentHandler())->getContent($annotation)
- );
- }
- }
-
- if (empty($requestBody->getContent())) {
- return $path->setRequestBody($requestBody);
- }
-
- return $path->setRequestBody($requestBody);
- }
-
- /**
- * @return DocBlock|null
- */
- private function getDocBlock() : ?DocBlock
- {
- if (empty($this->route->getController())) {
- return null;
- }
-
- $className = $this->route->getController() . 'Controller';
- $methodName = $this->route->getAction();
- $controller = $this->getControllerFromNamespaces($className);
-
- if (!class_exists($controller)) {
- return null;
- }
-
- try {
- return DocBlockUtility::getMethodDocBlock(new $controller, $methodName);
- } catch (Exception $e) {
- return null;
- }
- }
-
- /**
- * @param string $className
- * @return string|null
- */
- private function getControllerFromNamespaces(string $className) : ?string
- {
- $namespaces = $this->config->getNamespaces();
-
- if (!isset($namespaces['controllers']) || !is_array($namespaces['controllers'])) {
- throw new SwaggerBakeRunTimeException(
- 'Invalid configuration, missing SwaggerBake.namespaces.controllers'
- );
- }
-
- foreach ($namespaces['controllers'] as $namespace) {
- $entity = $namespace . 'Controller\\' . $className;
- if (class_exists($entity, true)) {
- return $entity;
- }
- }
-
- return null;
- }
-
- /**
- * @param string $className
- * @return bool
- */
- private function isControllerVisible(string $className) : bool
- {
- $className = $className . 'Controller';
- $controller = $this->getControllerFromNamespaces($className);
-
- if (!$controller) {
- return false;
- }
-
- $annotations = AnnotationUtility::getClassAnnotations($controller);
-
- foreach ($annotations as $annotation) {
- if ($annotation instanceof SwagAnnotation\SwagPath) {
- return $annotation->isVisible;
- }
- }
-
- return true;
- }
-
- /**
- * @param array $annotations
- * @return bool
- */
- private function isMethodVisible(array $annotations) : bool
- {
- foreach ($annotations as $annotation) {
- if ($annotation instanceof SwagAnnotation\SwagOperation) {
- return $annotation->isVisible;
- }
- }
-
- return true;
- }
-
- /**
- * Check if this path/operation is deprecated
- *
- * @return bool
- */
- private function isDeprecated() : bool
- {
- if (!$this->dockBlock || !$this->dockBlock instanceof DocBlock) {
- return false;
- }
-
- return $this->dockBlock->hasTag('deprecated');
- }
-
- /**
- * Defines external documentation using see tag
- * @param Path $path
- * @return Path
- */
- private function withExternalDoc(Path $path) : Path
- {
- if (!$this->dockBlock || !$this->dockBlock instanceof DocBlock) {
- return $path;
- }
-
- if (!$this->dockBlock->hasTag('see')) {
- return $path;
- }
-
- $tags = $this->dockBlock->getTagsByName('see');
- $seeTag = reset($tags);
- $str = $seeTag->__toString();
- $pieces = explode(' ', $str);
-
- if (!filter_var($pieces[0], FILTER_VALIDATE_URL)) {
- return $path;
- }
-
- $externalDoc = new OperationExternalDoc();
- $externalDoc->setUrl($pieces[0]);
-
- array_shift($pieces);
-
- if (!empty($pieces)) {
- $externalDoc->setDescription(implode(' ', $pieces));
- }
-
- return $path->setExternalDocs($externalDoc);
- }
-}
diff --git a/src/Lib/Factory/SchemaFactory.php b/src/Lib/Factory/SchemaFactory.php
index 9bb1fa32..0bdfb0ef 100644
--- a/src/Lib/Factory/SchemaFactory.php
+++ b/src/Lib/Factory/SchemaFactory.php
@@ -9,16 +9,21 @@
use ReflectionClass;
use SwaggerBake\Lib\Annotation\SwagEntity;
use SwaggerBake\Lib\Annotation\SwagEntityAttribute;
-use SwaggerBake\Lib\Annotation\SwagEntityAttributeHandler;
use SwaggerBake\Lib\Configuration;
use SwaggerBake\Lib\Exception\SwaggerBakeRunTimeException;
-use SwaggerBake\Lib\Model\ExpressiveAttribute;
-use SwaggerBake\Lib\Model\ExpressiveModel;
+use SwaggerBake\Lib\Decorator\PropertyDecorator;
+use SwaggerBake\Lib\Decorator\EntityDecorator;
use SwaggerBake\Lib\OpenApi\Schema;
use SwaggerBake\Lib\OpenApi\SchemaProperty;
use SwaggerBake\Lib\Utility\AnnotationUtility;
use SwaggerBake\Lib\Utility\DataTypeConversion;
+/**
+ * Class SchemaFactory
+ * @package SwaggerBake\Lib\Factory
+ *
+ * Creates an instance of SwaggerBake\Lib\OpenApi\Schema per OpenAPI specifications
+ */
class SchemaFactory
{
/** @var string[] */
@@ -36,24 +41,24 @@ public function __construct(Configuration $config)
}
/**
- * @param ExpressiveModel $model
+ * @param EntityDecorator $entity
* @return Schema|null
*/
- public function create(ExpressiveModel $model) : ?Schema
+ public function create(EntityDecorator $entity) : ?Schema
{
- if (!$this->isSwaggable($model)) {
+ if (!$this->isSwaggable($entity)) {
return null;
}
- $this->validator = $this->getValidator($model->getName());
+ $this->validator = $this->getValidator($entity->getName());
- $docBlock = $this->getDocBlock($model);
+ $docBlock = $this->getDocBlock($entity);
- $properties = $this->getProperties($model);
+ $properties = $this->getProperties($entity);
$schema = new Schema();
$schema
- ->setName($model->getName())
+ ->setName($entity->getName())
->setDescription($docBlock ? $docBlock->getSummary() : '')
->setType('object')
->setProperties($properties)
@@ -71,14 +76,14 @@ public function create(ExpressiveModel $model) : ?Schema
}
/**
- * @param ExpressiveModel $model
+ * @param EntityDecorator $entity
* @return array
*/
- private function getProperties(ExpressiveModel $model) : array
+ private function getProperties(EntityDecorator $entity) : array
{
- $return = $this->getSwagPropertyAnnotations($model);
+ $return = $this->getSwagPropertyAnnotations($entity);
- foreach ($model->getAttributes() as $attribute) {
+ foreach ($entity->getProperties() as $attribute) {
$name = $attribute->getName();
if (isset($return[$name])) {
continue;
@@ -91,15 +96,13 @@ private function getProperties(ExpressiveModel $model) : array
}
/**
- * @param ExpressiveModel $model
+ * @param EntityDecorator $entity
* @return DocBlock|null
*/
- private function getDocBlock(ExpressiveModel $model) : ?DocBlock
+ private function getDocBlock(EntityDecorator $entity) : ?DocBlock
{
- $entity = $this->getEntityFromNamespaces($model->getName());
-
try {
- $instance = new $entity;
+ $instance = $entity->getEntity();
$reflectionClass = new ReflectionClass(get_class($instance));
} catch (\Exception $e) {
return null;
@@ -115,30 +118,6 @@ private function getDocBlock(ExpressiveModel $model) : ?DocBlock
return $docFactory->create($comments);
}
- /**
- * @param string $className
- * @return string|null
- */
- private function getEntityFromNamespaces(string $className) : ?string
- {
- $namespaces = $this->config->getNamespaces();
-
- if (!isset($namespaces['entities']) || !is_array($namespaces['entities'])) {
- throw new SwaggerBakeRunTimeException(
- 'Invalid configuration, missing SwaggerBake.namespaces.entities'
- );
- }
-
- foreach ($namespaces['entities'] as $namespace) {
- $entity = $namespace . 'Model\Entity\\' . $className;
- if (class_exists($entity, true)) {
- return $entity;
- }
- }
-
- return null;
- }
-
/**
* @param string $className
* @return string|null
@@ -164,56 +143,62 @@ private function getTableFromNamespaces(string $className) : ?string
}
/**
- * @param ExpressiveAttribute $attribute
+ * @param PropertyDecorator $property
* @return SchemaProperty
*/
- private function getSchemaProperty(ExpressiveAttribute $attribute) : SchemaProperty
+ private function getSchemaProperty(PropertyDecorator $property) : SchemaProperty
{
- $isReadOnlyField = in_array($attribute->getName(), self::READ_ONLY_FIELDS);
- $isDateTimeField = in_array($attribute->getType(), self::DATETIME_TYPES);
-
- $property = new SchemaProperty();
- $property
- ->setName($attribute->getName())
- ->setType(DataTypeConversion::convert($attribute->getType()))
- ->setReadOnly(($attribute->isPrimaryKey() || ($isReadOnlyField && $isDateTimeField)))
- ->setRequired($this->isAttributeRequired($attribute))
+ $isReadOnlyField = in_array($property->getName(), self::READ_ONLY_FIELDS);
+ $isDateTimeField = in_array($property->getType(), self::DATETIME_TYPES);
+
+ $schemaProperty = new SchemaProperty();
+ $schemaProperty
+ ->setName($property->getName())
+ ->setType(DataTypeConversion::convert($property->getType()))
+ ->setReadOnly(($property->isPrimaryKey() || ($isReadOnlyField && $isDateTimeField)))
+ ->setRequired($this->isAttributeRequired($property))
;
- return $property;
+ return $schemaProperty;
}
/**
* Returns key-value pair of property name => SchemaProperty
*
- * @param ExpressiveModel $model
+ * @param EntityDecorator $entity
* @return SchemaProperty[]
*/
- private function getSwagPropertyAnnotations(ExpressiveModel $model) : array
+ private function getSwagPropertyAnnotations(EntityDecorator $entity) : array
{
$return = [];
- $entity = $this->getEntityFromNamespaces($model->getName());
- $annotations = AnnotationUtility::getClassAnnotations($entity);
+ $annotations = AnnotationUtility::getClassAnnotationsFromInstance($entity->getEntity());
- foreach ($annotations as $annotation) {
- if ($annotation instanceof SwagEntityAttribute) {
- $schemaProperty = (new SwagEntityAttributeHandler())->getSchemaProperty($annotation);
- $return[$schemaProperty->getName()] = $schemaProperty;
- }
- }
+ $swagEntityAttributes = array_filter($annotations, function ($annotation) {
+ return $annotation instanceof SwagEntityAttribute;
+ });
+
+ foreach ($swagEntityAttributes as $swagEntityAttribute) {
+ $return[$swagEntityAttribute->name] = (new SchemaProperty())
+ ->setName($swagEntityAttribute->name)
+ ->setDescription($swagEntityAttribute->description)
+ ->setType($swagEntityAttribute->type)
+ ->setReadOnly($swagEntityAttribute->readOnly)
+ ->setWriteOnly($swagEntityAttribute->writeOnly)
+ ->setRequired($swagEntityAttribute->required)
+ ;
+ }
return $return;
}
/**
- * @param ExpressiveModel $model
+ * @param EntityDecorator $entity
* @return bool
*/
- private function isSwaggable(ExpressiveModel $model) : bool
+ private function isSwaggable(EntityDecorator $entity) : bool
{
- $entity = $this->getEntityFromNamespaces($model->getName());
- $annotations = AnnotationUtility::getClassAnnotations($entity);
+ $annotations = AnnotationUtility::getClassAnnotationsFromInstance($entity->getEntity());
foreach ($annotations as $annotation) {
if ($annotation instanceof SwagEntity) {
@@ -225,16 +210,16 @@ private function isSwaggable(ExpressiveModel $model) : bool
}
/**
- * @param ExpressiveAttribute $attribute
+ * @param PropertyDecorator $property
* @return bool
*/
- private function isAttributeRequired(ExpressiveAttribute $attribute) : bool
+ private function isAttributeRequired(PropertyDecorator $property) : bool
{
if (!$this->validator) {
return false;
}
- $validationSet = $this->validator->field($attribute->getName());
+ $validationSet = $this->validator->field($property->getName());
if (!$validationSet->isEmptyAllowed()) {
return true;
}
diff --git a/src/Lib/Factory/SwaggerFactory.php b/src/Lib/Factory/SwaggerFactory.php
index f3aa7204..6bb2f307 100644
--- a/src/Lib/Factory/SwaggerFactory.php
+++ b/src/Lib/Factory/SwaggerFactory.php
@@ -1,9 +1,7 @@
getMethods() as $method) {
- $annotations = $this->reader->getMethodAnnotations($method);
- if (empty($annotations)) {
- continue;
- }
-
- foreach ($annotations as $annotation) {
- if ($annotation instanceof SwagAnnotation\SwagForm) {
- $return = array_merge(
- $return,
- [
- (new SwagAnnotation\SwagFormHandler())->getSchemaProperty($annotation)
- ]
- );
- }
- }
- }
-
- return $return;
- }
-}
\ No newline at end of file
diff --git a/src/Lib/HeaderParameter.php b/src/Lib/HeaderParameter.php
deleted file mode 100644
index dc8cd0eb..00000000
--- a/src/Lib/HeaderParameter.php
+++ /dev/null
@@ -1,34 +0,0 @@
-getMethods() as $method) {
- $annotations = $this->reader->getMethodAnnotations($method);
- if (empty($annotations)) {
- continue;
- }
- foreach ($annotations as $annotation) {
- if ($annotation instanceof SwagAnnotation\SwagHeader) {
- $return = array_merge(
- $return,
- [(new SwagAnnotation\SwagHeaderHandler())->getHeaderParameters($annotation)]
- );
- }
- }
- }
-
- return $return;
- }
-}
\ No newline at end of file
diff --git a/src/Lib/Model/ExpressiveModel.php b/src/Lib/Model/ExpressiveModel.php
deleted file mode 100644
index beb48d5e..00000000
--- a/src/Lib/Model/ExpressiveModel.php
+++ /dev/null
@@ -1,50 +0,0 @@
-name;
- }
-
- /**
- * @param string $name
- * @return ExpressiveModel
- */
- public function setName(string $name): ExpressiveModel
- {
- $this->name = $name;
- return $this;
- }
-
- /**
- * @return array
- */
- public function getAttributes(): array
- {
- return $this->attributes;
- }
-
- /**
- * @param array $attributes
- * @return ExpressiveModel
- */
- public function setAttributes(array $attributes): ExpressiveModel
- {
- $this->attributes = $attributes;
- return $this;
- }
-}
\ No newline at end of file
diff --git a/src/Lib/OpenApi/Content.php b/src/Lib/OpenApi/Content.php
index 79a0e12c..53afc373 100644
--- a/src/Lib/OpenApi/Content.php
+++ b/src/Lib/OpenApi/Content.php
@@ -1,10 +1,14 @@
httpMethod, ['GET', 'DELETE']) || empty($vars['requestBody'])) {
+ unset($vars['requestBody']);
+ }
+ if (empty($vars['security'])) {
+ unset($vars['security']);
+ }
+ if (empty($vars['externalDocs'])) {
+ unset($vars['externalDocs']);
+ }
+
+ return $vars;
+ }
+
+ /**
+ * @return array|mixed
+ */
+ public function jsonSerialize()
+ {
+ return $this->toArray();
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasSuccessResponseCode() : bool
+ {
+ $results = array_filter($this->getResponses(), function ($response) {
+ return ($response->getCode() >= 200 && $response->getCode() < 300);
+ });
+
+ return count($results) > 0;
+ }
+
+ /**
+ * Gets httpMethod as UPPERCASE string
+ * @return string
+ */
+ public function getHttpMethod(): string
+ {
+ return strtoupper($this->httpMethod);
+ }
+
+ /**
+ * @param string $httpMethod
+ * @return Operation
+ */
+ public function setHttpMethod(string $httpMethod): Operation
+ {
+ $httpMethod = strtoupper($httpMethod);
+ if (!in_array($httpMethod, ['GET','PUT', 'POST', 'PATCH', 'DELETE'])) {
+ throw new InvalidArgumentException("Invalid HTTP METHOD: $httpMethod");
+ }
+
+ $this->httpMethod = $httpMethod;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getTags(): array
+ {
+ return $this->tags;
+ }
+
+ /**
+ * @param array $tags
+ * @return Operation
+ */
+ public function setTags(array $tags): Operation
+ {
+ $this->tags = $tags;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getOperationId(): string
+ {
+ return $this->operationId;
+ }
+
+ /**
+ * @param string $operationId
+ * @return Operation
+ */
+ public function setOperationId(string $operationId): Operation
+ {
+ $this->operationId = $operationId;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getParameters(): array
+ {
+ return $this->parameters;
+ }
+
+ /**
+ * @param array $parameters
+ * @return Operation
+ */
+ public function setParameters(array $parameters): Operation
+ {
+ $this->parameters = $parameters;
+ return $this;
+ }
+
+ /**
+ * @param Parameter $parameter
+ * @return Operation
+ */
+ public function pushParameter(Parameter $parameter): Operation
+ {
+ $this->parameters[] = $parameter;
+ return $this;
+ }
+
+ /**
+ * @return RequestBody|null
+ */
+ public function getRequestBody() : ?RequestBody
+ {
+ return $this->requestBody;
+ }
+
+ /**
+ * @param RequestBody $requestBody
+ * @return Operation
+ */
+ public function setRequestBody(RequestBody $requestBody) : Operation
+ {
+ $this->requestBody = $requestBody;
+ return $this;
+ }
+
+ /**
+ * @return Response[]
+ */
+ public function getResponses(): array
+ {
+ return $this->responses;
+ }
+
+ /**
+ * @param int $code
+ * @return Response|null
+ */
+ public function getResponseByCode(int $code) : ?Response
+ {
+ return isset($this->responses[$code]) ? $this->responses[$code] : null;
+ }
+
+ /**
+ * @param array $array
+ * @return Operation
+ */
+ public function setResponses(array $array) : Operation
+ {
+ $this->responses = $array;
+ return $this;
+ }
+
+ /**
+ * @param Response $response
+ * @return Operation
+ */
+ public function pushResponse(Response $response): Operation
+ {
+ $code = $response->getCode();
+ $existingResponse = $this->getResponseByCode($response->getCode());
+ if ($this->getResponseByCode($response->getCode())) {
+ $content = $existingResponse->getContent() + $response->getContent();
+ $existingResponse->setContent($content);
+ $this->responses[$code] = $existingResponse;
+ return $this;
+ }
+ $this->responses[$code] = $response;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getSecurity(): array
+ {
+ return $this->security;
+ }
+
+ /**
+ * @param array $security
+ * @return Operation
+ */
+ public function setSecurity(array $security): Operation
+ {
+ $this->security = $security;
+ return $this;
+ }
+
+ /**
+ * @param PathSecurity $security
+ * @return Operation
+ */
+ public function pushSecurity(PathSecurity $security): Operation
+ {
+ $this->security[] = $security;
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isDeprecated(): bool
+ {
+ return $this->deprecated;
+ }
+
+ /**
+ * @param bool $deprecated
+ * @return Operation
+ */
+ public function setDeprecated(bool $deprecated): Operation
+ {
+ $this->deprecated = $deprecated;
+ return $this;
+ }
+
+ /**
+ * @return OperationExternalDoc
+ */
+ public function getExternalDocs() : OperationExternalDoc
+ {
+ return $this->externalDocs;
+ }
+
+ /**
+ * @param OperationExternalDoc $externalDoc
+ * @return Operation
+ */
+ public function setExternalDocs(OperationExternalDoc $externalDoc) : Operation
+ {
+ $this->externalDocs = $externalDoc;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getSummary(): string
+ {
+ return $this->summary;
+ }
+
+ /**
+ * @param string $summary
+ * @return Operation
+ */
+ public function setSummary(string $summary): Operation
+ {
+ $this->summary = $summary;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ /**
+ * @param string $description
+ * @return Operation
+ */
+ public function setDescription(string $description): Operation
+ {
+ $this->description = $description;
+ return $this;
+ }
+}
diff --git a/src/Lib/OpenApi/OperationExternalDoc.php b/src/Lib/OpenApi/OperationExternalDoc.php
index decf2743..8f377073 100644
--- a/src/Lib/OpenApi/OperationExternalDoc.php
+++ b/src/Lib/OpenApi/OperationExternalDoc.php
@@ -4,6 +4,11 @@
use JsonSerializable;
+/**
+ * Class OperationExternalDoc
+ * @package SwaggerBake\Lib\OpenApi
+ * @see https://swagger.io/docs/specification/paths-and-operations/
+ */
class OperationExternalDoc implements JsonSerializable
{
/** @var string */
diff --git a/src/Lib/OpenApi/Parameter.php b/src/Lib/OpenApi/Parameter.php
index 2af27641..5a6f9668 100644
--- a/src/Lib/OpenApi/Parameter.php
+++ b/src/Lib/OpenApi/Parameter.php
@@ -9,7 +9,8 @@
/**
* Class Parameter
- * @see https://swagger.io/specification/
+ * @package SwaggerBake\Lib\OpenApi
+ * @see https://swagger.io/docs/specification/describing-parameters/
*/
class Parameter implements JsonSerializable
{
diff --git a/src/Lib/OpenApi/Path.php b/src/Lib/OpenApi/Path.php
index 1a957f1e..6c0d8a4b 100644
--- a/src/Lib/OpenApi/Path.php
+++ b/src/Lib/OpenApi/Path.php
@@ -2,344 +2,94 @@
namespace SwaggerBake\Lib\OpenApi;
-use InvalidArgumentException;
+use JsonSerializable;
/**
* Class Path
- * @todo implement $ref
- * @see https://swagger.io/specification/
+ * @package SwaggerBake\Lib\OpenApi
+ * @see https://swagger.io/docs/specification/paths-and-operations/
*/
-class Path
+class Path implements JsonSerializable
{
- /** @var string */
- private $summary = '';
-
- /** @var string */
- private $description = '';
-
- /** @var OperationExternalDoc|null */
- private $externalDocs;
-
- /** @var string */
- private $type = '';
-
- /** @var string */
- private $path = '';
-
- /** @var string[] */
- private $tags = [];
-
- /** @var string */
- private $operationId = '';
-
- /** @var Parameter[] */
- private $parameters = [];
-
- /** @var RequestBody|null */
- private $requestBody;
-
- /** @var Response[] */
- private $responses = [];
-
- /** @var PathSecurity[] */
- private $security = [];
-
- /** @var bool */
- private $deprecated = false;
-
- public function toArray(): array
- {
- $vars = get_object_vars($this);
- unset($vars['type']);
- unset($vars['path']);
-
- if (in_array($this->type, ['get', 'delete'])) {
- unset($vars['requestBody']);
- }
- if (empty($vars['security'])) {
- unset($vars['security']);
- }
- if (empty($vars['externalDocs'])) {
- unset($vars['externalDocs']);
- }
-
- return $vars;
- }
-
- public function hasSuccessResponseCode() : bool
- {
- $results = array_filter($this->getResponses(), function ($response) {
- return ($response->getCode() >= 200 && $response->getCode() < 300);
- });
-
- return count($results) > 0;
- }
-
- /**
- * @return string
- */
- public function getSummary(): string
- {
- return $this->summary;
- }
-
/**
- * @param string $summary
- * @return Path
+ * The endpoint (resource) for the path
+ * @var string
*/
- public function setSummary(string $summary): Path
- {
- $this->summary = $summary;
- return $this;
- }
+ private $resource = '';
/**
- * @return string
+ * @var Operation[]
*/
- public function getDescription(): string
- {
- return $this->description;
- }
+ private $operations = [];
- /**
- * @param string $description
- * @return Path
- */
- public function setDescription(string $description): Path
- {
- $this->description = $description;
- return $this;
- }
-
- /**
- * @return string
- */
- public function getType(): string
+ public function toArray(): array
{
- return $this->type;
- }
+ $vars = get_object_vars($this);
+ unset($vars['resource']);
+ unset($vars['operations']);
- /**
- * @param string $type
- * @return Path
- */
- public function setType(string $type): Path
- {
- $type = strtolower($type);
- if (!in_array($type, ['get','put', 'post', 'patch', 'delete'])) {
- throw new InvalidArgumentException("type must be a valid HTTP METHOD, $type given");
+ foreach ($this->getOperations() as $operation) {
+ $vars[strtolower($operation->getHttpMethod())] = $operation;
}
- $this->type = $type;
- return $this;
- }
-
- /**
- * @return array
- */
- public function getTags(): array
- {
- return $this->tags;
+ return $vars;
}
/**
- * @param array $tags
- * @return Path
+ * @return array|mixed
*/
- public function setTags(array $tags): Path
+ public function jsonSerialize()
{
- $this->tags = $tags;
- return $this;
+ return $this->toArray();
}
/**
* @return string
*/
- public function getOperationId(): string
- {
- return $this->operationId;
- }
-
- /**
- * @param string $operationId
- * @return Path
- */
- public function setOperationId(string $operationId): Path
- {
- $this->operationId = $operationId;
- return $this;
- }
-
- /**
- * @return array
- */
- public function getParameters(): array
- {
- return $this->parameters;
- }
-
- /**
- * @param array $parameters
- * @return Path
- */
- public function setParameters(array $parameters): Path
- {
- $this->parameters = $parameters;
- return $this;
- }
-
- /**
- * @param Parameter $parameter
- * @return Path
- */
- public function pushParameter(Parameter $parameter): Path
- {
- $this->parameters[] = $parameter;
- return $this;
- }
-
- /**
- * @return RequestBody|null
- */
- public function getRequestBody() : ?RequestBody
+ public function getResource(): string
{
- return $this->requestBody;
+ return $this->resource;
}
/**
- * @param RequestBody $requestBody
+ * @param string $resource
* @return Path
*/
- public function setRequestBody(RequestBody $requestBody) : Path
+ public function setResource(string $resource): Path
{
- $this->requestBody = $requestBody;
+ $this->resource = $resource;
return $this;
}
/**
- * @return Response[]
- */
- public function getResponses(): array
- {
- return $this->responses;
- }
-
- /**
- * @param int $code
- * @return Response|null
- */
- public function getResponseByCode(int $code) : ?Response
- {
- return isset($this->responses[$code]) ? $this->responses[$code] : null;
- }
-
- /**
- * @param array $array
- * @return Path
+ * @return Operation[]
*/
- public function setResponses(array $array) : Path
+ public function getOperations(): array
{
- $this->responses = $array;
- return $this;
+ return $this->operations;
}
/**
- * @param Response $response
+ * @param Operation[] $operations
* @return Path
*/
- public function pushResponse(Response $response): Path
+ public function setOperations(array $operations): Path
{
- $code = $response->getCode();
- $existingResponse = $this->getResponseByCode($response->getCode());
- if ($this->getResponseByCode($response->getCode())) {
- $content = $existingResponse->getContent() + $response->getContent();
- $existingResponse->setContent($content);
- $this->responses[$code] = $existingResponse;
- return $this;
+ $this->operations = [];
+ foreach ($operations as $operation) {
+ $this->pushOperation($operation);
}
- $this->responses[$code] = $response;
return $this;
}
/**
- * @return array
- */
- public function getSecurity(): array
- {
- return $this->security;
- }
-
- /**
- * @param array $security
- * @return Path
- */
- public function setSecurity(array $security): Path
- {
- $this->security = $security;
- return $this;
- }
-
- /**
- * @param PathSecurity $security
+ * @param Operation $operation
* @return $this
*/
- public function pushSecurity(PathSecurity $security): Path
- {
- $this->security[] = $security;
- return $this;
- }
-
- /**
- * @return bool
- */
- public function isDeprecated(): bool
- {
- return $this->deprecated;
- }
-
- /**
- * @param bool $deprecated
- * @return Path
- */
- public function setDeprecated(bool $deprecated): Path
- {
- $this->deprecated = $deprecated;
- return $this;
- }
-
- /**
- * @return string
- */
- public function getPath(): string
- {
- return $this->path;
- }
-
- /**
- * @param string $path
- * @return Path
- */
- public function setPath(string $path): Path
- {
- $this->path = $path;
- return $this;
- }
-
- /**
- * @return OperationExternalDoc
- */
- public function getExternalDocs() : OperationExternalDoc
- {
- return $this->externalDocs;
- }
-
- /**
- * @param OperationExternalDoc $externalDoc
- * @return Path
- */
- public function setExternalDocs(OperationExternalDoc $externalDoc) : Path
+ public function pushOperation(Operation $operation) : Path
{
- $this->externalDocs = $externalDoc;
+ $httpMethod = strtolower($operation->getHttpMethod());
+ $this->operations[$httpMethod] = $operation;
return $this;
}
}
diff --git a/src/Lib/OpenApi/PathSecurity.php b/src/Lib/OpenApi/PathSecurity.php
index 931d1be6..7511aa6f 100644
--- a/src/Lib/OpenApi/PathSecurity.php
+++ b/src/Lib/OpenApi/PathSecurity.php
@@ -4,6 +4,11 @@
use JsonSerializable;
+/**
+ * Class PathSecurity
+ * @package SwaggerBake\Lib\OpenApi
+ * @see https://swagger.io/docs/specification/authentication/
+ */
class PathSecurity implements JsonSerializable
{
/** @var string */
diff --git a/src/Lib/OpenApi/RequestBody.php b/src/Lib/OpenApi/RequestBody.php
index 347576b3..b18b8d10 100644
--- a/src/Lib/OpenApi/RequestBody.php
+++ b/src/Lib/OpenApi/RequestBody.php
@@ -1,10 +1,14 @@
properties = $properties;
+ $this->properties = [];
+ foreach ($properties as $property) {
+ $this->pushProperty($property);
+ }
+
return $this;
}
diff --git a/src/Lib/OpenApi/SchemaProperty.php b/src/Lib/OpenApi/SchemaProperty.php
index ef0cc1d9..2c0e8ec8 100644
--- a/src/Lib/OpenApi/SchemaProperty.php
+++ b/src/Lib/OpenApi/SchemaProperty.php
@@ -1,10 +1,14 @@
hasTag('deprecated')) {
+ $operation->setDeprecated(true);
+ }
+
+ if (!$doc->hasTag('see')) {
+ return $operation;
+ }
+
+ $tags = $doc->getTagsByName('see');
+ $seeTag = reset($tags);
+ $str = $seeTag->__toString();
+ $pieces = explode(' ', $str);
+
+ if (!filter_var($pieces[0], FILTER_VALIDATE_URL)) {
+ return $operation;
+ }
+
+ $externalDoc = new OperationExternalDoc();
+ $externalDoc->setUrl($pieces[0]);
+
+ array_shift($pieces);
+
+ if (!empty($pieces)) {
+ $externalDoc->setDescription(implode(' ', $pieces));
+ }
+
+ return $operation->setExternalDocs($externalDoc);
+ }
+}
\ No newline at end of file
diff --git a/src/Lib/Operation/OperationFromRouteFactory.php b/src/Lib/Operation/OperationFromRouteFactory.php
new file mode 100644
index 00000000..7f50f440
--- /dev/null
+++ b/src/Lib/Operation/OperationFromRouteFactory.php
@@ -0,0 +1,129 @@
+config = $config;
+ }
+
+ /**
+ * Creates an instance of Operation
+ *
+ * @param RouteDecorator $route
+ * @param string $httpMethod
+ * @param null|Schema $schema
+ * @return Operation|null
+ */
+ public function create(RouteDecorator $route, string $httpMethod, ?Schema $schema) : ?Operation
+ {
+ if (empty($route->getMethods())) {
+ return null;
+ }
+
+ $className = $route->getController() . 'Controller';
+ $fullyQualifiedNameSpace = NamespaceUtility::getControllerFullQualifiedNameSpace($className, $this->config);
+
+ $doc = $this->getDocBlock($fullyQualifiedNameSpace, $route->getAction());
+ $methodAnnotations = AnnotationUtility::getMethodAnnotations($fullyQualifiedNameSpace, $route->getAction());
+
+ if (!$this->isVisible($methodAnnotations)) {
+ return null;
+ }
+
+ $operation = (new Operation())
+ ->setSummary($doc->getSummary())
+ ->setDescription($doc->getDescription())
+ ->setHttpMethod(strtolower($httpMethod))
+ ->setOperationId($route->getName())
+ ->setTags([
+ Inflector::humanize(Inflector::underscore($route->getController()))
+ ]);
+
+ $operation = (new OperationDocBlock())
+ ->getOperationWithDocBlock($operation, $doc);
+
+ $operation = (new OperationPath())
+ ->getOperationWithPathParameters($operation, $route);
+
+ $operation = (new OperationHeader())
+ ->getOperationWithHeaders($operation, $methodAnnotations);
+
+ $operation = (new OperationSecurity())
+ ->getOperationWithSecurity($operation, $methodAnnotations);
+
+ $operation = (new OperationQueryParameter())
+ ->getOperationWithQueryParameters($operation, $methodAnnotations);
+
+ $operation = (new OperationRequestBody($this->config, $operation, $doc, $methodAnnotations, $route, $schema))
+ ->getOperationWithRequestBody();
+
+ $operation = (new OperationResponse($this->config, $operation, $doc, $methodAnnotations, $route, $schema))
+ ->getOperationWithResponses();
+
+ return $operation;
+ }
+
+ /**
+ * Gets an instance of DocBlock from the controllers method
+ *
+ * @param string $fullyQualifiedNameSpace
+ * @param string $methodName
+ * @return DocBlock
+ */
+ private function getDocBlock(string $fullyQualifiedNameSpace, string $methodName) : DocBlock
+ {
+ $emptyDocBlock = DocBlockFactory::createInstance()->create('/** */');
+
+ if (!class_exists($fullyQualifiedNameSpace)) {
+ return $emptyDocBlock;
+ }
+
+ try {
+ return DocBlockUtility::getMethodDocBlock(new $fullyQualifiedNameSpace, $methodName) ?? $emptyDocBlock;
+ } catch (Exception $e) {
+ return $emptyDocBlock;
+ }
+ }
+
+ /**
+ * @param array $annotations
+ * @return bool
+ */
+ private function isVisible(array $annotations) : bool
+ {
+ $swagOperations = array_filter($annotations, function ($annotation) {
+ return $annotation instanceof SwagOperation;
+ });
+
+ if (empty($swagOperations)) {
+ return true;
+ }
+
+ $swagOperation = reset($swagOperations);
+
+ return $swagOperation->isVisible === false ? false : true;
+ }
+}
\ No newline at end of file
diff --git a/src/Lib/Operation/OperationFromYmlFactory.php b/src/Lib/Operation/OperationFromYmlFactory.php
new file mode 100644
index 00000000..0887f4a1
--- /dev/null
+++ b/src/Lib/Operation/OperationFromYmlFactory.php
@@ -0,0 +1,48 @@
+setHttpMethod($httpMethod)
+ ->setTags(isset($var['tags']) ? $var['tags'] : [])
+ ->setOperationId(isset($var['operationId']) ? $var['operationId'] : '')
+ ->setDeprecated((bool) isset($var['deprecated']) ? $var['deprecated'] : false);
+
+ if (isset($var['externalDocs']['url'])) {
+ $operation->setExternalDocs(
+ (new OperationExternalDoc())
+ ->setDescription(
+ isset($var['externalDocs']['description']) ? $var['externalDocs']['description'] : ''
+ )
+ ->setUrl($var['externalDocs']['url'])
+ );
+ }
+
+ if (isset($var['security']) && is_array($var['security'])) {
+ foreach ($var['security'] as $key => $scopes) {
+ $operation->pushSecurity((new PathSecurity())->setName($key)->setScopes($scopes));
+ }
+ }
+
+ return $operation;
+ }
+}
\ No newline at end of file
diff --git a/src/Lib/Operation/OperationHeader.php b/src/Lib/Operation/OperationHeader.php
new file mode 100644
index 00000000..58019b39
--- /dev/null
+++ b/src/Lib/Operation/OperationHeader.php
@@ -0,0 +1,43 @@
+setName($annotation->name)
+ ->setDescription($annotation->description)
+ ->setAllowEmptyValue(false)
+ ->setDeprecated(false)
+ ->setRequired($annotation->required)
+ ->setIn('header')
+ ->setSchema((new Schema())->setType($annotation->type))
+ ;
+
+ $operation->pushParameter($parameter);
+ }
+
+ return $operation;
+ }
+}
\ No newline at end of file
diff --git a/src/Lib/Operation/OperationPath.php b/src/Lib/Operation/OperationPath.php
new file mode 100644
index 00000000..d98ed799
--- /dev/null
+++ b/src/Lib/Operation/OperationPath.php
@@ -0,0 +1,49 @@
+getTemplate());
+ $results = array_filter($pieces, function ($piece) {
+ return substr($piece, 0, 1) == ':' ? true : null;
+ });
+
+ foreach ($results as $result) {
+
+ $name = strtolower($result);
+
+ if (substr($name, 0, 1) == ':') {
+ $name = substr($name, 1);
+ }
+
+ $operation->pushParameter(
+ (new Parameter())
+ ->setName($name)
+ ->setAllowEmptyValue(false)
+ ->setDeprecated(false)
+ ->setRequired(true)
+ ->setIn('path')
+ ->setSchema((new Schema())->setType('string'))
+ );
+ }
+
+ return $operation;
+ }
+}
\ No newline at end of file
diff --git a/src/Lib/Operation/OperationQueryParameter.php b/src/Lib/Operation/OperationQueryParameter.php
new file mode 100644
index 00000000..5cc60755
--- /dev/null
+++ b/src/Lib/Operation/OperationQueryParameter.php
@@ -0,0 +1,161 @@
+getHttpMethod() != 'GET') {
+ return $operation;
+ }
+
+ $operation = $this->withSwagPaginator($operation, $annotations);
+ $operation = $this->withSwagQuery($operation, $annotations);
+ try {
+ $operation = $this->withSwagDto($operation, $annotations);
+ } catch (\ReflectionException $e) {
+ throw new SwaggerBakeRunTimeException('ReflectionException: ' . $e->getMessage());
+ }
+
+ return $operation;
+ }
+
+ /**
+ * @param Operation $operation
+ * @param array $annotations
+ * @return Operation
+ */
+ private function withSwagPaginator(Operation $operation, array $annotations) : Operation
+ {
+ $swagPaginator = array_filter($annotations, function ($annotation) {
+ return $annotation instanceof SwagPaginator;
+ });
+
+ if (empty($swagPaginator)) {
+ return $operation;
+ }
+
+ $parameter = (new Parameter())
+ ->setAllowEmptyValue(false)
+ ->setDeprecated(false)
+ ->setRequired(false)
+ ->setIn('query');
+
+ $params = ['page' => 'integer', 'limit' => 'integer', 'sort' => 'string', 'direction' => 'string'];
+ foreach ($params as $name => $type) {
+ $operation->pushParameter(
+ (clone $parameter)->setName($name)->setSchema((new Schema())->setType($type))
+ );
+ }
+
+ return $operation;
+ }
+
+ /**
+ * @param Operation $operation
+ * @param array $annotations
+ * @return Operation
+ */
+ private function withSwagQuery(Operation $operation, array $annotations) : Operation
+ {
+ $swagQueries = array_filter($annotations, function ($annotation) {
+ return $annotation instanceof SwagQuery;
+ });
+
+ foreach ($swagQueries as $annotation) {
+ $parameter = (new Parameter())
+ ->setName($annotation->name)
+ ->setDescription($annotation->description)
+ ->setAllowEmptyValue(false)
+ ->setDeprecated(false)
+ ->setRequired($annotation->required)
+ ->setIn('query')
+ ->setSchema((new Schema())->setType($annotation->type))
+ ;
+
+ $operation->pushParameter($parameter);
+ }
+
+ return $operation;
+ }
+
+ /**
+ * @param Operation $operation
+ * @param array $annotations
+ * @return Operation
+ * @throws \ReflectionException
+ */
+ private function withSwagDto(Operation $operation, array $annotations) : Operation
+ {
+ $swagDtos = array_filter($annotations, function ($annotation) {
+ return $annotation instanceof SwagDto;
+ });
+
+ if (empty($swagDtos)) {
+ return $operation;
+ }
+
+ $dto = reset($swagDtos);
+ $class = $dto->class;
+
+ if (!class_exists($class)) {
+ return $operation;
+ }
+
+ $instance = (new ReflectionClass($class))->newInstanceWithoutConstructor();
+ $properties = DocBlockUtility::getProperties($instance);
+
+ if (empty($properties)) {
+ return $operation;
+ }
+
+ $filteredProperties = array_filter($properties, function ($property) use ($instance) {
+ if (!isset($property->class) || $property->class != get_class($instance)) {
+ return null;
+ }
+ return true;
+ });
+
+ foreach ($filteredProperties as $name => $reflectionProperty) {
+ $docBlock = DocBlockUtility::getPropertyDocBlock($reflectionProperty);
+ $vars = $docBlock->getTagsByName('var');
+ if (empty($vars)) {
+ throw new LogicException('@var must be set for ' . $class . '::' . $name);
+ }
+ $var = reset($vars);
+ $dataType = DocBlockUtility::getDocBlockConvertedVar($var);
+
+ $operation->pushParameter(
+ (new Parameter())
+ ->setName($name)
+ ->setIn('query')
+ ->setRequired(!empty($docBlock->getTagsByName('required')))
+ ->setDescription($docBlock->getSummary())
+ ->setSchema((new Schema())->setType($dataType))
+ );
+ }
+
+ return $operation;
+ }
+}
\ No newline at end of file
diff --git a/src/Lib/Operation/OperationRequestBody.php b/src/Lib/Operation/OperationRequestBody.php
new file mode 100644
index 00000000..d21f0926
--- /dev/null
+++ b/src/Lib/Operation/OperationRequestBody.php
@@ -0,0 +1,324 @@
+config = $config;
+ $this->operation = $operation;
+ $this->doc = $doc;
+ $this->annotations = $annotations;
+ $this->route = $route;
+ $this->schema = $schema;
+ }
+
+ /**
+ * Gets an Operation with RequestBody
+ *
+ * @return Operation
+ */
+ public function getOperationWithRequestBody() : Operation
+ {
+ if (!in_array($this->operation->getHttpMethod(), ['POST','PATCH','PUT'])) {
+ return $this->operation;
+ }
+
+ $this->assignSwagRequestBodyAnnotation();
+ $this->assignSwagRequestBodyContentAnnotations();
+ $this->assignSwagFormAnnotations();
+ $this->assignSwagDto();
+ $this->assignSchema();
+
+ return $this->operation;
+ }
+
+ /**
+ * Assigns @SwagRequestBody annotations
+ *
+ * @return void
+ */
+ private function assignSwagRequestBodyAnnotation() : void
+ {
+ $swagRequestBodies = array_filter($this->annotations, function ($annotation) {
+ return $annotation instanceof SwagRequestBody;
+ });
+
+ if (empty($swagRequestBodies)) {
+ return;
+ }
+
+ $swagRequestBody = reset($swagRequestBodies);
+
+ $requestBody = $this->operation->getRequestBody() ?? new RequestBody();
+
+ $requestBody
+ ->setDescription($swagRequestBody->description)
+ ->setRequired($swagRequestBody->required)
+ ;
+
+ $this->operation->setRequestBody($requestBody);
+ }
+
+ /**
+ * Assigns @SwagRequestBodyContent annotations
+ *
+ * @return void
+ */
+ private function assignSwagRequestBodyContentAnnotations() : void
+ {
+ $swagRequestBodyContents = array_filter($this->annotations, function ($annotation) {
+ return $annotation instanceof SwagRequestBodyContent;
+ });
+
+ if (empty($swagRequestBodyContents)) {
+ return;
+ }
+
+ $swagRequestBodyContent = reset($swagRequestBodyContents);
+
+ $requestBody = $this->operation->getRequestBody() ?? new RequestBody();
+
+ $requestBody->pushContent(
+ (new Content())
+ ->setMimeType($swagRequestBodyContent->mimeType)
+ ->setSchema($swagRequestBodyContent->refEntity)
+ );
+
+ $this->operation->setRequestBody($requestBody);
+ }
+
+ /**
+ * Adds @SwagForm annotations to the Operations Request Body
+ *
+ * @return void
+ */
+ private function assignSwagFormAnnotations() : void
+ {
+ $swagForms = array_filter($this->annotations, function ($annotation) {
+ return $annotation instanceof SwagForm;
+ });
+
+ if (empty($swagForms)) {
+ return;
+ }
+
+ $schema = (new Schema())->setType('object');
+
+ foreach ($swagForms as $annotation) {
+ $schema->pushProperty(
+ (new SchemaProperty())
+ ->setDescription($annotation->description)
+ ->setName($annotation->name)
+ ->setType($annotation->type)
+ ->setRequired($annotation->required)
+ );
+ }
+
+ $requestBody = $this->operation->getRequestBody() ?? new RequestBody();
+
+ $requestBody->pushContent(
+ (new Content())
+ ->setMimeType('application/x-www-form-urlencoded')
+ ->setSchema($schema)
+ );
+
+ $this->operation->setRequestBody($requestBody);
+ }
+
+ /**
+ * Adds @SwagDto annotations to the Operations Request Body
+ *
+ * @return void
+ */
+ private function assignSwagDto() : void
+ {
+ $swagDtos = array_filter($this->annotations, function ($annotation) {
+ return $annotation instanceof SwagDto;
+ });
+
+ if (empty($swagDtos)) {
+ return;
+ }
+
+ $dto = reset($swagDtos);
+ $class = $dto->class;
+
+ if (!class_exists($class)) {
+ return;
+ }
+
+ try {
+ $instance = (new ReflectionClass($class))->newInstanceWithoutConstructor();
+ $properties = DocBlockUtility::getProperties($instance);
+ } catch (ReflectionException $e) {
+ throw new SwaggerBakeRunTimeException('ReflectionException: ' . $e->getMessage());
+ }
+
+ if (empty($properties)) {
+ return;
+ }
+
+ $filteredProperties = array_filter($properties, function ($property) use ($instance) {
+ if (!isset($property->class) || $property->class != get_class($instance)) {
+ return null;
+ }
+ return true;
+ });
+
+ if (empty($filteredProperties)) {
+ return;
+ }
+
+ $requestBody = new RequestBody();
+ $schema = (new Schema())->setType('object');
+
+ foreach ($filteredProperties as $name => $reflectionProperty) {
+ $docBlock = DocBlockUtility::getPropertyDocBlock($reflectionProperty);
+ $vars = $docBlock->getTagsByName('var');
+ if (empty($vars)) {
+ throw new SwaggerBakeRunTimeException('@var must be set for ' . $class . '::' . $name);
+ }
+ $var = reset($vars);
+ $dataType = DocBlockUtility::getDocBlockConvertedVar($var);
+
+ $schema->pushProperty(
+ (new SchemaProperty())
+ ->setDescription($docBlock->getSummary())
+ ->setName($name)
+ ->setType($dataType)
+ ->setRequired(!empty($docBlock->getTagsByName('required')))
+ );
+ }
+
+ $this->operation->setRequestBody(
+ $requestBody->pushContent(
+ (new Content())
+ ->setMimeType('application/x-www-form-urlencoded')
+ ->setSchema($schema)
+ )
+ );
+ }
+
+ /**
+ * Adds Schema to the Operations Request Body
+ *
+ * @return void
+ */
+ private function assignSchema() : void
+ {
+ if (!$this->schema) {
+ return;
+ }
+
+ $requestBody = $this->operation->getRequestBody() ?? new RequestBody();
+
+ foreach ($this->config->getRequestAccepts() as $mimeType) {
+
+ if ($mimeType === 'application/x-www-form-urlencoded') {
+ $requestBody = $this->getRequestBodyWithFormSchema($requestBody);
+ continue;
+ }
+
+ if ($requestBody->getContentByType($mimeType)) {
+ continue;
+ }
+
+ $requestBody->pushContent(
+ (new Content())
+ ->setMimeType($mimeType)
+ ->setSchema($this->schema)
+ );
+ }
+
+ $this->operation->setRequestBody($requestBody);
+ }
+
+ /**
+ * Adds Schema to the Operations Request Body as application/x-www-form-urlencoded
+ *
+ * @param RequestBody $requestBody
+ * @return RequestBody
+ */
+ private function getRequestBodyWithFormSchema(RequestBody $requestBody) : RequestBody
+ {
+ $ignoreSchemas = array_filter($this->annotations, function ($annotation) {
+ return $annotation instanceof SwagRequestBody && $annotation->ignoreCakeSchema === true;
+ });
+
+ if (!empty($ignoreSchemas) || !isset($this->schema)) {
+ return $requestBody;
+ }
+
+ $properties = [];
+ if ($requestBody->getContentByType('application/x-www-form-urlencoded')) {
+ $properties = $requestBody
+ ->getContentByType('application/x-www-form-urlencoded')
+ ->getSchema()
+ ->getProperties();
+ }
+
+ $schema = clone $this->schema;
+ $schemaProperties = array_filter($schema->getProperties(), function ($property) {
+ return $property->isReadOnly() === false;
+ });
+
+ $properties = array_merge($schemaProperties, $properties);
+
+ $schema->setProperties($properties);
+
+ $requestBody->pushContent(
+ (new Content())
+ ->setMimeType('application/x-www-form-urlencoded')
+ ->setSchema($schema)
+ );
+
+ return $requestBody;
+ }
+}
\ No newline at end of file
diff --git a/src/Lib/Operation/OperationResponse.php b/src/Lib/Operation/OperationResponse.php
new file mode 100644
index 00000000..04d0ed93
--- /dev/null
+++ b/src/Lib/Operation/OperationResponse.php
@@ -0,0 +1,175 @@
+config = $config;
+ $this->operation = $operation;
+ $this->doc = $doc;
+ $this->annotations = $annotations;
+ $this->route = $route;
+ $this->schema = $schema;
+ }
+
+ /**
+ * Gets an Operation with Responses
+ * @return Operation
+ */
+ public function getOperationWithResponses() : Operation
+ {
+ $this->assignAnnotations();
+ $this->assignDocBlockExceptions();
+ $this->assignSchema();
+
+ return $this->operation;
+ }
+
+ /**
+ * Set Responses using SwagResponseSchema
+ * @return void
+ */
+ private function assignAnnotations() : void
+ {
+ $swagResponses = array_filter($this->annotations, function ($annotation) {
+ return $annotation instanceof SwagResponseSchema;
+ });
+
+ foreach ($swagResponses as $annotation) {
+ $response = (new Response())
+ ->setCode(intval($annotation->httpCode))
+ ->setDescription($annotation->description);
+
+ if (empty($annotation->schemaFormat) && empty($annotation->mimeType)) {
+ $this->operation->pushResponse($response);
+ continue;
+ }
+
+ $response->pushContent(
+ (new Content())
+ ->setSchema($annotation->refEntity)
+ ->setFormat($annotation->schemaFormat)
+ ->setType($annotation->schemaType)
+ ->setMimeType($annotation->mimeType)
+ );
+ $this->operation->pushResponse($response);
+ }
+ }
+
+ /**
+ * Sets error Responses using throw tags from Dock Block
+ * @return void
+ */
+ private function assignDocBlockExceptions() : void
+ {
+ if (!$this->doc->hasTag('throws')) {
+ return;
+ }
+
+ $throws = $this->doc->getTagsByName('throws');
+
+ $mimeTypes = $this->config->getResponseContentTypes();
+ $mimeType = reset($mimeTypes);
+
+ foreach ($throws as $throw) {
+ $exception = new ExceptionHandler($throw->getType()->__toString());
+
+ $this->operation->pushResponse(
+ (new Response())
+ ->setCode($exception->getCode())
+ ->setDescription($exception->getMessage())
+ ->pushContent(
+ (new Content())
+ ->setMimeType($mimeType)
+ ->setSchema('#/components/schemas/' . $this->config->getExceptionSchema())
+ )
+ );
+ }
+ }
+
+ /**
+ * Assigns Cake Models as Swagger Schema if possible
+ * @return void
+ */
+ private function assignSchema() : void
+ {
+ if (!$this->schema) {
+ return;
+ }
+
+ if ($this->operation->getResponseByCode(200)) {
+ return;
+ }
+
+ if (!in_array(strtolower($this->route->getAction()),['index','add','view','edit'])) {
+ return;
+ }
+
+ if (in_array(strtolower($this->route->getAction()),['index'])) {
+ $response = (new Response())->setCode(200);
+
+ foreach ($this->config->getResponseContentTypes() as $mimeType) {
+ $response->pushContent(
+ (new Content())
+ ->setSchema($this->schema)
+ ->setMimeType($mimeType)
+ );
+ }
+ $this->operation->pushResponse($response);
+ return;
+ }
+
+ if (in_array(strtolower($this->route->getAction()),['add','view','edit'])) {
+ $response = (new Response())->setCode(200);
+
+ foreach ($this->config->getResponseContentTypes() as $mimeType) {
+ $response->pushContent(
+ (new Content())
+ ->setSchema($this->schema)
+ ->setMimeType($mimeType)
+ );
+ }
+ $this->operation->pushResponse($response);
+ return;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Lib/Operation/OperationSecurity.php b/src/Lib/Operation/OperationSecurity.php
new file mode 100644
index 00000000..4392fd9b
--- /dev/null
+++ b/src/Lib/Operation/OperationSecurity.php
@@ -0,0 +1,36 @@
+pushSecurity(
+ (new PathSecurity())
+ ->setName($annotation->name)
+ ->setScopes($annotation->scopes)
+ );
+ }
+
+ return $operation;
+ }
+}
\ No newline at end of file
diff --git a/src/Lib/Path/PathFromRouteFactory.php b/src/Lib/Path/PathFromRouteFactory.php
new file mode 100644
index 00000000..39492aaf
--- /dev/null
+++ b/src/Lib/Path/PathFromRouteFactory.php
@@ -0,0 +1,108 @@
+config = $config;
+ $this->route = $route;
+ }
+
+ /**
+ * Creates a Path if possible, otherwise returns null
+ *
+ * @return Path|null
+ */
+ public function create() : ?Path
+ {
+ if (empty($this->route->getMethods())) {
+ return null;
+ }
+
+ $controller = $this->route->getController() . 'Controller';
+ $fullyQualifiedNamespace = NamespaceUtility::getControllerFullQualifiedNameSpace($controller, $this->config);
+
+ if (!$this->isVisible($fullyQualifiedNamespace)) {
+ return null;
+ }
+
+ return (new Path())->setResource($this->getResourceName());
+ }
+
+ /**
+ * @param string $fullyQualifiedNamespace
+ * @return bool
+ */
+ private function isVisible(string $fullyQualifiedNamespace) : bool
+ {
+ $annotations = AnnotationUtility::getClassAnnotationsFromFqns($fullyQualifiedNamespace);
+
+ $results = array_filter($annotations, function ($annotation) {
+ return $annotation instanceof SwagPath;
+ });
+
+ if (empty($results)) {
+ return true;
+ }
+
+ $swagPath = reset($results);
+
+ return $swagPath->isVisible;
+ }
+
+ /**
+ * Returns a routes resource (e.g. /api/model/action)
+ *
+ * @return string
+ */
+ private function getResourceName() : string
+ {
+ $pieces = $this->getRoutablePieces();
+
+ if ($this->config->getPrefix() == '/') {
+ return implode('/', $pieces);
+ }
+
+ return substr(
+ implode('/', $pieces),
+ strlen($this->config->getPrefix())
+ );
+ }
+
+ /**
+ * Splits the route (URL) into pieces with forward-slash "/" as the separator after removing path variables
+ *
+ * @return string[]
+ */
+ private function getRoutablePieces() : array
+ {
+ return array_map(
+ function ($piece) {
+ if (substr($piece, 0, 1) == ':') {
+ return '{' . str_replace(':', '', $piece) . '}';
+ }
+ return $piece;
+ },
+ explode('/', $this->route->getTemplate())
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Lib/Path/PathFromYmlFactory.php b/src/Lib/Path/PathFromYmlFactory.php
new file mode 100644
index 00000000..17645749
--- /dev/null
+++ b/src/Lib/Path/PathFromYmlFactory.php
@@ -0,0 +1,52 @@
+setResource($resource);
+
+ /*
+ foreach ($vars as $httpMethod => $var) {
+ $operation = (new Operation())
+ ->setHttpMethod($httpMethod)
+ ->setSummary(isset($var['summary']) ? $var['summary'] : '')
+ ->setDescription(isset($var['description']) ? $var['description'] : '')
+ ->setTags(isset($var['tags']) ? $var['tags'] : [])
+ ->setOperationId(isset($var['operationId']) ? $var['operationId'] : '')
+ ->setDeprecated((bool)isset($var['deprecated']) ? $var['deprecated'] : false);
+
+ if (isset($vars['externalDocs'])) {
+ $path->setExternalDocs(
+ (new OperationExternalDoc())
+ ->setDescription($vars['externalDocs']['description'])
+ ->setUrl($vars['externalDocs']['url'])
+ );
+ }
+
+ if (isset($vars['security']) && is_array($vars['security'])) {
+ foreach ($vars['security'] as $key => $scopes) {
+ $path->pushSecurity((new PathSecurity())->setName($key)->setScopes($scopes));
+ }
+ }
+ }
+ }
+ */
+ }
+}
\ No newline at end of file
diff --git a/src/Lib/QueryParameter.php b/src/Lib/QueryParameter.php
deleted file mode 100644
index a70ed0cd..00000000
--- a/src/Lib/QueryParameter.php
+++ /dev/null
@@ -1,43 +0,0 @@
-getMethods() as $method) {
- $annotations = $this->reader->getMethodAnnotations($method);
- if (empty($annotations)) {
- continue;
- }
-
- foreach ($annotations as $annotation) {
- if ($annotation instanceof SwagAnnotation\SwagPaginator) {
- $return = array_merge(
- $return,
- (new SwagAnnotation\SwagPaginatorHandler())->getQueryParameters($annotation)
- );
- }
- if ($annotation instanceof SwagAnnotation\SwagQuery) {
- $return = array_merge(
- $return,
- [
- (new SwagAnnotation\SwagQueryHandler())->getQueryParameter($annotation)
- ]
- );
- }
- }
- }
-
- return $return;
- }
-}
\ No newline at end of file
diff --git a/src/Lib/RequestBodyBuilder.php b/src/Lib/RequestBodyBuilder.php
deleted file mode 100644
index fdee1e2c..00000000
--- a/src/Lib/RequestBodyBuilder.php
+++ /dev/null
@@ -1,116 +0,0 @@
-path = $path;
- $this->route = $route;
- $this->swagger = $swagger;
- }
-
- /**
- * @return RequestBody|null
- */
- public function build() : ?RequestBody
- {
- $requestBody = $this->path->getRequestBody();
- if (!$requestBody) {
- $requestBody = new RequestBody();
- }
-
- if (!in_array($this->path->getType(), ['put','patch', 'post'])) {
- return null;
- }
-
- $requestBody->setRequired(true);
-
- return $this->requestBodyWithContent($requestBody);
- }
-
- /**
- * @param RequestBody $requestBody
- * @return RequestBody
- */
- private function requestBodyWithContent(RequestBody $requestBody) : RequestBody
- {
- $tags = $this->path->getTags();
- $tag = preg_replace('/\s+/', '', reset($tags));
- $tag = Inflector::singularize($tag);
-
- foreach ($this->swagger->getConfig()->getRequestAccepts() as $mimeType) {
-
- if ($requestBody->getContentByType($mimeType)) {
- continue;
- }
-
- if ($mimeType == 'application/x-www-form-urlencoded') {
- $schema = new Schema();
- $schema->setType('object');
- if (!$requestBody->isIgnoreCakeSchema()) {
- $schema = $this->withSchemaFromModel($schema, $tag);
- }
- $schema = $this->withSchemaFromAnnotations($schema);
- $requestBody->pushContent((new Content())->setMimeType($mimeType)->setSchema($schema));
- continue;
- }
-
- $schema = '#/components/schemas/' . $tag;
- $requestBody->pushContent((new Content())->setMimeType($mimeType)->setSchema($schema));
- }
-
- return $requestBody;
- }
-
- /**
- * @param Schema $schema
- * @param string $tag
- * @return Schema
- */
- private function withSchemaFromModel(Schema $schema, string $tag) : Schema
- {
- $className = Inflector::classify($tag);
- if (!$this->swagger->getSchemaByName($className)) {
- return $schema;
- }
-
- foreach ($this->swagger->getSchemaByName($className)->getProperties() as $property) {
-
- if ($property->isReadOnly()) {
- continue;
- }
-
- $schema->pushProperty($property);
- }
-
- return $schema;
- }
-
- /**
- * @param Schema $schema
- * @return Schema
- */
- private function withSchemaFromAnnotations(Schema $schema) : Schema
- {
- $schemaProperties = (new FormData($this->route, $this->swagger->getConfig()))->getSchemaProperties();
-
- if (empty($schemaProperties)) {
- return $schema;
- }
-
- foreach ($schemaProperties as $schemaProperty) {
- $schema->pushProperty($schemaProperty);
- }
-
- return $schema;
- }
-}
\ No newline at end of file
diff --git a/src/Lib/Security.php b/src/Lib/Security.php
deleted file mode 100644
index 2151e8d4..00000000
--- a/src/Lib/Security.php
+++ /dev/null
@@ -1,35 +0,0 @@
-getMethods() as $method) {
- $annotations = $this->reader->getMethodAnnotations($method);
- if (empty($annotations)) {
- continue;
- }
-
- foreach ($annotations as $annotation) {
- if ($annotation instanceof SwagAnnotation\SwagSecurity) {
- $return = array_merge(
- $return,
- [(new SwagAnnotation\SwagSecurityHandler())->getPathSecurity($annotation)]
- );
- }
- }
- }
-
- return $return;
- }
-}
\ No newline at end of file
diff --git a/src/Lib/Swagger.php b/src/Lib/Swagger.php
index 3c1d3568..73b67500 100644
--- a/src/Lib/Swagger.php
+++ b/src/Lib/Swagger.php
@@ -5,16 +5,19 @@
use Cake\Utility\Inflector;
use SwaggerBake\Lib\Exception\SwaggerBakeRunTimeException;
use SwaggerBake\Lib\Factory as Factory;
-use SwaggerBake\Lib\Model\ExpressiveRoute;
-use SwaggerBake\Lib\OpenApi\Content;
-use SwaggerBake\Lib\OpenApi\OperationExternalDoc;
+use SwaggerBake\Lib\Decorator\RouteDecorator;
+use SwaggerBake\Lib\OpenApi\Operation;
use SwaggerBake\Lib\OpenApi\Path;
-use SwaggerBake\Lib\OpenApi\PathSecurity;
-use SwaggerBake\Lib\OpenApi\Response;
use SwaggerBake\Lib\OpenApi\Schema;
use SwaggerBake\Lib\OpenApi\SchemaProperty;
+use SwaggerBake\Lib\Operation\OperationFromRouteFactory;
+use SwaggerBake\Lib\Path\PathFromRouteFactory;
use Symfony\Component\Yaml\Yaml;
+/**
+ * Class Swagger
+ * @package SwaggerBake\Lib
+ */
class Swagger
{
/** @var array */
@@ -34,7 +37,9 @@ public function __construct(CakeModel $cakeModel)
$this->cakeModel = $cakeModel;
$this->cakeRoute = $cakeModel->getCakeRoute();
$this->config = $cakeModel->getConfig();
- $this->buildFromDefaults();
+ $this->buildFromYml();
+ $this->buildSchemasFromModels();
+ $this->buildPathsFromRoutes();
}
/**
@@ -44,12 +49,9 @@ public function __construct(CakeModel $cakeModel)
*/
public function getArray(): array
{
- $this->buildSchemas();
- $this->buildPaths();
-
foreach ($this->array['paths'] as $method => $paths) {
foreach ($paths as $pathId => $path) {
- if (!is_array($path)) {
+ if ($path instanceof Path) {
$this->array['paths'][$method][$pathId] = $path->toArray();
}
}
@@ -94,7 +96,7 @@ public function toString()
*
* @param string $output
*/
- public function writeFile(string $output) : void
+ public function writeFile(string $output): void
{
if (!is_writable($output)) {
throw new SwaggerBakeRunTimeException("Output file is not writable, given $output");
@@ -141,11 +143,8 @@ public function getSchemaByName(string $name): ?Schema
*/
public function pushPath(Path $path): Swagger
{
- $route = $path->getPath();
- $methodType = $path->getType();
- if (!$this->hasPathByRouteAndMethodType($route, $methodType)) {
- $this->array['paths'][$route][$methodType] = $path;
- }
+ $resource = $path->getResource();
+ $this->array['paths'][$resource] = $path;
return $this;
}
@@ -154,25 +153,15 @@ public function pushPath(Path $path): Swagger
*
* @return Configuration
*/
- public function getConfig() : Configuration
+ public function getConfig(): Configuration
{
return $this->config;
}
- /**
- * @param string $route
- * @param string $methodType
- * @return Path|null|mixed
- */
- private function hasPathByRouteAndMethodType(string $route, string $methodType): bool
- {
- return isset($this->array['paths'][$route][$methodType]);
- }
-
/**
* Builds schemas from cake models
*/
- private function buildSchemas(): void
+ private function buildSchemasFromModels(): void
{
$schemaFactory = new Factory\SchemaFactory($this->config);
$models = $this->cakeModel->getModels();
@@ -192,158 +181,115 @@ private function buildSchemas(): void
/**
* Builds paths from cake routes
*/
- private function buildPaths(): void
+ private function buildPathsFromRoutes(): void
{
$routes = $this->cakeRoute->getRoutes();
+ $operationFactory = new OperationFromRouteFactory($this->config);
+
+ $ignorePaths = array_keys($this->array['paths']);
+
foreach ($routes as $route) {
- $path = (new Factory\PathFactory($route, $this->config))->create();
- if (is_null($path)) {
+ $resource = $this->convertCakePathToOpenApiResource($route->getTemplate());
+ if ($this->hasPathByResource($resource)) {
+ $path = $this->array['paths'][$resource];
+ } else {
+ $path = (new PathFromRouteFactory($route, $this->config))->create();
+ }
+
+ if (!$path instanceof Path) {
continue;
}
- if ($this->hasPathByRouteAndMethodType($path->getPath(), $path->getType())) {
+ if (in_array($path->getResource(), $ignorePaths)) {
continue;
}
- $path = $this->pathWithResponses($path);
- $path = $this->pathWithSecurity($path, $route);
- $path = $this->pathWithParameters($path, $route);
- $path = $this->pathWithRequestBody($path, $route);
+ foreach ($route->getMethods() as $httpMethod) {
- $this->pushPath($path);
+ if (strtolower($httpMethod) == 'put') {
+ continue;
+ }
+
+ $this->addArrayOfObjectsSchema($route);
+
+ $schema = $this->getSchemaFromRoute($route);
+
+ $operation = $operationFactory->create($route, $httpMethod, $schema);
+
+ if (!$operation instanceof Operation) {
+ continue;
+ }
+
+ $path->pushOperation($operation);
+ }
+
+ if (!empty($path->getOperations())) {
+ $this->pushPath($path);
+ }
}
}
/**
- * Sets security on a path
- *
- * @param Path $path
- * @param ExpressiveRoute $route
- * @return Path
+ * @param RouteDecorator $route
+ * @return Schema|null
*/
- private function pathWithSecurity(Path $path, ExpressiveRoute $route) : Path
+ private function getSchemaFromRoute(RouteDecorator $route) : ?Schema
{
- $path->setSecurity((new Security($route, $this->config))->getPathSecurity());
- return $path;
- }
+ $controller = $route->getController();
+ $name = preg_replace('/\s+/', '', $controller);
- /**
- * Sets header parameters on a path
- *
- * @param Path $path
- * @param ExpressiveRoute $route
- * @return Path
- */
- private function pathWithParameters(Path $path, ExpressiveRoute $route) : Path
- {
- $headers = (new HeaderParameter($route, $this->config))->getHeaderParameters();
- foreach ($headers as $parameter) {
- $path->pushParameter($parameter);
+ if (in_array(strtolower($route->getAction()),['index']) && $this->getSchemaByName($name)) {
+ return $this->getSchemaByName($name);
}
- $queries = (new QueryParameter($route, $this->config))->getQueryParameters();
- foreach ($queries as $parameter) {
- $path->pushParameter($parameter);
+ if (in_array(strtolower($route->getAction()),['add','view','edit'])) {
+ return $this->getSchemaByName(Inflector::singularize($name));
}
- return $path;
+
+ return null;
}
/**
- * Sets responses on a path
+ * Adds array of objects to #/components/schemas
*
- * @param Path $path
- * @return Path
+ * @param RouteDecorator $route
*/
- private function pathWithResponses(Path $path) : Path
+ private function addArrayOfObjectsSchema(RouteDecorator $route) : void
{
- foreach ($path->getTags() as $tag) {
- $className = Inflector::classify($tag);
-
- if ($path->hasSuccessResponseCode() || !$this->getSchemaByName($className)) {
- continue;
- }
-
- if ($path->getType() == 'get' && strstr($path->getOperationId(),':index')) {
- $tags = $path->getTags();
- $tag = preg_replace('/\s+/', '', reset($tags));
- $schema = (new Schema())
- ->setName($tag)
- ->setType('array')
- ->setItems(['$ref' => '#/components/schemas/' . $className])
- ;
- $this->pushSchema($schema);
-
- $response = (new Response())->setCode(200);
- $response = $this->responseWithContent($response, '#/components/schemas/' . $tag);
- $path->pushResponse($response);
- continue;
- }
-
- $response = (new Response())->setCode(200);
- $response = $this->responseWithContent($response, '#/components/schemas/' . $className);
- $path->pushResponse($response);
- }
-
- if (!$path->hasSuccessResponseCode()) {
- $path->pushResponse((new Response())->setCode(200));
+ if (!in_array('GET', $route->getMethods())) {
+ return;
}
- $exceptionSchema = $this->getSchemaByName($this->getConfig()->getExceptionSchema());
- if (!$exceptionSchema) {
- return $path;
+ if ($route->getAction() !== 'index') {
+ return;
}
- foreach ($path->getResponses() as $response) {
- if ($response->getCode() < 400) {
- continue;
- }
- $path->pushResponse(
- $this->responseWithContent($response, '#/components/schemas/' . $exceptionSchema->getName())
- );
+ if ($this->getSchemaByName($route->getController())) {
+ return;
}
- return $path;
- }
-
- /**
- * @param Response $response
- * @param string $schema
- * @return Response
- */
- private function responseWithContent(Response $response, string $schema) : Response
- {
- foreach ($this->config->getResponseContentTypes() as $mimeType) {
- $response->pushContent((new Content())->setMimeType($mimeType)->setSchema($schema));
+ if (!$this->getSchemaByName(Inflector::singularize($route->getController()))) {
+ return;
}
- return $response;
- }
- /**
- * Sets a request body on a path
- *
- * @param Path $path
- * @param ExpressiveRoute $route
- * @return Path
- */
- private function pathWithRequestBody(Path $path, ExpressiveRoute $route) : Path
- {
- $requestBody = (new RequestBodyBuilder($path, $this, $route))->build();
- if ($requestBody) {
- $path->setRequestBody($requestBody);
- }
- return $path;
+ $this->pushSchema(
+ (new Schema())
+ ->setName($route->getController())
+ ->setType('array')
+ ->setItems(['$ref' => '#/components/schemas/' . Inflector::singularize($route->getController())])
+ );
}
/**
* Constructs the primary array used in this class from pre-defined swagger.yml
*/
- private function buildFromDefaults() : void
+ private function buildFromYml() : void
{
$array = Yaml::parseFile($this->config->getYml());
- $array = $this->buildFromDefaultPaths($array);
- $array = $this->buildFromDefaultSchemas($array);
+ $array = $this->buildPathsFromYml($array);
+ $array = $this->buildSchemaFromYml($array);
$this->array = $array;
}
@@ -355,42 +301,13 @@ private function buildFromDefaults() : void
* @param $array
* @return array
*/
- private function buildFromDefaultPaths($array) : array
+ private function buildPathsFromYml($array) : array
{
if (!isset($array['paths'])) {
$array['paths'] = [];
}
return $array;
-
- /*
- foreach ($array['paths'] as $path => $operations) {
-
- foreach ($operations as $httpMethod => $vars) {
- $path = (new Path())
- ->setType($httpMethod)
- ->setSummary(isset($var['summary']) ? $var['summary'] : '')
- ->setDescription(isset($var['description']) ? $var['description'] : '')
- ->setTags(isset($var['tags']) ? $var['tags'] : [])
- ->setOperationId(isset($var['operationId']) ? $var['operationId'] : '')
- ->setDeprecated((bool) isset($var['deprecated']) ? $var['deprecated'] : false)
-
- if (isset($vars['externalDocs'])) {
- $path->setExternalDocs(
- (new OperationExternalDoc())
- ->setDescription($vars['externalDocs']['description'])
- ->setUrl($vars['externalDocs']['url'])
- );
- }
-
- if (isset($vars['security']) && is_array($vars['security'])) {
- foreach ($vars['security'] as $key => $scopes) {
- $path->pushSecurity((new PathSecurity())->setName($key)->setScopes($scopes));
- }
- }
- }
- }
- */
}
/**
@@ -399,7 +316,7 @@ private function buildFromDefaultPaths($array) : array
* @param $array
* @return array
*/
- private function buildFromDefaultSchemas($array) : array
+ private function buildSchemaFromYml($array) : array
{
if (!isset($array['components']['schemas'])) {
$array['components']['schemas'] = [];
@@ -431,6 +348,67 @@ private function buildFromDefaultSchemas($array) : array
return $array;
}
+ /**
+ * Converts Cake path parameters to OpenApi Spec
+ *
+ * @example /actor/:id to /actor/{id}
+ * @param string $resource
+ * @return string
+ */
+ private function convertCakePathToOpenApiResource(string $resource) : string
+ {
+ $pieces = array_map(
+ function ($piece) {
+ if (substr($piece, 0, 1) == ':') {
+ return '{' . str_replace(':', '', $piece) . '}';
+ }
+ return $piece;
+ },
+ explode('/', $resource)
+ );
+
+ if ($this->config->getPrefix() == '/') {
+ return implode('/', $pieces);
+ }
+
+ return substr(
+ implode('/', $pieces),
+ strlen($this->config->getPrefix())
+ );
+ }
+
+ /**
+ * @return Operation[]
+ */
+ public function getOperationsWithNoHttp20x() : array
+ {
+ $operations = [];
+
+ foreach ($this->array['paths'] as $path) {
+
+ if (!$path instanceof Path) {
+ continue;
+ }
+
+ $operations = array_merge(
+ $operations,
+ array_filter($path->getOperations(), function ($operation) {
+ return !$operation->hasSuccessResponseCode();
+ })
+ );
+ }
+ return $operations;
+ }
+
+ /**
+ * @param string $resource
+ * @return Path|null|mixed
+ */
+ private function hasPathByResource(string $resource): bool
+ {
+ return isset($this->array['paths'][$resource]);
+ }
+
public function __toString(): string
{
return $this->toString();
diff --git a/src/Lib/Utility/AnnotationUtility.php b/src/Lib/Utility/AnnotationUtility.php
index bf4c1245..03e2973b 100644
--- a/src/Lib/Utility/AnnotationUtility.php
+++ b/src/Lib/Utility/AnnotationUtility.php
@@ -6,17 +6,21 @@
use Exception;
use ReflectionClass;
+/**
+ * Class AnnotationUtility
+ * @package SwaggerBake\Lib\Utility
+ */
class AnnotationUtility
{
/**
- * Gets class annotations from full namespace argument
+ * Gets class annotations from namespace argument
*
* @uses AnnotationReader
* @uses ReflectionClass
* @param string $namespace
* @return array
*/
- public static function getClassAnnotations(string $namespace) : array
+ public static function getClassAnnotationsFromFqns(string $namespace) : array
{
try {
$instance = new $namespace;
@@ -36,6 +40,33 @@ public static function getClassAnnotations(string $namespace) : array
return $annotations;
}
+ /**
+ * Gets class annotations from instance
+ *
+ * @uses AnnotationReader
+ * @uses ReflectionClass
+ * @param object $instance
+ * @return array
+ */
+ public static function getClassAnnotationsFromInstance(object $instance) : array
+ {
+ try {
+ $reflectionClass = new ReflectionClass(get_class($instance));
+ } catch (Exception $e) {
+ return [];
+ }
+
+ $reader = new AnnotationReader();
+
+ $annotations = $reader->getClassAnnotations($reflectionClass);
+
+ if (!is_array($annotations)) {
+ return [];
+ }
+
+ return $annotations;
+ }
+
/**
* Returns an array of Lib/Annotation objects that can be applied to methods
*
diff --git a/src/Lib/Utility/DataTypeConversion.php b/src/Lib/Utility/DataTypeConversion.php
index e5df5398..2c75d1ec 100644
--- a/src/Lib/Utility/DataTypeConversion.php
+++ b/src/Lib/Utility/DataTypeConversion.php
@@ -1,9 +1,11 @@
getNamespaces();
+
+ if (!isset($namespaces['controllers']) || !is_array($namespaces['controllers'])) {
+ throw new SwaggerBakeRunTimeException(
+ 'Invalid configuration, missing SwaggerBake.namespaces.controllers'
+ );
+ }
+
+ foreach ($namespaces['controllers'] as $namespace) {
+ $entity = $namespace . 'Controller\\' . $className;
+ if (class_exists($entity, true)) {
+ return $entity;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets a FQNS of an Entity
+ *
+ * @param string $className
+ * @param Configuration $config
+ * @return string|null
+ */
+ public static function getEntityFullyQualifiedNameSpace(string $className, Configuration $config) : ?string
+ {
+ $namespaces = $config->getNamespaces();
+
+ if (!isset($namespaces['entities']) || !is_array($namespaces['entities'])) {
+ throw new SwaggerBakeRunTimeException(
+ 'Invalid configuration, missing SwaggerBake.namespaces.entities'
+ );
+ }
+
+ foreach ($namespaces['entities'] as $namespace) {
+ $entity = $namespace . 'Model\Entity\\' . $className;
+ if (class_exists($entity, true)) {
+ return $entity;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets a FQNS of a Table
+ *
+ * @param string $className
+ * @param Configuration $config
+ * @return string|null
+ */
+ public static function getTableFullyQualifiedNameSpace(string $className, Configuration $config) : ?string
+ {
+ $namespaces = $config->getNamespaces();
+
+ if (!isset($namespaces['tables']) || !is_array($namespaces['tables'])) {
+ throw new SwaggerBakeRunTimeException(
+ 'Invalid configuration, missing SwaggerBake.namespaces.tables'
+ );
+ }
+
+ foreach ($namespaces['tables'] as $namespace) {
+ $table = $namespace . 'Model\Table\\' . $className;
+ if (class_exists($table, true)) {
+ return $table;
+ }
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/Lib/Utility/OpenApiDataType.php b/src/Lib/Utility/OpenApiDataType.php
index d3fa7a85..356961cc 100644
--- a/src/Lib/Utility/OpenApiDataType.php
+++ b/src/Lib/Utility/OpenApiDataType.php
@@ -2,6 +2,10 @@
namespace SwaggerBake\Lib\Utility;
+/**
+ * Class OpenApiDataType
+ * @package SwaggerBake\Lib\Utility
+ */
class OpenApiDataType
{
public const TYPES = ['array', 'boolean', 'integer', 'number', 'object', 'string'];
diff --git a/src/Lib/Utility/ValidateConfiguration.php b/src/Lib/Utility/ValidateConfiguration.php
index 421c71d8..de4bf7d3 100644
--- a/src/Lib/Utility/ValidateConfiguration.php
+++ b/src/Lib/Utility/ValidateConfiguration.php
@@ -6,6 +6,10 @@
use SwaggerBake\Lib\Configuration;
use SwaggerBake\Lib\Exception\SwaggerBakeRunTimeException;
+/**
+ * Class ValidateConfiguration
+ * @package SwaggerBake\Lib\Utility\
+ */
class ValidateConfiguration
{
public static function validate() : void
diff --git a/src/Plugin.php b/src/Plugin.php
index f9a30bc2..49a7c2b4 100644
--- a/src/Plugin.php
+++ b/src/Plugin.php
@@ -8,9 +8,7 @@
use Cake\Core\Configure;
use Cake\Core\PluginApplicationInterface;
use SwaggerBake\Lib\AnnotationLoader;
-use SwaggerBake\Command\BakeCommand;
-use SwaggerBake\Command\ModelCommand;
-use SwaggerBake\Command\RouteCommand;
+use SwaggerBake\Command as Commands;
/**
* Class Plugin
@@ -21,15 +19,20 @@ class Plugin extends BasePlugin
public function bootstrap(PluginApplicationInterface $app) : void
{
parent::bootstrap($app);
+ if (!file_exists(CONFIG . 'swagger_bake.php')) {
+ triggerWarning('Missing configuration file for config/swagger_bake.php');
+ return;
+ }
Configure::load('swagger_bake', 'default');
AnnotationLoader::load();
}
public function console(CommandCollection $commands): CommandCollection
{
- $commands->add('swagger routes', RouteCommand::class);
- $commands->add('swagger bake', BakeCommand::class);
- $commands->add('swagger models', ModelCommand::class);
+ $commands->add('swagger routes', Commands\RouteCommand::class);
+ $commands->add('swagger bake', Commands\BakeCommand::class);
+ $commands->add('swagger models', Commands\ModelCommand::class);
+ $commands->add('swagger install', Commands\InstallCommand::class);
return $commands;
}
diff --git a/templates/element/default.php b/templates/element/default.php
new file mode 100644
index 00000000..2978da41
--- /dev/null
+++ b/templates/element/default.php
@@ -0,0 +1,15 @@
+
+
= $message ?>
\ No newline at end of file
diff --git a/templates/element/error.php b/templates/element/error.php
new file mode 100644
index 00000000..bf4eaf23
--- /dev/null
+++ b/templates/element/error.php
@@ -0,0 +1,11 @@
+
+= $message ?>
\ No newline at end of file
diff --git a/templates/layout/default.php b/templates/layout/default.php
index 12dc0a9f..aff25051 100644
--- a/templates/layout/default.php
+++ b/templates/layout/default.php
@@ -31,11 +31,25 @@
margin:0;
background: #fafafa;
}
+ /* Flash messages */
+ .message,.alert {
+ padding: 1rem;
+ border-width: 1px;
+ border-style: solid;
+ border-radius: 4px;
+ margin-bottom: 2rem;
+ }
+ .message.error, .alert.alert-danger {
+ background: #fcebea;
+ color: #cc1f1a;
+ border-color: #ef5753;
+ }
Flash->render();
echo $this->fetch('content');
?>
diff --git a/templates/layout/redoc.php b/templates/layout/redoc.php
index 008dca96..76ea2337 100644
--- a/templates/layout/redoc.php
+++ b/templates/layout/redoc.php
@@ -19,10 +19,29 @@
margin: 0;
padding: 0;
}
+ /* Flash messages */
+ .alert {
+ padding: 1rem;
+ background: #eff8ff;
+ color: #2779bd;
+ border-color: #6cb2eb;
+ border-width: 1px;
+ border-style: solid;
+ border-radius: 4px;
+ margin-bottom: 2rem;
+ }
+ .alert-error {
+ background: #fcebea;
+ color: #cc1f1a;
+ border-color: #ef5753;
+ }
Flash)) {
+ echo $this->Flash->render(); die;
+}
echo $this->fetch('content');
?>
diff --git a/tests/TestCase/Lib/Operation/ExceptionHandlerTest.php b/tests/TestCase/Lib/Operation/ExceptionHandlerTest.php
new file mode 100644
index 00000000..e5fc6d78
--- /dev/null
+++ b/tests/TestCase/Lib/Operation/ExceptionHandlerTest.php
@@ -0,0 +1,19 @@
+assertEquals(400, (new ExceptionHandler('BadRequestException'))->getCode());
+ $this->assertEquals(401, (new ExceptionHandler('UnauthorizedException'))->getCode());
+ $this->assertEquals(403, (new ExceptionHandler('ForbiddenException'))->getCode());
+ $this->assertEquals(404, (new ExceptionHandler('RecordNotFoundException'))->getCode());
+ $this->assertEquals(405, (new ExceptionHandler('MethodNotAllowedException'))->getCode());
+ $this->assertEquals(500, (new ExceptionHandler('Exception'))->getCode());
+ }
+}
\ No newline at end of file
diff --git a/tests/TestCase/Lib/Operation/OperationDocBlockTest.php b/tests/TestCase/Lib/Operation/OperationDocBlockTest.php
new file mode 100644
index 00000000..23c34085
--- /dev/null
+++ b/tests/TestCase/Lib/Operation/OperationDocBlockTest.php
@@ -0,0 +1,36 @@
+getOperationWithDocBlock(
+ new Operation(),
+ DocBlockFactory::createInstance()->create($block)
+ );
+
+ $doc = $operation->getExternalDocs();
+
+ $this->assertEquals('CakePHP', $doc->getDescription());
+ $this->assertEquals('http://www.cakephp.org', $doc->getUrl());
+ $this->assertTrue($operation->isDeprecated());
+ }
+}
\ No newline at end of file
diff --git a/tests/TestCase/Lib/Operation/OperationFromRouteFactoryTest.php b/tests/TestCase/Lib/Operation/OperationFromRouteFactoryTest.php
new file mode 100644
index 00000000..72c01bcf
--- /dev/null
+++ b/tests/TestCase/Lib/Operation/OperationFromRouteFactoryTest.php
@@ -0,0 +1,64 @@
+setExtensions(['json']);
+ $builder->resources('Employees', [
+ 'only' => 'index'
+ ]);
+ });
+ $this->router = $router;
+
+ $this->config = [
+ 'prefix' => '/api',
+ 'yml' => '/config/swagger-bare-bones.yml',
+ 'json' => '/webroot/swagger.json',
+ 'webPath' => '/swagger.json',
+ 'hotReload' => false,
+ 'exceptionSchema' => 'Exception',
+ 'requestAccepts' => ['application/x-www-form-urlencoded'],
+ 'responseContentTypes' => ['application/json'],
+ 'namespaces' => [
+ 'controllers' => ['\SwaggerBakeTest\App\\'],
+ 'entities' => ['\SwaggerBakeTest\App\\'],
+ 'tables' => ['\SwaggerBakeTest\App\\'],
+ ]
+ ];
+ }
+
+ public function testCreate()
+ {
+ $config = new Configuration($this->config, SWAGGER_BAKE_TEST_APP);
+ $cakeRoute = new CakeRoute($this->router, $config);
+
+ $routes = $cakeRoute->getRoutes();
+ $route = reset($routes);
+
+ $operation = (new OperationFromRouteFactory($config))->create($route, 'GET', null);
+ $this->assertInstanceOf(Operation::class, $operation);
+ $this->assertEquals('GET', $operation->getHttpMethod());
+ $this->assertEquals('employees:index', $operation->getOperationId());
+ }
+}
\ No newline at end of file
diff --git a/tests/TestCase/Lib/Operation/OperationFromYmlFactoryTest.php b/tests/TestCase/Lib/Operation/OperationFromYmlFactoryTest.php
new file mode 100644
index 00000000..694de5fb
--- /dev/null
+++ b/tests/TestCase/Lib/Operation/OperationFromYmlFactoryTest.php
@@ -0,0 +1,24 @@
+create('GET', [
+ 'tags' => ['hello'],
+ 'operationId' => 'operation:id',
+ 'deprecated' => false
+ ]);
+ $this->assertInstanceOf(Operation::class, $operation);
+ }
+}
\ No newline at end of file
diff --git a/tests/TestCase/Lib/Operation/OperationHeaderTest.php b/tests/TestCase/Lib/Operation/OperationHeaderTest.php
new file mode 100644
index 00000000..2c7712d6
--- /dev/null
+++ b/tests/TestCase/Lib/Operation/OperationHeaderTest.php
@@ -0,0 +1,25 @@
+getOperationWithHeaders(
+ new Operation(),
+ [new SwagHeader(['name' => 'X-HEADER','type' => 'string', 'description' => '', 'required' => false])]
+ );
+
+ $parameters = $operation->getParameters();
+ $param = reset($parameters);
+ $this->assertEquals('X-HEADER', $param->getName());
+ $this->assertEquals('header', $param->getIn());
+ }
+}
\ No newline at end of file
diff --git a/tests/TestCase/Lib/Operation/OperationPathTest.php b/tests/TestCase/Lib/Operation/OperationPathTest.php
new file mode 100644
index 00000000..2c89aaee
--- /dev/null
+++ b/tests/TestCase/Lib/Operation/OperationPathTest.php
@@ -0,0 +1,35 @@
+ ['GET'],
+ 'plugin' => '',
+ 'controller' => 'Employees',
+ 'action' => 'view'
+ ])
+ );
+
+ $operation = (new OperationPath())
+ ->getOperationWithPathParameters(
+ new Operation(),
+ $routeDecorator
+ );
+
+ $parameters = $operation->getParameters();
+ $param = reset($parameters);
+ $this->assertEquals('id', $param->getName());
+ $this->assertEquals('path', $param->getIn());
+ }
+}
\ No newline at end of file
diff --git a/tests/TestCase/Lib/Operation/OperationQueryParameterTest.php b/tests/TestCase/Lib/Operation/OperationQueryParameterTest.php
new file mode 100644
index 00000000..8c8cc44c
--- /dev/null
+++ b/tests/TestCase/Lib/Operation/OperationQueryParameterTest.php
@@ -0,0 +1,41 @@
+getOperationWithQueryParameters(
+ (new Operation())->setHttpMethod('GET'),
+ [
+ new SwagPaginator(),
+ new SwagQuery(['name' => 'test', 'type' => 'string', 'description' => '', 'required' => false]),
+ new SwagDto(['class' => '\SwaggerBakeTest\App\Dto\EmployeeData'])
+ ]
+ );
+
+ $parameters = $operation->getParameters();
+ $this->assertCount(7, $parameters);
+
+ $param = reset($parameters);
+ $this->assertEquals('page', $param->getName());
+ $this->assertEquals('query', $param->getIn());
+
+ $param = $parameters[4];
+ $this->assertEquals('test', $param->getName());
+ $this->assertEquals('query', $param->getIn());
+
+ $param = end($parameters);
+ $this->assertEquals('firstName', $param->getName());
+ $this->assertEquals('query', $param->getIn());
+ }
+}
\ No newline at end of file
diff --git a/tests/TestCase/Lib/Operation/OperationRequestBodyTest.php b/tests/TestCase/Lib/Operation/OperationRequestBodyTest.php
new file mode 100644
index 00000000..4b6be7a2
--- /dev/null
+++ b/tests/TestCase/Lib/Operation/OperationRequestBodyTest.php
@@ -0,0 +1,163 @@
+setExtensions(['json']);
+ $builder->resources('Employees', [
+ 'only' => ['create']
+ ]);
+ });
+ $this->router = $router;
+
+ $this->config = [
+ 'prefix' => '/api',
+ 'yml' => '/config/swagger-bare-bones.yml',
+ 'json' => '/webroot/swagger.json',
+ 'webPath' => '/swagger.json',
+ 'hotReload' => false,
+ 'exceptionSchema' => 'Exception',
+ 'requestAccepts' => ['application/x-www-form-urlencoded'],
+ 'responseContentTypes' => ['application/json'],
+ 'namespaces' => [
+ 'controllers' => ['\SwaggerBakeTest\App\\'],
+ 'entities' => ['\SwaggerBakeTest\App\\'],
+ 'tables' => ['\SwaggerBakeTest\App\\'],
+ ]
+ ];
+ }
+
+ public function testSwagFormGetOperationWithRequestBody()
+ {
+ $config = new Configuration($this->config, SWAGGER_BAKE_TEST_APP);
+ $cakeRoute = new CakeRoute($this->router, $config);
+
+ $routes = $cakeRoute->getRoutes();
+ $route = $routes['employees:add'];
+
+ $operationRequestBody = new OperationRequestBody(
+ $config,
+ (new Operation())->setHttpMethod('POST'),
+ DocBlockFactory::createInstance()->create('/** @throws Exception */'),
+ [
+ new SwagForm(['name' => 'test', 'type' => 'string', 'description' => '', 'required' => false])
+ ],
+ $route,
+ null
+ );
+
+ $operation = $operationRequestBody->getOperationWithRequestBody();
+
+ $requestBody = $operation->getRequestBody();
+ $content = $requestBody->getContentByType('application/x-www-form-urlencoded');
+
+ $schema = $content->getSchema();
+ $this->assertEquals('object', $schema->getType());
+
+ $properties = $schema->getProperties();
+ $this->assertArrayHasKey('test', $properties);;
+ }
+
+ public function testSwagDtoGetOperationWithRequestBody()
+ {
+ $config = new Configuration($this->config, SWAGGER_BAKE_TEST_APP);
+ $cakeRoute = new CakeRoute($this->router, $config);
+
+ $routes = $cakeRoute->getRoutes();
+ $route = $routes['employees:add'];
+
+ $operationRequestBody = new OperationRequestBody(
+ $config,
+ (new Operation())->setHttpMethod('POST'),
+ DocBlockFactory::createInstance()->create('/** @throws Exception */'),
+ [
+ new SwagDto(['class' => '\SwaggerBakeTest\App\Dto\EmployeeData'])
+ ],
+ $route,
+ null
+ );
+
+ $operation = $operationRequestBody->getOperationWithRequestBody();
+
+ $requestBody = $operation->getRequestBody();
+ $content = $requestBody->getContentByType('application/x-www-form-urlencoded');
+
+ $schema = $content->getSchema();
+ $this->assertEquals('object', $schema->getType());
+
+ $properties = $schema->getProperties();
+ $this->assertArrayHasKey('lastName', $properties);
+ $this->assertArrayHasKey('firstName', $properties);
+ }
+
+ public function testSchemaGetOperationWithRequestBodyForm()
+ {
+ $config = new Configuration($this->config, SWAGGER_BAKE_TEST_APP);
+ $cakeRoute = new CakeRoute($this->router, $config);
+
+ $routes = $cakeRoute->getRoutes();
+ $route = $routes['employees:add'];
+
+ $schema = (new Schema())
+ ->setType('object')
+ ->setName('Employee')
+ ->setProperties([
+ (new SchemaProperty())->setName('id')->setType('integer')->setReadOnly(true),
+ (new SchemaProperty())->setName('firstName')->setType('string')->setRequired(true),
+ (new SchemaProperty())->setName('otherField')->setType('string')
+ ])
+ ;
+
+ $operationRequestBody = new OperationRequestBody(
+ $config,
+ (new Operation())->setHttpMethod('POST'),
+ DocBlockFactory::createInstance()->create('/** */'),
+ [],
+ $route,
+ $schema
+ );
+
+ $operation = $operationRequestBody->getOperationWithRequestBody();
+
+ $content = $operation
+ ->getRequestBody()
+ ->getContentByType('application/x-www-form-urlencoded')
+ ;
+
+ $schema = $content->getSchema();
+ $this->assertEquals('object', $schema->getType());
+
+ $properties = $schema->getProperties();
+ $this->assertArrayNotHasKey('id', $properties);
+ $this->assertArrayNotHasKey('modified', $properties);
+ $this->assertTrue($properties['firstName']->isRequired());
+ $this->assertEquals('firstName', $properties['firstName']->getName());
+ $this->assertEquals('otherField', $properties['otherField']->getName());
+ }
+}
\ No newline at end of file
diff --git a/tests/TestCase/Lib/Operation/OperationResponseTest.php b/tests/TestCase/Lib/Operation/OperationResponseTest.php
new file mode 100644
index 00000000..eb33e175
--- /dev/null
+++ b/tests/TestCase/Lib/Operation/OperationResponseTest.php
@@ -0,0 +1,135 @@
+setExtensions(['json']);
+ $builder->resources('Employees', [
+ 'only' => ['index','create','delete']
+ ]);
+ });
+ $this->router = $router;
+
+ $this->config = [
+ 'prefix' => '/api',
+ 'yml' => '/config/swagger-bare-bones.yml',
+ 'json' => '/webroot/swagger.json',
+ 'webPath' => '/swagger.json',
+ 'hotReload' => false,
+ 'exceptionSchema' => 'Exception',
+ 'requestAccepts' => ['application/x-www-form-urlencoded'],
+ 'responseContentTypes' => ['application/json'],
+ 'namespaces' => [
+ 'controllers' => ['\SwaggerBakeTest\App\\'],
+ 'entities' => ['\SwaggerBakeTest\App\\'],
+ 'tables' => ['\SwaggerBakeTest\App\\'],
+ ]
+ ];
+ }
+
+ public function testGetOperationWithAnnotatedResponse()
+ {
+ $config = new Configuration($this->config, SWAGGER_BAKE_TEST_APP);
+ $cakeRoute = new CakeRoute($this->router, $config);
+
+ $routes = $cakeRoute->getRoutes();
+ $route = $routes['employees:index'];
+
+ $operationResponse = new OperationResponse(
+ $config,
+ new Operation(),
+ DocBlockFactory::createInstance()->create('/** @throws Exception */'),
+ [
+ new SwagResponseSchema([
+ 'refEntity' => '',
+ 'httpCode' => 200,
+ 'description' => '',
+ 'mimeType' => '',
+ 'schemaType' => '',
+ 'schemaFormat' => ''
+ ]),
+ ],
+ $route,
+ null
+ );
+
+ $operation = $operationResponse->getOperationWithResponses();
+
+ $this->assertInstanceOf(Response::class, $operation->getResponseByCode(200));
+ $this->assertInstanceOf(Response::class, $operation->getResponseByCode(500));
+ }
+
+ public function testGetOperationWithSchemaResponse()
+ {
+ $config = new Configuration($this->config, SWAGGER_BAKE_TEST_APP);
+ $cakeRoute = new CakeRoute($this->router, $config);
+
+ $routes = $cakeRoute->getRoutes();
+ $route = $routes['employees:add'];
+
+ $schema = (new Schema())
+ ->setName('Employee')
+ ->setType('object')
+ ;
+
+ $operationResponse = new OperationResponse(
+ $config,
+ new Operation(),
+ DocBlockFactory::createInstance()->create('/** */'),
+ [],
+ $route,
+ $schema
+ );
+
+ $operation = $operationResponse->getOperationWithResponses();
+
+ $this->assertInstanceOf(Response::class, $operation->getResponseByCode(200));
+ }
+
+ public function testGetOperationWithNoResponse()
+ {
+ $config = new Configuration($this->config, SWAGGER_BAKE_TEST_APP);
+ $cakeRoute = new CakeRoute($this->router, $config);
+
+ $routes = $cakeRoute->getRoutes();
+ $route = $routes['employees:delete'];
+
+ $operationResponse = new OperationResponse(
+ $config,
+ new Operation(),
+ DocBlockFactory::createInstance()->create('/** */'),
+ [],
+ $route,
+ null
+ );
+
+ $operation = $operationResponse->getOperationWithResponses();
+
+ $this->assertEmpty($operation->getResponses());
+ }
+}
\ No newline at end of file
diff --git a/tests/TestCase/Lib/Operation/OperationSecurityTest.php b/tests/TestCase/Lib/Operation/OperationSecurityTest.php
new file mode 100644
index 00000000..84b12ecd
--- /dev/null
+++ b/tests/TestCase/Lib/Operation/OperationSecurityTest.php
@@ -0,0 +1,27 @@
+getOperationWithSecurity(
+ new Operation(),
+ [new SwagSecurity(['name' => 'BearerAuth' , 'scopes' => ['read','write']])]
+ );
+
+ $securities = $operation->getSecurity();
+ $security = reset($securities);
+ $this->assertEquals('BearerAuth', $security->getName());
+ $this->assertCount(2, $security->getScopes());
+ }
+}
\ No newline at end of file
diff --git a/tests/TestCase/Lib/Path/PathFromRouteFactoryTest.php b/tests/TestCase/Lib/Path/PathFromRouteFactoryTest.php
new file mode 100644
index 00000000..e8a9a48e
--- /dev/null
+++ b/tests/TestCase/Lib/Path/PathFromRouteFactoryTest.php
@@ -0,0 +1,60 @@
+setExtensions(['json']);
+ $builder->resources('Employees', [
+ 'only' => 'index'
+ ]);
+ });
+ $this->router = $router;
+
+ $this->config = [
+ 'prefix' => '/api',
+ 'yml' => '/config/swagger-bare-bones.yml',
+ 'json' => '/webroot/swagger.json',
+ 'webPath' => '/swagger.json',
+ 'hotReload' => false,
+ 'exceptionSchema' => 'Exception',
+ 'requestAccepts' => ['application/x-www-form-urlencoded'],
+ 'responseContentTypes' => ['application/json'],
+ 'namespaces' => [
+ 'controllers' => ['\SwaggerBakeTest\App\\'],
+ 'entities' => ['\SwaggerBakeTest\App\\'],
+ 'tables' => ['\SwaggerBakeTest\App\\'],
+ ]
+ ];
+ }
+
+ public function testCreatePath()
+ {
+ $config = new Configuration($this->config, SWAGGER_BAKE_TEST_APP);
+ $cakeRoute = new CakeRoute($this->router, $config);
+
+ $routes = $cakeRoute->getRoutes();
+ $route = reset($routes);
+
+ $path = (new PathFromRouteFactory($route, $config))->create();
+ $this->assertInstanceOf(Path::class, $path);
+ $this->assertEquals('/employees', $path->getResource());
+ }
+}
\ No newline at end of file
diff --git a/tests/TestCase/Lib/Path/PathFromYmlFactoryTest.php b/tests/TestCase/Lib/Path/PathFromYmlFactoryTest.php
new file mode 100644
index 00000000..26e1e05a
--- /dev/null
+++ b/tests/TestCase/Lib/Path/PathFromYmlFactoryTest.php
@@ -0,0 +1,24 @@
+create('/pets', [
+ 'summary' => 'pet summary',
+ 'description' => 'lorem ipsum description'
+ ]);
+ $this->assertInstanceOf(Path::class, $path);
+ $this->assertEquals('/pets', $path->getResource());
+ }
+}
\ No newline at end of file
diff --git a/tests/TestCase/Lib/SwaggerOperationTest.php b/tests/TestCase/Lib/SwaggerOperationTest.php
index 705c3d5f..585d1eff 100644
--- a/tests/TestCase/Lib/SwaggerOperationTest.php
+++ b/tests/TestCase/Lib/SwaggerOperationTest.php
@@ -74,6 +74,103 @@ public function setUp(): void
AnnotationLoader::load();
}
+ public function testCrudOperationsExist()
+ {
+ $configuration = new Configuration($this->config, SWAGGER_BAKE_TEST_APP);
+
+ $cakeRoute = new CakeRoute($this->router, $configuration);
+ $swagger = new Swagger(new CakeModel($cakeRoute, $configuration));
+
+ $arr = json_decode($swagger->toString(), true);
+
+ $this->assertArrayHasKey('get', $arr['paths']['/employees']);
+ $this->assertArrayHasKey('post', $arr['paths']['/employees']);
+ $this->assertArrayHasKey('get', $arr['paths']['/employees/{id}']);
+ $this->assertArrayHasKey('patch', $arr['paths']['/employees/{id}']);
+ $this->assertArrayHasKey('delete', $arr['paths']['/employees/{id}']);
+
+ }
+
+ public function testDefaultResponseSchemaOnIndexMethod()
+ {
+ $configuration = new Configuration($this->config, SWAGGER_BAKE_TEST_APP);
+
+ $cakeRoute = new CakeRoute($this->router, $configuration);
+ $swagger = new Swagger(new CakeModel($cakeRoute, $configuration));
+
+ $arr = json_decode($swagger->toString(), true);
+
+ $employee = $arr['paths']['/employees']['get'];
+ $schema = $employee['responses'][200]['content']['application/json']['schema'];
+
+ $this->assertEquals('array', $schema['type']);
+ $this->assertEquals('#/components/schemas/Employee', $schema['items']['$ref']);
+ }
+
+ public function testDefaultRequestSchemaOnAddMethod()
+ {
+ $configuration = new Configuration($this->config, SWAGGER_BAKE_TEST_APP);
+
+ $cakeRoute = new CakeRoute($this->router, $configuration);
+ $swagger = new Swagger(new CakeModel($cakeRoute, $configuration));
+
+ $arr = json_decode($swagger->toString(), true);
+
+ $employee = $arr['paths']['/employees']['post'];
+ $schema = $employee['requestBody']['content']['application/x-www-form-urlencoded']['schema'];
+
+ $this->assertEquals('object', $schema['type']);
+ $this->assertCount(4, $schema['properties']);
+ }
+
+ public function testDefaultResponseSchemaOnAddMethod()
+ {
+ $configuration = new Configuration($this->config, SWAGGER_BAKE_TEST_APP);
+
+ $cakeRoute = new CakeRoute($this->router, $configuration);
+ $swagger = new Swagger(new CakeModel($cakeRoute, $configuration));
+
+ $arr = json_decode($swagger->toString(), true);
+
+ $employee = $arr['paths']['/employees']['post'];
+ $schema = $employee['responses'][200]['content']['application/json']['schema'];
+
+ $this->assertEquals('object', $schema['type']);
+ $this->assertCount(6, $schema['properties']);
+ }
+
+ public function testDefaultRequestSchemaOnEditMethod()
+ {
+ $configuration = new Configuration($this->config, SWAGGER_BAKE_TEST_APP);
+
+ $cakeRoute = new CakeRoute($this->router, $configuration);
+ $swagger = new Swagger(new CakeModel($cakeRoute, $configuration));
+
+ $arr = json_decode($swagger->toString(), true);
+
+ $employee = $arr['paths']['/employees/{id}']['patch'];
+ $schema = $employee['requestBody']['content']['application/x-www-form-urlencoded']['schema'];
+
+ $this->assertEquals('object', $schema['type']);
+ $this->assertCount(4, $schema['properties']);
+ }
+
+ public function testDefaultResponseSchemaOnEditMethod()
+ {
+ $configuration = new Configuration($this->config, SWAGGER_BAKE_TEST_APP);
+
+ $cakeRoute = new CakeRoute($this->router, $configuration);
+ $swagger = new Swagger(new CakeModel($cakeRoute, $configuration));
+
+ $arr = json_decode($swagger->toString(), true);
+
+ $employee = $arr['paths']['/employees/{id}']['patch'];
+ $schema = $employee['responses'][200]['content']['application/json']['schema'];
+
+ $this->assertEquals('object', $schema['type']);
+ $this->assertCount(6, $schema['properties']);
+ }
+
public function testHiddenOperation()
{
$configuration = new Configuration($this->config, SWAGGER_BAKE_TEST_APP);
diff --git a/tests/TestCase/Lib/SwaggerSchemaTest.php b/tests/TestCase/Lib/SwaggerSchemaTest.php
index 20104b37..3462d9e9 100644
--- a/tests/TestCase/Lib/SwaggerSchemaTest.php
+++ b/tests/TestCase/Lib/SwaggerSchemaTest.php
@@ -26,6 +26,7 @@ public function setUp(): void
$router::scope('/api', function (RouteBuilder $builder) {
$builder->setExtensions(['json']);
$builder->resources('Employees');
+ $builder->resources('EmployeeSalaries');
});
$this->router = $router;
@@ -62,5 +63,22 @@ public function testEmployeeTableProperties()
$this->assertCount(4, $employee['required']);
$this->assertEquals('birth_date', $employee['required'][0]);
$this->assertArrayHasKey('birth_date', $employee['properties']);
+
+ $this->assertTrue($employee['properties']['id']['readOnly']);
+ $this->assertEquals('integer', $employee['properties']['id']['type']);
+ }
+
+ public function testYmlSchemaTakesPrecedence()
+ {
+ $cakeRoute = new CakeRoute($this->router, $this->config);
+
+ $swagger = new Swagger(new CakeModel($cakeRoute, $this->config));
+
+ $arr = json_decode($swagger->toString(), true);
+
+ $this->assertArrayHasKey('EmployeeSalaries', $arr['components']['schemas']);
+ $employee = $arr['components']['schemas']['EmployeeSalaries'];
+
+ $this->assertEquals('Test YML schema cannot be overwritten', $employee['description']);
}
}
\ No newline at end of file
diff --git a/tests/test_app/config/swagger-with-existing.yml b/tests/test_app/config/swagger-with-existing.yml
index 0b234a57..c0170d51 100644
--- a/tests/test_app/config/swagger-with-existing.yml
+++ b/tests/test_app/config/swagger-with-existing.yml
@@ -146,6 +146,11 @@ components:
type: array
items:
$ref: "#/components/schemas/Pet"
+ EmployeeSalaries:
+ description: Test YML schema cannot be overwritten
+ type: array
+ items:
+ $ref: "#/components/schemas/EmployeeSalary"
Error:
type: object
required: