mattpolzin / VaporOpenAPI

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to make use of RequestBodyType

m-barthelemy opened this issue · comments

Anything implementing the RouteContext protocol must define RequestBodyType.
I was hoping that this would show up in the generated schema and documentation as, well, the request body type, but so far I couldn't get it to work (I see no information related to that in the outputted yaml schema).

I guess this is not fully implemented yet, but just wanted to confirm :-)
Is there any way to get it to work in my Vapor project, even if that means a little bit of additional/manual work for me?

Hi!

I actually implemented a route with a request body in the example app after seeing this question and I found a bug I needed to fix (so be sure to update your VaporOpenAPI version to 0.0.2).

With that bug fixed, here's the long and short of it: You need to conform your RequestBodyType to OpenAPIEncodedSchemaType for it to be included in your OpenAPI output.

There are a couple of possible strategies to conforming to OpenAPIEncodedSchemaType.

Less Work

The most hands off strategy is to let OpenAPIReflection guess the schema. If your type conforms to Sampleable, that looks like:

extension MyResponseType: OpenAPIEncodedSchemaType {
    static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema {
        return try genericOpenAPISchemaGuess(using: encoder)
    }
}

If your type does not conform to Sampleable, it looks like:

extension MyResponseType: OpenAPIEncodedSchemaType {
    static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema {
        return try OpenAPIReflection.genericOpenAPISchemaGuess(for: MyResponseType() , using: encoder)
    }
}

More Work

You can also implement the schema for your request model yourself, which gives you total control but also means you are more-or-less writing the OpenAPI by hand.

extension MyResponseType: OpenAPIEncodedSchemaType {
    static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema {
        return JSONSchema.object(
            properties: [
                "name": .string,
                "startOfWeek": .string(allowedValues: "Sunday", "Monday"),
                ...
            ]
        )
    }
}

Thanks for the quick reply - and for fixing the bug!

The first option, using genericOpenAPISchemaGuess did it for me.

Now I'm trying to figure out how to specify the request and response body types in a generic way for all my models. They are pretty standard, with a GET(list all or get one), POST(create), PUT(replace) and DELETE.
For all of them the following is true:

  • GET doesn't accept a request body, since it doesn't really make sense for GET requests
  • GET without an id parameter returns all the objects, thus an Array.
  • POST and PUT accept a request body (which I can now document properly thanks to your help), and return the model.
  • DELETE has no request body and no response body

In total 4 different cases for (currently) 8 models.

So, I figured out I'd create 4 generic RouteContext implementations and reuse them. For example, for GET all:

struct GetAll<T: MultitenantModel> : RouteContext {
    
    static var shared = T() //Static stored properties not supported in generic types
    
    typealias RequestBodyType = EmptyRequestBody
    
    static var responseBodyTuples: [(statusCode: Int, contentType: HTTPMediaType?, responseBodyType: Any.Type)] {
        get{
            return [
                (statusCode: 200, contentType: .json, responseBodyType: Array<T>.self),
                (statusCode: 401, contentType: .json, responseBodyType: ErrorResponse.self)
            ]
        }
    }
}

But of course I hit a wall with Swift not supporting static vars with generics.

Would you have any idea on how to have a reusable solution for that, without having to potentially implement (4 cases * 8 models) = 32 RouteContext ?
If not then feel free to ignore this message :)

Cool. That seems like a pretty reasonable direction to go with generalizing this.

The reason for a shared instance, if you did not gather this already, is to (1) Support Mirroring in OpenAPI generation code and (2) Not have the overhead of recreating the context for every request.

The simplest and least-performant solution is to give up on sharing the instance:

static var shared: Self { Self() }

That's not great, though.

The first viable solution that comes to mind is to create a cache that stores one of each type of context for the duration of runtime. I may want to build that out in this library eventually if I get the time, but you could do it in your codebase right away if you wanted to. I pushed a branch of the example app that you could snag the RouteContextCache.swift file from.

Branch: https://github.com/mattpolzin/VaporOpenAPIExample/tree/context-cache-play
Diff from master: https://github.com/mattpolzin/VaporOpenAPIExample/compare/context-cache-play

Thanks a lot! Using your first suggestion works nicely and I was able to implement my generic RouteContexts in a few minutes. The next step for me will be indeed to use your RouteContextCache.

My API doc is now taking shape and look pretty cool - and is almost usable!

I think I only have one small, non-critical question left.
Initially I thought I would be able to make use of Sampleable to return nice examples in my doc, with some fields values chosen by me. However, declaring the following:

extension UserDto: Sampleable {
    static var sample: UserDto {
        .init(email: "me@me.me")
    }
}

still returns the default "email": "string" for this property. I guess this directly relates to the examples property in the doc schema and it is currently not supported by VaporOpenAPI, but mentioning it just in case I'm wrong and there's a super easy fix. As I said, this one is totally non critical.

Anyway, thanks a ton for all the help you already gave me!

You are correct on all fronts. Making it Sampleable would be the intended way to provide an example but VaporOpenAPI does not yet turn that into an OpenAPI example.

Ok then I'll wait until you are able to implement it.

I also noticed that some String "extended types" do not seem to be implemented yet: if my model or DTO has UUID or URL properties they are rendered as object. Unless you tell me I'm missing something, then I'll wait :) Or will try to make a PR when I really want the features.

VaporOpenAPI v0.0.3 has a little support for OpenAPI examples (just added). Not tested much, but give it a go if you'd like.

It requires a type conforms to OpenAPIExampleProvider which in turn is easiest to use if your type conforms to Encodable and Sampleable (given these two conformances, OpenAPIExampleProvider is free).

On the plus side, you can remove your implementation of static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema required by OpenAPIEncodedSchemaType if you add OpenAPIExampleProvider because it gives you that implementation as a default.

... UUID or URL properties they are rendered as object.

I just tracked this as an issue in OpenAPIKit but you can add support for these yourself for now without much code.

extension UUID: OpenAPISchemaType {
    public static func openAPISchema() throws -> JSONSchema {
        return .string(format: .extended(.uuid))
    }
}

extension URL: OpenAPISchemaType {
    public static func openAPISchema() throws -> JSONSchema {
        return .string(format: .other("uri"))
    }
}

Yep support for OpenAPI examples seems to be working!

However, and I'm not sure if it's VaporOpenApi's "fault" or Redoc's, the example is rendered as a string: the double quotes are escaped and the whole example object appears as a single line.
Screenshot provided since it explains it much better.
Screen Shot 2020-03-22 at 6 26 53 PM

I thought ReDoc had fixed that, although it is debatable whether it is a bug or just the choice ReDoc makes when confronted with String examples of what it knows to be a JSON payload.

I changed the openAPIExample() function to return a structured example instead of a String. That should make ReDoc renderings of the example much more appealing.

[EDIT] This is with the version bump to 0.0.4

Hi, yes indeed it makes ReDoc super happy, thank you!

I have a few more questions but I think I've used enough of your time for now. Maybe I'll open new issues when I want to ask them :)