Skip to content

Commit

Permalink
Revamp Swagger Spec to OpenAPI v3 (#365)
Browse files Browse the repository at this point in the history
* Migrate to new swagger dist files. As of now, these are all local. Eventually will be delivered via CDN.

* Update parse-globals defaultGlobals to reflect openapi 3.0.2

* Add comments on swagger-ui html

* ifelse to switch

* remove unused top level items in swagger and add a blank paths list

* udpate rbuildignore

* add download script for swagger-ui

* add swagger-ui@3.20.2

* commit working code before gutting v2 spec

* comment unused code

* use new swagger wrapper function

* use relative swagger.json url

* relative url back one dir

* try basic server info for cloud dev

* use dynamic relative url given window.location information

* schemes should not be in globals

* use swagger pkg

* clean up swagger creation

* answer TODO about trailing slashes

* use swagger::swagger_spec

* have swagger params be done with a list and not a data.frame

helps to know how it will be formatted exactly

* do not allow asJSON to be passed into pr$swaggerFile()

not necessary if we are switching to using lists and not data.frames

* swagger endpoints will use list information and not clone r6 objects to then extract information

* recursively remove NA and NULL values from swagger output

Fixes #322
Fixes #323

See swagger-api/swagger-js#268

Thank you @Hong-Revo for a recursive blueprint in #323

* is.na(list()) returns NA. it should return FALSE

* move param type to schema

* add test to check swagger against a cli validator

* add notes on how to install node.js pkg

* remove ... from swaggerFile/openAPIFile

* use swagger master v3.20.3.9999

* add a manual testing file for manual testing before release

* add an index route to the static only plumber example

* update example ot use addGlobalProcessor instead of registerHook

* use `spec` and not `sf`for param name to swagger file fn

* add expect_silent (or comment) test for tests that have no tests

* run validation on all inst/examples for plumber

* make sure basic http protocol is supplied to rstudio swaggerCallback

* add news item and split to look like shiny's


Co-authored-by: James Blair <james.m.blair09@gmail.com>
Co-authored-by: Barret Schloerke <schloerke@gmail.com>"
  • Loading branch information
schloerke and blairj09 committed Jan 11, 2019
1 parent 4f2e73d commit c54259c
Show file tree
Hide file tree
Showing 16 changed files with 385 additions and 279 deletions.
18 changes: 11 additions & 7 deletions .Rbuildignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
^appveyor\.yml$
^.*\.Rproj$
^\.Rproj\.user$
.travis.yml
Dockerfile
inst/analog-keys.R
inst/examples/03-github/github-key.txt
.httr-oauth
docs
scripts
^\.travis\.yml$
^Dockerfile
^inst/analog-keys.R
^inst/examples/03-github/github-key.txt
^\.httr-oauth
^docs
^scripts
^revdep$
^cran-comments\.md$
^inst/swagger_ui/.*\.map
^yarn\.lock$
^node_modules
^package\.json$
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@
.idea
plumber.Rcheck
plumber_*.tar.gz
yarn.lock
node_modules/*
5 changes: 4 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ Suggests:
base64enc,
htmlwidgets,
visNetwork,
analogsea
analogsea,
swagger (> 3.20.3)
Remotes:
rstudio/swagger
Collate:
'content-types.R'
'cookie-parser.R'
Expand Down
44 changes: 36 additions & 8 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,47 @@
plumber 0.4.7
plumber 0.5.0
--------------------------------------------------------------------------------
* Add support for swagger for mounted routers (@bradleyhd, [#274](https://github.com/trestletech/plumber/issues/274)).
* BUGFIX: A multiline POST body is now collapsed to a single line ([#270](https://github.com/trestletech/plumber/issues/270)).
* The source files used in plumber **must use** the UTF-8 encoding if they contain
non-ASCII characters (@shrektan, [#312](https://github.com/trestletech/plumber/pull/312),
[#328](https://github.com/trestletech/plumber/pull/328)).
* SECURITY: Wrap `jsonlite::fromJSON` to ensure that `jsonlite` never reads
## Full changelog

### Security

* Wrap `jsonlite::fromJSON` to ensure that `jsonlite` never reads
input as a remote address (such as a file path or URL) and attempts to parse
that. The only known way to exploit this behavior in plumber unless an
API were using encrypted cookies and an attacker knew the encryption key in
order to craft arbitrary cookies. ([#325](https://github.com/trestletech/plumber/pull/325))
* BUGFIX: Plumber files are now only evaluated once. Prior plumber behavior sourced endpoint

### Breaking changes

* Plumber's swagger definition is now defined using
[OpenAPI 3](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md),
upgrading from Swagger Specification. ([#365](https://github.com/trestletech/plumber/pull/365))

* The source files used in plumber **must use** the UTF-8 encoding if they contain
non-ASCII characters (@shrektan, [#312](https://github.com/trestletech/plumber/pull/312),
[#328](https://github.com/trestletech/plumber/pull/328)).

### New features

* Added support to a router's run method to allow the `swagger` parameter to be a function that
enhances the existing swagger specification before being returned to `/openapi.json`. ([#365](https://github.com/trestletech/plumber/pull/365))

* Add support for swagger for mounted routers (@bradleyhd, [#274](https://github.com/trestletech/plumber/issues/274)).


### Minor new features and improvements

* Changed Swagger UI to use [swagger](https://github.com/rstudio/swagger) R package to display the
swagger page. ([#365](https://github.com/trestletech/plumber/pull/365))

* Plumber files are now only evaluated once. Prior plumber behavior sourced endpoint
functions twice and non-endpoint code blocks once.
([#328](https://github.com/trestletech/plumber/pull/328/commits/cde0d3d2543a654fd0c5799b670767ccb0e22e35))

### Bug fixes

* A multiline POST body is now collapsed to a single line (@robertdj, [#270](https://github.com/trestletech/plumber/issues/270) [#297](https://github.com/trestletech/plumber/pull/297)).



plumber 0.4.6
--------------------------------------------------------------------------------
Expand Down
14 changes: 5 additions & 9 deletions R/parse-globals.R
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,11 @@ parseGlobals <- function(lines){
fields
}

#' The default set of Swagger API globals. Some of these properties are subject
#' to being overridden by @api* annotations.
#' The default set of OpenAPI specification (OAS) globals. Some of these properties
#' are subject to being overridden by @api* annotations.
#' @noRd
defaultGlobals <- list(
swagger = "2.0",
info = list(description="API Description", title="API Title", version="1.0.0"),
host=NA,
schemes= I("http"),
produces=I("application/json")
#securityDefinitions = list(),
#definitions = list()
openapi = "3.0.2",
info = list(description = "API Description", title = "API Title", version = "1.0.0"),
paths = list()
)
168 changes: 99 additions & 69 deletions R/plumber.R
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ plumber <- R6Class(
private$serializer <- serializer_json()
private$errorHandler <- defaultErrorHandler()
private$notFoundHandler <- default404Handler

# Add in the initial filters
for (fn in names(filters)){
fil <- PlumberFilter$new(fn, filters[[fn]], private$envir, private$serializer, NULL)
Expand All @@ -195,8 +195,13 @@ plumber <- R6Class(
}

},
run = function(host='127.0.0.1', port=getOption('plumber.port'), swagger=interactive(),
debug=interactive(), swaggerCallback=getOption('plumber.swagger.url', NULL)){
run = function(
host = '127.0.0.1',
port = getOption('plumber.port'),
swagger = interactive(),
debug = interactive(),
swaggerCallback = getOption('plumber.swagger.url', NULL)
) {
port <- findPort(port)

message("Starting server to listen on port ", port)
Expand All @@ -211,54 +216,76 @@ plumber <- R6Class(
setwd(dirname(private$filename))
}

if (swagger){
sf <- self$swaggerFile()

if (is.na(sf$host)){
accessHost <- ifelse(host == "0.0.0.0", "127.0.0.1", host)
accessPath <- paste(accessHost, port, sep=":")
sf$host <- accessPath

if (!is.null(getOption("plumber.apiHost"))){
sf$host <- getOption("plumber.apiHost")
}

if (!is.null(getOption("plumber.apiScheme"))){
sf$schemes <- getOption("plumber.apiScheme")
}

if (!is.null(getOption("plumber.apiPath"))){
sf$basePath <- getOption("plumber.apiPath")
}
}
if (isTRUE(swagger) || is.function(swagger)) {
host <- getOption(
"plumber.apiHost",
ifelse(identical(host, "0.0.0.0"), "127.0.0.1", host)
)
spec <- self$swaggerFile()

# Create a function that's hardcoded to return the swaggerfile -- regardless of env.
fun <- function(schemes, host, path){
if (!missing(schemes)){
sf$schemes <- I(schemes)
}

if (!missing(host)){
sf$host <- host
swagger_fun <- function(req, res, ..., scheme = "deprecated", host = "deprecated", path = "deprecated") {
if (!missing(scheme) || !missing(host) || !missing(path)) {
warning("`scheme`, `host`, or `path` are not supported to produce swagger.json")
}
# allows swagger-ui to provide proper callback location given the referrer location
# ex: rstudio cloud
# use the HTTP_REFERER so RSC can find the swagger location to ask
## (can't directly ask for 127.0.0.1)
referrer_url <- req$HTTP_REFERER
referrer_url <- sub("index\\.html$", "", referrer_url)
referrer_url <- sub("__swagger__/$", "", referrer_url)
spec$servers <- list(
list(
url = referrer_url,
description = "OpenAPI"
)
)

if (!missing(path)){
sf$basePath <- path
if (is.function(swagger)) {
# allow users to update the swagger file themselves
ret <- swagger(self, spec, ...)
# Since users could have added more NA or NULL values...
ret <- removeNaOrNulls(ret)
} else {
# NA/NULL values already removed
ret <- spec
}
sf
ret
}
# https://swagger.io/specification/#document-structure
# "It is RECOMMENDED that the root OpenAPI document be named: openapi.json or openapi.yaml."
self$handle("GET", "/openapi.json", swagger_fun, serializer = serializer_unboxed_json())
# keeping for legacy purposes
self$handle("GET", "/swagger.json", swagger_fun, serializer = serializer_unboxed_json())

swagger_index <- function(...) {
swagger::swagger_spec(
'window.location.origin + window.location.pathname.replace(/\\(__swagger__\\\\/|__swagger__\\\\/index.html\\)$/, "") + "openapi.json"'
)
}
for (path in c("/__swagger__/index.html", "/__swagger__/")) {
self$handle(
"GET", path, swagger_index,
serializer = serializer_html()
)
}
self$mount("/__swagger__", PlumberStatic$new(swagger::swagger_path()))
swaggerUrl = paste0(host, ":", port, "/__swagger__/")
if (!grepl("^http://", swaggerUrl)) {
# must have http protocol for use within RStudio
# does not work if supplying "127.0.0.1:1234/route"
swaggerUrl <- paste0("http://", swaggerUrl)
}
self$handle("GET", "/swagger.json", fun, serializer=serializer_unboxed_json())
message("Running the swagger UI at ", swaggerUrl, sep = "")

plumberFileServer <- PlumberStatic$new(system.file("swagger-ui", package = "plumber"))
self$mount("/__swagger__", plumberFileServer)
swaggerUrl = paste(sf$schemes[1], "://", sf$host, "/__swagger__/", sep="")
message("Running the swagger UI at ", swaggerUrl, sep="")
if (!is.null(swaggerCallback) && is.function(swaggerCallback)){
# notify swaggerCallback of plumber swagger location
if (!is.null(swaggerCallback) && is.function(swaggerCallback)) {
swaggerCallback(swaggerUrl)
}
}

on.exit(private$runHooks("exit"), add=TRUE)
on.exit(private$runHooks("exit"), add = TRUE)

httpuv::runServer(host, port, self)
},
Expand Down Expand Up @@ -527,17 +554,24 @@ plumber <- R6Class(
filter <- PlumberFilter$new(name, expr, private$envir, serializer)
private$addFilterInternal(filter)
},
swaggerFile = function(){ #FIXME: test
swaggerFile = function() { #FIXME: test

endpoints <- private$swaggerFileWalkMountsInternal(self)
endpoints <- prepareSwaggerEndpoints(endpoints)
swaggerPaths <- private$swaggerFileWalkMountsInternal(self)

# Extend the previously parsed settings with the endpoints
def <- modifyList(private$globalSettings, list(paths=endpoints))
def <- modifyList(private$globalSettings, list(paths = swaggerPaths))

# Lay those over the default globals so we ensure that the required fields
# (like API version) are satisfied.
modifyList(defaultGlobals, def)
ret <- modifyList(defaultGlobals, def)

# remove NA or NULL values, which swagger doesn't like
ret <- removeNaOrNulls(ret)

ret
},
openAPIFile = function() {
self$swaggerFile()
},

### Legacy/Deprecated
Expand Down Expand Up @@ -686,36 +720,32 @@ plumber <- R6Class(
paste(x, y, sep = "/")
}

endpoints <- lapply(router$endpoints, function(endpoint) {
# clone and make path a full path
endpointEntries <- lapply(endpoint, function(endpointEntry) {
endpointEntry <- endpointEntry$clone()
endpointEntry$path <- join_paths(parentPath, endpointEntry$path)
endpointEntry
})
# make sure to use the full path
endpointList <- list()

endpointEntries
})
for (endpoint in router$endpoints) {
for (endpointEntry in endpoint) {
swaggerEndpoint <- prepareSwaggerEndpoint(
endpointEntry,
join_paths(parentPath, endpointEntry$path)
)
endpointList <- modifyList(endpointList, swaggerEndpoint)
}
}

# recursively gather mounted enpoint entries
mountedEndpoints <- mapply(
names(router$mounts),
router$mounts,
FUN = function(mountPath, mountedSubrouter) {
private$swaggerFileWalkMountsInternal(
mountedSubrouter,
if (length(router$mounts) > 0) {
for (mountPath in names(router$mounts)) {
mountEndpoints <- private$swaggerFileWalkMountsInternal(
router$mounts[[mountPath]],
join_paths(parentPath, mountPath)
)
endpointList <- modifyList(endpointList, mountEndpoints)
}
)

# returning a single list of entries,
# not nested entries using the filter / `__no-preempt__` as names within the list
# (the filter name is not required when making swagger docs and do not want to misrepresent the endpoints)
unname(append(
unlist(endpoints),
unlist(mountedEndpoints)
))
}

# returning a single list of swagger entries
endpointList
}
)
)
Loading

0 comments on commit c54259c

Please sign in to comment.