├── .github └── workflows │ ├── code-style.yml │ ├── php.yml │ └── phpstan.yml ├── .gitignore ├── .php_cs.dist ├── LICENSE ├── Makefile ├── README.md ├── bin └── php-openapi ├── composer.json ├── doc ├── class-diagram.png └── class-diagram.uml ├── docker-compose.yml ├── package.json ├── phpunit.xml.dist ├── schemas ├── openapi-v3.0.json └── openapi-v3.0.yaml ├── src ├── DocumentContextInterface.php ├── RawSpecDataInterface.php ├── Reader.php ├── ReferenceContext.php ├── ReferenceContextCache.php ├── SpecBaseObject.php ├── SpecObjectInterface.php ├── Writer.php ├── exceptions │ ├── IOException.php │ ├── ReadonlyPropertyException.php │ ├── TypeErrorException.php │ ├── UnknownPropertyException.php │ └── UnresolvableReferenceException.php ├── json │ ├── InvalidJsonPointerSyntaxException.php │ ├── JsonPointer.php │ ├── JsonReference.php │ ├── MalformedJsonReferenceObjectException.php │ └── NonexistentJsonPointerReferenceException.php └── spec │ ├── Callback.php │ ├── Components.php │ ├── Contact.php │ ├── Discriminator.php │ ├── Encoding.php │ ├── Example.php │ ├── ExternalDocumentation.php │ ├── Header.php │ ├── Info.php │ ├── License.php │ ├── Link.php │ ├── MediaType.php │ ├── OAuthFlow.php │ ├── OAuthFlows.php │ ├── OpenApi.php │ ├── Operation.php │ ├── Parameter.php │ ├── PathItem.php │ ├── Paths.php │ ├── Reference.php │ ├── RequestBody.php │ ├── Response.php │ ├── Responses.php │ ├── Schema.php │ ├── SecurityRequirement.php │ ├── SecurityRequirements.php │ ├── SecurityScheme.php │ ├── Server.php │ ├── ServerVariable.php │ ├── Tag.php │ ├── Type.php │ └── Xml.php ├── tests ├── .gitignore ├── IssueTest.php ├── ReaderTest.php ├── ReferenceContextTest.php ├── WriterTest.php ├── bootstrap.php ├── data │ └── issue │ │ ├── 155 │ │ ├── compiled-symfony-6.yml │ │ └── compiled-symfony-7.yml │ │ └── 175 │ │ ├── 401.json │ │ └── spec.json ├── docker │ └── Dockerfile ├── json │ └── JsonPointerTest.php └── spec │ ├── CallbackTest.php │ ├── ComponentsTest.php │ ├── HeaderTest.php │ ├── InfoTest.php │ ├── LinkTest.php │ ├── MediaTypeTest.php │ ├── OpenApiTest.php │ ├── OperationTest.php │ ├── ParameterTest.php │ ├── PathTest.php │ ├── ReferenceTest.php │ ├── RequestBodyTest.php │ ├── ResponseTest.php │ ├── SchemaTest.php │ ├── SecuritySchemeTest.php │ ├── ServerTest.php │ ├── TagTest.php │ ├── XmlTest.php │ └── data │ ├── empty-maps.json │ ├── path-params │ ├── global.yaml │ ├── openapi.yaml │ └── user.yaml │ ├── paths │ ├── openapi.yaml │ └── path-items.yaml │ ├── recursion.json │ ├── recursion2.yaml │ ├── recursion3_index.yaml │ ├── recursion3_menu_tree.yaml │ ├── reference │ ├── Food.yaml │ ├── InlineRelativeResolve │ │ ├── Schemas │ │ │ ├── ChildComponent.json │ │ │ └── Components.json │ │ └── sub │ │ │ └── dir │ │ │ └── Pathfile.json │ ├── ReferencedCommonParamsInReferencedPath.yml │ ├── base.yaml │ ├── definitions.yaml │ ├── models │ │ ├── Cat.yaml │ │ └── Pet.yaml │ ├── openapi_models.yaml │ ├── parameters │ │ └── TestParameter.yml │ ├── paths │ │ ├── ReferencesCommonParams.yml │ │ ├── examples │ │ │ └── user-example.json │ │ └── pets.json │ ├── playlist.json │ ├── structure.yaml │ ├── structure │ │ ├── paths.yml │ │ └── paths │ │ │ ├── cat.yml │ │ │ └── pet.yml │ ├── subdir.yaml │ └── subdir │ │ ├── Dog.yaml │ │ ├── Parameter.PetId.json │ │ └── Pet.yaml │ └── traits-mixins.yaml └── yarn.lock /.github/workflows/code-style.yml: -------------------------------------------------------------------------------- 1 | name: Code Style 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | code-style: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Install PHP 16 | uses: shivammathur/setup-php@v2 17 | with: 18 | php-version: 7.1 19 | 20 | - name: Check code style 21 | run: make check-style 22 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | phpunit: 11 | name: Tests 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ubuntu-latest] 16 | php: 17 | #- "7.1" 18 | #- "7.2" 19 | #- "7.3" 20 | - "7.4" 21 | - "8.0" 22 | - "8.1" 23 | - "8.2" 24 | - "8.3" 25 | dependencies: 26 | - "lowest" 27 | - "highest" 28 | #symfony-yaml: ['^3.4', '^4', '^5', '^6', '^7'] 29 | symfony-yaml: ['^5', '^6', '^7'] 30 | include: 31 | - os: "windows-latest" 32 | php: "8.0" 33 | dependencies: "highest" 34 | symfony-yaml: '5.4.2' 35 | - os: "macos-latest" 36 | php: "8.0" 37 | dependencies: "highest" 38 | symfony-yaml: '^5' 39 | exclude: 40 | # symfony/yaml v5 does not run on PHP 7.1 41 | #- php: '7.1' 42 | # symfony-yaml: '^5' 43 | # symfony/yaml v6 does not run on PHP 7.* 44 | #- php: '7.1' 45 | # symfony-yaml: '^6' 46 | #- php: '7.2' 47 | # symfony-yaml: '^6' 48 | #- php: '7.3' 49 | # symfony-yaml: '^6' 50 | - php: '7.4' 51 | symfony-yaml: '^6' 52 | # symfony/yaml v7 does not run on PHP <8.2 53 | - php: '7.4' 54 | symfony-yaml: '^7' 55 | - php: '8.0' 56 | symfony-yaml: '^7' 57 | - php: '8.1' 58 | symfony-yaml: '^7' 59 | # symfony/yaml v3.4 is not compatible with PHP 8.0 but has no upper-bound, so it installs on it 60 | #- php: '8.0' 61 | # symfony-yaml: '^3.4' 62 | #- php: '8.1' 63 | # symfony-yaml: '^3.4' 64 | 65 | runs-on: ${{ matrix.os }} 66 | 67 | steps: 68 | - uses: actions/checkout@v2 69 | 70 | - name: Install PHP 71 | uses: shivammathur/setup-php@v2 72 | with: 73 | php-version: ${{ matrix.php }} 74 | coverage: pcov 75 | tools: composer:v2 76 | 77 | - name: Require specific symfony/yaml version 78 | run: "composer require symfony/yaml:'${{ matrix.symfony-yaml }}' --no-interaction --ansi --no-install" 79 | 80 | - name: Require newer phpunit/phpunit version 81 | run: "composer require phpunit/phpunit '^9.5' --dev --no-interaction --ansi --no-install" 82 | if: matrix.php == '8.1' || matrix.php == '8.2' || matrix.php == '8.3' 83 | 84 | - name: "Install dependencies with Composer" 85 | uses: "ramsey/composer-install@v2" 86 | with: 87 | dependency-versions: "${{ matrix.dependencies }}" 88 | 89 | - name: Validate test data 90 | run: make lint 91 | 92 | - name: PHPUnit tests 93 | run: make test 94 | 95 | - name: Code coverage 96 | run: make coverage 97 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: PHPStan 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | phpstan: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Install PHP 16 | uses: shivammathur/setup-php@v2 17 | with: 18 | php-version: "7.1" 19 | tools: composer:v2 20 | 21 | - name: "Install dependencies with Composer" 22 | uses: "ramsey/composer-install@v2" 23 | 24 | - name: PHPStan analysis 25 | run: make stan 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock 3 | /composer.phar 4 | 5 | /node_modules 6 | 7 | /.php_cs.cache 8 | /.phpunit.result.cache 9 | 10 | php-cs-fixer.phar 11 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | setRules([ 5 | '@PSR2' => true, 6 | 'array_syntax' => ['syntax' => 'short'], 7 | 'general_phpdoc_annotation_remove' => ['annotations' => ['author']], 8 | 'header_comment' => [ 9 | 'comment_type' => 'PHPDoc', 10 | 'header' => << and contributors 12 | @license https://github.com/cebe/php-openapi/blob/master/LICENSE 13 | COMMENT 14 | ] 15 | ]) 16 | ; 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Carsten Brandt and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTCASE= 2 | XDEBUG=0 3 | PHPARGS=-dmemory_limit=512M 4 | XPHPARGS= 5 | ifeq ($(XDEBUG),1) 6 | XPHPARGS=-dzend_extension=xdebug.so -dxdebug.remote_enable=1 -dxdebug.remote_autostart=1 7 | endif 8 | 9 | # Run make with IN_DOCKER=1 to run yarn and php commands in a docker container 10 | DOCKER_PHP= 11 | DOCKER_NODE= 12 | IN_DOCKER=0 13 | ifeq ($(IN_DOCKER),1) 14 | DOCKER_PHP=docker-compose run --rm php 15 | DOCKER_NODE=docker-compose run --rm -w /app node 16 | endif 17 | 18 | all: 19 | @echo "the following commands are available:" 20 | @echo "" 21 | @echo "make check-style # check code style" 22 | @echo "make fix-style # fix code style" 23 | @echo "make install # install dependencies" 24 | @echo "make test # run PHPUnit tests" 25 | @echo "make lint # check validity of test data" 26 | @echo "make stan # check code with PHPStan" 27 | @echo "" 28 | @echo "You may add the IN_DOCKER parameter to run a command inside of docker container and not directly." 29 | @echo "make IN_DOCKER=1 ..." 30 | 31 | 32 | check-style: php-cs-fixer.phar 33 | PHP_CS_FIXER_IGNORE_ENV=1 ./php-cs-fixer.phar fix src/ --diff --dry-run 34 | 35 | fix-style: php-cs-fixer.phar 36 | $(DOCKER_PHP) vendor/bin/indent --tabs composer.json 37 | $(DOCKER_PHP) vendor/bin/indent --spaces .php_cs.dist 38 | $(DOCKER_PHP) ./php-cs-fixer.phar fix src/ --diff 39 | 40 | cli: 41 | docker-compose run --rm php bash 42 | 43 | install: composer.json package.json 44 | $(DOCKER_PHP) composer install --prefer-dist --no-interaction --no-progress --ansi 45 | $(DOCKER_NODE) yarn install 46 | 47 | test: unit test-recursion.json test-recursion2.yaml test-recursion3_index.yaml test-empty-maps.json 48 | 49 | unit: 50 | $(DOCKER_PHP) php $(PHPARGS) $(XPHPARGS) vendor/bin/phpunit --verbose --colors=always $(TESTCASE) 51 | 52 | # test specific JSON files in tests/spec/data/ 53 | # e.g. test-recursion will run validation on tests/spec/data/recursion.json 54 | test-%: tests/spec/data/% 55 | $(DOCKER_PHP) php $(PHPARGS) $(XPHPARGS) bin/php-openapi validate $< 56 | 57 | lint: install 58 | $(DOCKER_PHP) php $(PHPARGS) $(XPHPARGS) bin/php-openapi validate tests/spec/data/reference/playlist.json 59 | $(DOCKER_PHP) php $(PHPARGS) $(XPHPARGS) bin/php-openapi validate tests/spec/data/recursion.json 60 | $(DOCKER_PHP) php $(PHPARGS) $(XPHPARGS) bin/php-openapi validate tests/spec/data/recursion2.yaml 61 | $(DOCKER_PHP) php $(PHPARGS) $(XPHPARGS) bin/php-openapi validate tests/spec/data/empty-maps.json 62 | $(DOCKER_NODE) yarn run speccy lint tests/spec/data/reference/playlist.json 63 | $(DOCKER_NODE) yarn run speccy lint tests/spec/data/recursion.json 64 | 65 | stan: 66 | $(DOCKER_PHP) php $(PHPARGS) vendor/bin/phpstan analyse -l 5 src 67 | 68 | # copy openapi3 json schema 69 | schemas/openapi-v3.0.json: vendor/oai/openapi-specification/schemas/v3.0/schema.json 70 | cp $< $@ 71 | 72 | schemas/openapi-v3.0.yaml: vendor/oai/openapi-specification/schemas/v3.0/schema.yaml 73 | cp $< $@ 74 | 75 | php-cs-fixer.phar: 76 | wget -q https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases/download/v2.16.7/php-cs-fixer.phar && chmod +x php-cs-fixer.phar 77 | 78 | # find spec classes that are not mentioned in tests with @covers yet 79 | coverage: .php-openapi-covA .php-openapi-covB 80 | diff $^ 81 | .INTERMEDIATE: .php-openapi-covA .php-openapi-covB 82 | .php-openapi-covA: 83 | grep -rhPo '@covers .+' tests |cut -c 28- |sort > $@ 84 | .php-openapi-covB: 85 | grep -rhPo '^class \w+' src/spec/ | awk '{print $$2}' |grep -v '^Type$$' | sort > $@ 86 | 87 | .PHONY: all check-style fix-style install test lint coverage 88 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cebe/php-openapi", 3 | "description": "Read and write OpenAPI yaml/json files and make the content accessable in PHP objects.", 4 | "keywords": ["openapi"], 5 | "homepage": "https://github.com/cebe/php-openapi#readme", 6 | "type": "library", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Carsten Brandt", 11 | "email": "mail@cebe.cc", 12 | "homepage": "https://cebe.cc/", 13 | "role": "Creator" 14 | } 15 | ], 16 | "support": { 17 | "issues": "https://github.com/cebe/php-openapi/issues", 18 | "source": "https://github.com/cebe/php-openapi" 19 | }, 20 | "require": { 21 | "php": ">=7.1.0", 22 | "ext-json": "*", 23 | "symfony/yaml": "^3.4 || ^4 || ^5 || ^6 || ^7.0", 24 | "justinrainbow/json-schema": "^5.2 || ^6.0" 25 | }, 26 | "require-dev": { 27 | "cebe/indent": "*", 28 | "phpunit/phpunit": "^6.5 || ^7.5 || ^8.5 || ^9.4", 29 | "oai/openapi-specification": "3.0.3", 30 | "mermade/openapi3-examples": "1.0.0", 31 | "apis-guru/openapi-directory": "1.0.0", 32 | "nexmo/api-specification": "1.0.0", 33 | "phpstan/phpstan": "^0.12.0 || ^1.9" 34 | }, 35 | "conflict": { 36 | "symfony/yaml": "3.4.0 - 3.4.4 || 4.0.0 - 4.4.17 || 5.0.0 - 5.1.9 || 5.2.0" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "cebe\\openapi\\": "src/" 41 | } 42 | }, 43 | "extra": { 44 | "branch-alias": { 45 | "dev-master": "1.6.x-dev" 46 | } 47 | }, 48 | "bin": [ 49 | "bin/php-openapi" 50 | ], 51 | "repositories": [ 52 | { 53 | "type": "package", 54 | "package": { 55 | "name": "oai/openapi-specification", 56 | "version": "3.0.3", 57 | "source": { 58 | "url": "https://github.com/OAI/OpenAPI-Specification", 59 | "type": "git", 60 | "reference": "3.0.3" 61 | } 62 | } 63 | }, 64 | { 65 | "type": "package", 66 | "package": { 67 | "name": "mermade/openapi3-examples", 68 | "version": "1.0.0", 69 | "source": { 70 | "url": "https://github.com/Mermade/openapi3-examples", 71 | "type": "git", 72 | "reference": "3e8740c4994310a5d6a35d9b19e405862326f149" 73 | } 74 | } 75 | }, 76 | { 77 | "type": "package", 78 | "package": { 79 | "name": "apis-guru/openapi-directory", 80 | "version": "1.0.0", 81 | "source": { 82 | "url": "https://github.com/APIs-guru/openapi-directory", 83 | "type": "git", 84 | "reference": "9d2e0b6696a230a182d740a8e97ba27fb41b13bd" 85 | } 86 | } 87 | }, 88 | { 89 | "type": "package", 90 | "package": { 91 | "name": "nexmo/api-specification", 92 | "version": "1.0.0", 93 | "source": { 94 | "url": "https://github.com/cebe/nexmo-api-specification", 95 | "type": "git", 96 | "reference": "590fadf21f528ed8e05f6ff47c2e49d81f50a181" 97 | } 98 | } 99 | } 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /doc/class-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cebe/php-openapi/893ab104be1f5dfe5a39766703f583584e43c6e1/doc/class-diagram.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | php: 4 | build: 5 | dockerfile: tests/docker/Dockerfile 6 | context: . 7 | volumes: 8 | - ./tests/tmp/.composer:/root/.composer:rw 9 | - .:/app 10 | environment: 11 | - TZ=UTC 12 | - TIMEZONE=UTC 13 | - IN_DOCKER=docker 14 | - PHP_XDEBUG_ENABLED=1 15 | - XDEBUG_CONFIG="remote_host=host.docker.internal" 16 | - PHP_IDE_CONFIG="serverName=Docker" 17 | tty: true 18 | node: 19 | image: node:12 20 | volumes: 21 | - .:/app 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "speccy": "^0.11.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests 14 | 15 | 16 | 17 | 18 | ./src 19 | 20 | 21 | 22 | 23 | ./src 24 | 25 | 26 | ./vendor 27 | ./tests 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/DocumentContextInterface.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi; 9 | 10 | use cebe\openapi\json\JsonPointer; 11 | 12 | /** 13 | * Interface implemented by OpenAPI objects that provide functionality for context in the document. 14 | * 15 | * Allows an object to reference the base OpenAPI document as well as its own position inside of 16 | * the document in form of a [JSON pointer](https://tools.ietf.org/html/rfc6901). 17 | */ 18 | interface DocumentContextInterface 19 | { 20 | /** 21 | * Provide context information to the object. 22 | * 23 | * Context information contains a reference to the base object where it is contained in 24 | * as well as a JSON pointer to its position. 25 | * @param SpecObjectInterface $baseDocument 26 | * @param JsonPointer $jsonPointer 27 | */ 28 | public function setDocumentContext(SpecObjectInterface $baseDocument, JsonPointer $jsonPointer); 29 | 30 | /** 31 | * @return SpecObjectInterface|null returns the base document where this object is located in. 32 | * Returns `null` if no context information was provided by [[setDocumentContext]]. 33 | */ 34 | public function getBaseDocument(): ?SpecObjectInterface; 35 | /** 36 | * @return JsonPointer|null returns a JSON pointer describing the position of this object in the base document. 37 | * Returns `null` if no context information was provided by [[setDocumentContext]]. 38 | */ 39 | public function getDocumentPosition(): ?JsonPointer; 40 | } 41 | -------------------------------------------------------------------------------- /src/RawSpecDataInterface.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi; 9 | 10 | /** 11 | * Make raw spec data available to the implementing classes 12 | */ 13 | interface RawSpecDataInterface 14 | { 15 | public function getRawSpecData(): array; 16 | } 17 | -------------------------------------------------------------------------------- /src/Reader.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi; 9 | 10 | use cebe\openapi\exceptions\IOException; 11 | use cebe\openapi\exceptions\TypeErrorException; 12 | use cebe\openapi\exceptions\UnresolvableReferenceException; 13 | use cebe\openapi\json\InvalidJsonPointerSyntaxException; 14 | use cebe\openapi\json\JsonPointer; 15 | use cebe\openapi\spec\OpenApi; 16 | use Symfony\Component\Yaml\Yaml; 17 | 18 | /** 19 | * Utility class to simplify reading JSON or YAML OpenAPI specs. 20 | * 21 | */ 22 | class Reader 23 | { 24 | /** 25 | * Populate OpenAPI spec object from JSON data. 26 | * @phpstan-template T of SpecObjectInterface 27 | * @phpstan-param class-string $baseType 28 | * @phpstan-return T 29 | * @param string $json the JSON string to decode. 30 | * @param string $baseType the base Type to instantiate. This must be an instance of [[SpecObjectInterface]]. 31 | * The default is [[OpenApi]] which is the base type of a OpenAPI specification file. 32 | * You may choose a different type if you instantiate objects from sub sections of a specification. 33 | * @return SpecObjectInterface|OpenApi the OpenApi object instance. 34 | * The type of the returned object depends on the `$baseType` argument. 35 | * @throws TypeErrorException in case invalid spec data is supplied. 36 | */ 37 | public static function readFromJson(string $json, string $baseType = OpenApi::class): SpecObjectInterface 38 | { 39 | return new $baseType(json_decode($json, true)); 40 | } 41 | 42 | /** 43 | * Populate OpenAPI spec object from YAML data. 44 | * @phpstan-template T of SpecObjectInterface 45 | * @phpstan-param class-string $baseType 46 | * @phpstan-return T 47 | * @param string $yaml the YAML string to decode. 48 | * @param string $baseType the base Type to instantiate. This must be an instance of [[SpecObjectInterface]]. 49 | * The default is [[OpenApi]] which is the base type of a OpenAPI specification file. 50 | * You may choose a different type if you instantiate objects from sub sections of a specification. 51 | * @return SpecObjectInterface|OpenApi the OpenApi object instance. 52 | * The type of the returned object depends on the `$baseType` argument. 53 | * @throws TypeErrorException in case invalid spec data is supplied. 54 | */ 55 | public static function readFromYaml(string $yaml, string $baseType = OpenApi::class): SpecObjectInterface 56 | { 57 | return new $baseType(Yaml::parse($yaml)); 58 | } 59 | 60 | /** 61 | * Populate OpenAPI spec object from a JSON file. 62 | * @phpstan-template T of SpecObjectInterface 63 | * @phpstan-param class-string $baseType 64 | * @phpstan-return T 65 | * @param string $fileName the file name of the file to be read. 66 | * If `$resolveReferences` is true (the default), this should be an absolute URL, a `file://` URI or 67 | * an absolute path to allow resolving relative path references. 68 | * @param string $baseType the base Type to instantiate. This must be an instance of [[SpecObjectInterface]]. 69 | * The default is [[OpenApi]] which is the base type of a OpenAPI specification file. 70 | * You may choose a different type if you instantiate objects from sub sections of a specification. 71 | * @param bool|string $resolveReferences whether to automatically resolve references in the specification. 72 | * If `true`, all [[Reference]] objects will be replaced with their referenced spec objects by calling 73 | * [[SpecObjectInterface::resolveReferences()]]. 74 | * Since version 1.5.0 this can be a string indicating the reference resolving mode: 75 | * - `inline` only resolve references to external files. 76 | * - `all` resolve all references except recursive references. 77 | * @return SpecObjectInterface|OpenApi the OpenApi object instance. 78 | * The type of the returned object depends on the `$baseType` argument. 79 | * @throws TypeErrorException in case invalid spec data is supplied. 80 | * @throws UnresolvableReferenceException in case references could not be resolved. 81 | * @throws IOException when the file is not readable. 82 | * @throws InvalidJsonPointerSyntaxException in case an invalid JSON pointer string is passed to the spec references. 83 | */ 84 | public static function readFromJsonFile(string $fileName, string $baseType = OpenApi::class, $resolveReferences = true): SpecObjectInterface 85 | { 86 | $fileContent = file_get_contents($fileName); 87 | if ($fileContent === false) { 88 | $e = new IOException("Failed to read file: '$fileName'"); 89 | $e->fileName = $fileName; 90 | throw $e; 91 | } 92 | $spec = static::readFromJson($fileContent, $baseType); 93 | $context = new ReferenceContext($spec, $fileName); 94 | $spec->setReferenceContext($context); 95 | if ($resolveReferences !== false) { 96 | if (is_string($resolveReferences)) { 97 | $context->mode = $resolveReferences; 98 | } 99 | if ($spec instanceof DocumentContextInterface) { 100 | $spec->setDocumentContext($spec, new JsonPointer('')); 101 | } 102 | $spec->resolveReferences(); 103 | } 104 | return $spec; 105 | } 106 | 107 | /** 108 | * Populate OpenAPI spec object from YAML file. 109 | * @phpstan-template T of SpecObjectInterface 110 | * @phpstan-param class-string $baseType 111 | * @phpstan-return T 112 | * @param string $fileName the file name of the file to be read. 113 | * If `$resolveReferences` is true (the default), this should be an absolute URL, a `file://` URI or 114 | * an absolute path to allow resolving relative path references. 115 | * @param string $baseType the base Type to instantiate. This must be an instance of [[SpecObjectInterface]]. 116 | * The default is [[OpenApi]] which is the base type of a OpenAPI specification file. 117 | * You may choose a different type if you instantiate objects from sub sections of a specification. 118 | * @param bool|string $resolveReferences whether to automatically resolve references in the specification. 119 | * If `true`, all [[Reference]] objects will be replaced with their referenced spec objects by calling 120 | * [[SpecObjectInterface::resolveReferences()]]. 121 | * Since version 1.5.0 this can be a string indicating the reference resolving mode: 122 | * - `inline` only resolve references to external files. 123 | * - `all` resolve all references except recursive references. 124 | * @return SpecObjectInterface|OpenApi the OpenApi object instance. 125 | * The type of the returned object depends on the `$baseType` argument. 126 | * @throws TypeErrorException in case invalid spec data is supplied. 127 | * @throws UnresolvableReferenceException in case references could not be resolved. 128 | * @throws IOException when the file is not readable. 129 | */ 130 | public static function readFromYamlFile(string $fileName, string $baseType = OpenApi::class, $resolveReferences = true): SpecObjectInterface 131 | { 132 | $fileContent = file_get_contents($fileName); 133 | if ($fileContent === false) { 134 | $e = new IOException("Failed to read file: '$fileName'"); 135 | $e->fileName = $fileName; 136 | throw $e; 137 | } 138 | $spec = static::readFromYaml($fileContent, $baseType); 139 | $context = new ReferenceContext($spec, $fileName); 140 | $spec->setReferenceContext($context); 141 | if ($resolveReferences !== false) { 142 | if (is_string($resolveReferences)) { 143 | $context->mode = $resolveReferences; 144 | } 145 | if ($spec instanceof DocumentContextInterface) { 146 | $spec->setDocumentContext($spec, new JsonPointer('')); 147 | } 148 | $spec->resolveReferences(); 149 | } 150 | return $spec; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/ReferenceContextCache.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi; 9 | 10 | /** 11 | * ReferenceContextCache represents a cache storage for caching content of referenced files. 12 | */ 13 | class ReferenceContextCache 14 | { 15 | private $_cache = []; 16 | 17 | 18 | public function set($ref, $type, $data) 19 | { 20 | $this->_cache[$ref][$type ?? ''] = $data; 21 | 22 | // store fallback value for resolving with unknown type 23 | if ($type !== null && !isset($this->_cache[$ref][''])) { 24 | $this->_cache[$ref][''] = $data; 25 | } 26 | } 27 | 28 | public function get($ref, $type) 29 | { 30 | return $this->_cache[$ref][$type ?? ''] ?? null; 31 | } 32 | 33 | public function has($ref, $type) 34 | { 35 | return isset($this->_cache[$ref]) && 36 | array_key_exists($type ?? '', $this->_cache[$ref]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/SpecObjectInterface.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi; 9 | 10 | /** 11 | * This interface is implemented by all classes that represent objects from the OpenAPI Spec. 12 | */ 13 | interface SpecObjectInterface 14 | { 15 | /** 16 | * Create an object from spec data. 17 | * @param array $data spec data read from YAML or JSON 18 | */ 19 | public function __construct(array $data); 20 | 21 | /** 22 | * @return mixed returns the serializable data of this object for converting it 23 | * to JSON or YAML. 24 | */ 25 | public function getSerializableData(); 26 | 27 | /** 28 | * Validate object data according to OpenAPI spec. 29 | * @return bool whether the loaded data is valid according to OpenAPI spec 30 | * @see getErrors() 31 | */ 32 | public function validate(): bool; 33 | 34 | /** 35 | * @return string[] list of validation errors according to OpenAPI spec. 36 | * @see validate() 37 | */ 38 | public function getErrors(): array; 39 | 40 | /** 41 | * Resolves all Reference Objects in this object and replaces them with their resolution. 42 | */ 43 | public function resolveReferences(ReferenceContext $context = null); 44 | 45 | /** 46 | * Set context for all Reference Objects in this object. 47 | */ 48 | public function setReferenceContext(ReferenceContext $context); 49 | } 50 | -------------------------------------------------------------------------------- /src/Writer.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi; 9 | 10 | use cebe\openapi\exceptions\IOException; 11 | use cebe\openapi\spec\OpenApi; 12 | use Symfony\Component\Yaml\Yaml; 13 | 14 | /** 15 | * Utility class to simplify writing JSON or YAML OpenAPI specs. 16 | * 17 | */ 18 | class Writer 19 | { 20 | /** 21 | * Convert OpenAPI spec object to JSON data. 22 | * @param SpecObjectInterface|OpenApi $object the OpenApi object instance. 23 | * @param int $flags json_encode() flags. Parameter available since version 1.7.0. 24 | * @return string JSON string. 25 | */ 26 | public static function writeToJson(SpecObjectInterface $object, int $flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE): string 27 | { 28 | return json_encode($object->getSerializableData(), $flags); 29 | } 30 | 31 | /** 32 | * Convert OpenAPI spec object to YAML data. 33 | * @param SpecObjectInterface|OpenApi $object the OpenApi object instance. 34 | * @return string YAML string. 35 | */ 36 | public static function writeToYaml(SpecObjectInterface $object): string 37 | { 38 | return Yaml::dump($object->getSerializableData(), 256, 2, Yaml::DUMP_OBJECT_AS_MAP | Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE); 39 | } 40 | 41 | /** 42 | * Write OpenAPI spec object to JSON file. 43 | * @param SpecObjectInterface|OpenApi $object the OpenApi object instance. 44 | * @param string $fileName file name to write to. 45 | * @throws IOException when writing the file fails. 46 | */ 47 | public static function writeToJsonFile(SpecObjectInterface $object, string $fileName): void 48 | { 49 | if (file_put_contents($fileName, static::writeToJson($object)) === false) { 50 | throw new IOException("Failed to write file: '$fileName'"); 51 | } 52 | } 53 | 54 | /** 55 | * Write OpenAPI spec object to YAML file. 56 | * @param SpecObjectInterface|OpenApi $object the OpenApi object instance. 57 | * @param string $fileName file name to write to. 58 | * @throws IOException when writing the file fails. 59 | */ 60 | public static function writeToYamlFile(SpecObjectInterface $object, string $fileName): void 61 | { 62 | if (file_put_contents($fileName, static::writeToYaml($object)) === false) { 63 | throw new IOException("Failed to write file: '$fileName'"); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/exceptions/IOException.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\exceptions; 9 | 10 | /** 11 | * This exception is thrown when reading or writing of a file fails. 12 | * @since 1.2.1 13 | */ 14 | class IOException extends \Exception 15 | { 16 | /** 17 | * @var string|null if available, the name of the affected file. 18 | */ 19 | public $fileName; 20 | } 21 | -------------------------------------------------------------------------------- /src/exceptions/ReadonlyPropertyException.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\exceptions; 9 | 10 | /** 11 | * @deprecated this class will be removed in version 2.0. 12 | */ 13 | class ReadonlyPropertyException extends \Exception 14 | { 15 | } 16 | -------------------------------------------------------------------------------- /src/exceptions/TypeErrorException.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\exceptions; 9 | 10 | /** 11 | * This exception is thrown if the input data from OpenAPI spec 12 | * provides data in another type that is expected. 13 | * 14 | */ 15 | class TypeErrorException extends \Exception 16 | { 17 | } 18 | -------------------------------------------------------------------------------- /src/exceptions/UnknownPropertyException.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\exceptions; 9 | 10 | /** 11 | * 12 | * 13 | */ 14 | class UnknownPropertyException extends \Exception 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/exceptions/UnresolvableReferenceException.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\exceptions; 9 | 10 | use cebe\openapi\json\JsonPointer; 11 | 12 | /** 13 | * This exception is thrown on attempt to resolve a reference which points to a non-existing target. 14 | */ 15 | class UnresolvableReferenceException extends \Exception 16 | { 17 | /** 18 | * @var JsonPointer|null may contain context information in form of a JSON pointer to the position 19 | * of the broken reference in the document. 20 | */ 21 | public $context; 22 | } 23 | -------------------------------------------------------------------------------- /src/json/InvalidJsonPointerSyntaxException.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\json; 9 | 10 | use Exception; 11 | 12 | /** 13 | * InvalidJsonPointerSyntaxException represents the error condition "Invalid pointer syntax" of the JSON pointer specification. 14 | * 15 | * @link https://tools.ietf.org/html/rfc6901 (7. Error Handling) 16 | */ 17 | class InvalidJsonPointerSyntaxException extends Exception 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/json/JsonPointer.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\json; 9 | 10 | /** 11 | * Represents a JSON Pointer (RFC 6901) 12 | * 13 | * A JSON Pointer only works in the context of a single JSON document, 14 | * if you need to reference values in external documents, use [[JsonReference]] instead. 15 | * 16 | * @link https://tools.ietf.org/html/rfc6901 17 | * @see JsonReference 18 | */ 19 | final class JsonPointer 20 | { 21 | /** 22 | * @var string 23 | */ 24 | private $_pointer; 25 | 26 | /** 27 | * JSON Pointer constructor. 28 | * @param string $pointer The JSON Pointer. 29 | * Must be either an empty string (for referencing the whole document), or a string starting with `/`. 30 | * @throws InvalidJsonPointerSyntaxException in case an invalid JSON pointer string is passed 31 | */ 32 | public function __construct(string $pointer) 33 | { 34 | if (!preg_match('~^(/[^/]*)*$~', $pointer)) { 35 | throw new InvalidJsonPointerSyntaxException("Invalid JSON Pointer syntax: $pointer"); 36 | } 37 | $this->_pointer = $pointer; 38 | } 39 | 40 | public function __toString() 41 | { 42 | return $this->_pointer; 43 | } 44 | 45 | /** 46 | * @return string returns the JSON Pointer. 47 | */ 48 | public function getPointer(): string 49 | { 50 | return $this->_pointer; 51 | } 52 | 53 | /** 54 | * @return array the JSON pointer path as array. 55 | */ 56 | public function getPath(): array 57 | { 58 | if ($this->_pointer === '') { 59 | return []; 60 | } 61 | $pointer = substr($this->_pointer, 1); 62 | return array_map([get_class($this), 'decode'], explode('/', $pointer)); 63 | } 64 | 65 | /** 66 | * Append a new part to the JSON path. 67 | * @param string $subpath the path element to append. 68 | * @return JsonPointer a new JSON pointer pointing to the subpath. 69 | */ 70 | public function append(string $subpath): JsonPointer 71 | { 72 | return new JsonPointer($this->_pointer . '/' . static::encode($subpath)); 73 | } 74 | 75 | /** 76 | * Returns a JSON pointer to the parent path element of this pointer. 77 | * @return JsonPointer|null a new JSON pointer pointing to the parent element 78 | * or null if this pointer already points to the document root. 79 | */ 80 | public function parent(): ?JsonPointer 81 | { 82 | $path = $this->getPath(); 83 | if (empty($path)) { 84 | return null; 85 | } 86 | array_pop($path); 87 | if (empty($path)) { 88 | return new JsonPointer(''); 89 | } 90 | return new JsonPointer('/' . implode('/', array_map([get_class($this), 'encode'], $path))); 91 | } 92 | 93 | /** 94 | * Evaluate the JSON Pointer on the provided document. 95 | * 96 | * Note that this does only resolve the JSON Pointer, it will not load external 97 | * documents by URI. Loading the Document from the URI is supposed to be done outside of this class. 98 | * 99 | * @param mixed $jsonDocument 100 | * @return mixed 101 | * @throws NonexistentJsonPointerReferenceException 102 | */ 103 | public function evaluate($jsonDocument) 104 | { 105 | $currentReference = $jsonDocument; 106 | $currentPath = ''; 107 | 108 | foreach ($this->getPath() as $part) { 109 | if (is_array($currentReference)) { 110 | // if (!preg_match('~^([1-9]*[0-9]|-)$~', $part)) { 111 | // throw new NonexistentJsonPointerReferenceException( 112 | // "Failed to evaluate pointer '$this->_pointer'. Invalid pointer path '$part' for Array at path '$currentPath'." 113 | // ); 114 | // } 115 | if ($part === '-' || !array_key_exists($part, $currentReference)) { 116 | throw new NonexistentJsonPointerReferenceException( 117 | "Failed to evaluate pointer '$this->_pointer'. Array has no member $part at path '$currentPath'." 118 | ); 119 | } 120 | $currentReference = $currentReference[$part]; 121 | } elseif ($currentReference instanceof \ArrayAccess) { 122 | if (!$currentReference->offsetExists($part)) { 123 | throw new NonexistentJsonPointerReferenceException( 124 | "Failed to evaluate pointer '$this->_pointer'. Array has no member $part at path '$currentPath'." 125 | ); 126 | } 127 | $currentReference = $currentReference[$part]; 128 | } elseif (is_object($currentReference)) { 129 | if (!isset($currentReference->$part) && !property_exists($currentReference, $part)) { 130 | throw new NonexistentJsonPointerReferenceException( 131 | "Failed to evaluate pointer '$this->_pointer'. Object has no member $part at path '$currentPath'." 132 | ); 133 | } 134 | $currentReference = $currentReference->$part; 135 | } else { 136 | throw new NonexistentJsonPointerReferenceException( 137 | "Failed to evaluate pointer '$this->_pointer'. Value at path '$currentPath' is neither an array nor an object." 138 | ); 139 | } 140 | 141 | $currentPath = "$currentPath/$part"; 142 | } 143 | 144 | return $currentReference; 145 | } 146 | 147 | /** 148 | * Encodes a string for use inside of a JSON pointer. 149 | */ 150 | public static function encode(string $string): string 151 | { 152 | return strtr($string, [ 153 | '~' => '~0', 154 | '/' => '~1', 155 | ]); 156 | } 157 | 158 | /** 159 | * Decodes a string used inside of a JSON pointer. 160 | */ 161 | public static function decode(string $string): string 162 | { 163 | return strtr($string, [ 164 | '~1' => '/', 165 | '~0' => '~', 166 | ]); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/json/JsonReference.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\json; 9 | 10 | use JsonSerializable; 11 | 12 | /** 13 | * Represents a JSON Reference (IETF draft-pbryan-zyp-json-ref-03) 14 | * 15 | * Includes the URI to another JSON document and the JSON Pointer as 16 | * the fragment section of the URI. 17 | * 18 | * @link https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03 19 | * @see JsonPointer 20 | */ 21 | final class JsonReference implements JsonSerializable 22 | { 23 | /** 24 | * @var string 25 | */ 26 | private $_uri = ''; 27 | /** 28 | * @var JsonPointer 29 | */ 30 | private $_pointer; 31 | 32 | /** 33 | * Create a JSON Reference instance from a JSON document. 34 | * @param string $json the JSON object, e.g. `{ "$ref": "http://example.com/example.json#/foo/bar" }`. 35 | * @return JsonReference 36 | * @throws MalformedJsonReferenceObjectException 37 | * @throws InvalidJsonPointerSyntaxException if an invalid JSON pointer string is passed as part of the fragment section. 38 | */ 39 | public static function createFromJson(string $json): JsonReference 40 | { 41 | $refObject = json_decode($json, true); 42 | if (!isset($refObject['$ref'])) { 43 | throw new MalformedJsonReferenceObjectException('JSON Reference Object must contain the "$ref" member.'); 44 | } 45 | return static::createFromReference($refObject['$ref']); 46 | } 47 | 48 | /** 49 | * Create a JSON Reference instance from an URI and a JSON Pointer. 50 | * If no JSON Pointer is given this will be interpreted as an empty string JSON pointer, which 51 | * references the whole document. 52 | * @param string $uri the URI to the document without a fragment part. 53 | * @param JsonPointer $jsonPointer 54 | * @return JsonReference 55 | */ 56 | public static function createFromUri(string $uri, ?JsonPointer $jsonPointer = null): JsonReference 57 | { 58 | $jsonReference = static::createFromReference($uri); 59 | $jsonReference->_pointer = $jsonPointer ?: new JsonPointer(''); 60 | return $jsonReference; 61 | } 62 | 63 | /** 64 | * Create a JSON Reference instance from a reference URI. 65 | * @param string $referenceURI the JSON Reference URI, e.g. `"http://example.com/example.json#/foo/bar"`. 66 | * @return JsonReference 67 | * @throws InvalidJsonPointerSyntaxException if an invalid JSON pointer string is passed as part of the fragment section. 68 | */ 69 | public static function createFromReference(string $referenceURI): JsonReference 70 | { 71 | $jsonReference = new JsonReference(); 72 | if (strpos($referenceURI, '#') !== false) { 73 | list($uri, $fragment) = explode('#', $referenceURI, 2); 74 | $jsonReference->_uri = $uri; 75 | $jsonReference->_pointer = new JsonPointer(rawurldecode($fragment)); 76 | } else { 77 | $jsonReference->_uri = $referenceURI; 78 | $jsonReference->_pointer = new JsonPointer(''); 79 | } 80 | return $jsonReference; 81 | } 82 | 83 | private function __construct() 84 | { 85 | } 86 | 87 | public function __clone() 88 | { 89 | $this->_pointer = clone $this->_pointer; 90 | } 91 | 92 | 93 | public function getJsonPointer(): JsonPointer 94 | { 95 | return $this->_pointer; 96 | } 97 | 98 | /** 99 | * @return string returns the URI of the referenced JSON document without the fragment (JSON Pointer) part. 100 | */ 101 | public function getDocumentUri(): string 102 | { 103 | return $this->_uri; 104 | } 105 | 106 | /** 107 | * @return string returns the JSON Pointer in URI format. 108 | */ 109 | public function getReference(): string 110 | { 111 | // https://tools.ietf.org/html/rfc6901#section-6 112 | // A JSON Pointer can be represented in a URI fragment identifier by 113 | // encoding it into octets using UTF-8 [RFC3629], while percent-encoding 114 | // those characters not allowed by the fragment rule in [RFC3986]. 115 | // https://tools.ietf.org/html/rfc3986#page-25 116 | // The characters slash ("/") and question mark ("?") are allowed to 117 | // represent data within the fragment identifier. 118 | // https://tools.ietf.org/html/rfc3986#section-2.4 119 | // the "%7E" can be replaced by "~" without changing its interpretation. 120 | return $this->_uri . '#' . strtr(rawurlencode($this->_pointer->getPointer()), ['%2F' => '/', '%3F' => '?', '%7E' => '~']); 121 | } 122 | 123 | /** 124 | * Specify data which should be serialized to JSON 125 | * @link https://php.net/manual/en/jsonserializable.jsonserialize.php 126 | * @return mixed data which can be serialized by json_encode, 127 | * which is a value of any type other than a resource. 128 | */ 129 | #[\ReturnTypeWillChange] 130 | public function jsonSerialize() //: mixed 131 | { 132 | return (object)['$ref' => $this->getReference()]; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/json/MalformedJsonReferenceObjectException.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\json; 9 | 10 | use Exception; 11 | 12 | /** 13 | * MalformedJsonReferenceObjectException is thrown if a JSON Reference Object does not contain the "$ref" member. 14 | * 15 | * @link https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03 (3. Syntax) 16 | */ 17 | class MalformedJsonReferenceObjectException extends Exception 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/json/NonexistentJsonPointerReferenceException.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\json; 9 | 10 | use Exception; 11 | 12 | /** 13 | * NonexistentJsonPointerReferenceException represents the error condition 14 | * "A pointer that references a nonexistent value" of the JSON pointer specification. 15 | * 16 | * @link https://tools.ietf.org/html/rfc6901 (7. Error Handling) 17 | */ 18 | class NonexistentJsonPointerReferenceException extends Exception 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /src/spec/Callback.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\DocumentContextInterface; 11 | use cebe\openapi\exceptions\TypeErrorException; 12 | use cebe\openapi\exceptions\UnresolvableReferenceException; 13 | use cebe\openapi\json\JsonPointer; 14 | use cebe\openapi\ReferenceContext; 15 | use cebe\openapi\SpecObjectInterface; 16 | 17 | /** 18 | * A map of possible out-of band callbacks related to the parent operation. 19 | * 20 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#callbackObject 21 | * 22 | */ 23 | class Callback implements SpecObjectInterface, DocumentContextInterface 24 | { 25 | /** 26 | * @var string|null 27 | */ 28 | private $_url; 29 | /** 30 | * @var PathItem 31 | */ 32 | private $_pathItem; 33 | /** 34 | * @var array 35 | */ 36 | private $_errors = []; 37 | /** 38 | * @var SpecObjectInterface|null 39 | */ 40 | private $_baseDocument; 41 | /** 42 | * @var JsonPointer|null 43 | */ 44 | private $_jsonPointer; 45 | 46 | 47 | /** 48 | * Create an object from spec data. 49 | * @param array $data spec data read from YAML or JSON 50 | * @throws TypeErrorException in case invalid data is supplied. 51 | */ 52 | public function __construct(array $data) 53 | { 54 | if (count($data) !== 1) { 55 | $this->_errors[] = 'Callback object must have exactly one URL.'; 56 | return; 57 | } 58 | $this->_pathItem = new PathItem(reset($data)); 59 | $this->_url = key($data); 60 | } 61 | 62 | /** 63 | * @return mixed returns the serializable data of this object for converting it 64 | * to JSON or YAML. 65 | */ 66 | public function getSerializableData() 67 | { 68 | return (object) [$this->_url => ($this->_pathItem === null) ? null : $this->_pathItem->getSerializableData()]; 69 | } 70 | 71 | /** 72 | * @return string 73 | */ 74 | public function getUrl() 75 | { 76 | return $this->_url; 77 | } 78 | 79 | /** 80 | * @param string $url 81 | */ 82 | public function setUrl(string $url): void 83 | { 84 | $this->_url = $url; 85 | } 86 | 87 | /** 88 | * @return PathItem 89 | */ 90 | public function getRequest(): ?PathItem 91 | { 92 | return $this->_pathItem; 93 | } 94 | 95 | /** 96 | * @param PathItem $request 97 | */ 98 | public function setRequest(?PathItem $request): void 99 | { 100 | $this->_pathItem = $request; 101 | } 102 | 103 | /** 104 | * Validate object data according to OpenAPI spec. 105 | * @return bool whether the loaded data is valid according to OpenAPI spec 106 | * @see getErrors() 107 | */ 108 | public function validate(): bool 109 | { 110 | $pathItemValid = $this->_pathItem === null || $this->_pathItem->validate(); 111 | return $pathItemValid && empty($this->_errors); 112 | } 113 | 114 | /** 115 | * @return string[] list of validation errors according to OpenAPI spec. 116 | * @see validate() 117 | */ 118 | public function getErrors(): array 119 | { 120 | if (($pos = $this->getDocumentPosition()) !== null) { 121 | $errors = array_map(function ($e) use ($pos) { 122 | return "[{$pos}] $e"; 123 | }, $this->_errors); 124 | } else { 125 | $errors = $this->_errors; 126 | } 127 | 128 | $pathItemErrors = $this->_pathItem === null ? [] : $this->_pathItem->getErrors(); 129 | return array_merge($errors, $pathItemErrors); 130 | } 131 | 132 | /** 133 | * Resolves all Reference Objects in this object and replaces them with their resolution. 134 | * @throws UnresolvableReferenceException 135 | */ 136 | public function resolveReferences(ReferenceContext $context = null) 137 | { 138 | if ($this->_pathItem !== null) { 139 | $this->_pathItem->resolveReferences($context); 140 | } 141 | } 142 | 143 | /** 144 | * Set context for all Reference Objects in this object. 145 | */ 146 | public function setReferenceContext(ReferenceContext $context) 147 | { 148 | if ($this->_pathItem !== null) { 149 | $this->_pathItem->setReferenceContext($context); 150 | } 151 | } 152 | 153 | /** 154 | * Provide context information to the object. 155 | * 156 | * Context information contains a reference to the base object where it is contained in 157 | * as well as a JSON pointer to its position. 158 | * @param SpecObjectInterface $baseDocument 159 | * @param JsonPointer $jsonPointer 160 | */ 161 | public function setDocumentContext(SpecObjectInterface $baseDocument, JsonPointer $jsonPointer) 162 | { 163 | $this->_baseDocument = $baseDocument; 164 | $this->_jsonPointer = $jsonPointer; 165 | 166 | if ($this->_pathItem instanceof DocumentContextInterface) { 167 | $this->_pathItem->setDocumentContext($baseDocument, $jsonPointer->append($this->_url)); 168 | } 169 | } 170 | 171 | /** 172 | * @return SpecObjectInterface|null returns the base document where this object is located in. 173 | * Returns `null` if no context information was provided by [[setDocumentContext]]. 174 | */ 175 | public function getBaseDocument(): ?SpecObjectInterface 176 | { 177 | return $this->_baseDocument; 178 | } 179 | 180 | /** 181 | * @return JsonPointer|null returns a JSON pointer describing the position of this object in the base document. 182 | * Returns `null` if no context information was provided by [[setDocumentContext]]. 183 | */ 184 | public function getDocumentPosition(): ?JsonPointer 185 | { 186 | return $this->_jsonPointer; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/spec/Components.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * Holds a set of reusable objects for different aspects of the OAS. 14 | * 15 | * All objects defined within the components object will have no effect on the API unless they are explicitly referenced 16 | * from properties outside the components object. 17 | * 18 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#componentsObject 19 | * 20 | * @property Schema[]|Reference[] $schemas 21 | * @property Response[]|Reference[] $responses 22 | * @property Parameter[]|Reference[] $parameters 23 | * @property Example[]|Reference[] $examples 24 | * @property RequestBody[]|Reference[] $requestBodies 25 | * @property Header[]|Reference[] $headers 26 | * @property SecurityScheme[]|Reference[] $securitySchemes 27 | * @property Link[]|Reference[] $links 28 | * @property Callback[]|Reference[] $callbacks 29 | * 30 | * 31 | */ 32 | class Components extends SpecBaseObject 33 | { 34 | /** 35 | * @return array array of attributes available in this object. 36 | */ 37 | protected function attributes(): array 38 | { 39 | return [ 40 | 'schemas' => [Type::STRING, Schema::class], 41 | 'responses' => [Type::STRING, Response::class], 42 | 'parameters' => [Type::STRING, Parameter::class], 43 | 'examples' => [Type::STRING, Example::class], 44 | 'requestBodies' => [Type::STRING, RequestBody::class], 45 | 'headers' => [Type::STRING, Header::class], 46 | 'securitySchemes' => [Type::STRING, SecurityScheme::class], 47 | 'links' => [Type::STRING, Link::class], 48 | 'callbacks' => [Type::STRING, Callback::class], 49 | ]; 50 | } 51 | 52 | /** 53 | * Perform validation on this object, check data against OpenAPI Specification rules. 54 | */ 55 | protected function performValidation() 56 | { 57 | // All the fixed fields declared above are objects that MUST use keys that match the regular expression: ^[a-zA-Z0-9\.\-_]+$. 58 | foreach (array_keys($this->attributes()) as $attribute) { 59 | if (is_array($this->$attribute)) { 60 | foreach ($this->$attribute as $k => $v) { 61 | if (!preg_match('~^[a-zA-Z0-9\.\-_]+$~', $k)) { 62 | $this->addError("Invalid key '$k' used in Components Object for attribute '$attribute', does not match ^[a-zA-Z0-9\.\-_]+\$."); 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/spec/Contact.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * Contact information for the exposed API. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#contactObject 16 | * 17 | * @property string $name 18 | * @property string $url 19 | * @property string $email 20 | * 21 | */ 22 | class Contact extends SpecBaseObject 23 | { 24 | /** 25 | * @return array array of attributes available in this object. 26 | */ 27 | protected function attributes(): array 28 | { 29 | return [ 30 | 'name' => Type::STRING, 31 | 'url' => Type::STRING, 32 | 'email' => Type::STRING, 33 | ]; 34 | } 35 | 36 | /** 37 | * Perform validation on this object, check data against OpenAPI Specification rules. 38 | */ 39 | protected function performValidation() 40 | { 41 | $this->validateEmail('email'); 42 | $this->validateUrl('url'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/spec/Discriminator.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * When request bodies or response payloads may be one of a number of different schemas, a discriminator object can be used to aid in serialization, deserialization, and validation. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#discriminatorObject 16 | * 17 | * @property string $propertyName 18 | * @property string[] $mapping 19 | * 20 | */ 21 | class Discriminator extends SpecBaseObject 22 | { 23 | /** 24 | * @return array array of attributes available in this object. 25 | */ 26 | protected function attributes(): array 27 | { 28 | return [ 29 | 'propertyName' => Type::STRING, 30 | 'mapping' => [Type::STRING, Type::STRING], 31 | ]; 32 | } 33 | 34 | /** 35 | * Perform validation on this object, check data against OpenAPI Specification rules. 36 | */ 37 | protected function performValidation() 38 | { 39 | $this->requireProperties(['propertyName']); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/spec/Encoding.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\exceptions\TypeErrorException; 11 | use cebe\openapi\SpecBaseObject; 12 | 13 | /** 14 | * A single encoding definition applied to a single schema property. 15 | * 16 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#encodingObject 17 | * 18 | * @property string $contentType 19 | * @property Header[]|Reference[] $headers 20 | * @property string $style 21 | * @property boolean $explode 22 | * @property boolean $allowReserved 23 | */ 24 | class Encoding extends SpecBaseObject 25 | { 26 | 27 | /** 28 | * @return array array of attributes available in this object. 29 | */ 30 | protected function attributes(): array 31 | { 32 | return [ 33 | 'contentType' => Type::STRING, 34 | 'headers' => [Type::STRING, Header::class], 35 | // TODO implement default values for style 36 | // https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#encodingObject 37 | 'style' => Type::STRING, 38 | 'explode' => Type::BOOLEAN, 39 | 'allowReserved' => Type::BOOLEAN, 40 | ]; 41 | } 42 | 43 | private $_attributeDefaults = []; 44 | 45 | /** 46 | * @return array array of attributes default values. 47 | */ 48 | protected function attributeDefaults(): array 49 | { 50 | return $this->_attributeDefaults; 51 | } 52 | 53 | /** 54 | * Create an object from spec data. 55 | * @param array $data spec data read from YAML or JSON 56 | * @throws TypeErrorException in case invalid data is supplied. 57 | */ 58 | public function __construct(array $data, ?Schema $schema = null) 59 | { 60 | if (isset($data['style'])) { 61 | // Spec: When style is form, the default value is true. 62 | $this->_attributeDefaults['explode'] = ($data['style'] === 'form'); 63 | } 64 | if ($schema !== null) { 65 | // Spec: Default value depends on the property type: 66 | // for string with format being binary – application/octet-stream; 67 | // for other primitive types – text/plain; 68 | // for object - application/json; 69 | // for array – the default is defined based on the inner type. 70 | switch ($schema->type === 'array' ? ($schema->items->type ?? 'array') : $schema->type) { 71 | case Type::STRING: 72 | if ($schema->format === 'binary') { 73 | $this->_attributeDefaults['contentType'] = 'application/octet-stream'; 74 | break; 75 | } 76 | // no break here 77 | case Type::BOOLEAN: 78 | case Type::INTEGER: 79 | case Type::NUMBER: 80 | $this->_attributeDefaults['contentType'] = 'text/plain'; 81 | break; 82 | case 'object': 83 | $this->_attributeDefaults['contentType'] = 'application/json'; 84 | break; 85 | } 86 | } 87 | parent::__construct($data); 88 | } 89 | 90 | /** 91 | * Perform validation on this object, check data against OpenAPI Specification rules. 92 | */ 93 | protected function performValidation() 94 | { 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/spec/Example.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * Example Object 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#exampleObject 16 | * 17 | * @property string $summary 18 | * @property string $description 19 | * @property mixed $value 20 | * @property string $externalValue 21 | */ 22 | class Example extends SpecBaseObject 23 | { 24 | /** 25 | * @return array array of attributes available in this object. 26 | */ 27 | protected function attributes(): array 28 | { 29 | return [ 30 | 'summary' => Type::STRING, 31 | 'description' => Type::STRING, 32 | 'value' => Type::ANY, 33 | 'externalValue' => Type::STRING, 34 | ]; 35 | } 36 | 37 | /** 38 | * Perform validation on this object, check data against OpenAPI Specification rules. 39 | */ 40 | protected function performValidation() 41 | { 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/spec/ExternalDocumentation.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * Allows referencing an external resource for extended documentation. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#externalDocumentationObject 16 | * 17 | * @property string $description 18 | * @property string $url 19 | * 20 | */ 21 | class ExternalDocumentation extends SpecBaseObject 22 | { 23 | /** 24 | * @return array array of attributes available in this object. 25 | */ 26 | protected function attributes(): array 27 | { 28 | return [ 29 | 'description' => Type::STRING, 30 | 'url' => Type::STRING, 31 | ]; 32 | } 33 | 34 | /** 35 | * Perform validation on this object, check data against OpenAPI Specification rules. 36 | */ 37 | protected function performValidation() 38 | { 39 | $this->requireProperties(['url']); 40 | $this->validateUrl('url'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/spec/Header.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * The Header Object follows the structure of the Parameter Object with the following changes: 14 | * 15 | * 1. name MUST NOT be specified, it is given in the corresponding headers map. 16 | * 2. in MUST NOT be specified, it is implicitly in header. 17 | * 3. All traits that are affected by the location MUST be applicable to a location of header (for example, style). 18 | * 19 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#headerObject 20 | * 21 | */ 22 | class Header extends Parameter 23 | { 24 | public function performValidation() 25 | { 26 | if (!empty($this->name)) { 27 | $this->addError("'name' must not be specified in Header Object."); 28 | } 29 | if (!empty($this->in)) { 30 | $this->addError("'in' must not be specified in Header Object."); 31 | } 32 | if (!empty($this->content) && !empty($this->schema)) { 33 | $this->addError("A Header Object MUST contain either a schema property, or a content property, but not both. "); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/spec/Info.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * The object provides metadata about the API. 14 | * 15 | * The metadata MAY be used by the clients if needed, and MAY be presented in editing or documentation generation tools for convenience. 16 | * 17 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#infoObject 18 | * 19 | * @property string $title 20 | * @property string $description 21 | * @property string $termsOfService 22 | * @property Contact|null $contact 23 | * @property License|null $license 24 | * @property string $version 25 | * 26 | */ 27 | class Info extends SpecBaseObject 28 | { 29 | /** 30 | * @return array array of attributes available in this object. 31 | */ 32 | protected function attributes(): array 33 | { 34 | return [ 35 | 'title' => Type::STRING, 36 | 'description' => Type::STRING, 37 | 'termsOfService' => Type::STRING, 38 | 'contact' => Contact::class, 39 | 'license' => License::class, 40 | 'version' => Type::STRING, 41 | ]; 42 | } 43 | 44 | /** 45 | * Perform validation on this object, check data against OpenAPI Specification rules. 46 | */ 47 | protected function performValidation() 48 | { 49 | $this->requireProperties(['title', 'version']); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/spec/License.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * License information for the exposed API. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#licenseObject 16 | * 17 | * @property string $name 18 | * @property string $url 19 | * 20 | */ 21 | class License extends SpecBaseObject 22 | { 23 | /** 24 | * @return array array of attributes available in this object. 25 | */ 26 | protected function attributes(): array 27 | { 28 | return [ 29 | 'name' => Type::STRING, 30 | 'url' => Type::STRING, 31 | ]; 32 | } 33 | 34 | /** 35 | * Perform validation on this object, check data against OpenAPI Specification rules. 36 | */ 37 | protected function performValidation() 38 | { 39 | $this->requireProperties(['name']); 40 | $this->validateUrl('url'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/spec/Link.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * The Link object represents a possible design-time link for a response. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#linkObject 16 | * 17 | * @property string $operationRef 18 | * @property string $operationId 19 | * @property array $parameters 20 | * @property mixed $requestBody 21 | * @property string $description 22 | * @property Server|null $server 23 | * 24 | */ 25 | class Link extends SpecBaseObject 26 | { 27 | /** 28 | * @return array array of attributes available in this object. 29 | */ 30 | protected function attributes(): array 31 | { 32 | return [ 33 | 'operationRef' => Type::STRING, 34 | 'operationId' => Type::STRING, 35 | 'parameters' => [Type::STRING, Type::ANY], // TODO: how to specify {expression}? 36 | 'requestBody' => Type::ANY, // TODO: how to specify {expression}? 37 | 'description' => Type::STRING, 38 | 'server' => Server::class, 39 | ]; 40 | } 41 | 42 | /** 43 | * Perform validation on this object, check data against OpenAPI Specification rules. 44 | */ 45 | protected function performValidation() 46 | { 47 | if (!empty($this->operationId) && !empty($this->operationRef)) { 48 | $this->addError('Link: operationId and operationRef are mutually exclusive.'); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/spec/MediaType.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\exceptions\TypeErrorException; 11 | use cebe\openapi\SpecBaseObject; 12 | 13 | /** 14 | * Each Media Type Object provides schema and examples for the media type identified by its key. 15 | * 16 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#mediaTypeObject 17 | * 18 | * @property Schema|Reference|null $schema 19 | * @property mixed $example 20 | * @property Example[]|Reference[] $examples 21 | * @property Encoding[] $encoding 22 | */ 23 | class MediaType extends SpecBaseObject 24 | { 25 | /** 26 | * @return array array of attributes available in this object. 27 | */ 28 | protected function attributes(): array 29 | { 30 | return [ 31 | 'schema' => Schema::class, 32 | 'example' => Type::ANY, 33 | 'examples' => [Type::STRING, Example::class], 34 | 'encoding' => [Type::STRING, Encoding::class], 35 | ]; 36 | } 37 | 38 | /** 39 | * Create an object from spec data. 40 | * @param array $data spec data read from YAML or JSON 41 | * @throws TypeErrorException in case invalid data is supplied. 42 | */ 43 | public function __construct(array $data) 44 | { 45 | // instantiate Encoding by passing the schema for extracting default values 46 | $encoding = $data['encoding'] ?? null; 47 | unset($data['encoding']); 48 | 49 | parent::__construct($data); 50 | 51 | if (!empty($encoding)) { 52 | foreach ($encoding as $property => $encodingData) { 53 | if ($encodingData instanceof Encoding) { 54 | $encoding[$property] = $encodingData; 55 | } elseif (is_array($encodingData)) { 56 | $schema = $this->schema->properties[$property] ?? null; 57 | // Don't pass the schema if it's still an unresolved reference. 58 | if ($schema instanceof Reference) { 59 | $encoding[$property] = new Encoding($encodingData); 60 | } else { 61 | $encoding[$property] = new Encoding($encodingData, $schema); 62 | } 63 | } else { 64 | $givenType = gettype($encodingData); 65 | if ($givenType === 'object') { 66 | $givenType = get_class($encodingData); 67 | } 68 | throw new TypeErrorException(sprintf('Encoding MUST be either array or Encoding object, "%s" given', $givenType)); 69 | } 70 | } 71 | $this->encoding = $encoding; 72 | } 73 | } 74 | 75 | /** 76 | * Perform validation on this object, check data against OpenAPI Specification rules. 77 | */ 78 | protected function performValidation() 79 | { 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/spec/OAuthFlow.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * Configuration details for a supported OAuth Flow. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#oauthFlowObject 16 | * 17 | * @property string $authorizationUrl 18 | * @property string $tokenUrl 19 | * @property string $refreshUrl 20 | * @property string[] $scopes 21 | */ 22 | class OAuthFlow extends SpecBaseObject 23 | { 24 | 25 | /** 26 | * @return array array of attributes available in this object. 27 | */ 28 | protected function attributes(): array 29 | { 30 | return [ 31 | 'authorizationUrl' => Type::STRING, 32 | 'tokenUrl' => Type::STRING, 33 | 'refreshUrl' => Type::STRING, 34 | 'scopes' => [Type::STRING, Type::STRING], 35 | ]; 36 | } 37 | 38 | /** 39 | * Perform validation on this object, check data against OpenAPI Specification rules. 40 | * 41 | * Call `addError()` in case of validation errors. 42 | */ 43 | protected function performValidation() 44 | { 45 | $this->requireProperties(['scopes']); 46 | // TODO: Validation in context of the parent object 47 | // authorizationUrl is required if this object is in "implicit", "authorizationCode" 48 | // tokenUrl is required if this object is in "password", "clientCredentials", "authorizationCode" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/spec/OAuthFlows.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * Allows configuration of the supported OAuth Flows. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#oauthFlowsObject 16 | * 17 | * @property OAuthFlow|null $implicit 18 | * @property OAuthFlow|null $password 19 | * @property OAuthFlow|null $clientCredentials 20 | * @property OAuthFlow|null $authorizationCode 21 | */ 22 | class OAuthFlows extends SpecBaseObject 23 | { 24 | /** 25 | * @return array array of attributes available in this object. 26 | */ 27 | protected function attributes(): array 28 | { 29 | return [ 30 | 'implicit' => OAuthFlow::class, 31 | 'password' => OAuthFlow::class, 32 | 'clientCredentials' => OAuthFlow::class, 33 | 'authorizationCode' => OAuthFlow::class, 34 | ]; 35 | } 36 | 37 | /** 38 | * Perform validation on this object, check data against OpenAPI Specification rules. 39 | */ 40 | protected function performValidation() 41 | { 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/spec/OpenApi.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * This is the root document object of the OpenAPI document. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#openapi-object 16 | * 17 | * @property string $openapi 18 | * @property Info $info 19 | * @property Server[] $servers 20 | * @property Paths|PathItem[] $paths 21 | * @property Components|null $components 22 | * @property SecurityRequirement[] $security 23 | * @property Tag[] $tags 24 | * @property ExternalDocumentation|null $externalDocs 25 | * 26 | */ 27 | class OpenApi extends SpecBaseObject 28 | { 29 | /** 30 | * @return array array of attributes available in this object. 31 | */ 32 | protected function attributes(): array 33 | { 34 | return [ 35 | 'openapi' => Type::STRING, 36 | 'info' => Info::class, 37 | 'servers' => [Server::class], 38 | 'paths' => Paths::class, 39 | 'components' => Components::class, 40 | 'security' => SecurityRequirements::class, 41 | 'tags' => [Tag::class], 42 | 'externalDocs' => ExternalDocumentation::class, 43 | ]; 44 | } 45 | 46 | /** 47 | * @return array array of attributes default values. 48 | */ 49 | protected function attributeDefaults(): array 50 | { 51 | return [ 52 | // Spec: If the servers property is not provided, or is an empty array, 53 | // the default value would be a Server Object with a url value of /. 54 | 'servers' => [ 55 | new Server(['url' => '/']) 56 | ], 57 | ]; 58 | } 59 | 60 | public function __get($name) 61 | { 62 | $ret = parent::__get($name); 63 | // Spec: If the servers property is not provided, or is an empty array, 64 | // the default value would be a Server Object with a url value of /. 65 | if ($name === 'servers' && $ret === []) { 66 | return $this->attributeDefaults()['servers']; 67 | } 68 | return $ret; 69 | } 70 | 71 | /** 72 | * Perform validation on this object, check data against OpenAPI Specification rules. 73 | */ 74 | public function performValidation() 75 | { 76 | $this->requireProperties(['openapi', 'info', 'paths']); 77 | if (!empty($this->openapi) && !preg_match('/^3\.0\.\d+(-rc\d)?$/i', $this->openapi)) { 78 | $this->addError('Unsupported openapi version: ' . $this->openapi); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/spec/Operation.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * Describes a single API operation on a path. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#operationObject 16 | * 17 | * @property string[] $tags 18 | * @property string $summary 19 | * @property string $description 20 | * @property ExternalDocumentation|null $externalDocs 21 | * @property string $operationId 22 | * @property Parameter[]|Reference[] $parameters 23 | * @property RequestBody|Reference|null $requestBody 24 | * @property Responses|Response[]|null $responses 25 | * @property Callback[]|Reference[] $callbacks 26 | * @property bool $deprecated 27 | * @property SecurityRequirement[] $security 28 | * @property Server[] $servers 29 | */ 30 | class Operation extends SpecBaseObject 31 | { 32 | 33 | /** 34 | * @return array array of attributes available in this object. 35 | */ 36 | protected function attributes(): array 37 | { 38 | return [ 39 | 'tags' => [Type::STRING], 40 | 'summary' => Type::STRING, 41 | 'description' => Type::STRING, 42 | 'externalDocs' => ExternalDocumentation::class, 43 | 'operationId' => Type::STRING, 44 | 'parameters' => [Parameter::class], 45 | 'requestBody' => RequestBody::class, 46 | 'responses' => Responses::class, 47 | 'callbacks' => [Type::STRING, Callback::class], 48 | 'deprecated' => Type::BOOLEAN, 49 | 'security' => SecurityRequirements::class, 50 | 'servers' => [Server::class], 51 | ]; 52 | } 53 | 54 | protected function attributeDefaults(): array 55 | { 56 | return [ 57 | 'security' => null, 58 | ]; 59 | } 60 | 61 | /** 62 | * Perform validation on this object, check data against OpenAPI Specification rules. 63 | * 64 | * Call `addError()` in case of validation errors. 65 | */ 66 | protected function performValidation() 67 | { 68 | $this->requireProperties(['responses']); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/spec/Parameter.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\exceptions\TypeErrorException; 11 | use cebe\openapi\SpecBaseObject; 12 | 13 | /** 14 | * Describes a single operation parameter. 15 | * 16 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#parameterObject 17 | * 18 | * @property string $name 19 | * @property string $in 20 | * @property string $description 21 | * @property bool $required 22 | * @property bool $deprecated 23 | * @property bool $allowEmptyValue 24 | * 25 | * @property string $style 26 | * @property boolean $explode 27 | * @property boolean $allowReserved 28 | * @property Schema|Reference|null $schema 29 | * @property mixed $example 30 | * @property Example[] $examples 31 | * 32 | * @property MediaType[] $content 33 | */ 34 | class Parameter extends SpecBaseObject 35 | { 36 | /** 37 | * @return array array of attributes available in this object. 38 | */ 39 | protected function attributes(): array 40 | { 41 | return [ 42 | 'name' => Type::STRING, 43 | 'in' => Type::STRING, 44 | 'description' => Type::STRING, 45 | 'required' => Type::BOOLEAN, 46 | 'deprecated' => Type::BOOLEAN, 47 | 'allowEmptyValue' => Type::BOOLEAN, 48 | 49 | 'style' => Type::STRING, 50 | 'explode' => Type::BOOLEAN, 51 | 'allowReserved' => Type::BOOLEAN, 52 | 'schema' => Schema::class, 53 | 'example' => Type::ANY, 54 | 'examples' => [Type::STRING, Example::class], 55 | 56 | 'content' => [Type::STRING, MediaType::class], 57 | ]; 58 | } 59 | 60 | private $_attributeDefaults = []; 61 | 62 | /** 63 | * @return array array of attributes default values. 64 | */ 65 | protected function attributeDefaults(): array 66 | { 67 | return $this->_attributeDefaults; 68 | } 69 | 70 | /** 71 | * Create an object from spec data. 72 | * @param array $data spec data read from YAML or JSON 73 | * @throws TypeErrorException in case invalid data is supplied. 74 | */ 75 | public function __construct(array $data) 76 | { 77 | if (isset($data['in'])) { 78 | // Spec: Default values (based on value of in): 79 | // for query - form; 80 | // for path - simple; 81 | // for header - simple; 82 | // for cookie - form. 83 | switch ($data['in']) { 84 | case 'query': 85 | case 'cookie': 86 | $this->_attributeDefaults['style'] = 'form'; 87 | $this->_attributeDefaults['explode'] = true; 88 | break; 89 | case 'path': 90 | case 'header': 91 | $this->_attributeDefaults['style'] = 'simple'; 92 | $this->_attributeDefaults['explode'] = false; 93 | break; 94 | } 95 | } 96 | if (isset($data['style'])) { 97 | // Spec: When style is form, the default value is true. For all other styles, the default value is false. 98 | $this->_attributeDefaults['explode'] = ($data['style'] === 'form'); 99 | } 100 | parent::__construct($data); 101 | } 102 | 103 | /** 104 | * Perform validation on this object, check data against OpenAPI Specification rules. 105 | * 106 | * Call `addError()` in case of validation errors. 107 | */ 108 | protected function performValidation() 109 | { 110 | $this->requireProperties(['name', 'in']); 111 | if ($this->in === 'path') { 112 | $this->requireProperties(['required']); 113 | if (!$this->required) { 114 | $this->addError("Parameter 'required' must be true for 'in': 'path'."); 115 | } 116 | } 117 | if (!empty($this->content) && !empty($this->schema)) { 118 | $this->addError('A Parameter Object MUST contain either a schema property, or a content property, but not both.'); 119 | } 120 | if (!empty($this->content) && count($this->content) !== 1) { 121 | $this->addError('A Parameter Object with Content property MUST have A SINGLE content type.'); 122 | } 123 | 124 | $supportedSerializationStyles = [ 125 | 'path' => ['simple', 'label', 'matrix'], 126 | 'query' => ['form', 'spaceDelimited', 'pipeDelimited', 'deepObject'], 127 | 'header' => ['simple'], 128 | 'cookie' => ['form'], 129 | ]; 130 | if (isset($supportedSerializationStyles[$this->in]) && !in_array($this->style, $supportedSerializationStyles[$this->in])) { 131 | $this->addError('A Parameter Object DOES NOT support this serialization style.'); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/spec/RequestBody.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * Describes a single request body. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#requestBodyObject 16 | * 17 | * @property string $description 18 | * @property MediaType[] $content 19 | * @property boolean $required 20 | */ 21 | class RequestBody extends SpecBaseObject 22 | { 23 | /** 24 | * @return array array of attributes available in this object. 25 | */ 26 | protected function attributes(): array 27 | { 28 | return [ 29 | 'description' => Type::STRING, 30 | 'content' => [Type::STRING, MediaType::class], 31 | 'required' => Type::BOOLEAN, 32 | ]; 33 | } 34 | /** 35 | * Perform validation on this object, check data against OpenAPI Specification rules. 36 | */ 37 | protected function performValidation() 38 | { 39 | $this->requireProperties(['content']); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/spec/Response.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * Describes a single response from an API Operation, including design-time, static links to operations based on the response. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#responseObject 16 | * 17 | * @property string $description 18 | * @property Header[]|Reference[] $headers 19 | * @property MediaType[] $content 20 | * @property Link[]|Reference[] $links 21 | */ 22 | class Response extends SpecBaseObject 23 | { 24 | /** 25 | * @return array array of attributes available in this object. 26 | */ 27 | protected function attributes(): array 28 | { 29 | return [ 30 | 'description' => Type::STRING, 31 | 'headers' => [Type::STRING, Header::class], 32 | 'content' => [Type::STRING, MediaType::class], 33 | 'links' => [Type::STRING, Link::class], 34 | ]; 35 | } 36 | 37 | /** 38 | * Perform validation on this object, check data against OpenAPI Specification rules. 39 | */ 40 | protected function performValidation() 41 | { 42 | $this->requireProperties(['description']); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/spec/Schema.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\exceptions\TypeErrorException; 11 | use cebe\openapi\SpecBaseObject; 12 | 13 | /** 14 | * The Schema Object allows the definition of input and output data types. 15 | * 16 | * These types can be objects, but also primitives and arrays. This object is an extended subset of the 17 | * [JSON Schema Specification Wright Draft 00](http://json-schema.org/). 18 | * 19 | * For more information about the properties, see 20 | * [JSON Schema Core](https://tools.ietf.org/html/draft-wright-json-schema-00) and 21 | * [JSON Schema Validation](https://tools.ietf.org/html/draft-wright-json-schema-validation-00). 22 | * Unless stated otherwise, the property definitions follow the JSON Schema. 23 | * 24 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#schemaObject 25 | * 26 | * @property string $title 27 | * @property int|float $multipleOf 28 | * @property int|float $maximum 29 | * @property bool $exclusiveMaximum 30 | * @property int|float $minimum 31 | * @property bool $exclusiveMinimum 32 | * @property int $maxLength 33 | * @property int $minLength 34 | * @property string $pattern (This string SHOULD be a valid regular expression, according to the [ECMA 262 regular expression dialect](https://www.ecma-international.org/ecma-262/5.1/#sec-7.8.5)) 35 | * @property int $maxItems 36 | * @property int $minItems 37 | * @property bool $uniqueItems 38 | * @property int $maxProperties 39 | * @property int $minProperties 40 | * @property string[] $required list of required properties 41 | * @property array $enum 42 | * 43 | * @property string $type 44 | * @property Schema[]|Reference[] $allOf 45 | * @property Schema[]|Reference[] $oneOf 46 | * @property Schema[]|Reference[] $anyOf 47 | * @property Schema|Reference|null $not 48 | * @property Schema|Reference|null $items 49 | * @property Schema[]|Reference[] $properties 50 | * @property Schema|Reference|bool $additionalProperties 51 | * @property string $description 52 | * @property string $format 53 | * @property mixed $default 54 | * 55 | * @property bool $nullable 56 | * @property Discriminator|null $discriminator 57 | * @property bool $readOnly 58 | * @property bool $writeOnly 59 | * @property Xml|null $xml 60 | * @property ExternalDocumentation|null $externalDocs 61 | * @property mixed $example 62 | * @property bool $deprecated 63 | * 64 | */ 65 | class Schema extends SpecBaseObject 66 | { 67 | /** 68 | * @return array array of attributes available in this object. 69 | */ 70 | protected function attributes(): array 71 | { 72 | return [ 73 | // The following properties are taken directly from the JSON Schema definition and follow the same specifications: 74 | // types from https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-4 ff. 75 | 'title' => Type::STRING, 76 | 'multipleOf' => Type::NUMBER, 77 | 'maximum' => Type::NUMBER, 78 | 'exclusiveMaximum' => Type::BOOLEAN, 79 | 'minimum' => Type::NUMBER, 80 | 'exclusiveMinimum' => Type::BOOLEAN, 81 | 'maxLength' => Type::INTEGER, 82 | 'minLength' => Type::INTEGER, 83 | 'pattern' => Type::STRING, 84 | 'maxItems' => Type::INTEGER, 85 | 'minItems' => Type::INTEGER, 86 | 'uniqueItems' => Type::BOOLEAN, 87 | 'maxProperties' => Type::INTEGER, 88 | 'minProperties' => Type::INTEGER, 89 | 'required' => [Type::STRING], 90 | 'enum' => [Type::ANY], 91 | // The following properties are taken from the JSON Schema definition but their definitions were adjusted to the OpenAPI Specification. 92 | 'type' => Type::STRING, 93 | 'allOf' => [Schema::class], 94 | 'oneOf' => [Schema::class], 95 | 'anyOf' => [Schema::class], 96 | 'not' => Schema::class, 97 | 'items' => Schema::class, 98 | 'properties' => [Type::STRING, Schema::class], 99 | //'additionalProperties' => 'boolean' | ['string', Schema::class], handled in constructor 100 | 'description' => Type::STRING, 101 | 'format' => Type::STRING, 102 | 'default' => Type::ANY, 103 | // Other than the JSON Schema subset fields, the following fields MAY be used for further schema documentation: 104 | 'nullable' => Type::BOOLEAN, 105 | 'discriminator' => Discriminator::class, 106 | 'readOnly' => Type::BOOLEAN, 107 | 'writeOnly' => Type::BOOLEAN, 108 | 'xml' => Xml::class, 109 | 'externalDocs' => ExternalDocumentation::class, 110 | 'example' => Type::ANY, 111 | 'deprecated' => Type::BOOLEAN, 112 | ]; 113 | } 114 | 115 | /** 116 | * @return array array of attributes default values. 117 | */ 118 | protected function attributeDefaults(): array 119 | { 120 | return [ 121 | 'additionalProperties' => true, 122 | 'required' => null, 123 | 'enum' => null, 124 | 'allOf' => null, 125 | 'oneOf' => null, 126 | 'anyOf' => null, 127 | // nullable is only relevant, when a type is specified 128 | // return null as default when there is no type 129 | // return false as default when there is a type 130 | 'nullable' => $this->hasPropertyValue('type') ? false : null, 131 | 'exclusiveMinimum' => $this->hasPropertyValue('minimum') ? false : null, 132 | 'exclusiveMaximum' => $this->hasPropertyValue('maximum') ? false : null, 133 | ]; 134 | } 135 | 136 | /** 137 | * Create an object from spec data. 138 | * @param array $data spec data read from YAML or JSON 139 | * @throws TypeErrorException in case invalid data is supplied. 140 | */ 141 | public function __construct(array $data) 142 | { 143 | if (isset($data['additionalProperties'])) { 144 | if (is_array($data['additionalProperties'])) { 145 | $data['additionalProperties'] = $this->instantiate(Schema::class, $data['additionalProperties']); 146 | } elseif (!($data['additionalProperties'] instanceof Schema || $data['additionalProperties'] instanceof Reference || is_bool($data['additionalProperties']))) { 147 | $givenType = gettype($data['additionalProperties']); 148 | if ($givenType === 'object') { 149 | $givenType = get_class($data['additionalProperties']); 150 | } 151 | throw new TypeErrorException(sprintf('Schema::$additionalProperties MUST be either boolean or a Schema/Reference object, "%s" given', $givenType)); 152 | } 153 | } 154 | parent::__construct($data); 155 | } 156 | 157 | /** 158 | * Perform validation on this object, check data against OpenAPI Specification rules. 159 | */ 160 | protected function performValidation() 161 | { 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/spec/SecurityRequirement.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * A required security scheme to execute this operation. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#securityRequirementObject 16 | * 17 | */ 18 | class SecurityRequirement extends SpecBaseObject 19 | { 20 | private $_securityRequirement; 21 | public function __construct(array $data) 22 | { 23 | parent::__construct($data); 24 | $this->_securityRequirement = $data; 25 | } 26 | 27 | /** 28 | * @return array array of attributes available in this object. 29 | */ 30 | protected function attributes(): array 31 | { 32 | // this object does not have a fixed set of attribute names 33 | return []; 34 | } 35 | 36 | /** 37 | * Perform validation on this object, check data against OpenAPI Specification rules. 38 | * 39 | * Call `addError()` in case of validation errors. 40 | */ 41 | protected function performValidation() 42 | { 43 | } 44 | 45 | public function getSerializableData() 46 | { 47 | return $this->_securityRequirement; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/spec/SecurityRequirements.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * Lists the required security schemes to execute this operation. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#securityRequirementObject 16 | * 17 | */ 18 | class SecurityRequirements extends SpecBaseObject 19 | { 20 | private $_securityRequirements; 21 | 22 | public function __construct(array $data) 23 | { 24 | parent::__construct($data); 25 | 26 | foreach ($data as $index => $value) { 27 | if (is_numeric($index)) { // read 28 | $this->_securityRequirements[array_keys($value)[0]] = new SecurityRequirement(array_values($value)[0]); 29 | } else { // write 30 | $this->_securityRequirements[$index] = $value; 31 | } 32 | } 33 | if ($data === []) { 34 | $this->_securityRequirements = []; 35 | } 36 | } 37 | 38 | /** 39 | * @return array array of attributes available in this object. 40 | */ 41 | protected function attributes(): array 42 | { 43 | // this object does not have a fixed set of attribute names 44 | return []; 45 | } 46 | 47 | /** 48 | * Perform validation on this object, check data against OpenAPI Specification rules. 49 | * 50 | * Call `addError()` in case of validation errors. 51 | */ 52 | protected function performValidation() 53 | { 54 | } 55 | 56 | /** 57 | * {@inheritDoc} 58 | */ 59 | public function getSerializableData() 60 | { 61 | $data = []; 62 | foreach ($this->_securityRequirements ?? [] as $name => $securityRequirement) { 63 | /** @var SecurityRequirement $securityRequirement */ 64 | $data[] = [$name => $securityRequirement->getSerializableData()]; 65 | } 66 | return $data; 67 | } 68 | 69 | public function getRequirement(string $name) 70 | { 71 | return $this->_securityRequirements[$name] ?? null; 72 | } 73 | 74 | public function getRequirements() 75 | { 76 | return $this->_securityRequirements; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/spec/SecurityScheme.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * Defines a security scheme that can be used by the operations. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#securitySchemeObject 16 | * 17 | * @property string $type 18 | * @property string $description 19 | * @property string $name 20 | * @property string $in 21 | * @property string $scheme 22 | * @property string $bearerFormat 23 | * @property OAuthFlows|null $flows 24 | * @property string $openIdConnectUrl 25 | */ 26 | class SecurityScheme extends SpecBaseObject 27 | { 28 | private $knownTypes = [ 29 | "apiKey", 30 | "http", 31 | "oauth2", 32 | "openIdConnect" 33 | ]; 34 | 35 | /** 36 | * @return array array of attributes available in this object. 37 | */ 38 | protected function attributes(): array 39 | { 40 | return [ 41 | 'type' => Type::STRING, 42 | 'description' => Type::STRING, 43 | 'name' => Type::STRING, 44 | 'in' => Type::STRING, 45 | 'scheme' => Type::STRING, 46 | 'bearerFormat' => Type::STRING, 47 | 'flows' => OAuthFlows::class, 48 | 'openIdConnectUrl' => Type::STRING, 49 | ]; 50 | } 51 | 52 | /** 53 | * Perform validation on this object, check data against OpenAPI Specification rules. 54 | */ 55 | protected function performValidation() 56 | { 57 | $this->requireProperties(['type']); 58 | if (isset($this->type)) { 59 | if (!in_array($this->type, $this->knownTypes)) { 60 | $this->addError("Unknown Security Scheme type: $this->type"); 61 | } else { 62 | switch ($this->type) { 63 | case "apiKey": 64 | $this->requireProperties(['name', 'in']); 65 | if (isset($this->in)) { 66 | if (!in_array($this->in, ["query", "header", "cookie"])) { 67 | $this->addError("Invalid value for Security Scheme property 'in': $this->in"); 68 | } 69 | } 70 | break; 71 | case "http": 72 | $this->requireProperties(['scheme']); 73 | break; 74 | case "oauth2": 75 | $this->requireProperties(['flows']); 76 | break; 77 | case "openIdConnect": 78 | $this->requireProperties(['openIdConnectUrl']); 79 | break; 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/spec/Server.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * An object representing a Server. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#serverObject 16 | * 17 | * @property string $url 18 | * @property string $description 19 | * @property ServerVariable[] $variables 20 | * 21 | */ 22 | class Server extends SpecBaseObject 23 | { 24 | /** 25 | * @return array array of attributes available in this object. 26 | */ 27 | protected function attributes(): array 28 | { 29 | return [ 30 | 'url' => Type::STRING, 31 | 'description' => Type::STRING, 32 | 'variables' => [Type::STRING, ServerVariable::class], 33 | ]; 34 | } 35 | 36 | /** 37 | * Perform validation on this object, check data against OpenAPI Specification rules. 38 | */ 39 | protected function performValidation() 40 | { 41 | $this->requireProperties(['url']); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/spec/ServerVariable.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * An object representing a Server Variable for server URL template substitution. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#serverVariableObject 16 | * 17 | * @property string[] $enum 18 | * @property string $default 19 | * @property string $description 20 | * 21 | */ 22 | class ServerVariable extends SpecBaseObject 23 | { 24 | /** 25 | * @return array array of attributes available in this object. 26 | */ 27 | protected function attributes(): array 28 | { 29 | return [ 30 | 'enum' => [Type::STRING], 31 | 'default' => Type::STRING, 32 | 'description' => Type::STRING, 33 | ]; 34 | } 35 | 36 | /** 37 | * Perform validation on this object, check data against OpenAPI Specification rules. 38 | */ 39 | protected function performValidation() 40 | { 41 | $this->requireProperties(['default']); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/spec/Tag.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * Adds metadata to a single tag that is used by the Operation Object. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#tagObject 16 | * 17 | * @property string $name 18 | * @property string $description 19 | * @property ExternalDocumentation|null $externalDocs 20 | * 21 | */ 22 | class Tag extends SpecBaseObject 23 | { 24 | /** 25 | * @return array array of attributes available in this object. 26 | */ 27 | protected function attributes(): array 28 | { 29 | return [ 30 | 'name' => Type::STRING, 31 | 'description' => Type::STRING, 32 | 'externalDocs' => ExternalDocumentation::class, 33 | ]; 34 | } 35 | 36 | /** 37 | * Perform validation on this object, check data against OpenAPI Specification rules. 38 | */ 39 | protected function performValidation() 40 | { 41 | $this->requireProperties(['name']); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/spec/Type.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | /** 11 | * Data Types 12 | * 13 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#dataTypes 14 | */ 15 | class Type 16 | { 17 | const ANY = 'any'; 18 | const INTEGER = 'integer'; 19 | const NUMBER = 'number'; 20 | const STRING = 'string'; 21 | const BOOLEAN = 'boolean'; 22 | const OBJECT = 'object'; 23 | const ARRAY = 'array'; 24 | 25 | /** 26 | * Indicate whether a type is a scalar type, i.e. not an array or object. 27 | * 28 | * For ANY this will return false. 29 | * 30 | * @param string $type value from one of the type constants defined in this class. 31 | * @return bool whether the type is a scalar type. 32 | * @since 1.2.1 33 | */ 34 | public static function isScalar(string $type): bool 35 | { 36 | return in_array($type, [ 37 | self::INTEGER, 38 | self::NUMBER, 39 | self::STRING, 40 | self::BOOLEAN, 41 | ]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/spec/Xml.php: -------------------------------------------------------------------------------- 1 | and contributors 5 | * @license https://github.com/cebe/php-openapi/blob/master/LICENSE 6 | */ 7 | 8 | namespace cebe\openapi\spec; 9 | 10 | use cebe\openapi\SpecBaseObject; 11 | 12 | /** 13 | * A metadata object that allows for more fine-tuned XML model definitions. 14 | * 15 | * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#xmlObject 16 | * 17 | * @property string $name 18 | * @property string $namespace 19 | * @property string $prefix 20 | * @property boolean $attribute 21 | * @property boolean $wrapped 22 | */ 23 | class Xml extends SpecBaseObject 24 | { 25 | /** 26 | * @return array array of attributes available in this object. 27 | */ 28 | protected function attributes(): array 29 | { 30 | return [ 31 | 'name' => Type::STRING, 32 | 'namespace' => Type::STRING, 33 | 'prefix' => Type::STRING, 34 | 'attribute' => Type::BOOLEAN, 35 | 'wrapped' => Type::BOOLEAN, 36 | ]; 37 | } 38 | 39 | /** 40 | * Perform validation on this object, check data against OpenAPI Specification rules. 41 | */ 42 | protected function performValidation() 43 | { 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | /tmp -------------------------------------------------------------------------------- /tests/IssueTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(\cebe\openapi\SpecObjectInterface::class, $openapi); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/ReaderTest.php: -------------------------------------------------------------------------------- 1 | assertApiContent($openapi); 22 | } 23 | 24 | public function testReadYaml() 25 | { 26 | $openapi = \cebe\openapi\Reader::readFromYaml(<<assertApiContent($openapi); 37 | } 38 | 39 | /** 40 | * Test if reading YAML file with anchors works 41 | */ 42 | public function testReadYamlWithAnchors() 43 | { 44 | $openApiFile = __DIR__ . '/spec/data/traits-mixins.yaml'; 45 | $openapi = \cebe\openapi\Reader::readFromYamlFile($openApiFile); 46 | 47 | $this->assertApiContent($openapi); 48 | 49 | $putOperation = $openapi->paths['/foo']->put; 50 | $this->assertEquals('create foo', $putOperation->description); 51 | $this->assertTrue($putOperation->responses->hasResponse('200')); 52 | $this->assertTrue($putOperation->responses->hasResponse('404')); 53 | $this->assertTrue($putOperation->responses->hasResponse('428')); 54 | $this->assertTrue($putOperation->responses->hasResponse('default')); 55 | 56 | $respOk = $putOperation->responses->getResponse('200'); 57 | $this->assertEquals('request succeeded', $respOk->description); 58 | $this->assertEquals('the request id', $respOk->headers['X-Request-Id']->description); 59 | 60 | $resp404 = $putOperation->responses->getResponse('404'); 61 | $this->assertEquals('resource not found', $resp404->description); 62 | $this->assertEquals('the request id', $resp404->headers['X-Request-Id']->description); 63 | 64 | $resp428 = $putOperation->responses->getResponse('428'); 65 | $this->assertEquals('resource not found', $resp428->description); 66 | $this->assertEquals('the request id', $resp428->headers['X-Request-Id']->description); 67 | 68 | $respDefault = $putOperation->responses->getResponse('default'); 69 | $this->assertEquals('resource not found', $respDefault->description); 70 | $this->assertEquals('the request id', $respDefault->headers['X-Request-Id']->description); 71 | 72 | $foo = $openapi->components->schemas['Foo']; 73 | $this->assertArrayHasKey('uuid', $foo->properties); 74 | $this->assertArrayHasKey('name', $foo->properties); 75 | $this->assertArrayHasKey('id', $foo->properties); 76 | $this->assertArrayHasKey('description', $foo->properties); 77 | $this->assertEquals('uuid of the resource', $foo->properties['uuid']->description); 78 | } 79 | 80 | private function assertApiContent(\cebe\openapi\spec\OpenApi $openapi) 81 | { 82 | $result = $openapi->validate(); 83 | $this->assertEquals([], $openapi->getErrors()); 84 | $this->assertTrue($result); 85 | 86 | 87 | $this->assertEquals("3.0.0", $openapi->openapi); 88 | $this->assertEquals("Test API", $openapi->info->title); 89 | $this->assertEquals("1.0.0", $openapi->info->version); 90 | } 91 | 92 | /** 93 | * @see https://github.com/symfony/symfony/issues/34805 94 | */ 95 | public function testSymfonyYamlBugHunt() 96 | { 97 | $openApiFile = __DIR__ . '/../vendor/oai/openapi-specification/examples/v3.0/uspto.yaml'; 98 | $openapi = \cebe\openapi\Reader::readFromYamlFile($openApiFile); 99 | 100 | $inlineYamlExample = $openapi->paths['/']->get->responses['200']->content['application/json']->example; 101 | 102 | if (method_exists($this, 'assertIsArray')) { 103 | $this->assertIsArray($inlineYamlExample); 104 | } else { 105 | $this->assertInternalType('array', $inlineYamlExample); 106 | } 107 | 108 | $expectedArray = json_decode(<<assertEquals($expectedArray, $inlineYamlExample); 129 | } 130 | 131 | public function testGetRawSpecData() 132 | { 133 | $spec = <<assertSame($openapi->getRawSpecData(), [ 172 | 'openapi' => '3.0.0', 173 | 'info' => [ 174 | 'version' => '1.0.0', 175 | 'title' => 'Check storage of raw spec data', 176 | ], 177 | 'paths' => [ 178 | '/' => [ 179 | 'get' => [ 180 | 'summary' => 'List', 181 | 'operationId' => 'list', 182 | 'responses' => [ 183 | '200' => [ 184 | 'description' => 'The information', 185 | ] 186 | ] 187 | ] 188 | ] 189 | ], 190 | 'components' => [ 191 | 'schemas' => [ 192 | 'User' => [ 193 | 'type' => 'object', 194 | 'properties' => [ 195 | 'id' => [ 196 | 'type' => 'integer', 197 | ], 198 | 'name' => [ 199 | 'type' => 'string', 200 | ] 201 | ] 202 | ], 203 | 'Post' => [ 204 | 'type' => 'object', 205 | 'properties' => [ 206 | 'id' => [ 207 | 'type' => 'integer', 208 | ], 209 | 'title' => [ 210 | 'type' => 'string', 211 | ], 212 | 'user' => [ 213 | '$ref' => '#/components/schemas/User', 214 | ] 215 | ] 216 | ] 217 | ] 218 | ] 219 | ]); 220 | 221 | $this->assertSame($openapi->components->schemas['User']->getRawSpecData(), [ 222 | 'type' => 'object', 223 | 'properties' => [ 224 | 'id' => [ 225 | 'type' => 'integer', 226 | ], 227 | 'name' => [ 228 | 'type' => 'string', 229 | ] 230 | ] 231 | ]); 232 | 233 | $this->assertSame($openapi->components->schemas['Post']->properties['user']->getRawSpecData(), [ 234 | '$ref' => '#/components/schemas/User', 235 | ]); 236 | 237 | } 238 | 239 | 240 | // TODO test invalid JSON 241 | // TODO test invalid YAML 242 | } 243 | -------------------------------------------------------------------------------- /tests/WriterTest.php: -------------------------------------------------------------------------------- 1 | '3.0.0', 18 | 'info' => [ 19 | 'title' => 'Test API', 20 | 'version' => '1.0.0', 21 | ], 22 | 'paths' => [], 23 | ], $merge)); 24 | } 25 | 26 | public function testWriteJson() 27 | { 28 | $openapi = $this->createOpenAPI(); 29 | 30 | $json = \cebe\openapi\Writer::writeToJson($openapi); 31 | 32 | $this->assertEquals(preg_replace('~\R~', "\n", <<createOpenAPI(); 50 | 51 | $openapi->paths['/test'] = new \cebe\openapi\spec\PathItem([ 52 | 'description' => 'something' 53 | ]); 54 | 55 | $json = \cebe\openapi\Writer::writeToJson($openapi); 56 | 57 | $this->assertEquals(preg_replace('~\R~', "\n", <<createOpenAPI(); 79 | 80 | $yaml = \cebe\openapi\Writer::writeToYaml($openapi); 81 | 82 | 83 | $this->assertEquals(preg_replace('~\R~', "\n", <<createOpenAPI([ 99 | 'security' => [], 100 | ]); 101 | 102 | $json = \cebe\openapi\Writer::writeToJson($openapi); 103 | 104 | $this->assertEquals(preg_replace('~\R~', "\n", <<createOpenAPI([ 124 | 'security' => [], 125 | ]); 126 | 127 | $yaml = \cebe\openapi\Writer::writeToYaml($openapi); 128 | 129 | 130 | $this->assertEquals(preg_replace('~\R~', "\n", <<createOpenAPI([ 147 | 'security' => new SecurityRequirements([ 148 | 'Bearer' => new SecurityRequirement([]) 149 | ]), 150 | ]); 151 | 152 | $json = \cebe\openapi\Writer::writeToJson($openapi); 153 | 154 | $this->assertEquals(preg_replace('~\R~', "\n", <<createOpenAPI([ 178 | 'security' => new SecurityRequirements([ 179 | 'Bearer' => new SecurityRequirement([]) 180 | ]), 181 | ]); 182 | 183 | $yaml = \cebe\openapi\Writer::writeToYaml($openapi); 184 | 185 | 186 | $this->assertEquals(preg_replace('~\R~', "\n", <<createOpenAPI([ 205 | 'components' => new Components([ 206 | 'securitySchemes' => [ 207 | 'BearerAuth' => new SecurityScheme([ 208 | 'type' => 'http', 209 | 'scheme' => 'bearer', 210 | 'bearerFormat' => 'AuthToken and JWT Format' # optional, arbitrary value for documentation purposes 211 | ]), 212 | ], 213 | ]), 214 | 'paths' => [ 215 | '/test' => new PathItem([ 216 | 'get' => new Operation([ 217 | 'security' => new SecurityRequirements([ 218 | 'BearerAuth' => new SecurityRequirement([]), 219 | ]), 220 | 'responses' => new Responses([ 221 | 200 => new Response(['description' => 'OK']), 222 | ]) 223 | ]) 224 | ]) 225 | ] 226 | ]); 227 | 228 | $yaml = \cebe\openapi\Writer::writeToYaml($openapi); 229 | 230 | 231 | $this->assertEquals(preg_replace('~\R~', "\n", <<createOpenAPI([ 261 | 'components' => new Components([ 262 | 'securitySchemes' => [ 263 | 'BearerAuth' => new SecurityScheme([ 264 | 'type' => 'http', 265 | 'scheme' => 'bearer', 266 | 'bearerFormat' => 'AuthToken and JWT Format' # optional, arbitrary value for documentation purposes 267 | ]) 268 | ], 269 | ]), 270 | 'security' => new SecurityRequirements([ 271 | 'BearerAuth' => new SecurityRequirement([]) 272 | ]), 273 | 'paths' => [], 274 | ]); 275 | 276 | $yaml = \cebe\openapi\Writer::writeToYaml($openapi); 277 | 278 | 279 | $this->assertEquals(preg_replace('~\R~', "\n", << /etc/dpkg/dpkg.cfg.d/02apt-speedup && \ 6 | echo "Acquire::http {No-Cache=True;};" > /etc/apt/apt.conf.d/no-cache 7 | RUN apt-get update && \ 8 | apt-get -y install \ 9 | gnupg2 && \ 10 | apt-key update && \ 11 | apt-get update && \ 12 | apt-get install -y --no-install-recommends \ 13 | libzip-dev \ 14 | libonig-dev \ 15 | vim \ 16 | git \ 17 | unzip\ 18 | libxml2-dev \ 19 | curl \ 20 | libcurl4-openssl-dev \ 21 | libssl-dev \ 22 | --no-install-recommends && \ 23 | apt-get clean && \ 24 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ 25 | && pecl install xdebug-2.9.6 \ 26 | && docker-php-ext-enable xdebug \ 27 | && docker-php-ext-install \ 28 | zip \ 29 | curl \ 30 | mbstring 31 | 32 | # Install composer 33 | ENV COMPOSER_ALLOW_SUPERUSER=1 \ 34 | PHP_USER_ID=33 \ 35 | PHP_ENABLE_XDEBUG=1 \ 36 | COMPOSER_HOME=/root/.composer/ \ 37 | PATH=/app:/app/vendor/bin:/root/.composer/vendor/bin:$PATH 38 | 39 | RUN curl -o /tmp/composer-setup.php https://getcomposer.org/installer \ 40 | && curl -o /tmp/composer-setup.sig https://composer.github.io/installer.sig \ 41 | # Make sure we're installing what we think we're installing! 42 | && php -r "if (hash('SHA384', file_get_contents('/tmp/composer-setup.php')) !== trim(file_get_contents('/tmp/composer-setup.sig'))) { unlink('/tmp/composer-setup.php'); echo 'Invalid installer' . PHP_EOL; exit(1); }" \ 43 | && php /tmp/composer-setup.php --no-ansi --install-dir=/usr/local/bin --filename=composer \ 44 | && rm -f /tmp/composer-setup.* 45 | 46 | # Enable Xdebug 47 | ENV XDEBUGINI_PATH=/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini 48 | 49 | RUN echo "xdebug.idekey=PHP_STORM" >> $XDEBUGINI_PATH && \ 50 | echo "xdebug.default_enable=1" >> $XDEBUGINI_PATH && \ 51 | echo "xdebug.remote_enable=1" >> $XDEBUGINI_PATH && \ 52 | echo "xdebug.remote_connect_back=1" >> $XDEBUGINI_PATH && \ 53 | echo "xdebug.remote_log=xdebug_remote.log" >> $XDEBUGINI_PATH && \ 54 | echo "xdebug.remote_port=9000" >> $XDEBUGINI_PATH && \ 55 | echo "xdebug.remote_autostart=1" >> $XDEBUGINI_PATH 56 | 57 | WORKDIR /app 58 | -------------------------------------------------------------------------------- /tests/json/JsonPointerTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($encoded, JsonPointer::encode($decoded)); 31 | } 32 | 33 | /** 34 | * @dataProvider encodeDecodeData 35 | */ 36 | public function testDecode($encoded, $decoded) 37 | { 38 | $this->assertEquals($decoded, JsonPointer::decode($encoded)); 39 | } 40 | 41 | /** 42 | * @link https://tools.ietf.org/html/rfc6901#section-5 43 | */ 44 | public function rfcJsonDocument() 45 | { 46 | return <<rfcJsonDocument())], 70 | ["/foo" , "#/foo" , ["bar", "baz"]], 71 | ["/foo/0", "#/foo/0", "bar"], 72 | ["/" , "#/" , 0], 73 | ["/a~1b" , "#/a~1b" , 1], 74 | ["/c%d" , "#/c%25d", 2], 75 | ["/e^f" , "#/e%5Ef", 3], 76 | ["/g|h" , "#/g%7Ch", 4], 77 | ["/i\\j" , "#/i%5Cj", 5], 78 | ["/k\"l" , "#/k%22l", 6], 79 | ["/ " , "#/%20" , 7], 80 | ["/m~0n" , "#/m~0n" , 8], 81 | ]; 82 | foreach ($return as $example) { 83 | $example[3] = $this->rfcJsonDocument(); 84 | yield $example; 85 | } 86 | } 87 | 88 | public function allExamples() 89 | { 90 | yield from $this->rfcExamples(); 91 | 92 | yield ["/a#b" , "#/a%23b" , 16, '{"a#b": 16}']; 93 | } 94 | 95 | /** 96 | * @dataProvider allExamples 97 | */ 98 | public function testUriEncoding($jsonPointer, $uriJsonPointer, $expectedEvaluation) 99 | { 100 | $pointer = new JsonPointer($jsonPointer); 101 | $this->assertSame($jsonPointer, $pointer->getPointer()); 102 | $this->assertSame($uriJsonPointer, JsonReference::createFromUri('', $pointer)->getReference()); 103 | 104 | $reference = JsonReference::createFromReference($uriJsonPointer); 105 | $this->assertSame($jsonPointer, $reference->getJsonPointer()->getPointer()); 106 | $this->assertSame('', $reference->getDocumentUri()); 107 | $this->assertSame($uriJsonPointer, $reference->getReference()); 108 | 109 | $reference = JsonReference::createFromReference("somefile.json$uriJsonPointer"); 110 | $this->assertSame($jsonPointer, $reference->getJsonPointer()->getPointer()); 111 | $this->assertSame("somefile.json", $reference->getDocumentUri()); 112 | $this->assertSame("somefile.json$uriJsonPointer", $reference->getReference()); 113 | } 114 | 115 | /** 116 | * @dataProvider rfcExamples 117 | */ 118 | public function testEvaluation($jsonPointer, $uriJsonPointer, $expectedEvaluation) 119 | { 120 | $document = json_decode($this->rfcJsonDocument()); 121 | $pointer = new JsonPointer($jsonPointer); 122 | $this->assertEquals($expectedEvaluation, $pointer->evaluate($document)); 123 | 124 | $document = json_decode($this->rfcJsonDocument()); 125 | $reference = JsonReference::createFromReference($uriJsonPointer); 126 | $this->assertEquals($expectedEvaluation, $reference->getJsonPointer()->evaluate($document)); 127 | } 128 | 129 | public function testEvaluationCases() 130 | { 131 | $document = (object) [ 132 | "" => (object) [ 133 | "" => 42 134 | ] 135 | ]; 136 | $pointer = new JsonPointer('//'); 137 | $this->assertSame(42, $pointer->evaluate($document)); 138 | 139 | $document = [ 140 | "1" => null, 141 | ]; 142 | $pointer = new JsonPointer('/1'); 143 | $this->assertNull($pointer->evaluate($document)); 144 | 145 | $document = (object) [ 146 | "k" => null, 147 | ]; 148 | $pointer = new JsonPointer('/k'); 149 | $this->assertNull($pointer->evaluate($document)); 150 | } 151 | 152 | public function testParent() 153 | { 154 | $this->assertNull((new JsonPointer(''))->parent()); 155 | $this->assertSame('', (new JsonPointer('/'))->parent()->getPointer()); 156 | $this->assertSame('/', (new JsonPointer('//'))->parent()->getPointer()); 157 | $this->assertSame('', (new JsonPointer('/some'))->parent()->getPointer()); 158 | $this->assertSame('/some', (new JsonPointer('/some/path'))->parent()->getPointer()); 159 | $this->assertSame('', (new JsonPointer('/a~1b'))->parent()->getPointer()); 160 | $this->assertSame('/a~1b', (new JsonPointer('/a~1b/path'))->parent()->getPointer()); 161 | $this->assertSame('/some', (new JsonPointer('/some/a~1b'))->parent()->getPointer()); 162 | } 163 | 164 | public function testAppend() 165 | { 166 | $this->assertSame('/some', (new JsonPointer(''))->append('some')->getPointer()); 167 | $this->assertSame('/~1some', (new JsonPointer(''))->append('/some')->getPointer()); 168 | $this->assertSame('/~0some', (new JsonPointer(''))->append('~some')->getPointer()); 169 | $this->assertSame('/path/some', (new JsonPointer('/path'))->append('some')->getPointer()); 170 | $this->assertSame('/path/~1some', (new JsonPointer('/path'))->append('/some')->getPointer()); 171 | $this->assertSame('/path/~0some', (new JsonPointer('/path'))->append('~some')->getPointer()); 172 | $this->assertSame('/a~1b/some', (new JsonPointer('/a~1b'))->append('some')->getPointer()); 173 | $this->assertSame('/a~1b/~1some', (new JsonPointer('/a~1b'))->append('/some')->getPointer()); 174 | $this->assertSame('/a~1b/~0some', (new JsonPointer('/a~1b'))->append('~some')->getPointer()); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /tests/spec/CallbackTest.php: -------------------------------------------------------------------------------- 1 | validate(); 30 | $this->assertEquals([], $callback->getErrors()); 31 | $this->assertTrue($result); 32 | 33 | $this->assertEquals('http://notificationServer.com?transactionId={$request.body#/id}&email={$request.body#/email}', $callback->getUrl()); 34 | $this->assertInstanceOf(\cebe\openapi\spec\PathItem::class, $callback->getRequest()); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /tests/spec/ComponentsTest.php: -------------------------------------------------------------------------------- 1 | validate(); 85 | $this->assertEquals([], $components->getErrors()); 86 | $this->assertTrue($result); 87 | 88 | $this->assertAllInstanceOf(\cebe\openapi\spec\Schema::class, $components->schemas); 89 | $this->assertCount(3, $components->schemas); 90 | $this->assertArrayHasKey('GeneralError', $components->schemas); 91 | $this->assertArrayHasKey('Category', $components->schemas); 92 | $this->assertArrayHasKey('Tag', $components->schemas); 93 | $this->assertAllInstanceOf(\cebe\openapi\spec\Response::class, $components->responses); 94 | $this->assertCount(3, $components->responses); 95 | $this->assertArrayHasKey('NotFound', $components->responses); 96 | $this->assertArrayHasKey('IllegalInput', $components->responses); 97 | $this->assertArrayHasKey('GeneralError', $components->responses); 98 | $this->assertAllInstanceOf(\cebe\openapi\spec\Parameter::class, $components->parameters); 99 | $this->assertCount(2, $components->parameters); 100 | $this->assertArrayHasKey('skipParam', $components->parameters); 101 | $this->assertArrayHasKey('limitParam', $components->parameters); 102 | $this->assertAllInstanceOf(\cebe\openapi\spec\Example::class, $components->examples); 103 | $this->assertCount(0, $components->examples); // TODO 104 | $this->assertAllInstanceOf(\cebe\openapi\spec\RequestBody::class, $components->requestBodies); 105 | $this->assertCount(0, $components->requestBodies); // TODO 106 | $this->assertAllInstanceOf(\cebe\openapi\spec\Header::class, $components->headers); 107 | $this->assertCount(0, $components->headers); // TODO 108 | $this->assertAllInstanceOf(\cebe\openapi\spec\SecurityScheme::class, $components->securitySchemes); 109 | $this->assertCount(2, $components->securitySchemes); 110 | $this->assertArrayHasKey('api_key', $components->securitySchemes); 111 | $this->assertArrayHasKey('petstore_auth', $components->securitySchemes); 112 | $this->assertAllInstanceOf(\cebe\openapi\spec\Link::class, $components->links); 113 | $this->assertCount(0, $components->links); // TODO 114 | $this->assertAllInstanceOf(\cebe\openapi\spec\Callback::class, $components->callbacks); 115 | $this->assertCount(0, $components->callbacks); // TODO 116 | } 117 | 118 | public function assertAllInstanceOf($className, $array) 119 | { 120 | foreach($array as $k => $v) { 121 | $this->assertInstanceOf($className, $v, "Asserting that item with key '$k' is instance of $className"); 122 | } 123 | } 124 | 125 | } -------------------------------------------------------------------------------- /tests/spec/HeaderTest.php: -------------------------------------------------------------------------------- 1 | validate(); 25 | $this->assertEquals([], $header->getErrors()); 26 | $this->assertTrue($result); 27 | 28 | $this->assertEquals('The number of allowed requests in the current period', $header->description); 29 | $this->assertInstanceOf(\cebe\openapi\spec\Schema::class, $header->schema); 30 | $this->assertEquals('integer', $header->schema->type); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/spec/InfoTest.php: -------------------------------------------------------------------------------- 1 | validate(); 34 | $this->assertEquals([], $info->getErrors()); 35 | $this->assertTrue($result); 36 | 37 | $this->assertEquals('Sample Pet Store App', $info->title); 38 | $this->assertEquals('This is a sample server for a pet store.', $info->description); 39 | $this->assertEquals('http://example.com/terms/', $info->termsOfService); 40 | $this->assertEquals('1.0.1', $info->version); 41 | 42 | $this->assertInstanceOf(Contact::class, $info->contact); 43 | $this->assertEquals('API Support', $info->contact->name); 44 | $this->assertEquals('http://www.example.com/support', $info->contact->url); 45 | $this->assertEquals('support@example.com', $info->contact->email); 46 | $this->assertInstanceOf(License::class, $info->license); 47 | $this->assertEquals('Apache 2.0', $info->license->name); 48 | $this->assertEquals('https://www.apache.org/licenses/LICENSE-2.0.html', $info->license->url); 49 | } 50 | 51 | public function testReadInvalid() 52 | { 53 | /** @var $info Info */ 54 | $info = Reader::readFromYaml(<<<'YAML' 55 | description: This is a sample server for a pet store. 56 | termsOfService: http://example.com/terms/ 57 | contact: 58 | name: API Support 59 | url: http://www.example.com/support 60 | email: support@example.com 61 | YAML 62 | , Info::class); 63 | 64 | $result = $info->validate(); 65 | $this->assertEquals([ 66 | 'Info is missing required property: title', 67 | 'Info is missing required property: version', 68 | ], $info->getErrors()); 69 | $this->assertFalse($result); 70 | 71 | } 72 | 73 | public function testReadInvalidContact() 74 | { 75 | /** @var $info Info */ 76 | $info = Reader::readFromYaml(<<<'YAML' 77 | title: test 78 | version: 1.0 79 | contact: 80 | name: API Support 81 | url: www.example.com/support 82 | email: support.example.com 83 | YAML 84 | , Info::class); 85 | 86 | $result = $info->validate(); 87 | $this->assertEquals([ 88 | 'Contact::$email does not seem to be a valid email address: support.example.com', 89 | 'Contact::$url does not seem to be a valid URL: www.example.com/support', 90 | ], $info->getErrors()); 91 | $this->assertFalse($result); 92 | 93 | $this->assertInstanceOf(Contact::class, $info->contact); 94 | $this->assertNull($info->license); 95 | 96 | } 97 | 98 | public function testReadInvalidLicense() 99 | { 100 | /** @var $info Info */ 101 | $info = Reader::readFromYaml(<<<'YAML' 102 | title: test 103 | version: 1.0 104 | license: 105 | url: www.apache.org/licenses/LICENSE-2.0.html 106 | YAML 107 | , Info::class); 108 | 109 | $result = $info->validate(); 110 | $this->assertEquals([ 111 | 'License is missing required property: name', 112 | 'License::$url does not seem to be a valid URL: www.apache.org/licenses/LICENSE-2.0.html', 113 | ], $info->getErrors()); 114 | $this->assertFalse($result); 115 | 116 | $this->assertInstanceOf(License::class, $info->license); 117 | $this->assertNull($info->contact); 118 | 119 | } 120 | } -------------------------------------------------------------------------------- /tests/spec/LinkTest.php: -------------------------------------------------------------------------------- 1 | validate(); 25 | $this->assertEquals([], $link->getErrors()); 26 | $this->assertTrue($result); 27 | 28 | $this->assertEquals(null, $link->operationRef); 29 | $this->assertEquals('getUserAddress', $link->operationId); 30 | $this->assertEquals(['userId' => 'test.path.id'], $link->parameters); 31 | $this->assertEquals(null, $link->requestBody); 32 | $this->assertEquals(null, $link->server); 33 | } 34 | 35 | public function testValidateBothOperationIdAndOperationRef() 36 | { 37 | /** @var $link Link */ 38 | $link = Reader::readFromJson(<<validate(); 47 | $this->assertEquals([ 48 | 'Link: operationId and operationRef are mutually exclusive.' 49 | ], $link->getErrors()); 50 | $this->assertFalse($result); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/spec/MediaTypeTest.php: -------------------------------------------------------------------------------- 1 | validate(); 45 | $this->assertEquals([], $mediaType->getErrors()); 46 | $this->assertTrue($result); 47 | 48 | $this->assertInstanceOf(Reference::class, $mediaType->schema); 49 | 50 | if (method_exists($this, 'assertIsArray')) { 51 | $this->assertIsArray($mediaType->examples); 52 | } else { 53 | $this->assertInternalType('array', $mediaType->examples); 54 | } 55 | 56 | $this->assertCount(3, $mediaType->examples); 57 | $this->assertArrayHasKey('cat', $mediaType->examples); 58 | $this->assertArrayHasKey('dog', $mediaType->examples); 59 | $this->assertArrayHasKey('frog', $mediaType->examples); 60 | $this->assertInstanceOf(Example::class, $mediaType->examples['cat']); 61 | $this->assertInstanceOf(Example::class, $mediaType->examples['dog']); 62 | $this->assertInstanceOf(Reference::class, $mediaType->examples['frog']); 63 | 64 | $this->assertEquals('An example of a cat', $mediaType->examples['cat']->summary); 65 | $expectedCat = [ // TODO we might actually expect this to be an object of stdClass 66 | 'name' => 'Fluffy', 67 | 'petType' => 'Cat', 68 | 'color' => 'White', 69 | 'gender' => 'male', 70 | 'breed' => 'Persian', 71 | ]; 72 | $this->assertEquals($expectedCat, $mediaType->examples['cat']->value); 73 | 74 | } 75 | 76 | public function testCreateionFromObjects() 77 | { 78 | $mediaType = new MediaType([ 79 | 'schema' => new \cebe\openapi\spec\Schema([ 80 | 'type' => \cebe\openapi\spec\Type::OBJECT, 81 | 'properties' => [ 82 | 'id' => new \cebe\openapi\spec\Schema(['type' => 'string', 'format' => 'uuid']), 83 | 'profileImage' => new \cebe\openapi\spec\Schema(['type' => 'string', 'format' => 'binary']), 84 | ], 85 | ]), 86 | 'encoding' => [ 87 | 'id' => [], 88 | 'profileImage' => new \cebe\openapi\spec\Encoding([ 89 | 'contentType' => 'image/png, image/jpeg', 90 | 'headers' => [ 91 | 'X-Rate-Limit-Limit' => new \cebe\openapi\spec\Header([ 92 | 'description' => 'The number of allowed requests in the current period', 93 | 'schema' => new \cebe\openapi\spec\Schema(['type' => 'integer']), 94 | ]), 95 | ], 96 | ]), 97 | ], 98 | ]); 99 | 100 | // default value should be extracted 101 | $this->assertEquals('text/plain', $mediaType->encoding['id']->contentType); 102 | // object should be passed. 103 | $this->assertInstanceOf(\cebe\openapi\spec\Encoding::class, $mediaType->encoding['profileImage']); 104 | } 105 | 106 | public function badEncodingProvider() 107 | { 108 | yield [['encoding' => ['id' => 'foo']], 'Encoding MUST be either array or Encoding object, "string" given']; 109 | yield [['encoding' => ['id' => 42]], 'Encoding MUST be either array or Encoding object, "integer" given']; 110 | yield [['encoding' => ['id' => false]], 'Encoding MUST be either array or Encoding object, "boolean" given']; 111 | yield [['encoding' => ['id' => new stdClass()]], 'Encoding MUST be either array or Encoding object, "stdClass" given']; 112 | // The last one can be supported in future, but now SpecBaseObjects::__construct() requires array explicitly 113 | } 114 | 115 | /** 116 | * @dataProvider badEncodingProvider 117 | */ 118 | public function testPathsCanNotBeCreatedFromBullshit($config, $expectedException) 119 | { 120 | $this->expectException(\cebe\openapi\exceptions\TypeErrorException::class); 121 | $this->expectExceptionMessage($expectedException); 122 | 123 | new MediaType($config); 124 | } 125 | 126 | public function testUnresolvedReferencesInEncoding() 127 | { 128 | $yaml = Yaml::parse(<<<'YAML' 129 | openapi: "3.0.0" 130 | info: 131 | version: 1.0.0 132 | title: Encoding test 133 | paths: 134 | /pets: 135 | post: 136 | summary: Create a pet 137 | operationId: createPets 138 | requestBody: 139 | content: 140 | multipart/form-data: 141 | schema: 142 | type: object 143 | properties: 144 | pet: 145 | $ref: '#/components/schemas/Pet' 146 | petImage: 147 | type: string 148 | format: binary 149 | encoding: 150 | pet: 151 | contentType: application/json 152 | petImage: 153 | contentType: image/* 154 | application/json: 155 | schema: 156 | $ref: '#/components/schemas/Pet' 157 | responses: 158 | '201': 159 | description: Null response 160 | components: 161 | schemas: 162 | Pet: 163 | type: object 164 | properties: 165 | name: 166 | type: string 167 | YAML 168 | ); 169 | $openapi = new OpenApi($yaml); 170 | $result = $openapi->validate(); 171 | 172 | $this->assertEquals([], $openapi->getErrors()); 173 | $this->assertTrue($result); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /tests/spec/OperationTest.php: -------------------------------------------------------------------------------- 1 | validate(); 63 | $this->assertEquals([], $operation->getErrors()); 64 | $this->assertTrue($result); 65 | 66 | $this->assertCount(1, $operation->tags); 67 | $this->assertEquals(['pet'], $operation->tags); 68 | 69 | $this->assertEquals('Updates a pet in the store with form data', $operation->summary); 70 | $this->assertEquals('updatePetWithForm', $operation->operationId); 71 | 72 | $this->assertCount(1, $operation->parameters); 73 | $this->assertInstanceOf(\cebe\openapi\spec\Parameter::class, $operation->parameters[0]); 74 | $this->assertEquals('petId', $operation->parameters[0]->name); 75 | 76 | $this->assertInstanceOf(\cebe\openapi\spec\RequestBody::class, $operation->requestBody); 77 | $this->assertCount(1, $operation->requestBody->content); 78 | $this->assertArrayHasKey('application/x-www-form-urlencoded', $operation->requestBody->content); 79 | 80 | $this->assertInstanceOf(\cebe\openapi\spec\Responses::class, $operation->responses); 81 | 82 | $this->assertCount(1, $operation->security->getRequirements()); 83 | $this->assertInstanceOf(\cebe\openapi\spec\SecurityRequirements::class, $operation->security); 84 | $this->assertInstanceOf(\cebe\openapi\spec\SecurityRequirement::class, $operation->security->getRequirement('petstore_auth')); 85 | $this->assertCount(2, $operation->security->getRequirement('petstore_auth')->getSerializableData()); 86 | $this->assertEquals(['write:pets', 'read:pets'], $operation->security->getRequirement('petstore_auth')->getSerializableData()); 87 | 88 | $this->assertInstanceOf(ExternalDocumentation::class, $operation->externalDocs); 89 | $this->assertEquals('Find more info here', $operation->externalDocs->description); 90 | $this->assertEquals('https://example.com', $operation->externalDocs->url); 91 | 92 | // deprecated Default value is false. 93 | $this->assertFalse($operation->deprecated); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/spec/ParameterTest.php: -------------------------------------------------------------------------------- 1 | validate(); 29 | $this->assertEquals([], $parameter->getErrors()); 30 | $this->assertTrue($result); 31 | 32 | $this->assertEquals('token', $parameter->name); 33 | $this->assertEquals('header', $parameter->in); 34 | $this->assertEquals('token to be passed as a header', $parameter->description); 35 | $this->assertTrue($parameter->required); 36 | 37 | $this->assertInstanceOf(\cebe\openapi\spec\Schema::class, $parameter->schema); 38 | $this->assertEquals('array', $parameter->schema->type); 39 | 40 | $this->assertEquals('simple', $parameter->style); 41 | 42 | /** @var $parameter Parameter */ 43 | $parameter = Reader::readFromYaml(<<<'YAML' 44 | in: query 45 | name: coordinates 46 | content: 47 | application/json: 48 | schema: 49 | type: object 50 | required: 51 | - lat 52 | - long 53 | properties: 54 | lat: 55 | type: number 56 | long: 57 | type: number 58 | YAML 59 | , Parameter::class); 60 | 61 | $result = $parameter->validate(); 62 | $this->assertEquals([], $parameter->getErrors()); 63 | $this->assertTrue($result); 64 | 65 | $this->assertEquals('coordinates', $parameter->name); 66 | $this->assertEquals('query', $parameter->in); 67 | // required default value is false. 68 | $this->assertFalse($parameter->required); 69 | // deprecated default value is false. 70 | $this->assertFalse($parameter->deprecated); 71 | // allowEmptyValue default value is false. 72 | $this->assertFalse($parameter->allowEmptyValue); 73 | 74 | $this->assertInstanceOf(\cebe\openapi\spec\MediaType::class, $parameter->content['application/json']); 75 | $this->assertInstanceOf(\cebe\openapi\spec\Schema::class, $parameter->content['application/json']->schema); 76 | } 77 | 78 | public function testDefaultValuesQuery() 79 | { 80 | /** @var $parameter Parameter */ 81 | $parameter = Reader::readFromYaml(<<<'YAML' 82 | name: token 83 | in: query 84 | YAML 85 | , Parameter::class); 86 | 87 | $result = $parameter->validate(); 88 | $this->assertEquals([], $parameter->getErrors()); 89 | $this->assertTrue($result); 90 | 91 | // default value for style parameter in query param 92 | $this->assertEquals('form', $parameter->style); 93 | $this->assertTrue($parameter->explode); 94 | $this->assertFalse($parameter->allowReserved); 95 | } 96 | 97 | public function testDefaultValuesPath() 98 | { 99 | /** @var $parameter Parameter */ 100 | $parameter = Reader::readFromYaml(<<<'YAML' 101 | name: token 102 | in: path 103 | required: true 104 | YAML 105 | , Parameter::class); 106 | 107 | $result = $parameter->validate(); 108 | $this->assertEquals([], $parameter->getErrors()); 109 | $this->assertTrue($result); 110 | 111 | // default value for style parameter in query param 112 | $this->assertEquals('simple', $parameter->style); 113 | $this->assertFalse($parameter->explode); 114 | } 115 | 116 | public function testDefaultValuesHeader() 117 | { 118 | /** @var $parameter Parameter */ 119 | $parameter = Reader::readFromYaml(<<<'YAML' 120 | name: token 121 | in: header 122 | YAML 123 | , Parameter::class); 124 | 125 | $result = $parameter->validate(); 126 | $this->assertEquals([], $parameter->getErrors()); 127 | $this->assertTrue($result); 128 | 129 | // default value for style parameter in query param 130 | $this->assertEquals('simple', $parameter->style); 131 | $this->assertFalse($parameter->explode); 132 | } 133 | 134 | public function testDefaultValuesCookie() 135 | { 136 | /** @var $parameter Parameter */ 137 | $parameter = Reader::readFromYaml(<<<'YAML' 138 | name: token 139 | in: cookie 140 | YAML 141 | , Parameter::class); 142 | 143 | $result = $parameter->validate(); 144 | $this->assertEquals([], $parameter->getErrors()); 145 | $this->assertTrue($result); 146 | 147 | // default value for style parameter in query param 148 | $this->assertEquals('form', $parameter->style); 149 | $this->assertTrue($parameter->explode); 150 | } 151 | 152 | public function testItValidatesSchemaAndContentCombination() 153 | { 154 | /** @var $parameter Parameter */ 155 | $parameter = Reader::readFromYaml(<<<'YAML' 156 | name: token 157 | in: cookie 158 | schema: 159 | type: object 160 | content: 161 | application/json: 162 | schema: 163 | type: object 164 | YAML 165 | , Parameter::class); 166 | 167 | $result = $parameter->validate(); 168 | $this->assertEquals(['A Parameter Object MUST contain either a schema property, or a content property, but not both.'], $parameter->getErrors()); 169 | $this->assertFalse($result); 170 | } 171 | 172 | public function testItValidatesContentCanHaveOnlySingleKey() 173 | { 174 | /** @var $parameter Parameter */ 175 | $parameter = Reader::readFromYaml(<<<'YAML' 176 | name: token 177 | in: cookie 178 | content: 179 | application/json: 180 | schema: 181 | type: object 182 | application/xml: 183 | schema: 184 | type: object 185 | YAML 186 | , Parameter::class); 187 | 188 | $result = $parameter->validate(); 189 | $this->assertEquals(['A Parameter Object with Content property MUST have A SINGLE content type.'], $parameter->getErrors()); 190 | $this->assertFalse($result); 191 | } 192 | 193 | 194 | public function testItValidatesSupportedSerializationStyles() 195 | { 196 | // 1. Prepare test inputs 197 | $specTemplate = << ['simple', 'label', 'matrix'], 205 | 'query' => ['form', 'spaceDelimited', 'pipeDelimited', 'deepObject'], 206 | 'header' => ['simple'], 207 | 'cookie' => ['form'], 208 | ]; 209 | $badCombinations = [ 210 | 'path' => ['unknown', 'form', 'spaceDelimited', 'pipeDelimited', 'deepObject'], 211 | 'query' => ['unknown', 'simple', 'label', 'matrix'], 212 | 'header' => ['unknown', 'form', 'spaceDelimited', 'pipeDelimited', 'deepObject', 'matrix'], 213 | 'cookie' => ['unknown', 'spaceDelimited', 'pipeDelimited', 'deepObject', 'matrix', 'label', 'matrix'], 214 | ]; 215 | 216 | // 2. Run tests for valid input 217 | foreach($goodCombinations as $in=>$styles) { 218 | foreach($styles as $style) { 219 | /** @var $parameter Parameter */ 220 | $parameter = Reader::readFromYaml(sprintf($specTemplate, $in, $style) , Parameter::class); 221 | $result = $parameter->validate(); 222 | $this->assertEquals([], $parameter->getErrors()); 223 | $this->assertTrue($result); 224 | } 225 | } 226 | 227 | // 2. Run tests for invalid input 228 | foreach($badCombinations as $in=>$styles) { 229 | foreach($styles as $style) { 230 | /** @var $parameter Parameter */ 231 | $parameter = Reader::readFromYaml(sprintf($specTemplate, $in, $style) , Parameter::class); 232 | $result = $parameter->validate(); 233 | $this->assertEquals(['A Parameter Object DOES NOT support this serialization style.'], $parameter->getErrors()); 234 | $this->assertFalse($result); 235 | } 236 | } 237 | } 238 | } -------------------------------------------------------------------------------- /tests/spec/RequestBodyTest.php: -------------------------------------------------------------------------------- 1 | validate(); 32 | $this->assertEquals([], $requestBody->getErrors()); 33 | $this->assertTrue($result); 34 | 35 | $this->assertEquals('user to add to the system', $requestBody->description); 36 | $this->assertArrayHasKey("application/json", $requestBody->content); 37 | $this->assertInstanceOf(MediaType::class, $requestBody->content["application/json"]); 38 | 39 | // required Defaults to false. 40 | $this->assertFalse($requestBody->required); 41 | 42 | /** @var $response RequestBody */ 43 | $requestBody = Reader::readFromJson(<<<'JSON' 44 | { 45 | "description": "user to add to the system" 46 | } 47 | JSON 48 | , RequestBody::class); 49 | 50 | $result = $requestBody->validate(); 51 | $this->assertEquals([ 52 | 'RequestBody is missing required property: content', 53 | ], $requestBody->getErrors()); 54 | $this->assertFalse($result); 55 | } 56 | 57 | public function testEncoding() 58 | { 59 | /** @var $requestBody RequestBody */ 60 | $requestBody = Reader::readFromYaml(<<<'YAML' 61 | content: 62 | multipart/mixed: 63 | schema: 64 | type: object 65 | properties: 66 | id: 67 | # default is text/plain 68 | type: string 69 | format: uuid 70 | encoding: 71 | historyMetadata: 72 | # require XML Content-Type in utf-8 encoding 73 | contentType: application/xml; charset=utf-8 74 | profileImage: 75 | # only accept png/jpeg 76 | contentType: image/png, image/jpeg 77 | headers: 78 | X-Rate-Limit-Limit: 79 | description: The number of allowed requests in the current period 80 | schema: 81 | type: integer 82 | other: 83 | style: form 84 | YAML 85 | , RequestBody::class); 86 | 87 | $result = $requestBody->validate(); 88 | $this->assertEquals([], $requestBody->getErrors()); 89 | $this->assertTrue($result); 90 | 91 | $this->assertArrayHasKey("multipart/mixed", $requestBody->content); 92 | $this->assertInstanceOf(MediaType::class, $mediaType = $requestBody->content["multipart/mixed"]); 93 | 94 | $this->assertCount(3, $mediaType->encoding); 95 | $this->assertArrayHasKey("historyMetadata", $mediaType->encoding); 96 | $this->assertArrayHasKey("profileImage", $mediaType->encoding); 97 | $this->assertArrayHasKey("other", $mediaType->encoding); 98 | $this->assertInstanceOf(Encoding::class, $mediaType->encoding["profileImage"]); 99 | $this->assertInstanceOf(Encoding::class, $mediaType->encoding["historyMetadata"]); 100 | $this->assertInstanceOf(Encoding::class, $mediaType->encoding["other"]); 101 | 102 | $profileImage = $mediaType->encoding["profileImage"]; 103 | $this->assertEquals('image/png, image/jpeg', $profileImage->contentType); 104 | $this->assertInstanceOf(\cebe\openapi\spec\Header::class, $profileImage->headers['X-Rate-Limit-Limit']); 105 | $this->assertEquals('The number of allowed requests in the current period', $profileImage->headers['X-Rate-Limit-Limit']->description); 106 | 107 | // When style is form, the default value is true. For all other styles, the default value is false. 108 | $this->assertFalse($profileImage->explode); 109 | $this->assertTrue($mediaType->encoding["other"]->explode); 110 | 111 | // allowReserved default value is false 112 | $this->assertFalse($profileImage->allowReserved); 113 | $this->assertFalse($mediaType->encoding["other"]->allowReserved); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/spec/ResponseTest.php: -------------------------------------------------------------------------------- 1 | validate(); 35 | $this->assertEquals([], $response->getErrors()); 36 | $this->assertTrue($result); 37 | 38 | $this->assertEquals('A complex object array response', $response->description); 39 | $this->assertArrayHasKey("application/json", $response->content); 40 | $this->assertInstanceOf(MediaType::class, $response->content["application/json"]); 41 | 42 | /** @var $response Response */ 43 | $response = Reader::readFromJson(<<<'JSON' 44 | { 45 | "content": { 46 | "application/json": { 47 | "schema": { 48 | "type": "array", 49 | "items": { 50 | "$ref": "#/components/schemas/VeryComplexType" 51 | } 52 | } 53 | } 54 | } 55 | } 56 | JSON 57 | , Response::class); 58 | 59 | $result = $response->validate(); 60 | $this->assertEquals([ 61 | 'Response is missing required property: description', 62 | ], $response->getErrors()); 63 | $this->assertFalse($result); 64 | } 65 | 66 | public function testResponses() 67 | { 68 | /** @var $responses Responses */ 69 | $responses = Reader::readFromYaml(<<<'YAML' 70 | '200': 71 | description: a pet to be returned 72 | content: 73 | application/json: 74 | schema: 75 | $ref: '#/components/schemas/Pet' 76 | default: 77 | description: Unexpected error 78 | content: 79 | application/json: 80 | schema: 81 | $ref: '#/components/schemas/ErrorModel' 82 | YAML 83 | , Responses::class); 84 | 85 | $result = $responses->validate(); 86 | $this->assertEquals([], $responses->getErrors()); 87 | $this->assertTrue($result); 88 | 89 | $this->assertTrue($responses->hasResponse(200)); 90 | $this->assertFalse($responses->hasResponse(201)); 91 | $this->assertTrue($responses->hasResponse('200')); 92 | $this->assertFalse($responses->hasResponse('201')); 93 | $this->assertTrue($responses->hasResponse('default')); 94 | $this->assertTrue(isset($responses[200])); 95 | $this->assertFalse(isset($responses[201])); 96 | $this->assertTrue(isset($responses['200'])); 97 | $this->assertFalse(isset($responses['201'])); 98 | $this->assertTrue(isset($responses['default'])); 99 | 100 | $this->assertCount(2, $responses->getResponses()); 101 | $this->assertCount(2, $responses); 102 | $this->assertInstanceOf(Response::class, $responses->getResponses()[200]); 103 | $this->assertInstanceOf(Response::class, $responses->getResponses()['200']); 104 | $this->assertInstanceOf(Response::class, $responses->getResponses()['default']); 105 | 106 | $this->assertInstanceOf(Response::class, $responses->getResponse(200)); 107 | $this->assertInstanceOf(Response::class, $responses->getResponse('200')); 108 | $this->assertInstanceOf(Response::class, $responses->getResponse('default')); 109 | $this->assertNull($responses->getResponse('201')); 110 | $this->assertInstanceOf(Response::class, $responses[200]); 111 | $this->assertInstanceOf(Response::class, $responses['200']); 112 | $this->assertInstanceOf(Response::class, $responses['default']); 113 | $this->assertNull($responses['201']); 114 | 115 | $this->assertEquals('a pet to be returned', $responses->getResponse('200')->description); 116 | $this->assertEquals('a pet to be returned', $responses['200']->description); 117 | 118 | $keys = []; 119 | foreach($responses as $k => $response) { 120 | $keys[] = $k; 121 | $this->assertInstanceOf(Response::class, $response); 122 | } 123 | $this->assertEquals([200, 'default'], $keys); 124 | } 125 | 126 | public function testResponseCodes() 127 | { 128 | /** @var $responses Responses */ 129 | $responses = Reader::readFromYaml(<<<'YAML' 130 | '200': 131 | description: valid statuscode 132 | '99': 133 | description: invalid statuscode 134 | '302': 135 | description: valid statuscode 136 | '401': 137 | description: valid statuscode 138 | '601': 139 | description: invalid statuscode 140 | '6X1': 141 | description: invalid statuscode 142 | '2X1': 143 | description: invalid statuscode 144 | '2XX': 145 | description: valid statuscode 146 | 'default': 147 | description: valid statuscode 148 | 'example': 149 | description: valid statuscode 150 | YAML 151 | , Responses::class); 152 | 153 | $result = $responses->validate(); 154 | $this->assertEquals([ 155 | 'Responses: 99 is not a valid HTTP status code.', 156 | 'Responses: 601 is not a valid HTTP status code.', 157 | 'Responses: 6X1 is not a valid HTTP status code.', 158 | 'Responses: 2X1 is not a valid HTTP status code.', 159 | 'Responses: example is not a valid HTTP status code.', 160 | 161 | ], $responses->getErrors()); 162 | $this->assertFalse($result); 163 | 164 | } 165 | 166 | public function testCreateionFromObjects() 167 | { 168 | $responses = new Responses([ 169 | 200 => new Response(['description' => 'A list of pets.']), 170 | 404 => ['description' => 'The pets list is gone 🙀'], 171 | ]); 172 | 173 | $this->assertSame('A list of pets.', $responses->getResponse(200)->description); 174 | $this->assertSame('The pets list is gone 🙀', $responses->getResponse(404)->description); 175 | } 176 | 177 | public function badResponseProvider() 178 | { 179 | yield [['200' => 'foo'], 'Response MUST be either an array, a Response or a Reference object, "string" given']; 180 | yield [['200' => 42], 'Response MUST be either an array, a Response or a Reference object, "integer" given']; 181 | yield [['200' => false], 'Response MUST be either an array, a Response or a Reference object, "boolean" given']; 182 | yield [['200' => new stdClass()], 'Response MUST be either an array, a Response or a Reference object, "stdClass" given']; 183 | // The last one can be supported in future, but now SpecBaseObjects::__construct() requires array explicitly 184 | } 185 | 186 | /** 187 | * @dataProvider badResponseProvider 188 | */ 189 | public function testPathsCanNotBeCreatedFromBullshit($config, $expectedException) 190 | { 191 | $this->expectException(\cebe\openapi\exceptions\TypeErrorException::class); 192 | $this->expectExceptionMessage($expectedException); 193 | 194 | new Responses($config); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /tests/spec/SecuritySchemeTest.php: -------------------------------------------------------------------------------- 1 | validate(); 28 | $this->assertEquals([], $securityScheme->getErrors()); 29 | $this->assertTrue($result); 30 | 31 | $this->assertEquals('http', $securityScheme->type); 32 | $this->assertEquals('basic', $securityScheme->scheme); 33 | 34 | /** @var $securityScheme SecurityScheme */ 35 | $securityScheme = Reader::readFromYaml(<<validate(); 41 | $this->assertEquals(['SecurityScheme is missing required property: type'], $securityScheme->getErrors()); 42 | $this->assertFalse($result); 43 | 44 | /** @var $securityScheme SecurityScheme */ 45 | $securityScheme = Reader::readFromYaml(<<validate(); 51 | $this->assertEquals([ 52 | 'SecurityScheme is missing required property: name', 53 | 'SecurityScheme is missing required property: in', 54 | ], $securityScheme->getErrors()); 55 | $this->assertFalse($result); 56 | 57 | /** @var $securityScheme SecurityScheme */ 58 | $securityScheme = Reader::readFromYaml(<<validate(); 64 | $this->assertEquals([ 65 | 'SecurityScheme is missing required property: scheme', 66 | ], $securityScheme->getErrors()); 67 | $this->assertFalse($result); 68 | 69 | /** @var $securityScheme SecurityScheme */ 70 | $securityScheme = Reader::readFromYaml(<<validate(); 76 | $this->assertEquals([ 77 | 'SecurityScheme is missing required property: flows', 78 | ], $securityScheme->getErrors()); 79 | $this->assertFalse($result); 80 | 81 | /** @var $securityScheme SecurityScheme */ 82 | $securityScheme = Reader::readFromYaml(<<validate(); 88 | $this->assertEquals([ 89 | 'SecurityScheme is missing required property: openIdConnectUrl', 90 | ], $securityScheme->getErrors()); 91 | $this->assertFalse($result); 92 | } 93 | 94 | public function testOAuth2() 95 | { 96 | /** @var $securityScheme SecurityScheme */ 97 | $securityScheme = Reader::readFromYaml(<<validate(); 106 | $this->assertEquals([ 107 | 'OAuthFlow is missing required property: scopes', 108 | ], $securityScheme->getErrors()); 109 | $this->assertFalse($result); 110 | 111 | /** @var $securityScheme SecurityScheme */ 112 | $securityScheme = Reader::readFromYaml(<<validate(); 130 | $this->assertEquals([], $securityScheme->getErrors()); 131 | $this->assertTrue($result); 132 | 133 | $this->assertInstanceOf(OAuthFlows::class, $securityScheme->flows); 134 | $this->assertInstanceOf(OAuthFlow::class, $securityScheme->flows->implicit); 135 | $this->assertInstanceOf(OAuthFlow::class, $securityScheme->flows->authorizationCode); 136 | $this->assertNull($securityScheme->flows->clientCredentials); 137 | $this->assertNull($securityScheme->flows->password); 138 | 139 | $this->assertEquals('https://example.com/api/oauth/dialog', $securityScheme->flows->implicit->authorizationUrl); 140 | $this->assertEquals([ 141 | 'write:pets' => 'modify pets in your account', 142 | 'read:pets' => 'read your pets', 143 | ], $securityScheme->flows->implicit->scopes); 144 | } 145 | 146 | public function testSecurityRequirement() 147 | { 148 | /** @var $securityRequirement SecurityRequirement */ 149 | $securityRequirement = Reader::readFromYaml(<<validate(); 155 | $this->assertEquals([], $securityRequirement->getErrors()); 156 | $this->assertTrue($result); 157 | 158 | $this->assertSame([], $securityRequirement->api_key); 159 | 160 | /** @var $securityRequirement SecurityRequirement */ 161 | $securityRequirement = Reader::readFromYaml(<<validate(); 169 | $this->assertEquals([], $securityRequirement->getErrors()); 170 | $this->assertTrue($result); 171 | 172 | $this->assertSame(['write:pets', 'read:pets'], $securityRequirement->petstore_auth); 173 | } 174 | 175 | public function testDefaultSecurity() 176 | { 177 | $openapi = Reader::readFromYaml(<<assertSame([], $openapi->paths->getPath('/path/one')->post->security->getRequirements()); 204 | $this->assertSame(null, $openapi->paths->getPath('/path/two')->post->security); 205 | 206 | $this->assertCount(1, $openapi->security->getRequirements()); 207 | $this->assertSame([], $openapi->security->getRequirement('Bearer')->getSerializableData()); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /tests/spec/ServerTest.php: -------------------------------------------------------------------------------- 1 | validate(); 41 | $this->assertEquals([], $server->getErrors()); 42 | $this->assertTrue($result); 43 | 44 | $this->assertEquals('https://{username}.gigantic-server.com:{port}/{basePath}', $server->url); 45 | $this->assertEquals('The production API server', $server->description); 46 | $this->assertCount(3, $server->variables); 47 | $this->assertEquals('demo', $server->variables['username']->default); 48 | $this->assertEquals('this value is assigned by the service provider, in this example `gigantic-server.com`', $server->variables['username']->description); 49 | $this->assertEquals('8443', $server->variables['port']->default); 50 | 51 | /** @var $server Server */ 52 | $server = Reader::readFromJson(<<validate(); 60 | $this->assertEquals(['Server is missing required property: url'], $server->getErrors()); 61 | $this->assertFalse($result); 62 | 63 | 64 | /** @var $server Server */ 65 | $server = Reader::readFromJson(<<validate(); 79 | $this->assertEquals(['ServerVariable is missing required property: default'], $server->getErrors()); 80 | $this->assertFalse($result); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /tests/spec/TagTest.php: -------------------------------------------------------------------------------- 1 | validate(); 22 | $this->assertEquals([], $tag->getErrors()); 23 | $this->assertTrue($result); 24 | 25 | $this->assertEquals('pet', $tag->name); 26 | $this->assertEquals('Pets operations', $tag->description); 27 | $this->assertNull($tag->externalDocs); 28 | 29 | /** @var $tag Tag */ 30 | $tag = Reader::readFromYaml(<<validate(); 38 | $this->assertEquals(['Tag is missing required property: name'], $tag->getErrors()); 39 | $this->assertFalse($result); 40 | 41 | $this->assertInstanceOf(ExternalDocumentation::class, $tag->externalDocs); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/spec/XmlTest.php: -------------------------------------------------------------------------------- 1 | validate(); 24 | $this->assertEquals([], $xml->getErrors()); 25 | $this->assertTrue($result); 26 | 27 | $this->assertEquals('animal', $xml->name); 28 | $this->assertTrue($xml->attribute); 29 | $this->assertEquals('http://example.com/schema/sample', $xml->namespace); 30 | $this->assertEquals('sample', $xml->prefix); 31 | $this->assertFalse($xml->wrapped); 32 | 33 | /** @var $xml Xml */ 34 | $xml = Reader::readFromYaml(<<validate(); 40 | $this->assertEquals([], $xml->getErrors()); 41 | $this->assertTrue($result); 42 | 43 | // attribute Default value is false. 44 | $this->assertFalse($xml->attribute); 45 | // wrapped Default value is false. 46 | $this->assertFalse($xml->wrapped); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/spec/data/empty-maps.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "test", 5 | "version": "1.0" 6 | }, 7 | "paths": { 8 | "/products": { 9 | "description": "default", 10 | "get": { 11 | "responses": { 12 | "200": { 13 | "description": "Products", 14 | "headers": {}, 15 | "content": {}, 16 | "links": {} 17 | } 18 | } 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /tests/spec/data/path-params/global.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | parameters: 3 | Version: 4 | in: header 5 | name: api-version 6 | required: false 7 | schema: 8 | type: string 9 | format: date 10 | example: '2021-05-18' 11 | description: The API version 12 | OrganizationId: 13 | in: path 14 | name: organizationId 15 | required: true 16 | schema: 17 | type: string 18 | format: uuid 19 | description: The Organization ID 20 | responses: 21 | BadRequest: 22 | description: Bad Request 23 | Forbidden: 24 | description: Forbidden 25 | NotFound: 26 | description: Not Found 27 | Success: 28 | description: Success -------------------------------------------------------------------------------- /tests/spec/data/path-params/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: "2021-05-18" 4 | title: Test REST API 5 | description: Specifications for the Test REST API. 6 | contact: 7 | name: bplainia 8 | email: bplainia@lhespotlight.org 9 | 10 | servers: 11 | - url: 'http://localhost:8000' 12 | description: 'Test' 13 | 14 | paths: 15 | /v1/organizations/{organizationId}/user: 16 | $ref: 'user.yaml#/paths/Users' 17 | /v1/organizations/{organizationId}/user/{id}: 18 | $ref: 'user.yaml#/paths/UserId' -------------------------------------------------------------------------------- /tests/spec/data/path-params/user.yaml: -------------------------------------------------------------------------------- 1 | 2 | paths: 3 | Users: 4 | parameters: 5 | - $ref: 'global.yaml#/components/parameters/Version' 6 | - $ref: 'global.yaml#/components/parameters/OrganizationId' 7 | post: 8 | summary: Creates a user 9 | tags: 10 | - pets 11 | security: 12 | - BearerAuth: [] 13 | requestBody: 14 | required: true 15 | content: 16 | application/json: 17 | schema: 18 | type: object 19 | properties: 20 | data: 21 | $ref: '#/components/schemas/User' 22 | responses: 23 | '201': 24 | description: Created 25 | content: 26 | application/json: 27 | schema: 28 | type: object 29 | properties: 30 | data: 31 | $ref: '#/components/schemas/User' 32 | '400': 33 | $ref: 'global.yaml#/components/responses/BadRequest' 34 | '403': 35 | $ref: 'global.yaml#/components/responses/Forbidden' 36 | UserId: 37 | parameters: 38 | - $ref: 'global.yaml#/components/parameters/Version' 39 | - $ref: 'global.yaml#/components/parameters/OrganizationId' 40 | - $ref: '#/components/parameters/UserId' 41 | get: 42 | summary: Gets a user 43 | security: 44 | - BearerAuth: [] 45 | responses: 46 | '200': 47 | description: A bar 48 | content: 49 | application/json: 50 | schema: 51 | type: object 52 | properties: 53 | data: 54 | $ref: '#/components/schemas/User' 55 | '400': 56 | $ref: 'global.yaml#/components/responses/BadRequest' 57 | '403': 58 | $ref: 'global.yaml#/components/responses/Forbidden' 59 | '404': 60 | $ref: 'global.yaml#/components/responses/NotFound' 61 | components: 62 | schemas: 63 | User: 64 | type: object 65 | properties: 66 | id: 67 | type: string 68 | format: uuid 69 | name: 70 | type: string 71 | parameters: 72 | UserId: 73 | in: path 74 | name: id 75 | required: true 76 | schema: 77 | type: string 78 | format: uuid 79 | description: User's ID 80 | -------------------------------------------------------------------------------- /tests/spec/data/paths/openapi.yaml: -------------------------------------------------------------------------------- 1 | # https://github.com/OAI/OpenAPI-Specification/issues/1961#issuecomment-506026142 2 | openapi: 3.0.2 3 | info: 4 | title: My API 5 | version: 1.0.0 6 | x-extension-1: Extension1 7 | x-extension-2: Extension2 8 | X-EXTENSION: Invalid because of Uppercase X- 9 | xyz-extension: invalid extension 10 | paths: 11 | /foo: 12 | $ref: 'path-items.yaml#/~1foo' 13 | /bar: 14 | $ref: 'path-items.yaml#/~1bar' 15 | components: 16 | responses: 17 | Bar: 18 | description: A bar 19 | content: 20 | application/json: 21 | schema: { type: object } 22 | -------------------------------------------------------------------------------- /tests/spec/data/paths/path-items.yaml: -------------------------------------------------------------------------------- 1 | /foo: # this path item is empty 2 | /bar: 3 | get: 4 | operationId: getBar 5 | responses: 6 | '200': 7 | $ref: 'openapi.yaml#/components/responses/Bar' 8 | '404': 9 | $ref: '#/components/responses/404' 10 | 11 | components: 12 | responses: 13 | 404: 14 | description: non-existing resource 15 | -------------------------------------------------------------------------------- /tests/spec/data/recursion.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "test", 5 | "version": "1.0", 6 | "contact": { 7 | "email": "somebody@example.com" 8 | } 9 | }, 10 | "paths": { 11 | "/product": { 12 | "description": "default" 13 | } 14 | }, 15 | "tags": [ 16 | {"name": "Products"} 17 | ], 18 | "components": { 19 | "schemas": { 20 | "Product": { 21 | "title": "Product", 22 | "description": "A product.", 23 | "type": "object", 24 | "properties": { 25 | "isBundle": { 26 | "description": "Whether this product is a bundle of multiple products.", 27 | "type": "boolean" 28 | }, 29 | "bundleProducts": { 30 | "description": "If `isBundle` is `true`, `bundleProducts` will contain an array of products.", 31 | "oneOf": [ 32 | { 33 | "title": "Market Product", 34 | "type": "array", 35 | "items": { 36 | "$ref": "#/components/schemas/Product" 37 | } 38 | }, 39 | { 40 | "description": "If this is not a bundle of products, `bundleProducts` will be `false`.", 41 | "type": "boolean" 42 | } 43 | ] 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /tests/spec/data/recursion2.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | title: My API 4 | version: "123" 5 | components: 6 | schemas: 7 | SomeResponse: 8 | type: object 9 | properties: 10 | name: 11 | type: string 12 | description: Name of SomeResponse 13 | recursive: 14 | $ref: '#/components/schemas/RecursiveItem' 15 | 16 | AnotherResponse: 17 | type: object 18 | properties: 19 | uuid: 20 | type: string 21 | format: uuid 22 | description: UUID of AnotherResponse 23 | recursive: 24 | $ref: '#/components/schemas/RecursiveItem' 25 | 26 | 27 | RecursiveItem: 28 | type: object 29 | properties: 30 | children: 31 | type: array 32 | items: 33 | oneOf: 34 | - $ref: '#/components/schemas/RecursiveItem' 35 | - $ref: '#/components/schemas/SomeResponse' 36 | 37 | 38 | paths: 39 | '/': 40 | get: 41 | description: default 42 | responses: 43 | 200: 44 | description: ok -------------------------------------------------------------------------------- /tests/spec/data/recursion3_index.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Link Example 4 | version: 1.0.0 5 | #components: 6 | # parameters: 7 | # "Parameter.PetId": 8 | # "$ref": "./subdir/Parameter.PetId.json" 9 | paths: 10 | /contents/menus/{id}/trees: 11 | put: 12 | tags: 13 | - menus 14 | operationId: updateMenuTrees 15 | summary: '123' 16 | description: '456' 17 | # parameters: 18 | # - $ref: '#/components/parameters/PathId' 19 | requestBody: 20 | required: true 21 | content: 22 | application/json: 23 | schema: 24 | $ref: './recursion3_menu_tree.yaml#/UpdateMenuTreesRequest' 25 | responses: 26 | "200": 27 | description: Успешный ответ 28 | content: 29 | application/json: 30 | schema: 31 | $ref: './recursion3_menu_tree.yaml#/UpdateMenuTreesResponse' 32 | -------------------------------------------------------------------------------- /tests/spec/data/recursion3_menu_tree.yaml: -------------------------------------------------------------------------------- 1 | MenuTree: 2 | type: object 3 | properties: 4 | name: 5 | type: string 6 | description: 'Name' 7 | example: 'Home' 8 | url: 9 | type: string 10 | description: Link 11 | nullable: true 12 | example: '/about/' 13 | children: 14 | type: array 15 | items: 16 | $ref: '#/MenuTree' 17 | example: [] 18 | UpdateMenuTreesRequest: 19 | type: object 20 | properties: 21 | items: 22 | type: array 23 | items: 24 | $ref: '#/MenuTree' 25 | UpdateMenuTreesResponse: 26 | type: object 27 | properties: 28 | data: 29 | type: array 30 | items: 31 | $ref: '#/MenuTree' 32 | required: 33 | - data 34 | -------------------------------------------------------------------------------- /tests/spec/data/reference/Food.yaml: -------------------------------------------------------------------------------- 1 | properties: 2 | id: 3 | type: integer 4 | example: 1 5 | name: 6 | type: string 7 | -------------------------------------------------------------------------------- /tests/spec/data/reference/InlineRelativeResolve/Schemas/ChildComponent.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.2", 3 | "info": { 4 | "title": "", 5 | "version": "" 6 | }, 7 | "paths": {}, 8 | "components": { 9 | "schemas": { 10 | "ChildComponent": { 11 | "type": "object", 12 | "properties": { 13 | "foo": { 14 | "type": "string" 15 | } 16 | } 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /tests/spec/data/reference/InlineRelativeResolve/Schemas/Components.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.2", 3 | "info": { 4 | "title": "", 5 | "version": "" 6 | }, 7 | "paths": {}, 8 | "components": { 9 | "schemas": { 10 | "ListOfComponents": { 11 | "description": "", 12 | "type": "object", 13 | "properties": { 14 | "list": { 15 | "$ref": "#/components/schemas/SingleComponent" 16 | } 17 | } 18 | }, 19 | "SingleComponent": { 20 | "description": "", 21 | "type": "object", 22 | "properties": { 23 | "childs": { 24 | "type": "array", 25 | "items": { 26 | "$ref": "./ChildComponent.json#/components/schemas/ChildComponent" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/spec/data/reference/InlineRelativeResolve/sub/dir/Pathfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.2", 3 | "info": { 4 | "title": "", 5 | "version": "" 6 | }, 7 | "paths": { 8 | "/pathexample": { 9 | "get": { 10 | "responses": { 11 | "200": { 12 | "content": { 13 | "application/json": { 14 | "schema": { 15 | "type": "object", 16 | "properties": { 17 | "propertiesWithRefs": { 18 | "type": "array", 19 | "items": { 20 | "$ref": "../../Schemas/Components.json#/components/schemas/ListOfComponents" 21 | } 22 | } 23 | } 24 | } 25 | } 26 | }, 27 | "description": "Example with inline ChildComponents with relative files" 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /tests/spec/data/reference/ReferencedCommonParamsInReferencedPath.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Nested reference with common path params 4 | version: 1.0.0 5 | paths: 6 | /example: 7 | $ref: 'paths/ReferencesCommonParams.yml' 8 | /something: 9 | parameters: 10 | - $ref: './parameters/TestParameter.yml' 11 | x-something: something 12 | get: 13 | responses: 14 | 200: 15 | description: OK if common params can be references 16 | -------------------------------------------------------------------------------- /tests/spec/data/reference/base.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Link Example 4 | version: 1.0.0 5 | components: 6 | schemas: 7 | Pet: 8 | $ref: definitions.yaml#/Pet 9 | Dog: 10 | $ref: ##ABSOLUTEPATH##/definitions.yaml#/Dog 11 | paths: 12 | '/pet': 13 | get: 14 | responses: 15 | 200: 16 | description: return a pet 17 | -------------------------------------------------------------------------------- /tests/spec/data/reference/definitions.yaml: -------------------------------------------------------------------------------- 1 | Pet: 2 | type: object 3 | properties: 4 | id: 5 | type: integer 6 | format: int64 7 | Dog: 8 | type: object 9 | properties: 10 | name: 11 | type: string 12 | food: 13 | $ref: 'Food.yaml' 14 | typeD: 15 | type: string 16 | enum: 17 | - "Four" 18 | - "Five" 19 | -------------------------------------------------------------------------------- /tests/spec/data/reference/models/Cat.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | description: "A Cat" 3 | properties: 4 | id: 5 | type: integer 6 | format: int64 7 | name: 8 | type: string 9 | description: the cats name 10 | pet: 11 | $ref: '../openapi_models.yaml#/components/schemas/Pet' 12 | -------------------------------------------------------------------------------- /tests/spec/data/reference/models/Pet.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | description: "A Pet" 3 | properties: 4 | id: 5 | type: integer 6 | format: int64 7 | cat: 8 | $ref: '../openapi_models.yaml#/components/schemas/Cat' 9 | -------------------------------------------------------------------------------- /tests/spec/data/reference/openapi_models.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Link Example 4 | version: 1.0.0 5 | components: 6 | schemas: 7 | Pet: 8 | $ref: models/Pet.yaml 9 | Cat: 10 | $ref: models/Cat.yaml 11 | paths: 12 | '/pet': 13 | get: 14 | responses: 15 | 200: 16 | description: return a pet 17 | -------------------------------------------------------------------------------- /tests/spec/data/reference/parameters/TestParameter.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | in: header 3 | description: Test parameter to be referenced 4 | required: true 5 | schema: 6 | type: string 7 | enum: [ 'test' ] -------------------------------------------------------------------------------- /tests/spec/data/reference/paths/ReferencesCommonParams.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - $ref: '../parameters/TestParameter.yml' 3 | 4 | x-something: something 5 | get: 6 | responses: 7 | 200: 8 | description: OK if common params can be references 9 | request: 10 | content: 11 | application/json: 12 | examples: 13 | "user": 14 | "summary": "User Example" 15 | "externalValue": "./examples/user-example.json" 16 | "userex": 17 | "summary": "External User Example" 18 | "externalValue": "https://api.example.com/examples/user-example.json" 19 | -------------------------------------------------------------------------------- /tests/spec/data/reference/paths/examples/user-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "user", 3 | "properties": { 4 | "name": "John" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/spec/data/reference/paths/pets.json: -------------------------------------------------------------------------------- 1 | { 2 | "get": { 3 | "parameters": [ 4 | {"$ref": "../subdir.yaml#/components/parameters/Parameter.PetId"} 5 | ], 6 | "responses": { 7 | "200": { 8 | "description": "return a pet", 9 | "content": { 10 | "application/json": { 11 | "schema": {"$ref": "../subdir/Pet.yaml"} 12 | } 13 | } 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/spec/data/reference/playlist.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "test", 5 | "version": "1.0", 6 | "contact": { 7 | "email": "somebody@example.com" 8 | } 9 | }, 10 | "components": { 11 | "schemas": { 12 | "Playlist": { 13 | "type": "object", 14 | "properties": { 15 | "id": { 16 | "type": "integer" 17 | } 18 | } 19 | } 20 | } 21 | }, 22 | "tags": [ 23 | {"name": "Playlist"} 24 | ], 25 | "paths": { 26 | "/playlist": { 27 | "post": { 28 | "operationId": "createPlaylist", 29 | "summary": "create playlist", 30 | "tags": ["Playlist"], 31 | "responses": { 32 | "200": { 33 | "description": "OK" 34 | } 35 | }, 36 | "requestBody": { 37 | "description": "The playlist.", 38 | "required": true, 39 | "content": { 40 | "application/json": { 41 | "schema": { 42 | "$ref": "#/components/schemas/Playlist" 43 | } 44 | } 45 | } 46 | } 47 | } 48 | }, 49 | "/playlist/{id}": { 50 | "patch": { 51 | "operationId": "updatePlaylist", 52 | "summary": "update playlist", 53 | "tags": ["Playlist"], 54 | "parameters": [ 55 | { 56 | "name": "id", 57 | "description": "the playlist id", 58 | "in": "path", 59 | "required": true, 60 | "schema": { 61 | "type": "string" 62 | } 63 | } 64 | ], 65 | "responses": { 66 | "200": { 67 | "description": "OK" 68 | } 69 | }, 70 | "requestBody": { 71 | "$ref": "#/paths/~1playlist/post/requestBody" 72 | } 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /tests/spec/data/reference/structure.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Ref Example 4 | version: 1.0.0 5 | paths: 6 | '/pet': 7 | $ref: 'structure/paths.yml#/~1pet' 8 | '/cat': 9 | $ref: 'structure/paths.yml#/~1cat' 10 | -------------------------------------------------------------------------------- /tests/spec/data/reference/structure/paths.yml: -------------------------------------------------------------------------------- 1 | /pet: 2 | get: 3 | $ref: 'paths/pet.yml#/get' 4 | /cat: 5 | $ref: 'paths/cat.yml' 6 | -------------------------------------------------------------------------------- /tests/spec/data/reference/structure/paths/cat.yml: -------------------------------------------------------------------------------- 1 | get: 2 | responses: 3 | 200: 4 | description: return a cat 5 | -------------------------------------------------------------------------------- /tests/spec/data/reference/structure/paths/pet.yml: -------------------------------------------------------------------------------- 1 | get: 2 | responses: 3 | 200: 4 | description: return a pet 5 | -------------------------------------------------------------------------------- /tests/spec/data/reference/subdir.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Link Example 4 | version: 1.0.0 5 | components: 6 | schemas: 7 | Pet: 8 | $ref: 'subdir/Pet.yaml' 9 | Dog: 10 | $ref: 'subdir/Dog.yaml' 11 | parameters: 12 | "Parameter.PetId": 13 | "$ref": "./subdir/Parameter.PetId.json" 14 | paths: 15 | '/pet': 16 | get: 17 | responses: 18 | 200: 19 | description: return a pet 20 | '/pets': 21 | "$ref": "paths/pets.json" 22 | -------------------------------------------------------------------------------- /tests/spec/data/reference/subdir/Dog.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | name: 4 | type: string 5 | food: 6 | $ref: '../Food.yaml' -------------------------------------------------------------------------------- /tests/spec/data/reference/subdir/Parameter.PetId.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "petId", 3 | "description": "Represents the id of a pet", 4 | "in": "path", 5 | "required": true, 6 | "schema": { 7 | "type": "integer" 8 | }, 9 | "style": "simple" 10 | } 11 | -------------------------------------------------------------------------------- /tests/spec/data/reference/subdir/Pet.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | description: "A Pet" 3 | properties: 4 | id: 5 | type: integer 6 | format: int64 7 | -------------------------------------------------------------------------------- /tests/spec/data/traits-mixins.yaml: -------------------------------------------------------------------------------- 1 | # test whether we can read YAML files that use mixin feature 2 | x-ResourceCommon: &ResourceCommon 3 | uuid: 4 | type: string 5 | description: uuid of the resource 6 | name: 7 | type: string 8 | description: name of the resource 9 | 10 | openapi: 3.0.0 11 | info: 12 | title: Test API 13 | version: 1.0.0 14 | paths: 15 | /foo: 16 | put: 17 | description: create foo 18 | responses: 19 | '200': &RESP_OK 20 | description: request succeeded 21 | headers: &RESP_HEADERS 22 | X-Request-Id: 23 | description: the request id 24 | schema: 25 | $ref: '#/components/schemas/Foo' 26 | '404': &RESP_ERROR 27 | description: resource not found 28 | headers: *RESP_HEADERS 29 | schema: 30 | $ref: "#/components/schemas/Error" 31 | '428': *RESP_ERROR 32 | default: *RESP_ERROR 33 | components: 34 | responses: 35 | Bar: 36 | description: A bar 37 | content: 38 | application/json: 39 | schema: { type: object } 40 | schemas: 41 | Error: 42 | description: This is an error 43 | type: object 44 | properties: 45 | message: 46 | type: string 47 | code: 48 | type: number 49 | format: int64 50 | Foo: 51 | description: A foo resource type 52 | type: object 53 | properties: 54 | <<: *ResourceCommon 55 | id: 56 | type: string 57 | description: id of the foo 58 | description: 59 | type: string 60 | description: description of foo --------------------------------------------------------------------------------