rstudio / plumber

Turn your R code into a web API.
https://www.rplumber.io
Other
1.39k stars 256 forks source link

Annotations for return type(s)? #222

Open JHibbard opened 6 years ago

JHibbard commented 6 years ago

Is it possible to annotate endpoints with their response type(s)/structure? The swagger codegen project released (Sep 2017) code to generate R clients from swagger specs but requires these endpoint specs to deserialize responses; this along with plumber would allow a top to bottom RESTful api solution for R!

trestletech commented 6 years ago

You can document response codes (see https://github.com/trestletech/plumber/blob/master/inst/examples/11-car-inventory/plumber.R#L46), but currently not the response structure.

I haven't figured out a great way to enable the documentation of the response structure. I'm thinking there may be something built around R6 that would allow us to document the structure of each object that we're going to serialize in/out, but a.) jsonlite doesn't play nicely with R6 custom serialization at this point AFAIK, and b.) I'm not convinced that this is the best path forward.

How would you want to document the structure of your objects?

JHibbard commented 6 years ago

Ah, that sounds tricky.

I'd like to define a named json schema that I can reuse; e.g. I might have a "cookie" definition that maps to a (swagger/open api) json schema with {'type': , 'cost': }. This would allow me to have an endpoint that returns a single "cookie" and another that returns an array of "cookie" objects. swagger/OpenAPI client sdk generators could then deserialize these JSON objects into language-specific types; e.g. "cookie" might be a list with named members in R and a dictionary in Python. Without this extra type annotation client sdk's can't be generated.

A possible "quick fix" would be to allow a filter to be applied after an endpoint's function, on the response. Then you could inject this information into the swagger.json before it gets returned to callers. You could also use these post-filters to reformat JSON if you needed objects to be serialized in a very specific way.

trestletech commented 6 years ago

Yeah, I'd be tempted to just allow extensions/modifications to the Swagger generation at that point.

My gut is that there's an elegant solution hiding in here somewhere using R6. It would be a little extra work on the API author to have to compose their APIs using these R6 objects, but I feel like the type safety you'd get in exchange would be worth it.

Unfortunately, that might require some extensions to jsonlite to get that to work, as -- last I checked -- it wasn't supported.

svdwoude commented 6 years ago

I'd be very interested in exploring this further and implementing this.

I'm not quite sure I completely understand the approach you have in mind with R6. Could you provide a minimal example of the annotation you'd envision for the response schema? Or would you generate that response schema entirely based upon an 'example' output from the endpoint-function?

slodge commented 1 year ago

I'd be interested in exploring this area too.

For typical complex return types (e.g. single-depth lists and tibbles) I'm wondering whether we could:

I think I especially like that second idea... e.g. something like:

Plumber could either support declaring the dto using "normal R" or using a dto comment block

# doing this in "real R" would be easiest for coders to maintain:
cars_dto <- tibble(
   id = integer(),
   name = character(),
   mpg = double(),
   cyl = integer(),
   # etc
)
# doing this in "plumber comment blocks" would be easiest to get strict descriptions (and type conversions):
#* @dto cars_dto {
#*    id:integer
#*    name:string
#*    mpg:number
#*    cyl:integer
#*    # etc
#* }

Then the api code could look like

#* Return the cars
#* @get /cars
#* @return [cars_dto] the cars
function() {
  datasets::mtcars
}

#* Return a car
#* @param id:integer the car
#* @get /cars/<id>
#* @return cars_dto the car
function(id) {
  as.list(
    datasets::mtcars[id, ]
  )
}

Would have to think how that scaled/worked for other things (single values? vectors? nested data.frames? lists? lists of lists of data.frames of.... ?)

Whatever gets designed, it might also be nice to be able to use it to describe request bodies too...

#* add a car
#* @post /cars
#* @body cars_dto
#* @return [integer] the id of the added car
function(req, res) {
   # code to add a car
}

~... and we could also consider methods listing their error code responses as well their 200s (https://swagger.io/docs/specification/describing-responses/)~ - I think current response codes are ok - only 200 would benefit from some structure (sometimes)

Note:

slodge commented 1 year ago

Have had a go at this in #892 - it seems to work - but don't know how much R developers will invest in these definitions. We do love our (very) dynamic typing!