GithubHelp home page GithubHelp logo

thephpleague / openapi-psr7-validator Goto Github PK

View Code? Open in Web Editor NEW
508.0 23.0 90.0 746 KB

It validates PSR-7 messages (HTTP request/response) against OpenAPI specifications

License: MIT License

Shell 0.51% PHP 99.49%

openapi-psr7-validator's Introduction

Latest Stable Version Build Status License contributions welcome

OpenAPI PSR-7 Message (HTTP Request/Response) Validator

This package can validate PSR-7 messages against OpenAPI (3.0.x) specifications expressed in YAML or JSON.

Installation

composer require league/openapi-psr7-validator

OpenAPI (OAS) Terms

There are some specific terms that are used in the package. These terms come from OpenAPI:

  • specification - an OpenAPI document describing an API, expressed in JSON or YAML file
  • data - actual thing that we validate against a specification, including body and metadata
  • schema - the part of the specification that describes the body of the request / response
  • keyword - properties that are used to describe the instance are called key words, or schema keywords
  • path - a relative path to an individual endpoint
  • operation - a method that we apply on the path (like get /password)
  • response - described response (includes status code, content types etc)

How To Validate

ServerRequest Message

You can validate \Psr\Http\Message\ServerRequestInterface instance like this:

$yamlFile = "api.yaml";
$jsonFile = "api.json";

$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getServerRequestValidator();
#or
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYaml(file_get_contents($yamlFile))->getServerRequestValidator();
#or
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromJson(file_get_contents($jsonFile))->getServerRequestValidator();
#or
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromJsonFile($jsonFile)->getServerRequestValidator();
#or
$schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromSchema($schema)->getServerRequestValidator();

$match = $validator->validate($request);

As a result you would get and OperationAddress $match which has matched the given request. If you already know the operation which should match your request (i.e you have routing in your project), you can use RouterRequestValidator

$address = new \League\OpenAPIValidation\PSR7\OperationAddress('/some/operation', 'post');

$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromSchema($schema)->getRoutedRequestValidator();

$validator->validate($address, $request);

This would simplify validation a lot and give you more performance.

Request Message

You can validate \Psr\Http\Message\RequestInterface instance like this:

$yamlFile = "api.yaml";
$jsonFile = "api.json";

$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getRequestValidator();
#or
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYaml(file_get_contents($yamlFile))->getRequestValidator();
#or
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromJson(file_get_contents($jsonFile))->getRequestValidator();
#or
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromJsonFile($jsonFile)->getRequestValidator();
#or
$schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromSchema($schema)->getRequestValidator();

$match = $validator->validate($request);

Response Message

Validation of \Psr\Http\Message\ResponseInterface is a bit more complicated . Because you need not only YAML file and Response itself, but also you need to know which operation this response belongs to (in terms of OpenAPI).

Example:

$yamlFile = "api.yaml";
$jsonFile = "api.json";

$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getResponseValidator();
#or
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYaml(file_get_contents($yamlFile))->getResponseValidator();
#or
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromJson(file_get_contents($jsonFile))->getResponseValidator();
#or
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromJsonFile($jsonFile)->getResponseValidator();
#or
$schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromSchema($schema)->getResponseValidator();

$operation = new \League\OpenAPIValidation\PSR7\OperationAddress('/password/gen', 'get') ;

$validator->validate($operation, $response);

Reuse Schema After Validation

\League\OpenAPIValidation\PSR7\ValidatorBuilder reads and compiles schema in memory as instance of \cebe\openapi\spec\OpenApi. Validators use this instance to perform validation logic. You can reuse this instance after the validation like this:

$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getServerRequestValidator();
# or
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getResponseValidator();

/** @var \cebe\openapi\spec\OpenApi */
$openApi = $validator->getSchema();

PSR-15 Middleware

PSR-15 middleware can be used like this:

$yamlFile = 'api.yaml';
$jsonFile = 'api.json';

$psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromYamlFile($yamlFile)->getValidationMiddleware();
#or
$psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromYaml(file_get_contents($yamlFile))->getValidationMiddleware();
#or
$psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromJsonFile($jsonFile)->getValidationMiddleware();
#or
$psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromJson(file_get_contents($jsonFile))->getValidationMiddleware();
#or
$schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand
$validator = (new \League\OpenAPIValidation\PSR7\ValidationMiddlewareBuilder)->fromSchema($schema)->getValidationMiddleware();

SlimFramework Middleware

Slim framework uses slightly different middleware interface, so here is an adapter which you can use like this:

$yamlFile = 'api.yaml';
$jsonFile = 'api.json';

$psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromYamlFile($yamlFile)->getValidationMiddleware();
#or
$psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromYaml(file_get_contents($yamlFile))->getValidationMiddleware();
#or
$psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromJsonFile($jsonFile)->getValidationMiddleware();
#or
$psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromJson(file_get_contents($jsonFile))->getValidationMiddleware();
#or
$schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand
$validator = (new \League\OpenAPIValidation\PSR7\ValidationMiddlewareBuilder)->fromSchema($schema)->getValidationMiddleware();

$slimMiddleware = new \League\OpenAPIValidation\PSR15\SlimAdapter($psr15Middleware);

/** @var \Slim\App $app */
$app->add($slimMiddleware);

Caching Layer / PSR-6 Support

PSR-7 Validator has a built-in caching layer (based on PSR-6 interfaces) which saves time on parsing OpenAPI specs. It is optional. You enable caching if you pass a configured Cache Pool Object to the static constructor like this:

// Configure a PSR-6 Cache Pool
$cachePool = new ArrayCachePool();

// Pass it as a 2nd argument
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)
    ->fromYamlFile($yamlFile)
    ->setCache($cachePool)
    ->getResponseValidator();
# or
$psr15Middleware = (new \OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)
    ->fromYamlFile($yamlFile)
    ->setCache($cachePool)
    ->getValidationMiddleware();

You can use ->setCache($pool, $ttl) call for both PSR-7 and PSR-15 builder in order to set proper expiration ttl in seconds (or explicit null)

If you want take control over the cache key for schema item, or your cache does not support cache key generation by itself you can ->overrideCacheKey('my_custom_key') to ensure cache uses key you want.

Standalone OpenAPI Validator

The package contains a standalone validator which can validate any data against an OpenAPI schema like this:

$spec = <<<SPEC
schema:
  type: string
  enum:
  - a
  - b
SPEC;
$data = "c";

$spec   = cebe\openapi\Reader::readFromYaml($spec);
# (optional) reference resolving
$spec->resolveReferences(new ReferenceContext($spec, "/"));
$schema = new cebe\openapi\spec\Schema($spec->schema);

try {
    (new \League\OpenAPIValidation\Schema\SchemaValidator())->validate($data, $schema);
} catch(\League\OpenAPIValidation\Schema\Exception\KeywordMismatch $e) {
    // you can evaluate failure details
    // $e->keyword() == "enum"
    // $e->data() == "c"
    // $e->dataBreadCrumb()->buildChain() -- only for nested data
}

Custom Type Formats

As you know, OpenAPI allows you to add formats to types:

schema:
  type: string
  format: binary

This package contains a bunch of built-in format validators:

  • string type:
    • byte
    • date
    • date-time
    • email
    • hostname
    • ipv4
    • ipv6
    • uri
    • uuid (uuid4)
  • number type
    • float
    • double

You can also add your own formats. Like this:

# A format validator must be a callable
# It must return bool value (true if format matched the data, false otherwise)

# A callable class:
$customFormat = new class()
{
    function __invoke($value): bool
    {
        return $value === "good value";
    }
};

# Or just a closure:
$customFormat = function ($value): bool {
    return $value === "good value";
};

# Register your callable like this before validating your data
\League\OpenAPIValidation\Schema\TypeFormats\FormatsContainer::registerFormat('string', 'custom', $customFormat);

Exceptions

The package throws a list of various exceptions which you can catch and handle. There are some of them:

  • Schema related:
    • \League\OpenAPIValidation\Schema\Exception\KeywordMismatch - Indicates that data was not matched against a schema's keyword
      • \League\OpenAPIValidation\Schema\Exception\TypeMismatch - Validation for type keyword failed against a given data. For example type:string and value is 12
      • \League\OpenAPIValidation\Schema\Exception\FormatMismatch - data mismatched a given type format. For example type: string, format: email won't match not-email.
  • PSR7 Messages related:
    • \League\OpenAPIValidation\PSR7\Exception\NoContentType - HTTP message(request/response) contains no Content-Type header. General HTTP errors.
    • \League\OpenAPIValidation\PSR7\Exception\NoPath - path is not found in the spec
    • \League\OpenAPIValidation\PSR7\Exception\NoOperation - operation os not found in the path
    • \League\OpenAPIValidation\PSR7\Exception\NoResponseCode - response code not found under the operation in the spec
    • Validation exceptions (check parent exception for possible root causes):
      • \League\OpenAPIValidation\PSR7\Exception\ValidationFailed - generic exception for failed PSR-7 message
      • \League\OpenAPIValidation\PSR7\Exception\Validation\InvalidBody - body does not match schema
      • \League\OpenAPIValidation\PSR7\Exception\Validation\InvalidCookies - cookies does not match schema or missing required cookie
      • \League\OpenAPIValidation\PSR7\Exception\Validation\InvalidHeaders - header does not match schema or missing required header
      • \League\OpenAPIValidation\PSR7\Exception\Validation\InvalidPath - path does not match pattern or pattern values does not match schema
      • \League\OpenAPIValidation\PSR7\Exception\Validation\InvalidQueryArgs - query args does not match schema or missing required argument
      • \League\OpenAPIValidation\PSR7\Exception\Validation\InvalidSecurity - request does not match security schema or invalid security headers
    • Request related:
      • \League\OpenAPIValidation\PSR7\Exception\MultipleOperationsMismatchForRequest - request matched multiple operations in the spec, but validation failed for all of them.

Testing

You can run the tests with:

vendor/bin/phpunit

Contribution Guide

Feel free to open an Issue or add a Pull request. There is a certain code style that this package follows: doctrine/coding-standard.

To conform to this style please use a git hook, shipped with this package at .githooks/pre-commit.

How to use it:

  1. Clone the package locally and navigate to the folder
  2. Create a symlink to the hook like this: ln -s -f ../../.githooks/pre-commit .git/hooks/pre-commit
  3. Add execution rights: chmod +x .git/hooks/pre-commit
  4. Now commit any new changes and the code will be checked and formatted accordingly.
  5. If there are any issues with your code, check the log here: .phpcs-report.txt

Credits

People:

Resources:

  • Icons made by Freepik, licensed by CC 3.0 BY
  • cebe/php-openapi package for Reading OpenAPI files
  • slim3-psr15 package for Slim middleware adapter

License

The MIT License (MIT). Please see License.md file for more information.

TODO

  • Support Discriminator Object (note: apparently, this is not so straightforward, as discriminator can point to any external scheme)

openapi-psr7-validator's People

Contributors

bizurkur avatar brayniverse avatar camilledejoye avatar canvural avatar chtombleson avatar ckilb avatar ctipggroup avatar dabernathy89 avatar deegital avatar derrabus avatar devizzent avatar digilist avatar dmachehin avatar dmytro-demchyna avatar garethellis36 avatar hansott avatar identity-labs avatar imefisto avatar jimdelois avatar kentaroutakeda avatar lezhnev74 avatar m0003r avatar nektru avatar panvid avatar rogervila avatar scaytrase avatar sunkan avatar sunspikes avatar wolffc avatar yarre avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

openapi-psr7-validator's Issues

Please support respect/validation 2.1+

Hi,
Is it possible to support newer versions of respect/validation library?
I'm using ^2.1 version of respect/validation in my code and found a conflict when trying to require openapi-psr7-validator.
I'm aware that respect/validation requires php 7.3 while this requires php 7.2, maybe an upgrade of php version could be useful here too.
Thanks,
Frank

A package to validate HttpFoundation responses against OpenAPI definitions, built upon yours

Hey guys,

Once more, not an issue, more like a request for feedback :)

For a couple of years now I've been using your package in Laravel projects to validate the responses returned by API endpoints against an OpenAPI definition, as part of integration tests.

Laravel uses Symfony's HttpFoundation component under the hood for its HTTP messages, so what I'd do is I'd use Symfony's PSR-7 Bridge to convert the HttpFoundation responses to PSR-7 messages first, and then use your package to validate the responses.

That resulted in the same classes that I'd copy over again and again in all of my projects, so I've decided to make a package out of it, to make my life easier and also because I think it could be useful to other people, seeing how much HttpFoundation is used in major projects and frameworks.

Here is the package: https://github.com/osteel/openapi-httpfoundation-validator

It's not published yet, as I would very much like your feedback on it first: is this the kind of project you expected people to build on top of yours? Does it make sense to you? Can you spot anything obviously wrong I'd be doing?

The package itself is rather simple, as it's made of a couple of classes only, and not huge ones. Hopefully a quick look at the README file will make it clear how it's supposed to be used.

If you've got a few spare minutes to have a look, that'd be awesome :)

No worries if not though!

Cheers,

Yannick

allOf doesn't work with $ref

Whe using allOf to compose a reference with a single object, the object is ignored. Futhermore, writing a failing unit test for this issue isn't possible either, because the schema validator requires all allOf components to be a cebe\openapi\spec\Schema and doesn't allow cebe\openapi\spec\Reference.

False positive for missing required property

I am pretty new to OpenAPI so pardon me if I am totally of.

My first POC with this library was to remove the required property from the request but to my surprise the validation passed for the request.

Take for example the registration endpoint

OpenAPI schema:

  requestBody:
    content:
      application/json:
        schema:
          type: object
          properties:
            email:
              type: string
              format: email
              required: true
              example: [email protected]
            password:
              type: string
              example: $superSecret42

Request

{
  "password": "fooBar123"
}

See, no e-mail, yet the validation passes.

This line is to blame imho:

\League\OpenAPIValidation\Schema\Keywords\Properties::validate

if (! array_key_exists($propName, $data)) {
  continue;
}

Should it not throw an exception instead of just continuing?

Optionally validate entire schema before throwing exception

Currently when an error occurs (mismatch between data and schema) validation stops at the first error.

It would be useful to carry on validating the data to return as many errors at once as possible (e.g. so a consumer can be sent the errors from their request and they can address them all at once, or we can validate our response as it is sent and log any mismatches with our schema).

The snippet below shows the issue, the exception only includes details of the first issue (type error for title field) but doesn't include details of the second error (keyword mismatch due to the presence of dummy field, additional properties are not allowed).

use cebe\openapi\Reader as OpenApiReader;
use cebe\openapi\spec\Schema as OpenApiSchema;
use League\OpenAPIValidation\Schema\SchemaValidator;

$spec = <<<SPEC
schema:
  type: object
  additionalProperties: false
  required:
    - title
  properties:
    title:
      type: string
    aNumber:
      type: integer
      nullable: true
SPEC;

$spec = OpenApiReader::readFromYaml($spec);
$schema = new OpenApiSchema($spec->schema);

$data = [
    'title' => 23,
    'dummy' => 'blah'
];

try {
    (new SchemaValidator(SchemaValidator::VALIDATE_AS_REQUEST))->validate($data, $schema);
} catch(\League\OpenAPIValidation\Schema\Exception\KeywordMismatch $e) {
    // you can evaluate failure details
    // $e->keyword() == "enum"
    // $e->data() == "c"
    // $e->dataBreadCrumb()->buildChain() -- only for nested data
    var_dump($e->keyword());
    var_dump($e->data());
    var_dump($e->dataBreadCrumb());
    throw $e;
}

I realise that this would be quite a backwards compatibility break if this functionality always happened, but it could possibly be set as an option on the SchemaValidator?

If it's agreed that this would be useful functionality to have then I'm happy to work on a PR for it.

`findHeaderSpecs` fails when trying to find headers that have parameters with reference

Coming from the discussion here.

If a spec has header parameters with references, and references are not solved when creating the schema, findHeaderSpecs will fail with error Undefined property: cebe\openapi\spec\Reference::$in

Failing test case
public function testHeaderParametersWithReference(): void
    {
        $yaml = <<<'YAML'
openapi: 3.0.0
info:
  title: Product import API
  version: '1.0'
servers:
  - url: 'http://localhost:8000/api/v1'
paths:
  /products.create:
    post:
      parameters:
        - $ref: '#/components/parameters/appIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              properties:
                url:
                  type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                properties:
                  result: 
                    type: string
components:
  parameters:
    appIdHeader:
      schema:
        type: integer
      in: header
      required: true
      name: X-APP-ID
      description: App id used to identify request.
YAML;

        $schema     = (new ValidatorBuilder())
            ->setSchemaFactory(new PrecreatedSchemaFactory(Reader::readFromYaml($yaml)))
            ->getServerRequestValidator()
            ->getSchema();
        $specFinder = new SpecFinder($schema);

        $spec = $specFinder->findHeaderSpecs(new OperationAddress('/products.create', 'post'));

		// No assertions yet, cause above line fails
    }

Wrong exception message in Schema\Keyword\MinLength

// Schema/Keyword/MinLength.php
if (mb_strlen($data) < $minLength) {
    throw KeywordMismatch::fromKeyword(
        'minLength',
        $data,
        sprintf("Length of '%d' must be longer or equal to %d", $data, $minLength)
    );
}

This generates a "wrong" exception message (Length of '0' must be longer or equal to 8)

The first placeholder should be %s.

Beginner mistakes,

Hello, I'm testing the league / openapi-psr7-validator. Unfortunately I have a problem.
I originally use the Swagger Yaml file from https://editor.swagger.io.

My script:

$yamlFile = "path/to/my/petshop_swagger.yaml";
$yamlFileContent = file_get_contents($yamlFile);

$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getServerRequestValidator();

I get the error message:

Fatal error: Uncaught Error: Call to a member function setReferenceContext() on array in C:\xampp1\htdocs\swagger-php\vendor\cebe\php-openapi\src\spec\Reference.php on line 177

What is wrong ?

Your sincerly Stephan

Bug FormUrlencodedValidator::parseUrlencodedData

There is unstable behavior in class FormUrlencodedValidator, when it is getting body of the request, that can be already read by other middleware.
A similar case is covered in UnipartValidator by read body without calling getContent method.

Valid URIs are rejected if they are not a URL

The class StringURI uses filter_var($value, FILTER_VALIDATE_URL) !== false to validate properties of type string and format uri. This means that a value which is a valid URI but not a URL (e.g. auth:invalid) fails validation.

I think the StringURI could be updated to use the league/uri package instead. Would you be open to a PR for this?

Support PHP 8

Hi,

PHP 8 will be released at the end of this month. It'd be cool if we can add support for it.

I began to investigate what needs to be done to support PHP 8. Most packages we depend on have releases that support PHP 8. Here are the non-compatible packages and issues I found:

That's all I guess. What do you think?

Recursive references in openapi schema crashes the validator

This part of the schema copied from my API which causes the schema resolution go in infinite loop and eventually crash the application

openapi: 3.0.0
info:
  title: 'API'
  version: '1.0'
servers:
  -
    url: 'https://localhost/v1'
    description: Dev
paths:
  /userGroup/:
    get:
      tags:
        - user
      summary: 'Get all user groups'
      operationId: userGroupGetCollection
      parameters:
        -
          name: limit
          in: query
          description: 'Limit the number of entries in the collection (default=10)'
          required: false
          schema:
            type: integer
            format: int32
        -
          name: offset
          in: query
          description: 'Retrieve entries of the collection at this offset (default=0)'
          required: false
          schema:
            type: integer
            format: int32
      responses:
        '200':
          description: 'user group collection response'
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Collection' }
                  - { properties: { entries: { type: array, items: { $ref: '#/components/schemas/UserGroup' } } }, type: object }
                  - { properties: { _embedded: { properties: { '/user/{userId}': { $ref: '#/components/schemas/User' } }, type: object } }, type: object }
components:
  schemas:
    Collection:
      required:
        - limit
        - count
        - offset
        - entries
      properties:
        limit:
          type: integer
          minimum: 0
        count:
          type: integer
          minimum: 0
        offset:
          type: integer
          minimum: 0
        entries: {  }
        _embedded: {  }
      type: object
      additionalProperties: false
    User:
      title: 'User model'
      required:
        - id
      properties:
        _embedded:
          properties:
            '/userGroup/{userGroupId}':
              $ref: '#/components/schemas/UserGroup'
            '/user/{userId}':
              $ref: '#/components/schemas/User'
          type: object
        id:
          type: integer
          minimum: 1
      type: object
      additionalProperties: false
    UserGroup:
      title: 'User group model'
      required:
        - id
        - name
      properties:
        _embedded:
          properties:
            '/user/{userId}':
              $ref: '#/components/schemas/User'
          type: object
        id:
          type: integer
          minimum: 1
        name:
          type: string
          minLength: 1
          nullable: true
      type: object
      additionalProperties: false

Here the the User and UserGroup models have cross references and this causes the schema resolver to go in indefinite recursion.

A solution to this would be: #5

validation exception - how to differentiate between request and response validation exceptions?

I was following the examples in the readme and I realized that the middleware might have a flaw: there is no way to differentiate between validation exception that are thrown when validating the request (where I would like to return a 400 to the caller) and validation exceptions happening when the middleware is validating the response (where I would like to return a 500, since there is nothing the caller could do to fix it from their side).

Here an example that illustrates what do I mean:

$psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromYamlFile($yamlFile)->getValidationMiddleware();

$stack-add($psr15Middleware)

try {
      $response = $stack->handle($request);
} catch (\League\OpenAPIValidation\PSR7\Exception\Validation\InvalidBody $e) {
     # is this failing because of the request? or the response ?
}

Of course I can solve this by defining my own middleware that depends on the validator, but I'm wondering if am I not using it as intended? or was this overlooked?

Dictionary's are not currently supported although openapi v3 supports them

It seems OpenAPI v3 supports dictionaries as shown here: https://swagger.io/docs/specification/data-models/dictionaries/

Here is an example valid schema:

type: object
additionalProperties: false
properties:
  identifiers:
    description: Identifiers
    type: array
    items:
      type: string
      additionalProperties:
        type: string
      example:
        ean: "5039314069422"

The above currently doesn't work due to the ArrayHelper::isAssoc call here: https://github.com/thephpleague/openapi-psr7-validator/blob/master/src/Schema/Keywords/Type.php#L49

I'll try and have a go at creating a PR for this! I'll let you know how I get on

Thanks for the great package btw!

Date-time format should allow optional milliseconds

Currently the validator fails any date-times that contain a milliseconds component, such as those strings produced by Javascript's Date.prototype.toISOString() (eg. 2019-10-11T08:03:43.461Z).

The current Open API v3 spec says that strings using the 'date-time' format should follow RFC 3339, section 5, which clearly allows for this fractional component.

References:

image

InvalidQueryArgs::becauseValueDoesNotMatchSchema always expect query arguments to be a string

The function \League\OpenAPIValidation\PSR7\Exception\Validation\InvalidQueryArgs::becauseValueDoesNotMatchSchema(string $argumentName, string $argumentValue, OperationAddress $address, SchemaMismatch $prev) always expects $argumentValue to have type string but that may not be the case for all query arguments, e.g. for the url /api/fields[0]=id&fields[1]=name, the query parameter will be type an array

Invalid URI characters in password or username break $this-path

$this->path = (string) parse_url($uri, PHP_URL_PATH);

Pathfinder uses the Slim URIInterface in parse_url.
Problem is if you have a header with an Authorized field that contains a password with ? or $ or #, UtiInterface will build a Uri that cannot be directly parsed by parse_url. It needs to be pct-encoded first.

It's not a slim bug because Uri is meant to return a decoded URL.
It's not a php bug because parse_url (per doc) is meant to work with raw encoded HTTP URIs

Either work with the URIInterface object directly (instead of a string from it) or reencode an URL that parse_url can use before a call to PathFinder.

Or am I missing something?
Thanks

Spec Finder Not Always Finding

I found that SpecFinder as it was trying to find paths based on pure string matching which doesnt work if the path contains paramters

this PR fixes that: #40

Bad performance in the PathFinder

Hello,
First thanks for the great work you've done here.

I wanted to use this library to validate the requests/responses of our API within our codeception tests.
During my experimentation with this project I noticed a performance issue related to the PathFinder service.
I can't share my swagger.yaml but I did my tests with a 2MB file.

The issue is that searching a path is too long, this is because of the PathFinder::findServersForOperation() method.
More specifically the instruction $this->openApiSpec->getSerializableData() which takes in average 0.8s.

The method PathFinder::search() which trigger the previous calls is used in multiple places which result in an average time of 4s to validate both the request and the response.

Possible solution:
When investigating the issue I realized that this part seems unnecessary:

        // 3. Check servers on root level
        if (array_key_exists('servers', (array) $this->openApiSpec->getSerializableData()) &&
            count($this->openApiSpec->servers) > 0) {
            return $this->openApiSpec->servers;
        }

        // fallback
        return [new Server(['url' => '/'])];

Indeed in the OpenApi class, the servers property is declared with a default value of [new Server(['url' => '/'])], just as the fallback you used.
Therefore there is no need to test if the key actually exists neither as to ensure there is at least 1 server defined and this could simply be:

        return $this->openApiSpec->servers;

I'm not familiar with the OAS3 specification nor as I am with the cebe/php-openapi library, so I didn't want to open a PR without discussing it here first.
I don't think my proposition will have any impact since the SpecBaseObject::getSerializableData() method does not affect the state of the object.

Configure `additionalProperties` validation strategy

Hi, I would like to provide stricter validation by forcing additionalProperties to false in all schemas without requiring its presence. My current project has a large specification (hundreds of endpoints and even more schemas) and I need to thoroughly validate API contract and gradually adjust OpenAPI specification.

I can submit a PR, can you advise on what would be a good approach here? I see that Properties class has $additionalProperties constructor parameter which is then used exactly the way I need when its false. From what I see Properties::validate() is called only once in SchemaValidator line 136 and the value comes directly from the schema. Unfortunately, BodyValidator and other validators are created as ValidatorChain directly in RequestValidator and ResponseValidator which rules out customization through injecting custom options. SchemaValidator is created directly in BodyValidator as well.

What would be a good way to inject custom behaviour into these classes?

Regular expresions in path

Hi guys,

Do we have a way to describe an endpoint with paths with a variable number of segments? For example, I have a route declared with this:

'/open/{fullpath:.*}'

This let me to process routes like:

/open/some-file.ext
/open/some-folder/some-file.ext
/open/some-folder/some-folder2/some-file.ext
etc

While I don't have problems with the code, the validator doesn't allow this:

"OpenAPI spec contains no such operation [/open/some-folder/some-file.ext,get]"

Digging in the spec, I found this comment where a "/seg1/seg2/*" is mentioned:

OAI/OpenAPI-Specification#892 (comment)

Regards.

simple example openapi-psr7-validator

Hello,
i start to deal with the open
api-psr7-validator.

Is there any example for using the
openapi-psr7-validator ?

With friendly greetings

Stephan

Support validation using separate OpenAPI files

Hi, I have an OpenAPI description split to several top-level files (ie. openapi, info, paths, etc.) for each application module. Each file then imports paths (per resource) and schemas from other files in subdirectories. These files also contain top-level OpenAPI structure but are used only as JSON files to be imported elsewhere. Since the complete description contains tens of thousands of lines it is not feasible to merge them back. We validate request and response contract while running E2E scenarios so matching each file to a certain path is also not a viable solution.

From what I see ValidatorBuilder can be created using a single file only. Are there any plans on adding support for multiple OpenAPI files? In my case, these files do not overlap, but validating duplicate schemas names and paths would be an added benefit. Is there any other viable approach for that issue given the current state of this library? Thanks in advance for your reply.

Empty Content-Type generate a TypeError

When we try to validate a request with Content-Type set to empty string the system throw the error:

Return value of League\OpenAPIValidation\PSR7\Validators\BodyValidator\BodyValidator::messageContentType() must be of the type string or null, boolean returned

This happens because the function strtok can return a boolean.

This function may return Boolean FALSE, but may also return a non-Boolean value which evaluates to FALSE. Please read the section on Booleans for more information. Use the === operator for testing the return value of this function.

See https://www.php.net/manual/en/function.strtok.php#refsect1-function.strtok-notes for details.

Move code to the namespace under `League\`

The league packages tend to use this root namespace, so we should too. Should be pretty straightforward thing with help of IDE.

If no one volunteers to do that, I will this weekend.

Validating readOnly properties in query body

If I have defined in schema object readonly propety
id:
type: integer
description: Unique identifier
readOnly: true

then validator don't throw exception when this property is defined in body json object.

Spec says:
Relevant only for Schema "properties" definitions. Declares the property as "read only". This means that it MAY be sent as part of a response but SHOULD NOT be sent as part of the request. If the property is marked as readOnly being true and is in the required list, the required will take effect on the response only. A property MUST NOT be marked as both readOnly and writeOnly being true. Default value is false.

$validatorBuilder->getServerRequestValidator()->validate($psr17Request);

and request body is

[parsedBody:Nyholm\Psr7\ServerRequest:private] => Array
(
[id] => 5
[first_name] => name
)

AM I missing something?

Show which attribute is referenced in KeywordMismatch exception

Information

Package version: 0.12.1
PHP 7.4

Test case:

Consider the following attribute object:

+ Attributes (object)
    + username (string, required)
    + type (string, required)

With a response body, for example:

{
  "username": "whoami",
  "type": null
}

Currently when validating using above will throwing the following exceptios

League\OpenAPIValidation\PSR7\Exception\Validation\InvalidBody::class
League\OpenAPIValidation\Schema\Exception\KeywordMismatch::class

this will only show the following exception message:

"Keyword validation failed: Value cannot be null"

It is not clear which attribute caused this based on the message.
I found that there is no way to get the attribute name from the exception (or previous exception),
because the BreadCrumb class doesn't expose $compoundIndex which in my case holds the attribute name.

Suggestion

I suggest to expose $compountIndex value with a getter method within BreadCrumb class.

Path Validator Not Handling Missing Params

Firstly - thanks for this great library!

Found a fairly easy to fix issue though I think:

It's quite easy to get an undefined index error at

https://github.com/thephpleague/openapi-psr7-validator/blob/master/src/PSR7/Validators/PathValidator.php#L53

if you have not done written your spec properly, ie you have a path with a {param} but haven't actually added the param annotation to the request spec

This seems like an easy issue to catch and throw a useful exception as is being done elsewhere

I'd be happy to give it a go as a PR

Example use cases of when validating requests is useful?

Hi,

This might sound a bit weird, but I can't really think of a reason why I'd want to validate a request.

Validating responses makes a lot of sense to me โ€“ you've got an API, an OpenAPI definition describing it, and you want to make sure your API endpoints return responses which conform to the OpenAPI definition.

But the requests?

I might be missing something really obvious here, but hopefully you know of some example use cases that will make it click for me.

Cheers,

Yannick

Deprecation HTTP Header

User story.

As an API developer, I don't need to mark an endpoint as deprecated in OpenAPI and in my API code by emitting a HTTP Deprecated header.

Is your feature request related to a problem?

Currently setting deprecated: true only updates documentation, but what if it also emitted headers from the API? This falls slightly outside of validation, but talking to an endpoint which might vanish soon seems like something people should know about, and this package is about helping developers avoid rewriting stuff in code that's already handled nicely in OpenAPI...

Describe the solution you'd like

When a request comes in to an operation has deprecated: true, emit a Deprecated HTTP header.

Deprecated: true

Seeing as OpenAPI does not offer any other information, like a date or replacement information, we cannot do anything more than this, but this is enough to let developers know that something is up (if they're looking for it, but that is a whole other story).

Upcoming changes to reference resolving

Hi all,

just wanted to let you know about the changes I made to reference resolving.

We had this issue popping up before, where references are only partially resolved. These should be fixed by cebe/php-openapi#39 which will be in version 1.3 (will be released tomorrow if all works well).

So if you bump the required version in composer.json to 1.3, calling Reader::readFromYamlFile() will automaticall resolve all references in the context of the file (absolute Path or URL required) and calling resolveReferences() manually is not necessary anymore.

Trying to Import Library Question

Hey all, I'm pretty new to php and 3rd party libraries. I installed the library with composer and then try to import the library but I can't seem to get it to work just wondering if someone can help me with this newbie question. I'm using PHP 7.2

Here's the import

require_once("vendor/league/openapi-psr7-validator/src/PSR7/ValidatorBuilder.php");

$test_json_string = "{\"maximum\":\"100\", \"type\":\"int\"}";
$test_value = "20";

$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromJson($test_json_string)->getResponseValidator();

And I'm getting Fatal error: Uncaught Error: Class 'League\OpenAPIValidation\PSR7\SchemaFactory\JsonFactory' not found in /test_openapi/vendor/league/openapi-psr7-validator/src/PSR7/ValidatorBuilder.php:83

Migrate from travis-ci to github action

Why

Because travis-ci.org is closing and travis-ci.com wont provide free build minutes for open source projects in a simple way.

Alternatives

  1. Just rewrite the .travis.yml as a github action
  2. Use a github action like ktomk/run-travis-yml

I think the best solution is 1. because we have a very simple .travis.yml file

I can put together a pull-request if you want?

Unable to Differentiate Between Bad Paths and Bad Methods on Good Paths

Given something like the following (the rethrown exceptions are part of the local framework):

        try {
            $this->requestValidator->validate($request);
        } catch (Exception\NoOperation $e) {
            throw new HttpException\HttpMethodNotAllowedException($request, 'Method Not Allowed', $e);
        } catch (Exception\NoPath $e) {
            throw new HttpException\HttpNotFoundException($request, 'Not Found', $e);
        }

... It seems that whatever I catch first catches everything. That is to say, a bad path on the case above yields a 405 (unexpected), as does a bad method (expected). If I reverse the two catches, the opposite occurs... A bad path on the case yields a 404 (expected) and a bad method yields a 405 (unexpected).

The only way that's possible, based on one exception extending the other, is if the NoOperation is the only thing thrown in all cases.

From your own doc:

\League\OpenAPIValidation\PSR7\Exception\NoPath - path is not found in the spec
\League\OpenAPIValidation\PSR7\Exception\NoOperation - operation os not found in the path

I have not yet dug into the internals, but the documentation very strongly suggests that these are different outcomes, which would be in alignment with the HTTP spec. Evidently they are not.

It seems a pretty significant issue that one cannot differentiate something that should be bubbled as a 404 vs. a 405. There also seems to be something that isn't working with the NoContentType that I can't possible trigger in tests.

Can someone educate me on how to work with these different exceptions such that I can appropriately recast them up to the client? Is it merely a documentation issue that I'm not seeing? If I've overlooked something, can anyone provide some input?

Any assistance is much appreciated!! I'm assuming there's a shortcoming with myself, but if there's actually something to be improved within the library then perhaps I could eventually attempt to patch it.

Multipart form multiple file uploads

I seem to have run into an issue that seems to go against what I can find in online documentation.

According to the documentation on swagger's website on multiple file uploads, you need to specify a parameter as an array of binary strings.

However, this package says that is incorrect.

Instead, I get Body does not match schema for content-type \"multipart\/form-data\" for Request [put \/clam\/scan]:

image

However, if I set it to a singular binary string (not an array), it works with no issue:

image

I used exactly the same syntax that is provided in that documentation, except with the name "upload" instead of filename.

I'm not sure if swagger is wrong here, or if this validator is, but one of them is incorrect.

For my project, it's not a big deal, but something worth mentioning.

PathFinder is not managing refs as individual paths properly

The spec allows this case:

Some main file:

openapi: 3.0.2

paths:
  /foo:
    $ref: './referenced-path.yaml#/foo'

referenced-path.yaml:

foo:
  get:
    description: Foo

Buth path finder fails with:

League\OpenAPIValidation\PSR7\Exception\NoOperation: OpenAPI spec contains no such operation [/foo,get]

Investigating, I've found that the problem is in PathFinder. When a reference is added, the operation in cebe/openapi/Reader for path ends with a spec with an empty array "servers":

This is "print_r($path->getSerializableData());" on src/PSR7/PathFinder.php, added in method findServersForOperation after the first line:

stdClass Object
(
    [get] => stdClass Object
        (
            [description] => Foo
        )

    [servers] => Array
        (
        )

    [parameters] => Array
        (
        )

)

This empty array "servers" is causing the PathFinder returns an empty array for findServersForOperation, which causes that the loop in PathFinder::search looking for candidates do not found a matching one.

The proper behaviour is that if "servers" is not specificied, it should fallback to one with "url: /" as PathFinder does when references are not used.

Although I'm not sure if this should be handled on our side, I've added a PR to address this. I guess the alternative should be that, after using cebe/openapi/Reader, the spec should not have an empty array for servers if the yaml do not have a "servers" config. This would be consistent with the Reader's behaviour when references are not used.

Support PSR-16 cache

I think it would be nice if this lib would support not only PSR-6 but also PSR-16 caches.
I'm willing to provide a pull request for this feature.

Unexpected @OA\JsonContent() must be nested

Hi,

just upgraded to Unexpected @OA\JsonContent() must be nested v0.10.

Now I receive this message.
Unexpected @OA\JsonContent() must be nested
May be I am blind - I can't understand the problem.

Schema:

/**
 * @Schema(
 *     schema="WeatherList",
 *     description="List of WeatherElements",
 *     type="array",
 *     items=@JsonContent(ref="#/components/schemas/WeatherElement"),
 * ),
 *
 * @Schema(
 *     schema="WeatherElement",
 *     type="object",
 *     @Property(property="weekday", type="string", example="Donnerstag"),
 *     @Property(property="state", type="string", example="bedeckt"),
 *     @Property(property="low_temp", type="number", example="1"),
 *     @Property(property="high_temp", type="number", example="1"),
 *     @Property(property="icon", type="string", example="bedeckt.png"),
 * ),
Controller:
/**
     * List weather information
     * @Rest\Route("/api/services/weather", methods={"GET"}, name="api_services_weather"),
     *
     * @Get(path="/api/services/weather", tags={"Services"},
     *
     *     @Response(response="200", description="Returned when request successful",
     *          @JsonContent(ref="#/components/schemas/WeatherList")
     *     ),
     * )
     */

Any idea about how to fix that? Thanks in advance.

Validating dictionaries

Hello everyone,
from my understanding, a dictionary can set a type inside additionalProperties to define the type of values. So in the example below it should only allow string-string key-value pairs.

{
  "type": "object",
  "additionalProperties": {
    "type": "string"
  }
}

However, the validator does not validate whether the properties are of the specified type. E.g. following validates without error even though it should:

{ 
   "one":"1",
   "two":"2",
   "three":{ 
      "four":"4"
   }
}

From what I can tell from debugging is that it only does a type check on $data (if it's an object), but does not check the individual properties if they are the specified type defined in additionalProperties. Is it a bug/missing feature? I could also be very wrong so feel free to correct me.

P.s. thank you for all the hard work you have put into this tool, it's great!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.