diff --git a/README.md b/README.md index d7cd071..f8171d4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # php-openapi +TODO docs related to issue 242, 238 + Read and write [OpenAPI](https://www.openapis.org/) 3.0.x YAML and JSON files and make the content accessible in PHP objects. It also provides a CLI tool for validating and converting OpenAPI 3.0.x Description files. diff --git a/src/spec/SecurityRequirements.php b/src/spec/SecurityRequirements.php index cbda1ba..58a5d3b 100644 --- a/src/spec/SecurityRequirements.php +++ b/src/spec/SecurityRequirements.php @@ -24,12 +24,20 @@ public function __construct(array $data) parent::__construct($data); foreach ($data as $index => $value) { - if (is_numeric($index)) { // read - $this->_securityRequirements[array_keys($value)[0]] = new SecurityRequirement(array_values($value)[0]); - } else { // write - $this->_securityRequirements[$index] = $value; + if (is_numeric($index) && $value === []) { # empty Security Requirement Object (`{}`) = anonymous access + $this->_securityRequirements[$index] = []; + continue; + } + + if (is_string($index)) { + $this->_securityRequirements[] = [$index => $value instanceof SecurityRequirement ? $value : new SecurityRequirement($value)]; + } elseif (is_numeric($index)) { + foreach ($value as $innerIndex => $subValue) { + $this->_securityRequirements[$index][$innerIndex] = $subValue instanceof SecurityRequirement ? $subValue : new SecurityRequirement($subValue); + } } } + if ($data === []) { $this->_securityRequirements = []; } @@ -59,20 +67,50 @@ protected function performValidation() public function getSerializableData() { $data = []; - foreach ($this->_securityRequirements ?? [] as $name => $securityRequirement) { - /** @var SecurityRequirement $securityRequirement */ - $data[] = [$name => $securityRequirement->getSerializableData()]; + + foreach ($this->_securityRequirements ?? [] as $outerIndex => $content) { + if (is_string($outerIndex)) { + $data[] = [$outerIndex => $content->getSerializableData()]; + } elseif (is_numeric($outerIndex)) { + if ($content === []) { + $data[$outerIndex] = (object)$content; + continue; + } + $innerResult = []; + foreach ($content as $innerIndex => $innerContent) { + $result = is_object($innerContent) && method_exists($innerContent, 'getSerializableData') ? $innerContent->getSerializableData() : $innerContent; + $innerResult[$innerIndex] = $result; + } + $data[$outerIndex] = (object)$innerResult; + } } return $data; } public function getRequirement(string $name) { - return $this->_securityRequirements[$name] ?? null; + return static::searchKey($this->_securityRequirements, $name); } public function getRequirements() { return $this->_securityRequirements; } + + private static function searchKey(array $array, string $searchKey) + { + foreach ($array as $key => $value) { + if ($key === $searchKey) { + return $value; + } + if (is_array($value)) { + $mt = __METHOD__; + $result = $mt($value, $searchKey); + if ($result !== null) { + return $result; // key found in deeply nested/associative array + } + } + } + return null; // key not found + } } diff --git a/tests/data/issue/238/spec.yml b/tests/data/issue/238/spec.yml new file mode 100644 index 0000000..df9c183 --- /dev/null +++ b/tests/data/issue/238/spec.yml @@ -0,0 +1,28 @@ +openapi: 3.0.0 +info: + title: Secured API + version: 1.0.0 +paths: + /global-secured: + get: + responses: + '200': + description: OK + /path-secured: + get: + security: + - {} + responses: + '200': + description: OK +components: + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key + BearerAuth: + type: http + scheme: bearer +security: + - ApiKeyAuth: [] diff --git a/tests/data/issue/242/spec.json b/tests/data/issue/242/spec.json new file mode 100644 index 0000000..5321bb9 --- /dev/null +++ b/tests/data/issue/242/spec.json @@ -0,0 +1,41 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "My API", + "version": "1, 2" + }, + "paths": { + "/v1/users/profile": { + "get": { + "operationId": "V1GetUserProfile", + "summary": "Returns the user profile", + "responses": { + "200": { + "description": "dummy" + } + }, + "security": [ + { + "test_test": ["test:scope:foo"] + } + ] + } + } + }, + "components": { + "securitySchemes": { + "test_test": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "https://example.com/openid-connect/auth", + "tokenUrl": "https://example.com/openid-connect/token", + "scopes": { + "test:scope:foo": "test_scope" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/data/issue/242/spec2.json b/tests/data/issue/242/spec2.json new file mode 100644 index 0000000..331d865 --- /dev/null +++ b/tests/data/issue/242/spec2.json @@ -0,0 +1,45 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "API Documentation", + "description": "All API endpoints are presented here.", + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://127.0.0.1:8080/" + } + ], + "paths": { + "/endpoint": { + "get": { + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "apiKey": [], + "bearerAuth": [] + } + ] + } + } + }, + "components": { + "securitySchemes": { + "apiKey": { + "type": "apiKey", + "in": "header", + "name": "X-APi-Key" + }, + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "JWT Authorization header using the Bearer scheme." + } + } + } +} \ No newline at end of file diff --git a/tests/data/issue/242/spec2.yml b/tests/data/issue/242/spec2.yml new file mode 100644 index 0000000..eebe517 --- /dev/null +++ b/tests/data/issue/242/spec2.yml @@ -0,0 +1,31 @@ +# https://github.com/cebe/php-openapi/issues/242#issuecomment-2886431173 +openapi: 3.0.0 +info: + title: API Documentation + description: All API endpoints are presented here. + version: 1.0.0 +servers: + - url: http://127.0.0.1:8080/ + +paths: + + /endpoint: + get: + responses: + '200': + description: OK + security: + - apiKey: [] + bearerAuth: [] +components: + securitySchemes: + apiKey: + type: apiKey + in: header + name: X-APi-Key + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT Authorization header using the Bearer scheme. + diff --git a/tests/issues/238/Issue238Test.php b/tests/issues/238/Issue238Test.php new file mode 100644 index 0000000..530bd48 --- /dev/null +++ b/tests/issues/238/Issue238Test.php @@ -0,0 +1,137 @@ +assertInstanceOf(\cebe\openapi\SpecObjectInterface::class, $openapi); + $this->assertInstanceOf(\cebe\openapi\spec\SecurityRequirements::class, $openapi->paths->getPath('/path-secured')->getOperations()['get']->security); + $this->assertSame(json_decode(json_encode($openapi->paths->getPath('/path-secured')->getOperations()['get']->security->getSerializableData()), true), [[]]); + + $openapiJson = Reader::readFromJson(<<assertInstanceOf(\cebe\openapi\SpecObjectInterface::class, $openapiJson); + $this->assertInstanceOf(\cebe\openapi\spec\SecurityRequirements::class, $openapiJson->paths->getPath('/path-secured')->getOperations()['get']->security); + $this->assertSame(json_decode(json_encode($openapiJson->paths->getPath('/path-secured')->getOperations()['get']->security->getSerializableData()), true), [[]]); + } + + public function test238AddSupportForEmptySecurityRequirementObjectInSecurityRequirementWrite() + { + $openapi = $this->createOpenAPI([ + 'security' => new SecurityRequirements([ + [] + ]), + ]); + + $yaml = Writer::writeToYaml($openapi); + + $this->assertEquals(preg_replace('~\R~', "\n", <<createOpenAPI([ + 'security' => new SecurityRequirements([ + [] + ]), + ]); + + $json = Writer::writeToJson($openapiJson); + + $this->assertEquals(preg_replace('~\R~', "\n", << '3.0.0', + 'info' => [ + 'title' => 'Test API', + 'version' => '1.0.0', + ], + 'paths' => [], + ], $merge)); + } +} diff --git a/tests/issues/242/Issue242Test.php b/tests/issues/242/Issue242Test.php new file mode 100644 index 0000000..8c8b83e --- /dev/null +++ b/tests/issues/242/Issue242Test.php @@ -0,0 +1,145 @@ +assertInstanceOf(SpecObjectInterface::class, $openapi); + + $dirSep = DIRECTORY_SEPARATOR; + $cmd = 'php ' . dirname(__DIR__, 3) . "{$dirSep}bin{$dirSep}php-openapi validate " . $file . " 2>&1"; + exec($cmd, $op, $ec); + $this->assertSame($this->removeCliFormatting($op[0]), 'The supplied API Description validates against the OpenAPI v3.0 schema.'); + $this->assertSame(0, $ec); + } + + private function removeCliFormatting($string) + { + // Regex to remove ANSI escape codes + return preg_replace('/\e\[[0-9;]*m/', '', $string); + } + + public function test242Case2() # https://github.com/cebe/php-openapi/issues/242#issuecomment-2886431173 + { + // read in yml + $file = dirname(__DIR__, 2) . '/data/issue/242/spec2.yml'; + $openapi = Reader::readFromYamlFile($file); + $this->assertInstanceOf(SpecObjectInterface::class, $openapi); + $this->assertSame(json_decode(json_encode($openapi->paths['/endpoint']->get->security->getSerializableData()), true), [ + [ + 'apiKey' => [], + 'bearerAuth' => [] + ] + ]); + + # write back to yml + $json = Writer::writeToYaml($openapi); + $this->assertEquals(preg_replace('~\R~', "\n", <<assertInstanceOf(SpecObjectInterface::class, $openapi); + $this->assertSame(json_decode(json_encode($openapi->paths['/endpoint']->get->security->getSerializableData()), true), [ + [ + 'apiKey' => [], + 'bearerAuth' => [] + ] + ]); + + // write back in json + $json = Writer::writeToJson($openapi); + $this->assertEquals(preg_replace('~\R~', "\n", << $v) { + if (is_numeric($k) && $v === []) { # https://github.com/cebe/php-openapi/issues/238 + continue; + } + + if (is_array($v)) { + $this->{__FUNCTION__}($className, $v); + } else { + $this->assertInstanceOf($className, $v, "Asserting that item with key '$k' is instance of $className"); + } + } + } + public function specProvider() { // examples from https://github.com/OAI/OpenAPI-Specification/tree/master/examples/v3.0 @@ -222,7 +237,7 @@ public function testSpecs($openApiFile) // security $openapi->security !== null && $this->assertInstanceOf(\cebe\openapi\spec\SecurityRequirements::class, $openapi->security); - $openapi->security !== null && $this->assertAllInstanceOf(\cebe\openapi\spec\SecurityRequirement::class, $openapi->security->getRequirements()); + $openapi->security !== null && $this->assertFewInstanceOf(\cebe\openapi\spec\SecurityRequirement::class, $openapi->security->getRequirements()); // tags $this->assertAllInstanceOf(\cebe\openapi\spec\Tag::class, $openapi->tags);