diff --git a/DESCRIPTION b/DESCRIPTION index 40f0b42d7..b392ebaa7 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -8,6 +8,7 @@ Authors@R: c( person(family = "RStudio", role = c("cph", "fnd")), person("Jeff", "Allen", role="cre", email="cran@trestletech.com"), person("Barret", "Schloerke", role="aut", email="barret@rstudio.com"), + person("Bruno", "Tremblay", role="ctb", email="bruno.tremblay@lacapitale.com"), person("Frans", "van Dunné", role="ctb", email="frans@ixpantia.com"), person("Sebastiaan", "Vandewoude", role="ctb", email="sebastiaanvandewoude@gmail.com"), person(family="SmartBear Software", role=c("ctb", "cph"), comment="swagger-ui")) @@ -40,21 +41,23 @@ Suggests: htmlwidgets, visNetwork, analogsea (>= 0.7.0), - later + later, + redoc Remotes: + meztez/redoc, rstudio/swagger Collate: 'async.R' 'content-types.R' 'cookie-parser.R' - 'parse-globals.R' + 'plumb-globals.R' 'images.R' - 'parse-block.R' + 'plumb-block.R' 'globals.R' 'serializer-json.R' 'shared-secret-filter.R' - 'post-body.R' - 'query-string.R' + 'parse-body.R' + 'parse-query.R' 'plumber.R' 'default-handlers.R' 'digital-ocean.R' @@ -62,11 +65,13 @@ Collate: 'includes.R' 'json.R' 'new-rstudio-project.R' + 'openapi-spec.R' + 'openapi-types.R' + 'parsers-body.R' 'paths.R' + 'plumber-response.R' 'plumber-static.R' 'plumber-step.R' - 'post-parsers.R' - 'response.R' 'serializer-content-type.R' 'serializer-html.R' 'serializer-htmlwidget.R' @@ -74,7 +79,7 @@ Collate: 'serializer-xml.R' 'serializer.R' 'session-cookie.R' - 'swagger.R' + 'ui.R' 'utf8.R' 'zzz.R' RoxygenNote: 7.1.0 diff --git a/NAMESPACE b/NAMESPACE index f88bbfbcd..e089bfc9b 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -36,10 +36,8 @@ import(R6) import(crayon) import(promises) import(stringi) -importFrom(httpuv,runServer) importFrom(jsonlite,fromJSON) importFrom(jsonlite,toJSON) importFrom(jsonlite,validate) importFrom(stats,runif) -importFrom(stringi,stri_match_first_regex) importFrom(webutils,parse_multipart) diff --git a/NEWS.md b/NEWS.md index 02d607cfd..2281b6b95 100644 --- a/NEWS.md +++ b/NEWS.md @@ -42,7 +42,7 @@ plumber 0.5.0 ### New features -* Added Swagger support for array parameters using syntax `name:[type]` and new type `list` (synonym df, data.frame). (@meztez, #532) +* Added Swagger support for array parameters using syntax `name:[type]` and new type `list` (synonym df, data.frame). (@meztez, #532) * Added support for promises in endpoints, filters, and hooks. (#248) @@ -75,6 +75,8 @@ plumber 0.5.0 * Modified images serialization to use content-type serializer. Fixes issue with images pre/postserialize hooks (@meztez, #518). +* Fix bug in setting openapi.json url when HTTP_REFERER is null (@meztez, #528) + * Fix possible bugs due to mounted routers without leading slashes (@atheriel, #476 #501). * Fix bug preventing error handling when a serializer fails (@antoine-sachet, #490) diff --git a/R/content-types.R b/R/content-types.R index bd2fdc28c..fb2613386 100644 --- a/R/content-types.R +++ b/R/content-types.R @@ -52,7 +52,6 @@ getContentType <- function(ext, defaultType='application/octet-stream') { return(ct) } -#4x perf improvement when contentType is set #' Request character set #' @param contentType Request Content-Type header #' @return Default to `UTF-8`. Otherwise return `charset` defined in request header. diff --git a/R/default-handlers.R b/R/default-handlers.R index c6addfb6c..7c9635505 100644 --- a/R/default-handlers.R +++ b/R/default-handlers.R @@ -1,6 +1,6 @@ #' @include plumber.R default404Handler <- function(req, res){ - res$status <- 404 + res$status <- 404L list(error="404 - Resource Not Found") } diff --git a/R/globals.R b/R/globals.R index f33915762..aed49f159 100644 --- a/R/globals.R +++ b/R/globals.R @@ -2,3 +2,4 @@ .globals$serializers <- list() .globals$processors <- new.env() .globals$parsers <- list(func = list(), pattern = list()) +.globals$interfaces <- list() diff --git a/R/swagger.R b/R/openapi-spec.R similarity index 53% rename from R/swagger.R rename to R/openapi-spec.R index 92e16b972..bd3ae73d5 100644 --- a/R/swagger.R +++ b/R/openapi-spec.R @@ -1,153 +1,7 @@ - - -# calculate all swagger type information at once and use created information throughout package -swaggerTypeInfo <- list() -plumberToSwaggerTypeMap <- list() -defaultSwaggerType <- structure("string", default = TRUE) -defaultSwaggerIsArray <- structure(FALSE, default = TRUE) - -addSwaggerInfo_onLoad <- function() { - addSwaggerInfo <- function(swaggerType, plumberTypes, - regex = NULL, converter = NULL, - format = NULL, - location = NULL, - realType = NULL, - arraySupport = FALSE) { - swaggerTypeInfo[[swaggerType]] <<- - list( - regex = regex, - converter = converter, - format = format, - location = location, - arraySupport = arraySupport, - realType = realType - ) - - if (arraySupport == TRUE) { - swaggerTypeInfo[[swaggerType]] <<- utils::modifyList( - swaggerTypeInfo[[swaggerType]], - list(regexArray = paste0("(?:(?:", regex, "),?)+"), - # Q: Do we need to safe guard against special characters, such as `,`? - # https://github.com/rstudio/plumber/pull/532#discussion_r439584727 - # A: https://swagger.io/docs/specification/serialization/ - # > Additionally, the allowReserved keyword specifies whether the reserved - # > characters :/?#[]@!$&'()*+,;= in parameter values are allowed to be sent as they are, - # > or should be percent-encoded. By default, allowReserved is false, and reserved characters - # > are percent-encoded. For example, / is encoded as %2F (or %2f), so that the parameter - # > value quotes/h2g2.txt will be sent as quotes%2Fh2g2.txt - converterArray = function(x) {converter(stri_split_fixed(x, ",")[[1]])}) - ) - } - - for (plumberType in plumberTypes) { - plumberToSwaggerTypeMap[[plumberType]] <<- swaggerType - } - # make sure it could be called again - plumberToSwaggerTypeMap[[swaggerType]] <<- swaggerType - - invisible(TRUE) - } - - addSwaggerInfo( - "boolean", - c("bool", "boolean", "logical"), - "[01tfTF]|true|false|TRUE|FALSE", - as.logical, - location = c("query", "path"), - arraySupport = TRUE - ) - addSwaggerInfo( - "number", - c("dbl", "double", "float", "number", "numeric"), - "-?\\\\d*\\\\.?\\\\d+", - as.numeric, - format = "double", - location = c("query", "path"), - arraySupport = TRUE - ) - addSwaggerInfo( - "integer", - c("int", "integer"), - "-?\\\\d+", - as.integer, - format = "int64", - location = c("query", "path"), - arraySupport = TRUE - ) - addSwaggerInfo( - "string", - c("chr", "str", "character", "string"), - "[^/]+", - as.character, - location = c("query", "path"), - arraySupport = TRUE - ) - addSwaggerInfo( - "object", - c("list", "data.frame", "df"), - location = "requestBody" - ) - addSwaggerInfo( - "file", - c("file", "binary"), - location = "requestBody", - format = "binary", - realType = "string" - ) -} - - -#' Parse the given plumber type and return the typecast value -#' @noRd -plumberToSwaggerType <- function(type, inPath = FALSE) { - if (length(type) > 1) { - return(vapply(type, plumberToSwaggerType, character(1), inPath, USE.NAMES = FALSE)) - } - # default type is "string" type - if (is.na(type)) { - return(defaultSwaggerType) - } - - swaggerType <- plumberToSwaggerTypeMap[[as.character(type)]] - if (is.null(swaggerType)) { - warning( - "Unrecognized type: ", type, ". Using type: ", defaultSwaggerType, - call. = FALSE - ) - swaggerType <- defaultSwaggerType - } - if (inPath && !"path" %in% swaggerTypeInfo[[swaggerType]]$location) { - warning( - "Unsupported path parameter type: ", type, ". Using type: ", defaultSwaggerType, - call. = FALSE - ) - swaggerType <- defaultSwaggerType - } - - return(swaggerType) -} - -#' Check if swagger type supports array -#' @noRd -supportsArray <- function(swaggerTypes) { - vapply( - swaggerTypeInfo[swaggerTypes], - `[[`, - logical(1), - "arraySupport", - USE.NAMES = FALSE) -} - -#' Filter swagger type -#' @noRd -filterSwaggerTypes <- function(matches, property) { - names(Filter(function(x) {any(matches %in% x[[property]])}, swaggerTypeInfo)) -} - #' Convert the endpoints as they exist on the router to a list which can -#' be converted into a swagger definition for these endpoints +#' be converted into a openapi specification for these endpoints #' @noRd -prepareSwaggerEndpoint <- function(routerEndpointEntry, path = routerEndpointEntry$path) { +endpointSpecification <- function(routerEndpointEntry, path = routerEndpointEntry$path) { ret <- list() # We are sensitive to trailing slashes. Should we be? @@ -160,14 +14,14 @@ prepareSwaggerEndpoint <- function(routerEndpointEntry, path = routerEndpointEnt # Get the params from endpoint func funcParams <- routerEndpointEntry$getFuncParams() for (verb in routerEndpointEntry$verbs) { - params <- extractSwaggerParams(routerEndpointEntry$params, pathParams, funcParams) + params <- parametersSpecification(routerEndpointEntry$params, pathParams, funcParams) # If we haven't already documented a path param, we should add it here. # FIXME: warning("Undocumented path parameters: ", paste0()) - resps <- extractResponses(routerEndpointEntry$responses) + resps <- responsesSpecification(routerEndpointEntry$responses) - endptSwag <- list( + endptSpec <- list( summary = routerEndpointEntry$comments, responses = resps, parameters = params$parameters, @@ -175,37 +29,37 @@ prepareSwaggerEndpoint <- function(routerEndpointEntry, path = routerEndpointEnt tags = routerEndpointEntry$tags ) - ret[[cleanedPath]][[tolower(verb)]] <- endptSwag + ret[[cleanedPath]][[tolower(verb)]] <- endptSpec } ret } -defaultResp <- list( +defaultResponse <- list( "default" = list( description = "Default response." ) ) -extractResponses <- function(resps){ +responsesSpecification <- function(resps){ if (is.null(resps) || is.na(resps)){ - resps <- defaultResp + resps <- defaultResponse } else if (!("default" %in% names(resps))){ - resps <- c(resps, defaultResp) + resps <- c(resps, defaultResponse) } resps } -#' Extract the swagger-friendly parameter definitions from the endpoint +#' Extract the OpenAPI parameter specification from the endpoint #' paramters. #' @noRd -extractSwaggerParams <- function(endpointParams, pathParams, funcParams = NULL){ +parametersSpecification <- function(endpointParams, pathParams, funcParams = NULL){ params <- list( parameters = list(), requestBody = list() ) - inBody <- filterSwaggerTypes("requestBody", "location") - inRaw <- filterSwaggerTypes("binary", "format") + inBody <- filterDataTypes("requestBody", "location") + inRaw <- filterDataTypes("binary", "format") for (p in unique(c(names(endpointParams), pathParams$name, names(funcParams)))) { # Dealing with priorities endpointParams > pathParams > funcParams @@ -226,12 +80,12 @@ extractSwaggerParams <- function(endpointParams, pathParams, funcParams = NULL){ required <- TRUE style <- "simple" explode <- FALSE - type <- priorizeProperty(defaultSwaggerType, + type <- priorizeProperty(defaultDataType, pathParams[pathParams$name == p,]$type, endpointParams[[p]]$type, funcParams[[p]]$type) - type <- plumberToSwaggerType(type, inPath = TRUE) - isArray <- priorizeProperty(defaultSwaggerIsArray, + type <- plumberToDataType(type, inPath = TRUE) + isArray <- priorizeProperty(defaultIsArray, pathParams[pathParams$name == p,]$isArray, endpointParams[[p]]$isArray, funcParams[[p]]$isArray) @@ -239,18 +93,18 @@ extractSwaggerParams <- function(endpointParams, pathParams, funcParams = NULL){ location <- "query" style <- "form" explode <- TRUE - type <- priorizeProperty(defaultSwaggerType, + type <- priorizeProperty(defaultDataType, endpointParams[[p]]$type, funcParams[[p]]$type) - type <- plumberToSwaggerType(type) - isArray <- priorizeProperty(defaultSwaggerIsArray, + type <- plumberToDataType(type) + isArray <- priorizeProperty(defaultIsArray, endpointParams[[p]]$isArray, funcParams[[p]]$isArray) required <- priorizeProperty(funcParams[[p]]$required, endpointParams[[p]]$required) } - # Building openapi definition + # Building OpenAPI specification if (type %in% inBody) { if (length(params$requestBody) == 0L) { params$requestBody$content$`application/json`[["schema"]] <- @@ -258,13 +112,13 @@ extractSwaggerParams <- function(endpointParams, pathParams, funcParams = NULL){ } property <- list( type = type, - format = swaggerTypeInfo[[type]]$format, + format = dataTypesInfo[[type]]$format, example = funcParams[[p]]$example, description = endpointParams[[p]]$desc ) if (type %in% inRaw) { names(params$requestBody$content) <- "multipart/form-data" - property$type <- swaggerTypeInfo[[type]]$realType + property$type <- dataTypesInfo[[type]]$realType } params$requestBody[[1]][[1]][[1]]$properties[[p]] <- property if (required) { params$requestBody[[1]][[1]][[1]]$required <- @@ -277,7 +131,7 @@ extractSwaggerParams <- function(endpointParams, pathParams, funcParams = NULL){ required = required, schema = list( type = type, - format = swaggerTypeInfo[[type]]$format, + format = dataTypesInfo[[type]]$format, default = funcParams[[p]]$default ) ) @@ -286,7 +140,7 @@ extractSwaggerParams <- function(endpointParams, pathParams, funcParams = NULL){ type = "array", items = list( type = type, - format = swaggerTypeInfo[[type]]$format + format = dataTypesInfo[[type]]$format ), default = funcParams[[p]]$default ) @@ -300,58 +154,6 @@ extractSwaggerParams <- function(endpointParams, pathParams, funcParams = NULL){ params } -#' Check na -#' @noRd -isNa <- function(x) { - if (is.list(x)) { - return(FALSE) - } - is.na(x) -} - -#' Check na or null -#' @noRd -isNaOrNull <- function(x) { - any(isNa(x)) || is.null(x) -} - -#' Remove na or null -#' @noRd -removeNaOrNulls <- function(x) { - # preemptively stop - if (!is.list(x)) { - return(x) - } - if (length(x) == 0) { - return(x) - } - # Prevent example from being wiped out - if (!isNaOrNull(x$example)) { - saveExample <- TRUE - savedExample <- x$example - x$example <- NULL - } else { - saveExample <- FALSE - } - - # remove any `NA` or `NULL` elements - toRemove <- vapply(x, isNaOrNull, logical(1)) - if (any(toRemove)) { - x[toRemove] <- NULL - } - - # recurse through list - ret <- lapply(x, removeNaOrNulls) - class(ret) <- class(x) - - # Put example back in - if (saveExample) { - ret$example <- savedExample - } - - ret -} - #' For openapi definition #' @noRd priorizeProperty <- function(...) { @@ -385,7 +187,7 @@ getArgsMetadata <- function(plumberExpression){ #return same format as getTypedParams or params? if (!is.function(plumberExpression)) plumberExpression <- eval(plumberExpression) args <- formals(plumberExpression) - lapply(args[!names(args) %in% c("...", "res", "req")], function(arg) { + lapply(args[!names(args) %in% c("...", "req", "res")], function(arg) { required <- identical(arg, formals(function(x){})$x) if (is.call(arg) || is.name(arg)) { arg <- tryCatch( @@ -395,21 +197,73 @@ getArgsMetadata <- function(plumberExpression){ # Check that it is possible to transform arg value into # an example for the openAPI spec. Valid transform are # either a logical, a numeric, a character or a list that - # is json serializable. Otherwise set to NA. Otherwise - # it + # is json serializable. Otherwise set to NA. if (!is.logical(arg) && !is.numeric(arg) && !is.character(arg) && !(is.list(arg) && isJSONserializable(arg))) { message("Argument of class ", class(arg), " cannot be used to set default value in OpenAPI specifications.") arg <- NA } type <- if (isNaOrNull(arg)) {NA} else {typeof(arg)} - type <- plumberToSwaggerType(type) + type <- plumberToDataType(type) + isArray <- {if (length(arg) > 1L && type %in% filterDataTypes(TRUE, "arraySupport")) TRUE else defaultIsArray} list( default = arg, example = arg, required = required, - isArray = {if (length(arg) > 1L & supportsArray(type)) TRUE else defaultSwaggerIsArray}, + isArray = isArray, type = type ) }) } + +#' Check na +#' @noRd +isNa <- function(x) { + if (is.list(x)) { + return(FALSE) + } + is.na(x) +} + +#' Check na or null +#' @noRd +isNaOrNull <- function(x) { + any(isNa(x)) || is.null(x) +} + +#' Remove na or null +#' @noRd +removeNaOrNulls <- function(x) { + # preemptively stop + if (!is.list(x)) { + return(x) + } + if (length(x) == 0) { + return(x) + } + # Prevent example from being wiped out + if (!isNaOrNull(x$example)) { + saveExample <- TRUE + savedExample <- x$example + x$example <- NULL + } else { + saveExample <- FALSE + } + + # remove any `NA` or `NULL` elements + toRemove <- vapply(x, isNaOrNull, logical(1)) + if (any(toRemove)) { + x[toRemove] <- NULL + } + + # recurse through list + ret <- lapply(x, removeNaOrNulls) + class(ret) <- class(x) + + # Put example back in + if (saveExample) { + ret$example <- savedExample + } + + ret +} diff --git a/R/openapi-types.R b/R/openapi-types.R new file mode 100644 index 000000000..88e710752 --- /dev/null +++ b/R/openapi-types.R @@ -0,0 +1,132 @@ +# calculate all OpenAPI type information at once and use created information throughout package +dataTypesInfo <- list() +dataTypesMap <- list() +defaultDataType <- structure("string", default = TRUE) +defaultIsArray <- structure(FALSE, default = TRUE) + +addDataTypeInfo_onLoad <- function() { + addDataTypeInfo <- function(dataType, plumberTypes, + regex = NULL, converter = NULL, + format = NULL, + location = NULL, + realType = NULL, + arraySupport = FALSE) { + dataTypesInfo[[dataType]] <<- + list( + regex = regex, + converter = converter, + format = format, + location = location, + arraySupport = arraySupport, + realType = realType + ) + + if (arraySupport == TRUE) { + dataTypesInfo[[dataType]] <<- utils::modifyList( + dataTypesInfo[[dataType]], + list(regexArray = paste0("(?:(?:", regex, "),?)+"), + # Q: Do we need to safe guard against special characters, such as `,`? + # https://github.com/rstudio/plumber/pull/532#discussion_r439584727 + # A: https://swagger.io/docs/specification/serialization/ + # > Additionally, the allowReserved keyword specifies whether the reserved + # > characters :/?#[]@!$&'()*+,;= in parameter values are allowed to be sent as they are, + # > or should be percent-encoded. By default, allowReserved is false, and reserved characters + # > are percent-encoded. For example, / is encoded as %2F (or %2f), so that the parameter + # > value quotes/h2g2.txt will be sent as quotes%2Fh2g2.txt + converterArray = function(x) {converter(stri_split_fixed(x, ",")[[1]])}) + ) + } + + for (plumberType in plumberTypes) { + dataTypesMap[[plumberType]] <<- dataType + } + # make sure it could be called again + dataTypesMap[[dataType]] <<- dataType + + invisible(TRUE) + } + + addDataTypeInfo( + "boolean", + c("bool", "boolean", "logical"), + "[01tfTF]|true|false|TRUE|FALSE", + as.logical, + location = c("query", "path"), + arraySupport = TRUE + ) + addDataTypeInfo( + "number", + c("dbl", "double", "float", "number", "numeric"), + "-?\\\\d*\\\\.?\\\\d+", + as.numeric, + format = "double", + location = c("query", "path"), + arraySupport = TRUE + ) + addDataTypeInfo( + "integer", + c("int", "integer"), + "-?\\\\d+", + as.integer, + format = "int64", + location = c("query", "path"), + arraySupport = TRUE + ) + addDataTypeInfo( + "string", + c("chr", "str", "character", "string"), + "[^/]+", + as.character, + location = c("query", "path"), + arraySupport = TRUE + ) + addDataTypeInfo( + "object", + c("list", "data.frame", "df"), + location = "requestBody" + ) + addDataTypeInfo( + "file", + c("file", "binary"), + location = "requestBody", + format = "binary", + realType = "string" + ) +} + + +#' Parse the given plumber type and return the typecast value +#' @noRd +plumberToDataType <- function(type, inPath = FALSE) { + if (length(type) > 1) { + return(vapply(type, plumberToDataType, character(1), inPath, USE.NAMES = FALSE)) + } + # default type is "string" type + if (is.na(type)) { + return(defaultDataType) + } + + dataType <- dataTypesMap[[as.character(type)]] + if (is.null(dataType)) { + warning( + "Unrecognized type: ", type, ". Using type: ", defaultDataType, + call. = FALSE + ) + dataType <- defaultDataType + } + if (inPath && !"path" %in% dataTypesInfo[[dataType]]$location) { + warning( + "Unsupported path parameter type: ", type, ". Using type: ", defaultDataType, + call. = FALSE + ) + dataType <- defaultDataType + } + + return(dataType) +} + +#' Filter data type, return dataTypes where `matches` is found in `property` +#' @noRd +filterDataTypes <- function(matches, property) { + names(Filter(function(x) {any(matches %in% x[[property]])}, dataTypesInfo)) +} diff --git a/R/post-body.R b/R/parse-body.R similarity index 100% rename from R/post-body.R rename to R/parse-body.R diff --git a/R/query-string.R b/R/parse-query.R similarity index 86% rename from R/query-string.R rename to R/parse-query.R index b434d1b83..e30930eeb 100644 --- a/R/query-string.R +++ b/R/parse-query.R @@ -84,9 +84,9 @@ createPathRegex <- function(pathDef, funcParams = NULL){ match <- stri_match_all( pathDef, # capture any plumber type (). - # plumberToSwaggerType(types) will yell if it is unknown + # plumberToDataType(types) will yell if it is unknown # and can not be guessed from endpoint function args) - # will be given the TYPE `defaultSwaggerType` + # will be given the TYPE `defaultDataType` regex = "/<(\\.?[a-zA-Z][\\w_\\.]*)(?::([^>]*))?>" )[[1]] names <- match[,2] @@ -106,10 +106,10 @@ createPathRegex <- function(pathDef, funcParams = NULL){ plumberTypes <- stri_replace_all(match[,3], "$1", regex = "^\\[([^\\]]*)\\]$") if (length(funcParams) > 0) { # Override with detection of function args if type not found in map - idx <- !(plumberTypes %in% names(plumberToSwaggerTypeMap)) + idx <- !(plumberTypes %in% names(dataTypesMap)) plumberTypes[idx] <- sapply(funcParams, `[[`, "type")[names[idx]] } - swaggerTypes <- plumberToSwaggerType(plumberTypes, inPath = TRUE) + dataTypes <- plumberToDataType(plumberTypes, inPath = TRUE) areArrays <- stri_detect_regex(match[,3], "^\\[[^\\]]*\\]$") if (length(funcParams) > 0) { @@ -117,11 +117,11 @@ createPathRegex <- function(pathDef, funcParams = NULL){ idx <- (is.na(areArrays) | !areArrays) areArrays[idx] <- sapply(funcParams, `[[`, "isArray")[names[idx]] } - areArrays <- areArrays & supportsArray(swaggerTypes) - areArrays[is.na(areArrays)] <- defaultSwaggerIsArray + areArrays <- areArrays & dataTypes %in% filterDataTypes(TRUE, "arraySupport") + areArrays[is.na(areArrays)] <- defaultIsArray pathRegex <- pathDef - regexps <- typesToRegexps(swaggerTypes, areArrays) + regexps <- typesToRegexps(dataTypes, areArrays) for (regex in regexps) { pathRegex <- stri_replace_first_regex( pathRegex, @@ -132,36 +132,33 @@ createPathRegex <- function(pathDef, funcParams = NULL){ list( names = names, - types = swaggerTypes, + types = dataTypes, regex = paste0("^", pathRegex, "$"), - converters = typesToConverters(swaggerTypes, areArrays), + converters = typesToConverters(dataTypes, areArrays), areArrays = areArrays ) } - -typesToRegexps <- function(swaggerTypes, areArrays = FALSE) { +typesToRegexps <- function(dataTypes, areArrays = FALSE) { # return vector of regex strings mapply( function(x, y) {x[[y]]}, - swaggerTypeInfo[swaggerTypes], + dataTypesInfo[dataTypes], ifelse(areArrays, "regexArray", "regex"), USE.NAMES = FALSE ) } - -typesToConverters <- function(swaggerTypes, areArrays = FALSE) { +typesToConverters <- function(dataTypes, areArrays = FALSE) { # return list of functions mapply( function(x, y) {x[[y]]}, - swaggerTypeInfo[swaggerTypes], + dataTypesInfo[dataTypes], ifelse(areArrays, "converterArray", "converter"), USE.NAMES = FALSE ) } - # Extract the params from a given path # @param def is the output from createPathRegex extractPathParams <- function(def, path){ diff --git a/R/post-parsers.R b/R/parsers-body.R similarity index 99% rename from R/post-parsers.R rename to R/parsers-body.R index 526e9b491..bb46e3225 100644 --- a/R/post-parsers.R +++ b/R/parsers-body.R @@ -1,5 +1,3 @@ - - #' Plumber Parsers #' #' Parsers are used in Plumber to transform the raw body content received diff --git a/R/parse-block.R b/R/plumb-block.R similarity index 97% rename from R/parse-block.R rename to R/plumb-block.R index 3ca7cd09d..504dff967 100644 --- a/R/parse-block.R +++ b/R/plumb-block.R @@ -12,7 +12,7 @@ stopOnLine <- function(lineNum, line, msg){ #' @param lineNum The line number just above the function we're documenting #' @param file A character vector representing all the lines in the file #' @noRd -parseBlock <- function(lineNum, file){ +plumbBlock <- function(lineNum, file){ paths <- NULL preempt <- NULL filter <- NULL @@ -181,10 +181,10 @@ parseBlock <- function(lineNum, file){ stopOnLine(lineNum, line, "No parameter specified.") } type <- stri_replace_all(paramMat[1,4], "$1", regex = "^\\[([^\\]]*)\\]$") - type <- plumberToSwaggerType(type) + type <- plumberToDataType(type) isArray <- stri_detect_regex(paramMat[1,4], "^\\[[^\\]]*\\]$") - isArray <- isArray && supportsArray(type) - isArray[is.na(isArray)] <- defaultSwaggerIsArray + isArray <- isArray && type %in% filterDataTypes(TRUE, "arraySupport") + isArray[is.na(isArray)] <- defaultIsArray required <- identical(paramMat[1,5], "*") params[[name]] <- list(desc=paramMat[1,6], type=type, required=required, isArray=isArray) @@ -231,7 +231,7 @@ parseBlock <- function(lineNum, file){ evaluateBlock <- function(srcref, file, expr, envir, addEndpoint, addFilter, mount) { lineNum <- srcref[1] - 1 - block <- parseBlock(lineNum, file) + block <- plumbBlock(lineNum, file) if (sum(!is.null(block$filter), !is.null(block$paths), !is.null(block$assets)) > 1){ stopOnLine(lineNum, file[lineNum], "A single function can only be a filter, an API endpoint, or an asset (@filter AND @get, @post, @assets, etc.)") diff --git a/R/parse-globals.R b/R/plumb-globals.R similarity index 91% rename from R/parse-globals.R rename to R/plumb-globals.R index 0369cc3d2..bed036021 100644 --- a/R/parse-globals.R +++ b/R/plumb-globals.R @@ -5,7 +5,7 @@ #' If this line represents what was once multiple lines, intermediate comment #' prefixes should have been removed. #' @noRd -parseOneGlobal <- function(fields, argument){ +plumbOneGlobal <- function(fields, argument){ if (nchar(argument) == 0){ return(fields) } @@ -70,9 +70,9 @@ parseOneGlobal <- function(fields, argument){ argRegex <- "^#['\\*]\\s*(@(api\\w+)\\s+)?(.*)$" #' Parse out the global API settings of a given set of lines and return a -#' swagger-compliant list describing the global API. +#' OpenAPI-compliant list describing the global API. #' @noRd -parseGlobals <- function(lines){ +plumbGlobals <- function(lines){ # Build up the entire argument here; needed since a single directive # might wrap multiple lines fullArg <- "" @@ -90,19 +90,19 @@ parseGlobals <- function(lines){ fullArg <- paste(fullArg, parsedLine[4]) } else { # New argument, parse the buffer and start a new one - fields <- parseOneGlobal(fields, fullArg) + fields <- plumbOneGlobal(fields, fullArg) fullArg <- line } } else { # This isn't a line we can underestand. Parse what we have in the # buffer and then reset - fields <- parseOneGlobal(fields, fullArg) + fields <- plumbOneGlobal(fields, fullArg) fullArg <- "" } } # Clear out the buffer - fields <- parseOneGlobal(fields, fullArg) + fields <- plumbOneGlobal(fields, fullArg) fields } @@ -111,7 +111,7 @@ parseGlobals <- function(lines){ #' are subject to being overridden by @api* annotations. #' @noRd defaultGlobals <- list( - openapi = "3.0.2", + openapi = "3.0.3", info = list(description = "API Description", title = "API Title", version = "1.0.0"), paths = list() ) diff --git a/R/response.R b/R/plumber-response.R similarity index 100% rename from R/response.R rename to R/plumber-response.R diff --git a/R/plumber-step.R b/R/plumber-step.R index e0e9929bb..ff88ab60e 100644 --- a/R/plumber-step.R +++ b/R/plumber-step.R @@ -132,7 +132,6 @@ getRelevantArgs <- function(args, plumberExpression){ #' #' Defines a terminal handler in a PLumber router. #' -#' @importFrom stringi stri_match_first_regex #' @export PlumberEndpoint <- R6Class( "PlumberEndpoint", diff --git a/R/plumber.R b/R/plumber.R index e256c8066..05d1a751c 100644 --- a/R/plumber.R +++ b/R/plumber.R @@ -11,10 +11,21 @@ enumerateVerbs <- function(v) { toupper(v) } -#' @rdname plumber -#' @param file path to file to plumb -#' @param dir dir path where to look for file to plumb +#' Plumber Router +#' +#' Routers are the core request handler in plumber. A router is responsible for +#' taking an incoming request, submitting it through the appropriate filters and +#' eventually to a corresponding endpoint, if one is found. +#' +#' See \url{http://www.rplumber.io/docs/programmatic/} for additional +#' details on the methods available on this object. +#' @param file The file to parse as the plumber router definition. +#' @param dir The directory containing the `plumber.R` file to parse as the +#' plumber router definition. Alternatively, if an `entrypoint.R` file is +#' found, it will take precedence and be responsible for returning a runnable +#' router. #' @export +#' @rdname plumber plumb <- function(file = NULL, dir = ".") { if (!is.null(file) && !identical(dir, ".")) { @@ -82,8 +93,8 @@ plumb <- function(file = NULL, dir = ".") { } -#' @include query-string.R -#' @include post-body.R +#' @include parse-query.R +#' @include parse-body.R #' @include cookie-parser.R #' @include shared-secret-filter.R defaultPlumberFilters <- list( @@ -169,32 +180,18 @@ hookable <- R6Class( ) ) - -#' Plumber Router -#' -#' Routers are the core request handler in plumber. A router is responsible for -#' taking an incoming request, submitting it through the appropriate filters and -#' eventually to a corresponding endpoint, if one is found. -#' -#' See \url{http://www.rplumber.io/docs/programmatic/} for additional -#' details on the methods available on this object. -#' @param file The file to parse as the plumber router definition -#' @param dir The directory containing the `plumber.R` file to parse as the -#' plumber router definition. Alternatively, if an `entrypoint.R` file is -#' found, it will take precedence and be responsible for returning a runnable -#' Plumber router. #' @include globals.R #' @include serializer-json.R -#' @include parse-block.R -#' @include parse-globals.R +#' @include plumb-block.R +#' @include plumb-globals.R #' @export -#' @importFrom httpuv runServer #' @import crayon plumber <- R6Class( "plumber", inherit = hookable, public = list( #' @description Create a new `plumber` router + #' @param file The file to parse as the plumber router definition. #' @param filters a list of plumber filters #' @param envir an environment to be used as the enclosure for the routers execution #' @return A new `plumber` router @@ -246,59 +243,39 @@ plumber <- R6Class( private$addFilterInternal, self$mount) } - private$globalSettings <- parseGlobals(private$lines) + private$globalSettings <- plumbGlobals(private$lines) } }, - #' @description Start an a server using `plumber` object. + #' @description Start a server using `plumber` object. #' @param host a string that is a valid IPv4 or IPv6 address that is owned by #' this server, which the application will listen on. "0.0.0.0" represents #' all IPv4 addresses and "::/0" represents all IPv6 addresses. #' @param port a number or integer that indicates the server port that should #' be listened on. Note that on most Unix-like systems including Linux and #' Mac OS X, port numbers smaller than 1025 require root privileges. - #' @param swagger a function that enhances the existing swagger specification. + #' @param ui a logical or a character vector. The default UI when `TRUE` is `Swagger`. + #' Other valid values are `FALSE` and `Redoc`. #' @param debug `TRUE` provides more insight into your API errors. - #' @param swaggerCallback a callback function for taking action on the url for swagger page. + #' @param callback a callback function for taking action on UI url. + #' @param ... Other params to be passed down to ui functions, such as + #' `redoc_options` \code{redoc::\link[redoc]{redoc_spec}}. #' @details #' `port` does not need to be explicitly assigned. - #' It will be attributed automatically in the following priority order : - #' option `plumber.port`, `port` value in global environment, a random port - #' between 3000 and 10000 that is not blacklisted. When in unable to find - #' an available port, method will fail. - #' - #' `swagger` should be either a logial or set to a function . When `TRUE` or a - #' function, multiple handles will be added to `plumber` object. OpenAPI json - #' file will be served on paths `/openapi.json` and `/swagger.json`. Swagger UI - #' will be served on paths `/__swagger__/index.html` and `/__swagger__/`. When - #' using a function, it will receive the plumber router as the first parameter - #' and currrent swagger specifications as the second. This function should return a - #' list containing swagger specifications. - #' See \url{https://swagger.io/docs/specification/} #' - #' `swaggerCallback` When set, it will be called with a character string corresponding - #' to the swagger UI url. It allows RStudio to open swagger UI when plumber router - #' run method is executed using default `plumber.swagger.url` option. - #' @examples - #' \dontrun{ - #' pr <- plumber$new - #' swagger <- function(pr_, spec) { - #' spec$servers[[1]]$description <- "MyCustomAPISpec" - #' spec - #' } - #' pr$run(swagger = swagger) - #' } + #' When `ui` is used, multiple handles will be added to `plumber` router. + #' OpenAPI json file will be served on paths `/openapi.json`. + #' UI will be served on path `/__{ui}__/index.html` and `/__{ui}__/`. run = function( host = '127.0.0.1', port = getOption('plumber.port'), - swagger = interactive(), + ui = interactive(), debug = interactive(), - swaggerCallback = getOption('plumber.swagger.url', NULL) + callback = getOption('plumber.ui.callback', getOption('plumber.swagger.url', NULL)), + ... ) { port <- findPort(port) - - - message("Running plumber API at ", urlHost(host, port, changeHostLocation = FALSE)) + message("Running plumber API at ", urlHost(host = host, port = port, changeHostLocation = FALSE)) priorDebug <- getOption("plumber.debug") on.exit({ options("plumber.debug" = priorDebug) }) @@ -311,72 +288,7 @@ plumber <- R6Class( setwd(dirname(private$filename)) } - if (isTRUE(swagger) || is.function(swagger)) { - if (!requireNamespace("swagger")) { - stop("swagger must be installed for the Swagger UI to be displayed") - } - spec <- self$swaggerFile() - - # Create a function that's hardcoded to return the swaggerfile -- regardless of env. - 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 (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 - } - 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"', - version = "3" - ) - } - 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( - urlHost(getOption("plumber.apiHost", host), port, changeHostLocation = TRUE), - "/__swagger__/" - ) - message("Running Swagger UI at ", swaggerUrl, sep = "") - # notify swaggerCallback of plumber swagger location - if (!is.null(swaggerCallback) && is.function(swaggerCallback)) { - swaggerCallback(swaggerUrl) - } - } + if (isTRUE(ui) || is.character(ui)) mountUI(self, host, port, ui, callback, ...) on.exit(private$runHooks("exit"), add = TRUE) @@ -890,29 +802,30 @@ plumber <- R6Class( filter <- PlumberFilter$new(name, expr, private$envir, serializer) private$addFilterInternal(filter) }, - #' @description Retrieve openAPI file - swaggerFile = function() { #FIXME: test - - swaggerPaths <- private$swaggerFileWalkMountsInternal(self) - - # Extend the previously parsed settings with the endpoints - def <- utils::modifyList(private$globalSettings, list(paths = swaggerPaths)) - - # Lay those over the default globals so we ensure that the required fields - # (like API version) are satisfied. + #' @description Retrieve OpenAPI specification + openAPISpec = function() { + routerSpec <- private$routerSpecificationInternal(self) + def <- utils::modifyList(private$globalSettings, list(paths = routerSpec)) ret <- utils::modifyList(defaultGlobals, def) - - # remove NA or NULL values, which swagger doesn't like + if (is.list(self$customSpec)) { + ret <- utils::modifyList(ret, self$customSpec) + } ret <- removeNaOrNulls(ret) - ret }, - #' @description Retrieve openAPI file - openAPIFile = function() { - self$swaggerFile() - }, + #' @field customSpec A list of custom spec to overlay over openAPI spec + #' generated from a plumber router. + customSpec = NULL, ### Legacy/Deprecated + #' @description Retrieve OpenAPI specification (deprecated and will be removed in a coming release) + openAPIFile = function() { + self$openAPISpec() + }, + #' @description Retrieve OpenAPI specification (deprecated and will be removed in a coming release) + swaggerFile = function() { + self$openAPISpec() + }, #' @details addEndpoint has been deprecated in v0.4.0 and will be removed in a coming release. Please use `handle()` instead. #' @param verbs verbs #' @param path path @@ -963,6 +876,7 @@ plumber <- R6Class( warning("addGlobalProcessor has been deprecated in v0.4.0 and will be removed in a coming release. Please use `registerHook`(s) instead.") self$registerHooks(proc) } + ), active = list( #' @field endpoints plumber router endpoints read-only endpoints = function(){ @@ -1071,7 +985,7 @@ plumber <- R6Class( private$ends[[preempt]] <- c(private$ends[[preempt]], ep) }, - swaggerFileWalkMountsInternal = function(router, parentPath = "") { + routerSpecificationInternal = function(router, parentPath = "") { remove_trailing_slash <- function(x) { sub("[/]$", "", x) } @@ -1089,18 +1003,18 @@ plumber <- R6Class( for (endpoint in router$endpoints) { for (endpointEntry in endpoint) { - swaggerEndpoint <- prepareSwaggerEndpoint( + endpointSpec <- endpointSpecification( endpointEntry, join_paths(parentPath, endpointEntry$path) ) - endpointList <- utils::modifyList(endpointList, swaggerEndpoint) + endpointList <- utils::modifyList(endpointList, endpointSpec) } } # recursively gather mounted enpoint entries if (length(router$mounts) > 0) { for (mountPath in names(router$mounts)) { - mountEndpoints <- private$swaggerFileWalkMountsInternal( + mountEndpoints <- private$routerSpecificationInternal( router$mounts[[mountPath]], join_paths(parentPath, mountPath) ) @@ -1108,7 +1022,7 @@ plumber <- R6Class( } } - # returning a single list of swagger entries + # returning a single list of OpenAPI entries endpointList } ) @@ -1121,7 +1035,7 @@ plumber <- R6Class( -urlHost <- function(host, port, changeHostLocation = FALSE) { +urlHost <- function(scheme = "http", host, port, path = "", changeHostLocation = FALSE) { if (isTRUE(changeHostLocation)) { # upgrade swaggerCallback location to be localhost and not catch-all addresses # shiny: https://github.com/rstudio/shiny/blob/95173f6/R/server.R#L781-L786 @@ -1139,13 +1053,7 @@ urlHost <- function(host, port, changeHostLocation = FALSE) { if (grepl(":[^/]", host)) { host <- paste0("[", host, "]") } - # if no match against a protocol - if (!grepl("://", host)) { - # add http protocol - # RStudio IDE does NOT like empty protocols like "127.0.0.1:1234/route" - # Works if supplying "http://127.0.0.1:1234/route" - host <- paste0("http://", host) - } - paste0(host, ":", port) + paste0(scheme, "://", host, ":", port, path) + } diff --git a/R/ui.R b/R/ui.R new file mode 100644 index 000000000..293db24c3 --- /dev/null +++ b/R/ui.R @@ -0,0 +1,134 @@ +# Mount OpenAPI and UI +#' @noRd +mountUI <- function(pr, host, port, ui, callback, ...) { + + # Build api url + api_url <- getOption( + "plumber.apiURL", + urlHost( + scheme = getOption("plumber.apiScheme", "http"), + host = getOption("plumber.apiHost", host), + port = getOption("plumber.apiPort", port), + path = getOption("plumber.apiPath", ""), + changeHostLocation = TRUE + ) + ) + + # Mount openAPI spec paths openapi.json + mountOpenAPI(pr, api_url) + + # Mount UIs + if (isTRUE(ui)) { + ui <- getOption("plumber.ui", "Swagger") + } + interface <- ui[1] + ui_mount <- .globals$interfaces[[interface]] + if (!is.null(ui_mount)) { + ui_url <- ui_mount(pr, api_url,...) + message("Running ", interface, " UI at ", ui_url, sep = "") + } else { + message("Ignored unknown user interface ", interface,". Supports ", + paste0('"', names(.globals$interfaces), '"', collapse = ", ")) + } + + # Use callback when defined + if (is.function(callback)) { + callback(ui_url) + } + + return(invisible()) + +} + +#' Mount OpenAPI spec to a plumber router +#' @noRd +mountOpenAPI <- function(pr, api_server_url) { + + spec <- pr$openAPISpec() + + # Create a function that's hardcoded to return the OpenAPI specification -- regardless of env. + openapi_fun <- function(req) { + # use the HTTP_REFERER so RSC can find the swagger location to ask + ## (can't directly ask for 127.0.0.1) + if (is.null(getOption("plumber.apiURL")) && + is.null(getOption("plumber.apiHost"))) { + if (is.null(req$HTTP_REFERER)) { + # Prevent leaking host and port if option is not set + api_server_url <- character(1) + } + else { + # Use HTTP_REFERER as fallback + api_server_url <- req$HTTP_REFERER + api_server_url <- sub("index\\.html$", "", api_server_url) + api_server_url <- sub("__[swagerdoc]+__/$", "", api_server_url) + } + } + + utils::modifyList(list(servers = list(list(url = api_server_url, description = "OpenAPI"))), spec) + + } + # http://spec.openapis.org/oas/v3.0.3#document-structure + # "It is RECOMMENDED that the root OpenAPI document be named: openapi.json" + pr$handle("GET", "/openapi.json", openapi_fun, serializer = serializer_unboxed_json()) + + return(invisible()) + +} + +#' Mount Swagger UI +#' @noRd +mountSwagger <- function(pr, url, ...) { + if (!requireNamespace("swagger", quietly = TRUE)) { + stop("swagger must be installed for the Swagger UI to be displayed") + } + + swaggerUrl <- paste0(url, "/__swagger__/") + + swagger_index <- function(...) { + swagger::swagger_spec( + 'window.location.origin + window.location.pathname.replace(/\\(__swagger__\\\\/|__swagger__\\\\/index.html\\)$/, "") + "openapi.json"', + version = "3" + ) + } + for (path in c("/__swagger__/index.html", "/__swagger__/")) { + pr$handle( + "GET", path, swagger_index, + serializer = serializer_html() + ) + } + pr$mount("/__swagger__", PlumberStatic$new(swagger::swagger_path())) + + return(swaggerUrl) +} + +#' @include globals.R +.globals$interfaces[["swagger"]] <- mountSwagger +.globals$interfaces[["Swagger"]] <- mountSwagger + +#' Mount Redoc UI +#' @noRd +mountRedoc <- function(pr, url, redoc_options = structure(list(), names = character())) { + if (!requireNamespace("redoc", quietly = TRUE)) { + stop("redoc must be installed for the Redoc UI to be displayed") + } + + redocUrl <- paste0(url, "/__redoc__/") + + redoc_index <- function(...) { + redoc::redoc_spec( + "\' + window.location.origin + window.location.pathname.replace(/\\(__redoc__\\\\/|__redoc__\\\\/index.html\\)$/, '') + 'openapi.json' + \'", + redoc_options) + } + for (path in c("/__redoc__/index.html", "/__redoc__/")) { + pr$handle( + "GET", path, redoc_index, + serializer = serializer_html() + ) + } + pr$mount("/__redoc__", PlumberStatic$new(redoc::redoc_path())) + + return(redocUrl) +} + +.globals$interfaces[["redoc"]] <- mountRedoc +.globals$interfaces[["Redoc"]] <- mountRedoc diff --git a/R/zzz.R b/R/zzz.R index c24aaa24a..c2d2fe0d2 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,7 +1,7 @@ .onLoad <- function(...) { - addSwaggerInfo_onLoad() + addDataTypeInfo_onLoad() addParsers_onLoad() diff --git a/inst/examples/15-swagger-spec/entrypoint.R b/inst/examples/15-openapi-spec/entrypoint.R similarity index 100% rename from inst/examples/15-swagger-spec/entrypoint.R rename to inst/examples/15-openapi-spec/entrypoint.R diff --git a/man/PlumberStatic.Rd b/man/PlumberStatic.Rd index def773153..e18ab2d6c 100644 --- a/man/PlumberStatic.Rd +++ b/man/PlumberStatic.Rd @@ -37,6 +37,7 @@ Creates a router that is backed by a directory of files on disk. \item \out{}\href{../../plumber/html/plumber.html#method-onHeaders}{\code{plumber::plumber$onHeaders()}}\out{} \item \out{}\href{../../plumber/html/plumber.html#method-onWSOpen}{\code{plumber::plumber$onWSOpen()}}\out{} \item \out{}\href{../../plumber/html/plumber.html#method-openAPIFile}{\code{plumber::plumber$openAPIFile()}}\out{} +\item \out{}\href{../../plumber/html/plumber.html#method-openAPISpec}{\code{plumber::plumber$openAPISpec()}}\out{} \item \out{}\href{../../plumber/html/plumber.html#method-registerHook}{\code{plumber::plumber$registerHook()}}\out{} \item \out{}\href{../../plumber/html/plumber.html#method-route}{\code{plumber::plumber$route()}}\out{} \item \out{}\href{../../plumber/html/plumber.html#method-run}{\code{plumber::plumber$run()}}\out{} diff --git a/man/addParser.Rd b/man/addParser.Rd index de3941aa9..610299104 100644 --- a/man/addParser.Rd +++ b/man/addParser.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/post-parsers.R +% Please edit documentation in R/parsers-body.R \name{addParser} \alias{addParser} \title{Add a Parsers} diff --git a/man/parsers.Rd b/man/parsers.Rd index 903b4e349..e05a5558d 100644 --- a/man/parsers.Rd +++ b/man/parsers.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/post-parsers.R +% Please edit documentation in R/parsers-body.R \name{parsers} \alias{parsers} \alias{parser_json} diff --git a/man/plumber.Rd b/man/plumber.Rd index ded441ed1..17c8a67f9 100644 --- a/man/plumber.Rd +++ b/man/plumber.Rd @@ -5,120 +5,27 @@ \alias{plumber} \title{Plumber Router} \usage{ -plumb(file, dir = ".") +plumb(file = NULL, dir = ".") } \arguments{ -\item{file}{path to file to plumb} +\item{file}{The file to parse as the plumber router definition.} -\item{dir}{dir path where to look for file to plumb} +\item{dir}{The directory containing the \code{plumber.R} file to parse as the +plumber router definition. Alternatively, if an \code{entrypoint.R} file is +found, it will take precedence and be responsible for returning a runnable +router.} } \description{ -Plumber Router - -Plumber Router -} -\details{ Routers are the core request handler in plumber. A router is responsible for taking an incoming request, submitting it through the appropriate filters and eventually to a corresponding endpoint, if one is found. - +} +\details{ See \url{http://www.rplumber.io/docs/programmatic/} for additional details on the methods available on this object. - -\code{port} does not need to be explicitly assigned. -It will be attributed automatically in the following priority order : -option \code{plumber.port}, \code{port} value in global environment, a random port -between 3000 and 10000 that is not blacklisted. When in unable to find -an available port, method will fail. - -\code{swagger} should be either a logial or set to a function . When \code{TRUE} or a -function, multiple handles will be added to \code{plumber} object. OpenAPI json -file will be served on paths \verb{/openapi.json} and \verb{/swagger.json}. Swagger UI -will be served on paths \verb{/__swagger__/index.html} and \verb{/__swagger__/}. When -using a function, it will receive the plumber router as the first parameter -and currrent swagger specifications as the second. This function should return a -list containing swagger specifications. -See \url{https://swagger.io/docs/specification/} - -\code{swaggerCallback} When set, it will be called with a character string corresponding -to the swagger UI url. It allows RStudio to open swagger UI when plumber router -run method is executed using default \code{plumber.swagger.url} option. - -Plumber routers can be “nested” by mounting one into another -using the \code{mount()} method. This allows you to compartmentalize your API -by paths which is a great technique for decomposing large APIs into smaller files. - -Plumber routers support the notion of "hooks" that can be registered -to execute some code at a particular point in the lifecycle of a request. -Plumber routers currently support four hooks: -\enumerate{ -\item \code{preroute(data, req, res)} -\item \code{postroute(data, req, res, value)} -\item \code{preserialize(data, req, res, value)} -\item \code{postserialize(data, req, res, value)} -} -In all of the above you have access to a disposable environment in the \code{data} -parameter that is created as a temporary data store for each request. Hooks -can store temporary data in these hooks that can be reused by other hooks -processing this same request. - -One feature when defining hooks in Plumber routers is the ability to modify -the returned value. The convention for such hooks is: any function that accepts -a parameter named \code{value} is expected to return the new value. This could -be an unmodified version of the value that was passed in, or it could be a -mutated value. But in either case, if your hook accepts a parameter -named \code{value}, whatever your hook returns will be used as the new value -for the response. - -You can add hooks using the \code{registerHook} method, or you can add multiple -hooks at once using the \code{registerHooks} method which takes a name list in -which the names are the names of the hooks, and the values are the -handlers themselves. - -The “handler” functions that you define in these handle calls -are identical to the code you would have defined in your plumber.R file -if you were using annotations to define your API. The handle() method -takes additional arguments that allow you to control nuanced behavior -of the endpoint like which filter it might preempt or which serializer -it should use. - -required for httpuv interface - -required for httpuv interface - -required for httpuv interface - -Sets the default serializer of the router. - -Sets the handler that gets called if an -incoming request can’t be served by any filter, endpoint, or sub-router. - -Sets the error handler which gets invoked if any filter or -endpoint generates an error. - -addEndpoint has been deprecated in v0.4.0 and will be removed in a coming release. Please use \code{handle()} instead. - -addAssets has been deprecated in v0.4.0 and will be removed in a coming release. Please use \code{mount} and \code{PlumberStatic$new()} instead. - -addFilter has been deprecated in v0.4.0 and will be removed in a coming release. Please use \code{filter} instead. - -addGlobalProcessor has been deprecated in v0.4.0 and will be removed in a coming release. Please use \code{registerHook}(s) instead. } \examples{ -## ------------------------------------------------ -## Method `plumber$run` -## ------------------------------------------------ - -\dontrun{ -pr <- plumber$new -swagger <- function(pr_, spec) { - spec$servers[[1]]$description <- "MyCustomAPISpec" - spec -} -pr$run(swagger = swagger) -} - ## ------------------------------------------------ ## Method `plumber$mount` ## ------------------------------------------------ @@ -192,6 +99,14 @@ pr$setErrorHandler(function(req, res) {cat(res$body)}) \section{Super class}{ \code{\link[plumber:hookable]{plumber::hookable}} -> \code{plumber} } +\section{Public fields}{ +\if{html}{\out{
}} +\describe{ +\item{\code{customSpec}}{A list of custom spec to overlay over openAPI spec +generated from a plumber router.} +} +\if{html}{\out{
}} +} \section{Active bindings}{ \if{html}{\out{
}} \describe{ @@ -225,8 +140,9 @@ pr$setErrorHandler(function(req, res) {cat(res$body)}) \item \href{#method-set404Handler}{\code{plumber$set404Handler()}} \item \href{#method-setErrorHandler}{\code{plumber$setErrorHandler()}} \item \href{#method-filter}{\code{plumber$filter()}} -\item \href{#method-swaggerFile}{\code{plumber$swaggerFile()}} +\item \href{#method-openAPISpec}{\code{plumber$openAPISpec()}} \item \href{#method-openAPIFile}{\code{plumber$openAPIFile()}} +\item \href{#method-swaggerFile}{\code{plumber$swaggerFile()}} \item \href{#method-addEndpoint}{\code{plumber$addEndpoint()}} \item \href{#method-addAssets}{\code{plumber$addAssets()}} \item \href{#method-addFilter}{\code{plumber$addFilter()}} @@ -253,7 +169,7 @@ Create a new \code{plumber} router \subsection{Arguments}{ \if{html}{\out{
}} \describe{ -\item{\code{file}}{The file to parse as the plumber router definition} +\item{\code{file}}{The file to parse as the plumber router definition.} \item{\code{filters}}{a list of plumber filters} @@ -269,14 +185,15 @@ A new \code{plumber} router \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-run}{}}} \subsection{Method \code{run()}}{ -Start an a server using \code{plumber} object. +Start a server using \code{plumber} object. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{plumber$run( host = "127.0.0.1", port = getOption("plumber.port"), - swagger = interactive(), + ui = interactive(), debug = interactive(), - swaggerCallback = getOption("plumber.swagger.url", NULL) + callback = getOption("plumber.ui.callback", getOption("plumber.swagger.url", NULL)), + ... )}\if{html}{\out{
}} } @@ -291,27 +208,24 @@ all IPv4 addresses and "::/0" represents all IPv6 addresses.} be listened on. Note that on most Unix-like systems including Linux and Mac OS X, port numbers smaller than 1025 require root privileges.} -\item{\code{swagger}}{a function that enhances the existing swagger specification.} +\item{\code{ui}}{a logical or a character vector. The default UI when \code{TRUE} is \code{Swagger}. +Other valid values are \code{FALSE} and \code{Redoc}.} \item{\code{debug}}{\code{TRUE} provides more insight into your API errors.} -\item{\code{swaggerCallback}}{a callback function for taking action on the url for swagger page.} +\item{\code{callback}}{a callback function for taking action on UI url.} + +\item{\code{...}}{Other params to be passed down to ui functions, such as +\code{redoc_options} \code{redoc::\link[redoc]{redoc_spec}}.} } \if{html}{\out{
}} } -\subsection{Examples}{ -\if{html}{\out{
}} -\preformatted{\dontrun{ -pr <- plumber$new -swagger <- function(pr_, spec) { - spec$servers[[1]]$description <- "MyCustomAPISpec" - spec -} -pr$run(swagger = swagger) -} -} -\if{html}{\out{
}} +\subsection{Details}{ +\code{port} does not need to be explicitly assigned. +When \code{ui} is used, multiple handles will be added to \code{plumber} router. +OpenAPI json file will be served on paths \verb{/openapi.json}. +UI will be served on path \verb{/__\{ui\}__/index.html} and \verb{/__\{ui\}__/}. } } @@ -333,6 +247,12 @@ Mount a plumber router } \if{html}{\out{
}} } +\subsection{Details}{ +Plumber routers can be “nested” by mounting one into another +using the \code{mount()} method. This allows you to compartmentalize your API +by paths which is a great technique for decomposing large APIs into smaller files. +} + \subsection{Examples}{ \if{html}{\out{
}} \preformatted{\dontrun{ @@ -371,6 +291,35 @@ Register a hook } \if{html}{\out{
}} } +\subsection{Details}{ +Plumber routers support the notion of "hooks" that can be registered +to execute some code at a particular point in the lifecycle of a request. +Plumber routers currently support four hooks: +\enumerate{ +\item \code{preroute(data, req, res)} +\item \code{postroute(data, req, res, value)} +\item \code{preserialize(data, req, res, value)} +\item \code{postserialize(data, req, res, value)} +} +In all of the above you have access to a disposable environment in the \code{data} +parameter that is created as a temporary data store for each request. Hooks +can store temporary data in these hooks that can be reused by other hooks +processing this same request. + +One feature when defining hooks in Plumber routers is the ability to modify +the returned value. The convention for such hooks is: any function that accepts +a parameter named \code{value} is expected to return the new value. This could +be an unmodified version of the value that was passed in, or it could be a +mutated value. But in either case, if your hook accepts a parameter +named \code{value}, whatever your hook returns will be used as the new value +for the response. + +You can add hooks using the \code{registerHook} method, or you can add multiple +hooks at once using the \code{registerHooks} method which takes a name list in +which the names are the names of the hooks, and the values are the +handlers themselves. +} + \subsection{Examples}{ \if{html}{\out{
}} \preformatted{\dontrun{ @@ -429,6 +378,15 @@ Define endpoints } \if{html}{\out{
}} } +\subsection{Details}{ +The “handler” functions that you define in these handle calls +are identical to the code you would have defined in your plumber.R file +if you were using annotations to define your API. The handle() method +takes additional arguments that allow you to control nuanced behavior +of the endpoint like which filter it might preempt or which serializer +it should use. +} + \subsection{Examples}{ \if{html}{\out{
}} \preformatted{\dontrun{ @@ -522,6 +480,10 @@ httpuv interface call function } \if{html}{\out{
}} } +\subsection{Details}{ +required for httpuv interface +} + } \if{html}{\out{
}} \if{html}{\out{}} @@ -539,6 +501,10 @@ httpuv interface onHeaders function } \if{html}{\out{}} } +\subsection{Details}{ +required for httpuv interface +} + } \if{html}{\out{
}} \if{html}{\out{}} @@ -556,6 +522,10 @@ httpuv interface onWSOpen function } \if{html}{\out{}} } +\subsection{Details}{ +required for httpuv interface +} + } \if{html}{\out{
}} \if{html}{\out{}} @@ -572,6 +542,10 @@ httpuv interface onWSOpen function } \if{html}{\out{}} } +\subsection{Details}{ +Sets the default serializer of the router. +} + } \if{html}{\out{
}} \if{html}{\out{}} @@ -588,6 +562,11 @@ httpuv interface onWSOpen function } \if{html}{\out{}} } +\subsection{Details}{ +Sets the handler that gets called if an +incoming request can’t be served by any filter, endpoint, or sub-router. +} + \subsection{Examples}{ \if{html}{\out{
}} \preformatted{\dontrun{ @@ -615,6 +594,11 @@ pr$set404Handler(function(req, res) {cat(req$PATH_INFO)}) } \if{html}{\out{
}} } +\subsection{Details}{ +Sets the error handler which gets invoked if any filter or +endpoint generates an error. +} + \subsection{Examples}{ \if{html}{\out{
}} \preformatted{\dontrun{ @@ -649,12 +633,12 @@ Add a filter to plumber router } } \if{html}{\out{
}} -\if{html}{\out{}} -\if{latex}{\out{\hypertarget{method-swaggerFile}{}}} -\subsection{Method \code{swaggerFile()}}{ -Retrieve openAPI file +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-openAPISpec}{}}} +\subsection{Method \code{openAPISpec()}}{ +Retrieve OpenAPI specification \subsection{Usage}{ -\if{html}{\out{
}}\preformatted{plumber$swaggerFile()}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{plumber$openAPISpec()}\if{html}{\out{
}} } } @@ -662,11 +646,21 @@ Retrieve openAPI file \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-openAPIFile}{}}} \subsection{Method \code{openAPIFile()}}{ -Retrieve openAPI file +Retrieve OpenAPI specification (deprecated and will be removed in a coming release) \subsection{Usage}{ \if{html}{\out{
}}\preformatted{plumber$openAPIFile()}\if{html}{\out{
}} } +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-swaggerFile}{}}} +\subsection{Method \code{swaggerFile()}}{ +Retrieve OpenAPI specification (deprecated and will be removed in a coming release) +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{plumber$swaggerFile()}\if{html}{\out{
}} +} + } \if{html}{\out{
}} \if{html}{\out{}} @@ -706,6 +700,10 @@ Retrieve openAPI file } \if{html}{\out{
}} } +\subsection{Details}{ +addEndpoint has been deprecated in v0.4.0 and will be removed in a coming release. Please use \code{handle()} instead. +} + } \if{html}{\out{
}} \if{html}{\out{}} @@ -726,6 +724,10 @@ Retrieve openAPI file } \if{html}{\out{}} } +\subsection{Details}{ +addAssets has been deprecated in v0.4.0 and will be removed in a coming release. Please use \code{mount} and \code{PlumberStatic$new()} instead. +} + } \if{html}{\out{
}} \if{html}{\out{}} @@ -748,6 +750,10 @@ Retrieve openAPI file } \if{html}{\out{}} } +\subsection{Details}{ +addFilter has been deprecated in v0.4.0 and will be removed in a coming release. Please use \code{filter} instead. +} + } \if{html}{\out{
}} \if{html}{\out{}} @@ -764,6 +770,10 @@ Retrieve openAPI file } \if{html}{\out{}} } +\subsection{Details}{ +addGlobalProcessor has been deprecated in v0.4.0 and will be removed in a coming release. Please use \code{registerHook}(s) instead. +} + } \if{html}{\out{
}} \if{html}{\out{}} diff --git a/scripts/manual_testing.R b/scripts/manual_testing.R index 64a6dbf0d..82c9c2386 100644 --- a/scripts/manual_testing.R +++ b/scripts/manual_testing.R @@ -7,23 +7,27 @@ library(plumber) -test_that("custom swagger file update function works", { +test_that("custom openapi file update function works", { pr <- plumber$new() pr$handle("GET", "/:path/here", function(){}) - - pr$run( - port = 1234, - swagger = function(pr_, spec, ...) { - spec$info$title <- Sys.time() - spec - } - ) + pr$customSpec <- list(info = list(title = Sys.time())) + pr$run(port = 1234) # validate that http://127.0.0.1:1234/__swagger__/ displays the system time as the api title # http://127.0.0.1:1234/__swagger__/ }) +test_that("custom swagger and redoc works", { + pr <- plumber$new() + pr$handle("GET", "/:path/here", function(){}) + pr$run(ui = c("swagger", "redoc", "myLittleUI")) + + # validate that http://127.0.0.1:1234/__swagger__/ and + # http://127.0.0.1:1234/__redoc__/ works + # Message contains Ignored unknown ... +}) + test_that("host doesn't change for messages, but does for RStudio IDE", { diff --git a/tests/testthat/files/include/test.md b/tests/testthat/files/include/test.md index 8a8989b51..bf9ee403a 100644 --- a/tests/testthat/files/include/test.md +++ b/tests/testthat/files/include/test.md @@ -1,5 +1,5 @@ --- -title: "test" +title: "This is a markdown document" --- Overview diff --git a/tests/testthat/test-deprecated.R b/tests/testthat/test-deprecated.R deleted file mode 100644 index 78c492358..000000000 --- a/tests/testthat/test-deprecated.R +++ /dev/null @@ -1,32 +0,0 @@ -context("Deprecated") - -test_that("addEndpoint continues to work", { - pr <- plumber$new() - expect_warning(pr$addEndpoint("GET", "/", function(){ 123 })) - expect_error(expect_warning(pr$addEndpoint("GET", "/", function(){ 123 }, comments="break"))) - - val <- pr$route(make_req("GET", "/"), PlumberResponse$new()) - expect_equal(val, 123) -}) - -test_that("addFilter continues to work", { - pr <- plumber$new() - expect_warning(pr$addFilter("f1", function(req){ req$filtered <- TRUE })) - pr$handle("GET", "/", function(req){ req$filtered }) - - val <- pr$route(make_req("GET", "/"), PlumberResponse$new()) - expect_true(val) -}) - -test_that("addGlobalProcessor continues to work", { - pr <- plumber$new() - expect_warning(pr$addGlobalProcessor(sessionCookie("secret", "cookieName"))) -}) - -test_that("addAssets continues to work", { - pr <- plumber$new() - expect_warning(pr$addAssets(test_path("./files/static"), "/public")) - res <- PlumberResponse$new() - val <- pr$route(make_req("GET", "/public/test.txt"), res) - expect_true(inherits(val, "PlumberResponse")) -}) diff --git a/tests/testthat/test-globals.R b/tests/testthat/test-globals.R index b36423172..e4a7458a2 100644 --- a/tests/testthat/test-globals.R +++ b/tests/testthat/test-globals.R @@ -1,22 +1,22 @@ context("global settings") -test_that("parseOneGlobal parses with various formats", { +test_that("plumbOneGlobal parses with various formats", { fields <- list(info=list()) # No leading space - g <- parseOneGlobal(fields, "#'@apiTitle Title") + g <- plumbOneGlobal(fields, "#'@apiTitle Title") expect_equal(g$info$title, "Title") # Plumber-style - g <- parseOneGlobal(fields, "#* @apiTitle Title") + g <- plumbOneGlobal(fields, "#* @apiTitle Title") expect_equal(g$info$title, "Title") #Extra space - g <- parseOneGlobal(fields, "#* @apiTitle Title ") + g <- plumbOneGlobal(fields, "#* @apiTitle Title ") expect_equal(g$info$title, "Title") }) -test_that("parseGlobals works", { +test_that("plumbGlobals works", { # Test all fields lines <- c("#' @apiTitle title", "#' @apiDescription description", @@ -32,7 +32,7 @@ test_that("parseGlobals works", { "#' @apiTag tag description", "#' @apiTag tag2 description2") - fields <- parseGlobals(lines) + fields <- plumbGlobals(lines) expect_equal(fields, list( info=list( @@ -55,5 +55,5 @@ test_that("parseGlobals works", { test_that("Globals can't contain duplicate tags", { lines <- c("#* @apiTag test description1", "#* @apiTag test description2") - expect_error(parseGlobals(lines), "Duplicate tag definition specified.") + expect_error(plumbGlobals(lines), "Duplicate tag definition specified.") }) diff --git a/tests/testthat/test-swagger.R b/tests/testthat/test-openapi.R similarity index 80% rename from tests/testthat/test-swagger.R rename to tests/testthat/test-openapi.R index 9336708e3..ea476518f 100644 --- a/tests/testthat/test-swagger.R +++ b/tests/testthat/test-openapi.R @@ -1,22 +1,22 @@ -context("swagger") +context("OpenAPI") -test_that("plumberToSwaggerType works", { - expect_equal(plumberToSwaggerType("bool"), "boolean") - expect_equal(plumberToSwaggerType("logical"), "boolean") +test_that("plumberToDataType works", { + expect_equal(plumberToDataType("bool"), "boolean") + expect_equal(plumberToDataType("logical"), "boolean") - expect_equal(plumberToSwaggerType("double"), "number") - expect_equal(plumberToSwaggerType("numeric"), "number") + expect_equal(plumberToDataType("double"), "number") + expect_equal(plumberToDataType("numeric"), "number") - expect_equal(plumberToSwaggerType("int"), "integer") + expect_equal(plumberToDataType("int"), "integer") - expect_equal(plumberToSwaggerType("character"), "string") + expect_equal(plumberToDataType("character"), "string") - expect_equal(plumberToSwaggerType("df"), "object") - expect_equal(plumberToSwaggerType("list"), "object") - expect_equal(plumberToSwaggerType("data.frame"), "object") + expect_equal(plumberToDataType("df"), "object") + expect_equal(plumberToDataType("list"), "object") + expect_equal(plumberToDataType("data.frame"), "object") expect_warning({ - expect_equal(plumberToSwaggerType("flargdarg"), defaultSwaggerType) + expect_equal(plumberToDataType("flargdarg"), defaultDataType) }, "Unrecognized type:") }) @@ -27,14 +27,14 @@ test_that("response attributes are parsed", { "#' @response 202 Here's second", "#' @response 203 Here's third", "#' @response default And default") - b <- parseBlock(length(lines), lines) + b <- plumbBlock(length(lines), lines) expect_length(b$responses, 4) expect_equal(b$responses$`201`, list(description="This is response 201")) expect_equal(b$responses$`202`, list(description="Here's second")) expect_equal(b$responses$`203`, list(description="Here's third")) expect_equal(b$responses$default, list(description="And default")) - b <- parseBlock(1, "") + b <- plumbBlock(1, "") expect_null(b$responses) }) @@ -45,28 +45,28 @@ test_that("params are parsed", { "#' @param required:character* Required param", "#' @param another:int Another docs", "#' @param multi:[int]* Required array param") - b <- parseBlock(length(lines), lines) + b <- plumbBlock(length(lines), lines) expect_length(b$params, 4) expect_equal(b$params$another, list(desc="Another docs", type="integer", required=FALSE, isArray = FALSE)) - expect_equal(b$params$test, list(desc="Test docs", type=defaultSwaggerType, required=FALSE, isArray = FALSE)) + expect_equal(b$params$test, list(desc="Test docs", type=defaultDataType, required=FALSE, isArray = FALSE)) expect_equal(b$params$required, list(desc="Required param", type="string", required=TRUE, isArray = FALSE)) expect_equal(b$params$multi, list(desc="Required array param", type="integer", required=TRUE, isArray = TRUE)) - b <- parseBlock(1, "") + b <- plumbBlock(1, "") expect_null(b$params) }) # TODO -#test_that("prepareSwaggerEndpoints works", { +#test_that("endpointSpecification works", { #}) -test_that("swaggerFile works with mounted routers", { +test_that("OpenAPI specifications works with mounted routers", { # parameter in path pr <- plumber$new() pr$handle("GET", "/nested/:path/here", function(){}) pr$handle("POST", "/nested/:path/here", function(){}) - # static file handler + # static file handler stat <- PlumberStatic$new(".") # multiple entries @@ -110,34 +110,34 @@ test_that("swaggerFile works with mounted routers", { pr$mount("/sub4", pr4) pr4$mount("/", pr5) - paths <- names(pr$swaggerFile()$paths) + paths <- names(pr$openAPISpec()$paths) expect_length(paths, 7) expect_equal(paths, c("/nested/:path/here", "/sub2/something", - "/sub2/", "/sub2/sub3/else", "/sub2/sub3/", "/sub4/completely", - "/sub4/trailing_slash/" + "/sub2/", "/sub2/sub3/else", "/sub2/sub3/", "/sub4/completely", + "/sub4/trailing_slash/" )) pr <<- pr }) -test_that("extractResponses works", { +test_that("responsesSpecification works", { # Empty - r <- extractResponses(NULL) - expect_equal(r, defaultResp) + r <- responsesSpecification(NULL) + expect_equal(r, defaultResponse) # Response constructor actually defaults to NA, so that's an important case, too - r <- extractResponses(NA) - expect_equal(r, defaultResp) + r <- responsesSpecification(NA) + expect_equal(r, defaultResponse) # Responses with no default customResps <- list("200" = list()) - r <- extractResponses(customResps) + r <- responsesSpecification(customResps) expect_length(r, 2) - expect_equal(r$default, defaultResp$default) + expect_equal(r$default, defaultResponse$default) expect_equal(r$`200`, customResps$`200`) }) -test_that("extractSwaggerParams works", { +test_that("parametersSpecification works", { ep <- list(id=list(desc="Description", type="integer", required=FALSE), id2=list(desc="Description2", required=FALSE), # No redundant type specification make=list(desc="Make description", type="string", required=FALSE), @@ -145,7 +145,7 @@ test_that("extractSwaggerParams works", { claims=list(desc="Insurance claims", type="object", required = FALSE)) pp <- data.frame(name=c("id", "id2", "owners"), type=c("int", "int", "chr"), isArray = c(FALSE, FALSE, TRUE), stringsAsFactors = FALSE) - params <- extractSwaggerParams(ep, pp) + params <- parametersSpecification(ep, pp) expect_equal(params$parameters[[1]], list(name="id", description="Description", @@ -212,7 +212,7 @@ test_that("extractSwaggerParams works", { description = "Insurance claims"))))))) # If id were not a path param it should not be promoted to required - params <- extractSwaggerParams(ep, NULL) + params <- parametersSpecification(ep, NULL) idParam <- params$parameters[[which(vapply(params$parameters, `[[`, character(1), "name") == "id")]] expect_equal(idParam$required, FALSE) expect_equal(idParam$schema$type, "integer") @@ -226,7 +226,7 @@ test_that("extractSwaggerParams works", { } # Check if we can pass a single path parameter without a @param line match - params <- extractSwaggerParams(NULL, pp[3,]) + params <- parametersSpecification(NULL, pp[3,]) expect_equal(params$parameters[[1]], list(name="owners", description=NULL, @@ -241,7 +241,7 @@ test_that("extractSwaggerParams works", { style="simple", explode=FALSE)) - params <- extractSwaggerParams(NULL, NULL) + params <- parametersSpecification(NULL, NULL) expect_equal(sum(sapply(params, length)), 0) }) @@ -274,7 +274,7 @@ test_that("api kitchen sink", { } validate_spec <- function(pr) { - spec <- jsonlite::toJSON(pr$swaggerFile(), auto_unbox = TRUE) + spec <- jsonlite::toJSON(pr$openAPISpec(), auto_unbox = TRUE) tmpfile <- tempfile(fileext = ".json") on.exit({ unlink(tmpfile) @@ -344,16 +344,28 @@ test_that("multiple variations in function extract correct metadata", { list(var0 = 420.69, var1 = NA, var2 = 1L:2L, var3 = NA, var4 = NA, var5 = FALSE, var6 = list(name = c("luke", "bob"), lastname = c("skywalker", "ross")), var7 = NA, var8 = NA)) expect_identical(lapply(funcParams, `[[`, "isArray"), - list(var0 = defaultSwaggerIsArray, var1 = defaultSwaggerIsArray, var2 = TRUE, - var3 = defaultSwaggerIsArray, var4 = defaultSwaggerIsArray, - var5 = defaultSwaggerIsArray, var6 = defaultSwaggerIsArray, - var7 = defaultSwaggerIsArray, var8 = defaultSwaggerIsArray)) + list(var0 = defaultIsArray, var1 = defaultIsArray, var2 = TRUE, + var3 = defaultIsArray, var4 = defaultIsArray, + var5 = defaultIsArray, var6 = defaultIsArray, + var7 = defaultIsArray, var8 = defaultIsArray)) expect_identical(lapply(funcParams, `[[`, "type"), - list(var0 = "number", var1 = defaultSwaggerType, var2 = "integer", var3 = defaultSwaggerType, var4 = defaultSwaggerType, - var5 = "boolean", var6 = "object", var7 = defaultSwaggerType, var8 = defaultSwaggerType)) + list(var0 = "number", var1 = defaultDataType, var2 = "integer", var3 = defaultDataType, var4 = defaultDataType, + var5 = "boolean", var6 = "object", var7 = defaultDataType, var8 = defaultDataType)) }) +test_that("custom spec works", { + pr <- plumber$new() + pr$handle("POST", "/func1", function(){}) + pr$handle("GET", "/func2", function(){}) + pr$handle("GET", "/func3", function(){}) + pr$customSpec <- list(info = list(description = "My Custom Spec", title = "This is only a test")) + spec <- pr$openAPISpec() + expect_equal(spec$info$description, "My Custom Spec") + expect_equal(spec$info$title, "This is only a test") + expect_equal(class(spec$openapi), "character") +}) + test_that("priorize works as expected", { expect_identical("abc", priorizeProperty(structure("zzz", default = TRUE), NULL, "abc")) expect_identical(NULL, priorizeProperty(NULL, NULL, NULL)) diff --git a/tests/testthat/test-parse-block.R b/tests/testthat/test-parse-block.R index e31da3615..0e338b08c 100644 --- a/tests/testthat/test-parse-block.R +++ b/tests/testthat/test-parse-block.R @@ -6,13 +6,13 @@ test_that("trimws works", { expect_equal(trimws("hi "), "hi") }) -test_that("parseBlock works", { +test_that("plumbBlock works", { lines <- c( "#' @get /", "#' @post /", "#' @filter test", "#' @serializer json") - b <- parseBlock(length(lines), lines) + b <- plumbBlock(length(lines), lines) expect_length(b$path, 2) expect_equal(b$path[[1]], list(verb="POST", path="/")) expect_equal(b$path[[2]], list(verb="GET", path="/")) @@ -25,44 +25,44 @@ test_that("parseBlock works", { expect_equal_functions(b$serializer, serializer_json()) }) -test_that("parseBlock images", { +test_that("plumbBlock images", { lines <- c("#'@png") - b <- parseBlock(length(lines), lines) + b <- plumbBlock(length(lines), lines) expect_equal(b$image, "png") expect_equal(b$imageAttr, "") lines <- c("#'@jpeg") - b <- parseBlock(length(lines), lines) + b <- plumbBlock(length(lines), lines) expect_equal(b$image, "jpeg") expect_equal(b$imageAttr, "") # Whitespace is fine lines <- c("#' @jpeg \t ") - b <- parseBlock(length(lines), lines) + b <- plumbBlock(length(lines), lines) expect_equal(b$image, "jpeg") expect_equal(b$imageAttr, "") # No whitespace is fine lines <- c("#' @jpeg(w=1)") - b <- parseBlock(length(lines), lines) + b <- plumbBlock(length(lines), lines) expect_equal(b$image, "jpeg") expect_equal(b$imageAttr, "(w=1)") # Additional chars after name don't count as image tags lines <- c("#' @jpegs") - b <- parseBlock(length(lines), lines) + b <- plumbBlock(length(lines), lines) expect_null(b$image) expect_null(b$imageAttr) # Properly formatted arguments work lines <- c("#'@jpeg (width=100)") - b <- parseBlock(length(lines), lines) + b <- plumbBlock(length(lines), lines) expect_equal(b$image, "jpeg") expect_equal(b$imageAttr, "(width=100)") # Ill-formatted arguments return a meaningful error lines <- c("#'@jpeg width=100") - expect_error(parseBlock(length(lines), lines), "Supplemental arguments to the image serializer") + expect_error(plumbBlock(length(lines), lines), "Supplemental arguments to the image serializer") }) test_that("Block can't be multiple mutually exclusive things", { @@ -81,7 +81,7 @@ test_that("Block can't be multiple mutually exclusive things", { test_that("Block can't contain duplicate tags", { lines <- c("#* @tag test", "#* @tag test") - expect_error(parseBlock(length(lines), lines), "Duplicate tag specified.") + expect_error(plumbBlock(length(lines), lines), "Duplicate tag specified.") }) test_that("@json parameters work", { @@ -90,12 +90,12 @@ test_that("@json parameters work", { testthat::skip_on_covr() expect_block_fn <- function(lines, fn) { - b <- parseBlock(length(lines), lines) + b <- plumbBlock(length(lines), lines) expect_equal_functions(b$serializer, fn) } expect_block_error <- function(lines, ...) { expect_error({ - parseBlock(length(lines), lines) + plumbBlock(length(lines), lines) }, ...) } @@ -131,12 +131,12 @@ test_that("@html parameters produce an error", { testthat::skip_on_covr() expect_block_fn <- function(lines, fn) { - b <- parseBlock(length(lines), lines) + b <- plumbBlock(length(lines), lines) expect_equal_functions(b$serializer, fn) } expect_block_error <- function(lines, ...) { expect_error({ - parseBlock(length(lines), lines) + plumbBlock(length(lines), lines) }, ...) } diff --git a/tests/testthat/test-path-subst.R b/tests/testthat/test-path-subst.R index 08d7199f7..14ee105da 100644 --- a/tests/testthat/test-path-subst.R +++ b/tests/testthat/test-path-subst.R @@ -137,13 +137,13 @@ test_that("multiple variations in path works nicely with function args detection var7 = .GlobalEnv, var8 = list(a = 2, b = mean, c = .GlobalEnv)) {} funcParams <- getArgsMetadata(dummy) - regex <- suppressWarnings(createPathRegex(pathDef, funcParams)) + expect_warning(regex <- createPathRegex(pathDef, funcParams), "Unsupported path parameter type") expect_equal(regex$types, c("string", "string", "integer", "string", "string", "boolean", "string", "string")) expect_equal(regex$areArrays, c(FALSE, FALSE, TRUE, FALSE, FALSE, TRUE, FALSE, TRUE)) # Throw sand at it pathDef <- "/<>/<:chr*>/<:chr>/" - regex <- suppressWarnings(createPathRegex(pathDef, funcParams)) + regex <- createPathRegex(pathDef, funcParams) expect_equivalent(regex$types, "string") expect_equal(regex$names, "henry") # Since type IV is converted to string, areArrays can be TRUE diff --git a/tests/testthat/test-plumber.R b/tests/testthat/test-plumber.R index 2c89f2757..6d5ec57ec 100644 --- a/tests/testthat/test-plumber.R +++ b/tests/testthat/test-plumber.R @@ -483,27 +483,31 @@ test_that("filters and endpoints executed in the appropriate environment", { test_that("host is updated properly for printing", { expect_identical( - urlHost("1:1:1", 1234), + urlHost(host = "1:1:1", port = 1234), "http://[1:1:1]:1234" ) expect_identical( - urlHost("::", 1234, FALSE), + urlHost(host = "::", port = 1234, changeHostLocation = FALSE), "http://[::]:1234" ) expect_identical( - urlHost("::", 1234, TRUE), + urlHost(host = "::", port = 1234, changeHostLocation = TRUE), "http://[::1]:1234" ) expect_identical( - urlHost("1.2.3.4", 1234), + urlHost(host = "1.2.3.4", port = 1234), "http://1.2.3.4:1234" ) expect_identical( - urlHost("0.0.0.0", 1234, FALSE), + urlHost(host = "0.0.0.0", port = 1234, changeHostLocation = FALSE), "http://0.0.0.0:1234" ) expect_identical( - urlHost("0.0.0.0", 1234, TRUE), + urlHost(host = "0.0.0.0", port = 1234, changeHostLocation = TRUE), "http://127.0.0.1:1234" ) + expect_identical( + urlHost(scheme = "http", host = "0.0.0.0", port = 1234, path = "/v1", changeHostLocation = TRUE), + "http://127.0.0.1:1234/v1" + ) }) diff --git a/tests/testthat/test-static.R b/tests/testthat/test-static.R index 9ceec95bf..37e7a3c04 100644 --- a/tests/testthat/test-static.R +++ b/tests/testthat/test-static.R @@ -4,7 +4,7 @@ context("Static") pr <- PlumberStatic$new(test_path("files/static")) -test_that("the response is reurned", { +test_that("the response is returned", { res <- PlumberResponse$new() val <- pr$route(make_req("GET", "/test.txt"), res) expect_true(inherits(val, "PlumberResponse"))