Code Monkey home page Code Monkey logo

openapi-psr7-validator's Introduction

Latest Stable Version Build Status License contributions welcome

NOTICE - THE PACKAGE HAS BEEN CONTRIBUTED TO THE PHP LEAGUE

Go to https://github.com/thephpleague/openapi-psr7-validator

This package is here for existing users only.

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 lezhnev74/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 \OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getServerRequestValidator();
#or
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromYaml(file_get_contents($yamlFile))->getServerRequestValidator();
#or
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromJson(file_get_contents($jsonFile))->getServerRequestValidator();
#or
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromJsonFile($jsonFile)->getServerRequestValidator();
#or
$schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand
$validator = (new \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 \OpenAPIValidation\PSR7\OperationAddress('/some/operation', 'post');

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

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

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

Reuse Schema After Validation

\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 \OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getServerRequestValidator();
# or
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getResponseValidator();

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

Request Message

\Psr\Http\Message\RequestInterface validation is not implemented.

PSR-15 Middleware

PSR-15 middleware can be used like this:

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

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

$slimMiddleware = new \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 \OpenAPIValidation\PSR7\ValidatorBuilder)
    ->fromYamlFile($yamlFile)
    ->setCache($cachePool)
    ->getResponseValidator();
# or
\OpenAPIValidation\PSR15\ValidationMiddleware::fromYamlFile($yamlFile, $cachePool);

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

brayniverse avatar chtombleson avatar cleptric avatar digilist avatar dmytro-demchyna avatar imefisto avatar lezhnev74 avatar panvid avatar samnela avatar scaytrase 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

openapi-psr7-validator's Issues

Missing inner exception message/detail when using AnyOf/OneOf

Not sure that could be considered as a bug or maybe an improvement...

When checking the solution applied for #57 I realised that this library discards the exceptions from the inner validations done with OneOf and AnyOf:

Here, for example src/Schema/Keywords/OneOf.php:

        foreach ($oneOf as $schema) {
            try {
                $schemaValidator->validate($data, $schema, $this->dataBreadCrumb);
                $matchedCount++;
            } catch (SchemaMismatch $e) {
                // that did not match... its ok         <<<<<<<<< HERE >>>>>>>>>>
            }
        }

        if ($matchedCount !== 1) {
            throw KeywordMismatch::fromKeyword('oneOf', $data, sprintf('Data must match exactly one schema, but matched %d', $matchedCount));
        }

so if we have in definition something like:

      properties:
        some_property_here:
          oneOf:
            - type: object
            - type: string
              maxLength: 10
              minLength: 1

And send a request with:

{
    "some_property_here": "String with more than 10 characters"
}

One would expected to see the message:
Keyword validation failed: Data must match exactly one schema, but matched 0 from the OneOf (which is ok and displaying correctly) but also the reason why it didn't match:
Length of '35' must be shorter or equal to 10 but this last message gets discarded when we catch SchemaMismatch $e and don't do anything with it...

So maybe there's a way to display those failed constraints and messages to get more specific errors?

The idea I had was to use the previous feature from exceptions as you're doing already in some places. So here (with some line breaks to make it more readable):

        $prev = null;
        foreach ($oneOf as $schema) {
            try {
                $schemaValidator->validate($data, $schema, $this->dataBreadCrumb);
                $matchedCount++;
            } catch (SchemaMismatch $e) {
                $prev = $e; // that did not match... its ok
            }
        }

...

            throw KeywordMismatch::fromKeyword(
                'oneOf',
                $data,
                sprintf('Data must match exactly one schema, but matched %d', $matchedCount),
                $prev // <<< HERE, there's already this argument in `fromKeyword` method
            );

Of course you need to consider that this catch block might get executed more than once, and you would need to somehow get/group all those inner exceptions (with my example I'm overriding $prev everytime)... idk, maybe we could start with at least 1 specific exception (which is better than none) and implement this multiple exception handling later ๐Ÿคทโ€โ™‚ ๐Ÿ™‚

Issues using readFromYamlFile() on Windows

There is an issue with the underlying cebe/php-openapi library for Windows users that prevents us from calling readFromYamlFile(). @scaytrase has opened an issue on their repo so hopefully it'll be fixed soon. However, this bug is fixed in the underlying library the examples in the README file will not work as described for Windows users.

In order to read the YAML file I had to do something slightly different to the example:

$yamlFile = "api.yaml";

$validator = new \OpenAPIValidation\PSR7\ResponseValidator(
    \cebe\openapi\Reader::readFromYaml(file_get_contents($yamlFile))
);

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

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

Would you like me to submit a PR to update the README examples to use file_get_contents() as a temporary solution?


I have also found that I am unable to run the test suite for the same reason. For reference, this is the error I get:

OpenAPIValidationTests\PSR7\CompleteTest::test_request_green
cebe\openapi\exceptions\UnresolvableReferenceException: Can not resolve references for a specification given as a relative path.

I tried updating the failing test to use file_get_contents() but I get another error:

OpenAPIValidationTests\PSR7\CompleteTest::test_request_green
OpenAPIValidation\PSR7\Exception\Request\RequestBodyMismatch: OpenAPI spec does not match the body of the request [/complete/{param1}/{param2},post]: Argument 1 passed to OpenAPIValidation\Schema\Validator::__construct() must be an instance of cebe\openapi\spec\Schema, instance of cebe\openapi\spec\Reference given

I'll have a crack at getting the test suite to run but I thought I'd bring this up in case it's affecting anyone else.

Get the name of the invalid property

I'm trying to use this library to generate error messages that are visible to the end-user. Thus I need a way to find out the name of the property that's not valid.

As far as I can tell there is no way to this right now. I'm thinking of something like that:

try {
 // Validation
} catch (OpenAPIValidation\PSR7\Exception\Validation\InvalidBody $e) {
    $property = $e->getInvalidPropertyName();
    echo "The field '$property' is not valid. Please correct it and try again."
}

Is something like that possible and if not are there any plans to implement this?

Thanks

Can we access parsed OpenApi schema after validation is done?

Validation logic parses and caches \cebe\openapi\spec\OpenApi internally. It uses it for validation purposes, but this instance (which is already in memory) can be valuable for later use AFTER validation is done.

I have this use case in my head:

  1. We built a validator from the yaml file.
  2. The schema is cached somewhere and used inside.
  3. After validation is complete, can we somehow continue use this finder to maybe find extra details about the matched request or response?
    3.1 RequestValidator returns OperationAddress, maybe we can use it to find extra details in the schema?

In other words, can we access the parsed \cebe\openapi\spec\OpenApi from the validator after the validation is done?

Content-Type:application/json with charset

So i'm trying out your project, and i'm a huge fan.

Well i'm designing a restful api, and i'm testing my responses.
While i was doing so, i found out that your body validator, doesn't like if the content-type is having a charset. This is kind of the standard of the framework that i'm using for the restapi.

As seen in the documentation from mozilla, there it's fully allowed to have both charset, and boundry as a parameter. link

Symptoms

When the response is validated, it's not taken as json, even though application/json is in the content-type. Therefore, the response-body is not json decoded and taken as a string instead.

Example

application/json; charset=UTF-8

Suggested fix

/PSR7/Validators/Body.php:43 change '#^application/.*json$#' to '#^application/.*json#'
Now i know, this isn't bulletproof because it's not ending up with json, as the last word, but it allows a the charset string to be implementet.
I have though tested this and it now

Validation of callbacks

Some way to validate the request and response of callbacks would be great.

I think the validation per se would work, but there needs to be some other way to resolve the specs. Right now, the specs are loaded based on a passed OperationAddress which doesn't fit for callbacks.

"Required" option for header parameters is ignored

Request validator ignores "required" option for header parameters, and treats all such parameters as mandatory, opposed to OpenAPI Specification https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#parameterObject.

required boolean Determines whether this parameter is mandatory. If theย parameter locationย is "path", this property isย REQUIREDย and its value MUST beย true. Otherwise, the property MAY be included and its default value isย false.

[Feature] Incapsulate exceptions

Currently not only library exceptions are being thrown while validation, but also respect/validation library exception comes to the end user like

All of the required rules must pass for 18497

(expecting int[] here, not int)

It's very hard to manually process these exceptions and enrich their messages with things like validation data path where the error occured and also make end user create complex try..catch blocks.

I suggest to rethrow these exceptions and thus totally isolate end user from the used validation library knownledge. This will make library usage easier and allows further refactoring without being bond to the validation library because of exception contracts.

Also, at the moment, respect/validation exceptions should be part of validate method signature, since they are thrown and should be catched

Automatically casts numeric strings to integer even if spec says it's a string

This issue is closely related to #58 .

I have a path parameter that is a string. Though, if I pass a string that is numeric such as "123" you automatically cast it to an integer:

https://github.com/lezhnev74/openapi-psr7-validator/blob/master/src/PSR7/OperationAddress.php#L98

This leads to the validation failing because the parameter is expected to be a string but it isn't because you cast it to an integer.

You should probably check if the specification says that the parameter is a string and if so don't cast it to an integer.

Add body validation for "multipart/form-data" requests

Currently, the body of the request is validated as a string:

$body = (string) $message->getBody();

This does not work with schema validation at the moment. The problem is that we CAN specify multipart body format with a schema.

Review and decide how to change that.

Refs:

Exceptions consistency

Here I will create a list of inconsistent exceptions or exception hierarchy:

  • (https://github.com/lezhnev74/openapi-psr7-validator/blob/master/src/Schema/Keywords/Type.php#L113)[FormatMismatch] exception is derived from \LogicException, which according to the manual happens only if code logic is broken and there is a bug or misbehaviour in the library
  • Same stands for ValidationKeywordFailed
  • I'm not sure there is sense in catching any \Throwable since it too broad (i.e it would catch \ParseError and convert it to KeywordValidationFailed too)
  • Most other exceptions are derived from \RuntimeException, like MissedRequestCookie
  • MultipleOperationsMismatchForRequest extends Exception this looks correct but misaligned with other exceptions :)

Notes: \LogicException and \RuntimeException are kind of "Unchecked exceptions" analogue in PHP and they are often excluded from IDE static analisys by default

Collaboration

Hey there! I think there is a great chance for you folks to collaborate here. I am opening up the conversation here as it has the most stars, but it could be anywhere. They are all good implementations.

These packages all do the same thing.

  1. Read a JSON or YAML file
  2. Allow general PSR-7/PSR-15 middleware to be created for this file
  3. Offer at least request validation (maybe response too)
  4. Offer errors in various formats (RFC 7807 or others)
  5. Support custom formats

I think you could all team up on a new @thephpleague project. I bet they'd take you if you all worked together, I can provide some guidance if needed, and then you just archive things on GitHub, mark them abandoned on Packagist, and the mega-package lives on forever despite whatever hurdles life throw at you individually.

LMK if you like the sound of this. OpenAPI can be just too much for single maintainers to handle solo. ๐Ÿคทโ€โ™‚

Library does not play well with references

Given schema with reference and valid JSON input

        $yaml = /** @lang yaml */
            <<<'YAML'
openapi: 3.0.0
info:
  title: Product import API
  version: '1.0'
servers:
  - url: 'http://localhost:8000'
paths:
  /products.create:
    post:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                test: 
                  $ref: "#/components/schemas/TestObject" 
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                properties:
                  result: 
                    type: string
components:
  schemas:
    TestObject:
      type: object
      required:
        - input
      properties:
        input:
          type: string
          example: 5201f887-e28a-45df-ad93-bde1021bae11
YAML;

        $cebeSchema = CebeReader::readFromYaml($yaml);
        $validator = new ServerRequestValidator($cebeSchema);

        $psrRequest = new ServerRequest(
            'POST',
            'http://localhost:8000/products.create',
            [
                'Content-Type' => 'application/json',
            ],
            <<<JSON
{
    "test": {
        "input": "some data"
    }
}
JSON
        );


            $validator->validate($psrRequest);

I expect validator to pass this object.

Actual results:

OpenAPI spec does not match the body of the request [/products.create,post]: All of the required rules must pass for { "test": `[object] (cebe\openapi\spec\Reference: { })` }

Because of https://github.com/lezhnev74/openapi-psr7-validator/blob/master/src/Schema/Keywords/Properties.php#L64
(each validator has this line)

If I comment out these lines and add the following hack

        $cebeSchema = CebeReader::readFromYaml($yaml);
+       $cebeSchema->setReferenceContext(new ReferenceContext($cebeSchema, '/'));
        $validator = new ServerRequestValidator($cebeSchema);

And add

            if ($schema instanceof Reference) {
                $schema = $schema->resolve();
            }

To each validator this starts working better, but it ends up here

https://github.com/lezhnev74/openapi-psr7-validator/blob/master/src/Schema/Keywords/Required.php#L61

since property could be a reference to a schema, not a real property.

Manually resolving all references before validation helps. but this looks too complicated to end user to know about manual references resolution

        $cebeSchema = CebeReader::readFromYaml($yaml);
        $cebeSchema->setReferenceContext(new ReferenceContext($cebeSchema, '/'));
+        $cebeSchema->resolveReferences();
        $validator = new ServerRequestValidator($cebeSchema);

Validation returns OperationAddress - just wondering why?

I'm trying a simple bit of validation:

$myJsonSpec = '{...}';
$serverRequest = // PSR-7 message constructed by hand
$validator = \OpenAPIValidation\PSR7\ServerRequestValidator::fromJson($myJsonSpec);
$result = $validator->validate($serverRequest);

If all validates correctly I get this object returned:

OpenAPIValidation\PSR7\OperationAddress

For example:

object(OpenAPIValidation\PSR7\OperationAddress)#47 (2) {
  ["method":protected]=>
  string(3) "get"
  ["path":protected]=>
  string(15) "/ccodes/{ccode}"
}

The method and the path are keys to the operation, so I'm wondering whether it would be more useful to return the operation? The operationId could then be used to fire off further parsing of the server request (I'm looking at generated code to do this). I may be missing the reason for returning OperationAddress though.

Without knowing the operationId at this point, further work needs to be done to decode the method and path to find it. It may already be available somewhere I'm not just looking.

Also, the validate() method either returns this object, or throws an exception. The exception is just for one of what may be many validation errors. Is there a way to find all the validation errors rather than just reporting on one? Looking at the validator, I can see it scatters _errors across many of the nodes of its internal data structures, so I'm guessing it does scan for multiple validation errors at once.

multipart with fields without content type won't pass validation

In #44 support for multipart request was improved. If you send fields with files (which have content-type), it works ok. But if a field without content type is included, an exception will be thrown:

Message: preg_match() expects parameter 2 to be string, null given
File: /app/vendor/lezhnev74/openapi-psr7-validator/src/PSR7/Validators/BodyValidator/MultipartValidator.php
Line: 178

Added a quick fix in fork

Tries to validate content even if no content was specified

Hi,

When deleting a resource and the response has no content, the ResponseValidator still tries to validate the body:

// 2. Validate Body
try {
    $bodyValidator = new Body();
    $bodyValidator->validate($response, $spec->content);
} catch (Throwable $e) {
    ...

If my OAS response is defined like this:

delete:
  summary: Delete a resource
  responses:
    '204':
      description: No content

In the previous code sample $spec->content is an empty array.

The Body validator subsequently tries to get the Content-Type header which doesn't exist, and throws a NoContentType exception.

I think the whole body validation should be skipped if no content's been defined in the OAS specification (which is valid as per Swagger's doc).

EDIT: Or maybe still check whether there's a body despite the fact that no content was specified in the OAS spec, which I guess should throw an exception.

Caching of parsed OpenAPI definitions

This is more a question than anything. Hopefully it is covered somewhere.

So, for every inbound request to an API, the payload needs to be validated against an OpenAPI definition. That definition needs to be parsed/lexed/whatever into some form that can be used to do the validation. That will take time and resources to do.

So, is that parsed form of the OpenAPI definition cached? Can it even be cached? Does the full parsing happen on every API call, maybe it's not as resource intensive as I think it may be (but judging by the speed of client-based OpenAPI editors once a 3.0 definition goes above a few hundred lines, I would assume it is fairly intense).

I haven't tried it yet, but it looks ideal for a project I'm working on now.

Needless dependency on symfony/yaml

Hi,

Related to #65 and idea of moving this package to php league.

I personally believe that in order to spread good practices and allow as many folks as possible to use openapi validation, we should focus on having as little dependencies as possible.

Currently, this package uses cebe/php-openapi which forces installation of symfony/yaml component. This indirectly forces a minimal version of Symfony (version >=4 I believe) and cuts off some potential developers. IMO input source (json/yaml) should be separated from parsing library. It should be up to developer to parse yaml/json (symfony/yaml could be a suggestion in composer.json) and then pass array or stdClass to cebe/php-openapi. This may seem like a bit of extra effort from developer but it will result in less dependencies and higher adoption.

Handle JSON:API's application/vnd.api+json content type

Hi,

I'm currently trying out your package to run functional tests against an OpenAPI schema in a Laravel application.

We're following the JSON:API specification for our API, meaning the Content-Type and Accept headers are expected to be application/vnd.api+json.

But in the Body validator, the body will only be JSON-decoded if the Content-Type header is application/json.

Would it be possible to change the regex to also handle application/vnd.api+json?

Or are we expected to extend this behaviour somehow?

I'd also like to seize this opportunity to thank you for you work and encourage you to push it further - this is definitely headed in the right direction and very much needed by the PHP community!

Thanks,

Yannick

Composite validation rules' related exceptions are lost

Hi,

The InvalidSchema exception (and maybe others) will only display the wrapping error and omit the related ones when the error is a composite one (such as Respect/Validation's AllOf).

The issue with this is we don't know what the errors actually are, which makes it really hard to troubleshoot, e.g:

OpenAPIValidation\PSR7\Exception\Response\ResponseBodyMismatch: OpenAPI spec does not match the body of the response [/api/test,get,200]: Schema(or data) validation failed: All of the required rules must pass for "{\"data\":\"test\"}"

This doesn't tell you what the actual (related) exceptions are.

Hope that makes sense!

Thanks,

Yannick

Bug with numeric strings

I have in definition something like:

    patch:
      summary: Do Something
      description: Do Something
      operationId: someOperation
      parameters:
        - name: id
          in: path
          description: ID of something
          required: true
          schema:
            type: string

and this throws me an error when the request contains a numeric string (which is possible):

Argument 2 passed to OpenAPIValidation\\PSR7\\Exception\\Validation\\InvalidPath::becauseValueDoesNotMatchSchema() must be of the type string, integer given, called in /srv/app/vendor/lezhnev74/openapi-psr7-validator/src/PSR7/Validators/PathValidator.php on line 56

That happens because on lezhnev74/openapi-psr7-validator/src/PSR7/Validators/PathValidator.php:

        foreach ($pathParsedParams as $name => $value) {
            try {
                $validator->validate($value, $specs[$name]->schema);
            } catch (SchemaMismatch $e) {
                throw InvalidPath::becauseValueDoesNotMatchSchema($name, $value, $addr, $e);
            }
        }

$value is integer; you're parsing parameters with:

            // cast numeric
            if (is_numeric($value)) {
                $value += 0; // that will cast it properly
            }

on lezhnev74/openapi-psr7-validator/src/PSR7/OperationAddress.php
but then on lezhnev74/openapi-psr7-validator/src/PSR7/Exception/Validation/InvalidPath.php you're type hinting with string:

    public static function becauseValueDoesNotMatchSchema(string $parameterName, string $parameterValue, OperationAddress $address, SchemaMismatch $prev) : self
    {
        $exception          = static::fromAddrAndPrev($address, $prev);
        $exception->message = sprintf('Value "%s" for parameter "%s" is invalid for %s', $parameterValue, $parameterName, $address);

so maybe you don't cast it or remove type hinting from InvalidPath?

Improve urlencoded body parsing

When you have an urlencoded body with arrays, and the field is required according to the specification, validation won't pass.

I've added an array field to the message in tests (BodyValidatorTest::dataProviderFormUrlencodedGreen): (phones[0]=123-456&phones[1]=456-789. When urlencoded, the full message will be:

address=Moscow%2C+ulitsa+Rusakova%2C+d.15&id=59731930-a95a-11e9-a2a3-2a2ae2dbcce4&phones%5B0%5D=123-456&phones%5B1%5D=456-789

After current body parsing (FormUrlencodedValidator::parseUrlencodedData), it will result in:

Array
(
[address] => Moscow%2C+ulitsa+Rusakova%2C+d.15
[id] => 59731930-a95a-11e9-a2a3-2a2ae2dbcce4
[phones%5B0%5D] => 123-456
[phones%5B1%5D] => 456-789
)

If you have declared "phones" as a required field, validation will fail because this field is not present in the parsed body.

Relative paths in servers objects

When url field in server object containts relative path, for example "/v2" or "/", validator can't match request with operation definition.

For example, if we have definition:

servers:
  - url: '/v1'
paths:
  /operations:
    post:
...

and try to validate POST request "http://localhost/v1/operations", we get an exception "OpenAPI spec contains no such operation [/operations,post]". But if we specify server url as "http://localhost/v1", everything works fine.

If I understand it correctly, there is no possibility to match any request, when server path is relative (or if "servers" section is omitted altogether, and default value is one server with url="/").

From OpenAPI documentaion it is not very clear what to do in this case
https://swagger.io/docs/specification/api-host-and-base-path/#relative-urls
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#serverObject

The URLs in the servers array can be relative, such as /v2. In this case, the URL is resolved against the server that hosts the given OpenAPI definition.

Should validator ignore server name from request, when path is relative? Or somehow get "server that hosts the given OpenAPI definition" and match it to server name from request?

Do you have any suggestions?

Invalid invalid type handling

If data type does not match schema code still tries to utilize it as a scalar value. moreother it tries utilize value itself rather than type, and this can lead to huge error texts

notice: "Array to string conversion in /data/www/content.leos.lamoda.ru/vendor/lezhnev74/openapi-psr7-validator/src/Schema/Keywords/Type.php on line 81"

I'll try to fix it

Bug with oneOf and catch

If you have your definition with something like:

    SomeObjectHere:
      type: object
      properties:
        some_property_here:
          oneOf:
            - type: object
            - type: string
              maxLength: 50
              minLength: 1

You're supposed to accept requests like:

{
    "some_property_here": {}
}

and

{
    "some_property_here": "Some Value Here"
}

The second requests works fine but the first one complains that { } must be a string, because on lezhnev74/openapi-psr7-validator/src/Schema/Keywords/OneOf.php:

        foreach ($oneOf as $schema) {
            try {
                $schemaValidator->validate($data, $schema, $this->dataBreadCrumb);
                $matchedCount++;
            } catch (SchemaMismatch $e) {
                // that did not match... its ok
            }
        }

you're supposed to catch the exceptions that validate might throw, ignore them and count it. But for this scenario you're not catching the exception with the MaxLength:

        try {
            Validator::stringType()->assert($data);
            Validator::intType()->assert($maxLength);
            Validator::trueVal()->assert($maxLength >= 0);
        } catch (ExceptionInterface $e) {
            throw InvalidSchema::becauseDefensiveSchemaValidationFailed($e);
        }

and you're checking MaxLength for {} (which is not string) so Validator::stringType()->assert($data) fails, throwing InvalidSchema and you're catching only SchemaMismatch so either you do the checks for maxLength, minLength etc. only for correspondent types (iirc you can only apply certain constraints to certain types and you're checking all constraints for all types) or you add |InvalidSchema on OneOf and AnyOf.

For now I worked around this issue using AnyOf which processes until any criteria is valid and placing type: object before type: string so that it gets evaluated first.

Validator ignores base uri path

Given openapi spec with
Server base uri http://localhost:8000/api/v1
POST operation /products.create

I expect POST request http://localhost:8000/api/v1/products.create to be valid

Actual result

OpenAPI spec contains no such operation [/api/v1/products.create,post]
        $yaml = /** @lang 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:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              properties:
                test:
                  type: integer
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                properties:
                  result: 
                    type: string
YAML;

        $validator = new ServerRequestValidator(CebeReader::readFromYaml($yaml));

        $psrRequest = new ServerRequest(
            'POST',
            'http://localhost:8000/api/v1/products.create'
        );

        $validator->validate($psrRequest);

According to https://swagger.io/docs/specification/api-host-and-base-path/ having base url path prefix is totally valid

Validating non string query parameters

Hello,

I'm trying to validate a request containing two query parameters.

image

Upon doing so i am receiving a RequestQueryArgumentMismatch due to the query arguments of the request not being a type 'integer', and a 'string' given.

As of version 3.0 of openapi, query parameters can be specified as an integer along with other non string data types, i would have thought the PSR-7 validator would be able to look at a query parameter and check if it is an integer within a string.

https://swagger.io/docs/specification/describing-parameters/

Query string parameter validation of Arrays with `style: form` and `explode: false`

Consider this swagger parameter definition (in YAML):

    -
        name: filter
        in: query
        description: Filter something
        explode: false
        style: form
        schema:
            type: array
            items:
                type: string

According to the Open Api Specification this will result in a query parameter ?filter=a,b,c (which is also generated by Swagger UI)

However, the validation does not consider a URL of that format as an array, but as a string:

OpenAPI spec does not match the query argument 'with' of the request [/test,get]: Value expected to be 'array', 'string' given.

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.