GithubHelp home page GithubHelp logo

mdwheele / laravel-openapi Goto Github PK

View Code? Open in Web Editor NEW
20.0 3.0 2.0 73 KB

Let's bring OpenAPI-driven routing, request and response validation to Laravel!

License: MIT License

PHP 88.95% Dockerfile 11.05%
laravel openapi json-schema

laravel-openapi's Introduction

OpenAPI-driven routing and validation for Laravel

Latest Version on Packagist PHP from Packagist Total Downloads CircleCI

This package allows you to create a single specification for your Laravel application that will register routes and validate all requests your API receives as well as all responses that you generate.

The OpenAPI development experience in PHP feels disjoint...

  • I can update my OpenAPI specification with no impact on the actual implementation, leaving room for drift.
  • I can try and glue them together with process and custom tooling, but I feel like I'm gluing 9,001 pieces of the internet together and it's different for each project. I'd prefer if someone else to do that work.
  • Documentation generators are AMAZING, but if there's nothing to stop implementation from drifting away from documentation, then is it worth it?
  • Tooling to validate JSON Schema is great, but the error messages I get back are hard to grok for beginners and aren't always obvious.

This package aims to create a positive developer experience where you truly have a single source of record, your OpenAPI specification. From this, the package will automatically register routes with Laravel. Additionally, it will attach a Middleware to these routes that will validate all incoming requests and outgoing responses. When the package detects a mismatch in implementation and specification, you'll get a helpful error message that hints at what to do next.

Installation

You can install the package through Composer.

composer require mdwheele/laravel-openapi

Optionally, you can publish the config file of this package with this command:

php artisan vendor:public --provider="Mdwheele\OpenApi\OpenApiServiceProvider"

The following config file will be published in config/openapi.php:

<?php

return [

    /*
     * The path to your OpenApi specification root document.
     */
    'path' => env('OPENAPI_PATH'),

    /*
     * Whether or not to validate response schemas. You may want to
     * enable this in development and disable in production. Do as you
     * wish!
     */
    'validate_responses' => env('OPENAPI_VALIDATE_RESPONSES', true)

];

Usage

Configure OPENAPI_PATH to point at your top-level specification. The package will parse your OpenAPI specification to create appropriate routes and attach the ValidateOpenApi middleware. The middleware validates all requests coming to your API as well as all responses that you generate from your Controllers. If the middleware encounters a validation error, it will throw an OpenApiException, which will have a summary error message along with a bag of detailed errors describing what's wrong (as best as we can).

It is a good idea to incorporate this into your normal exception handler like so:

class Handler extends Exception Handler
{
    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Exception  $exception
     * @return \Illuminate\Http\Response
     */
    public function render($request, Exception $exception)
    {
        // This is only an example. You can format this however you 
        // wish. The point is that the library gives you easy access to 
        // "what went wrong" so you can react accordingly.
        if ($exception instanceof OpenApiException) {
            return response()->json([
                'message' => $exception->getMessage(),
                'errors' => $exception->getErrors(),
            ], 400);
        }

        return parent::render($request, $exception);
    }
}

When you generate a response that doesn't match the OpenApi schema you've specified, you'll get something like the following:

{
  "message": "The response from CoreFeaturesController@healthCheck does not match your OpenAPI specification.",
  "errors": [
    "The [status] property must be one of [ok, warning, critical].",
    "The [updates[0].timestamp] property is missing. It must be included.",
    "The property unsupported is not defined and the definition for [updates[0]] does not allow additional properties."
  ]
}

As a further example, check out the following API specification.

openapi: "3.0.0"
info:
  version: 1.0.0
  title: Your Application
servers:
  - url: https://localhost/api
paths:
  /pets:
    get:
      summary: List all pets
      operationId: App\Http\Controllers\PetsController@index
      responses:
        '200':
          description: An array of Pets.
          content:
            application/json:
              schema:
                type: array
                items: 
                  $ref: '#/components/schemas/Pet'
components:
  schemas:
    Pet:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string

This specification says that there will be an endpoint at https://localhost/api/pets that can receive a GET request and will only return responses with a 200 status code. Those successful responses will return application/json that contains an array of JavaScript objects that MUST have both an id (that is an integer) and a name (that can be any string).

Any of the following circumstances will trigger an OpenApiException that will include more information on what's needed in order to resolve the mismatch between your implementation and the OpenAPI specification you've designed:

  • If you return 403 response from /api/pets, you'll get an exception that explains that there is no specified response for 403 and there is no default handler.
  • If you return anything other than application/json, you'll get a similar exception explaining the acceptable media types that can be returned.
  • If you return JavaScript objects that use a string-based id (e.g. id: 'foo'), you'll be told that the response your controller generated does not match the specified JSON Schema. Additionally, you'll be given some pointers as to what, specifically, was wrong and some hints on how to resolve.

Caution!

๐Ÿ”‡ Opinion Alert ... and feel free to take with grain of salt.

Just as over-specifying tests can leave a bad taste in your mouth, over-specifying your API can lead you down a path of resistance and analysis paralysis. When you're using JSON Schema to specify request bodies, parameters and responses, take care to understand that you are specifying valid HTTP messages, not necessarily every business rule in your system.

For example, I've seen many folks get stuck with "But @mdwheele! I need to have conditional responses because when X happens, I need one response. But when Y happens, I need a totally different response.". My advice on this is to write tests ๐Ÿ˜€. What this library does for you is allows confidence to not have to write tons of structural tests just to make sure everything is under a top-level data envelope; that filter is allowed as a query parameter, etc.

Another way to think of this is the difference between "form validation" and "business invariants". There is an overlap many times, but the goals are different. Form validation (or OpenAPI specification validation) says "Do I have a valid HTTP message?" while business rules are more nuanced (e.g. "Is this user approved to create purchase orders totaling more than $5,000?").

Roadmap

  • Continue to improve error messages to be as helpful as possible. In the mean time, use the package and if it's ever unclear how to respond to an error message, send it in as a bug.
  • Add additional specification examples to guarantee we're casting a wide net to accommodate as many use-cases as possible.
  • Improve framework integration error handling.

Testing

composer test

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security related issues, please email [email protected] instead of using the issue tracker.

Credits

License

The MIT License (MIT). Please see License File for more information.

laravel-openapi's People

Stargazers

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

Watchers

 avatar  avatar  avatar

laravel-openapi's Issues

Latest package not published in composer

The composer installed package still has
Reader::readFromYamlFile(config('openapi.spec'))
Which is somewhat behind and that key does not exist in the openapi.php

In composer lock:

            "name": "mdwheele/laravel-openapi",
            "version": "0.1.0",
            "source": {
                "type": "git",
                "url": "https://github.com/mdwheele/laravel-openapi.git",
                "reference": "12302330623340ba7028c1736f7edc34c2128316"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/mdwheele/laravel-openapi/zipball/12302330623340ba7028c1736f7edc34c2128316",
                "reference": "12302330623340ba7028c1736f7edc34c2128316",
                "shasum": ""
            },

Likely because of missing version change.

Use Server Object to determine the Laravel Route prefix?

Currently, we hard-code the API prefix to be /api. This means that any specified Paths Object is prefixed with /api when registered with the framework. I kinda see 3 ways forward:

  1. Don't prefix anything. Don't do any magic. Use exactly what is in the Paths Object to determine routes.

    This is absolutely ship-able right now and might be better than doing a hard-coded magic prefix. It's also verbose and doesn't take advantage of the Server Object specification.

  2. Use a package configuration option (e.g. config('openapi.prefix')) to control the prefix and don't make OpenAPI responsible for that.

    This feels like a work-around for me and moves away from a primary goal of the project: to make the OpenAPI specification THE source of truth for an API. That said, it solves the problem and may be worth the trade-off in the short term?

  3. Use the Server Object to compute what the prefix path should be based on APP_URL and the first matching Server Object. The "diff" becomes the prefix.

    I like this, but we need this to be suuuuuuper obvious for a variety of hosting situations. For example, how does this behave when I have my API on a sub-domain or in a subfolder (e.g. api.example.org vs example.org/api vs example.org/path/to/api). In the case that we use APP_URL as our basis of "where the application is hosted", then we do a diff between server object and that configuration.

    For example, if APP_URL is https://example.org and we have a Server Object URL of https://example.org/api/v2, then the Route Prefix is /api/v2 and that's what we send to the framework. The problem is handling APIs that might be available from multiple URLs? I personally have NEVER had that use-case, but it's allowed by the specification as far as I can tell.

Support media type ranges for request body and response validation.

Currently, the library only supports exact matches of media-type for request body and response validation. To work as expected, we need to match an incoming request or outgoing response to the most specific media type specified.

This means that if you have a request body like the following...

requestBody:
  content:
    application/json:
      ...
    text/plain:
      ...
    */*:
      ...

... and you send a request with content like application/xml, it just straight-up fails even though technically, it should match to */* and actually go through validation (where it may fail anyway). This also means we can't have things like text/* that accepts and validates text/plain and text/html, etc.

We need to find a library that helps us do this matching to pick the most specific. There are plenty of content negotiation libraries for PHP. We just need to pick one and integrate it with appropriate test cases.

For now, I'm punting this to the road-map so that I can continue getting a somewhat-solid test foundation laid down that we can exercise to flesh this feature out.

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.