diff --git a/README.md b/README.md index 91cf57a..7506419 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ A new file called `openapi.json` has been generated ! class Controller { #[ GET("/path/{id}", ["Tag1", "Tag2"], "Description of the method"), - Property(PropertyType::STRING, "prop1", description: "Property description", enum: ["val1", "val2"]), - Property(PropertyType::INT, "prop2", example: 1), - Property(PropertyType::BOOLEAN, "prop3"), - Property(PropertyType::REF, "prop4", ref: RefSchema::class) + Property(Type::STRING, "prop1", description: "Property description", enum: ["val1", "val2"]), + Property(Type::INT, "prop2", example: 1), + Property(Type::BOOLEAN, "prop3"), + Property(Type::REF, "prop4", ref: RefSchema::class) Response(ref: SchemaName::class, description: "Response description") ] public function get(#[Parameter("Parameter description")] int $id): JsonResponse { diff --git a/composer.json b/composer.json index 6b1d5e9..dc63880 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,8 @@ "require": { "php": ">=8.0", "symfony/finder": "^6.0", - "symfony/http-foundation": "^6.0" + "symfony/http-foundation": "^6.0", + "symfony/string": "^6.2" }, "autoload": { "psr-4": { diff --git a/opag b/opag index cda2146..caef9a5 100755 --- a/opag +++ b/opag @@ -35,8 +35,15 @@ foreach ($files as $autoload) { include_once $autoload->getPathName(); } -$generator = \OpenApiGenerator\Generator::create()->generate(); +try { + $generator = \OpenApiGenerator\Generator::create()->generate(); +} catch (\Exception $e) { + echo "[ERROR] ".$e->getMessage(); + die; +} $schema = json_encode($generator, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); file_put_contents($outputFile, $schema); + +echo "DONE !"; diff --git a/src/ApiDescriptionChecker.php b/src/ApiDescriptionChecker.php index ee916ff..d61e332 100644 --- a/src/ApiDescriptionChecker.php +++ b/src/ApiDescriptionChecker.php @@ -14,6 +14,8 @@ private function __construct(private array $definition) $this->checkOpenApiVersion(); $this->checkInfo(); $this->checkServers(); + $this->checkSecurityScheme(); + $this->checkSecurity(); } /** @@ -65,6 +67,71 @@ private function checkServers(): void } } + private function checkSecurityScheme(): void + { + if (!isset($this->definition['securitySchemes'])) { + return; + } + + foreach ($this->definition['securitySchemes'] as $securityScheme) { + switch ($securityScheme["type"]) { + case "apiKey": + if (empty($securityScheme["name"]) || !in_array( + $securityScheme["in"], + ['query', 'header', 'cookie'], + true + )) { + throw new \InvalidArgumentException("SecurityScheme: apiKey must have name and in"); + } + break; + case "http": + if (empty($securityScheme["scheme"])) { + throw new \InvalidArgumentException("SecurityScheme: http must have scheme"); + } + break; + case "mutualTLS": + break; + case "oauth2": + if (empty($securityScheme["flows"])) { + throw new \InvalidArgumentException("SecurityScheme: oauth2 must have flows"); + } + break; + case "openIdConnect": + if (empty($securityScheme["openIdConnectUrl"])) { + throw new \InvalidArgumentException("SecurityScheme: openIdConnect must have openIdConnectUrl"); + } + break; + default: + throw new \InvalidArgumentException( + 'Invalid security scheme type: should be one of "apiKey", "http", "oauth2", "openIdConnect"' + ); + } + } + } + + private function checkSecurity(): void + { + if (!isset($this->definition['security'])) { + return; + } + + foreach ($this->definition['security'] as $security) { + if ($security instanceof \stdClass) { + continue; + } + + $availableValues = array_keys($this->definition['components']['securitySchemes']); + $securityName = array_keys($security)[0]; + + if (!in_array($securityName, $availableValues, true)) { + throw new \InvalidArgumentException( + "Security: security scheme not found. Please choose one of the followings: " . + implode(', ', $availableValues) + ); + } + } + } + /** * Check the API description for any omitted mandatory fields or wrong formats. */ diff --git a/src/Attributes/Security.php b/src/Attributes/Security.php new file mode 100644 index 0000000..05031ce --- /dev/null +++ b/src/Attributes/Security.php @@ -0,0 +1,34 @@ +securitySchemeKeys)) { + return [new \stdClass()]; + } + + return array_map(fn (string $key) => [$key => []], $this->securitySchemeKeys); + } +} diff --git a/src/Attributes/SecurityScheme.php b/src/Attributes/SecurityScheme.php index 16bf7b0..493d3d9 100644 --- a/src/Attributes/SecurityScheme.php +++ b/src/Attributes/SecurityScheme.php @@ -6,19 +6,21 @@ use Attribute; use JsonSerializable; +use Symfony\Component\String\UnicodeString; #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] class SecurityScheme implements JsonSerializable { public function __construct( - private string $securityKey = '', - private string $type = '', - private string $name= '', - private string $in = '', - private string $bearerFormat = '', - private string $scheme = '', + private string $type, + private ?string $description = null, + private ?string $name = null, + private ?string $in = null, + private ?string $scheme = null, + private ?string $bearerFormat = null, + private array $flows = [], + private ?string $openIdConnectUrl = null, ) { - // } /** @@ -26,14 +28,20 @@ public function __construct( */ public function jsonSerialize(): array { + $slugger = new UnicodeString($this->name ?: $this->type); + $slugName = (string)$slugger->snake(); + return [ - $this->securityKey => [ + $slugName => array_filter([ 'type' => $this->type, + 'description' => $this->description, 'name' => $this->name, 'in' => $this->in, - 'bearerFormat' => $this->bearerFormat, 'scheme' => $this->scheme, - ], + 'bearerFormat' => $this->bearerFormat, + 'flows' => $this->flows, + 'openIdConnectUrl' => $this->openIdConnectUrl, + ]) ]; } } diff --git a/src/Generator.php b/src/Generator.php index d14a3ea..1d11f27 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -8,6 +8,7 @@ use OpenApiGenerator\Attributes\Controller; use OpenApiGenerator\Attributes\Info; use OpenApiGenerator\Attributes\Schema; +use OpenApiGenerator\Attributes\Security; use OpenApiGenerator\Attributes\SecurityScheme; use OpenApiGenerator\Attributes\Server; use ReflectionAttribute; @@ -16,7 +17,7 @@ class Generator { - public const OPENAPI_VERSION = "3.0.0"; + public const OPENAPI_VERSION = "3.1.0"; /** * API description @@ -68,6 +69,7 @@ public function generate(): array $this->loadSchema($reflectionClass); $this->loadServer($reflectionClass); $this->loadSecurityScheme($reflectionClass); + $this->loadSecurity($reflectionClass); } $this->description['paths'] = $this->generatorHttp->build(); @@ -158,18 +160,26 @@ private function loadServer(ReflectionClass $reflectionClass): void private function loadSecurityScheme(ReflectionClass $reflectionClass): void { if (count($reflectionClass->getAttributes(SecurityScheme::class))) { - $securitySchemas = $reflectionClass->getAttributes(SecurityScheme::class); + $securitySchemes = $reflectionClass->getAttributes(SecurityScheme::class); - foreach ($securitySchemas as $item) { + foreach ($securitySchemes as $item) { $data = $item->newInstance()->jsonSerialize(); $key = array_keys($data)[0]; $this->description['components']['securitySchemes'][$key] = $data[$key]; } + } + } + /** + * @param ReflectionClass $reflectionClass + * @return void + */ + private function loadSecurity(ReflectionClass $reflectionClass): void + { + if (count($reflectionClass->getAttributes(Security::class))) { + $securityAttributes = $reflectionClass->getAttributes(Security::class); + $security = reset($securityAttributes); - $this->description['security'] = array_map( - fn(string $canonicalName) => [$canonicalName => []], - array_keys($this->description['components']['securitySchemes']) - ); + $this->description['security'] = $security->newInstance()->jsonSerialize(); } } } diff --git a/tests/Examples/Controller/SimpleController.php b/tests/Examples/Controller/SimpleController.php index 1dc38be..af71e83 100644 --- a/tests/Examples/Controller/SimpleController.php +++ b/tests/Examples/Controller/SimpleController.php @@ -11,6 +11,7 @@ use OpenApiGenerator\Attributes\PathParameter; use OpenApiGenerator\Attributes\Property; use OpenApiGenerator\Attributes\Response; +use OpenApiGenerator\Attributes\Security; use OpenApiGenerator\Attributes\SecurityScheme; use OpenApiGenerator\Attributes\Server; use OpenApiGenerator\Type; @@ -19,14 +20,8 @@ Server('same server1', 'same url1'), Info("title", "1.0.0", "The description"), Server('same server2', 'same url2'), - SecurityScheme( - 'bearerAuth', - 'http', - 'bearerAuth', - 'header', - 'JWT', - 'bearer', - ), + Security(['http']), + SecurityScheme(type: 'http', scheme: 'Bearer', bearerFormat: 'JWT bearer token'), Controller, ] class SimpleController