Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add validate_api_spec() #633

Merged
merged 13 commits into from Aug 7, 2020
Merged
5 changes: 5 additions & 0 deletions .github/workflows/R-CMD-check.yaml
Expand Up @@ -48,6 +48,11 @@ jobs:
steps:
- uses: actions/checkout@v2

- name: Install Node.js
uses: actions/setup-node@v1
with:
node-version: '12.x'

- uses: r-lib/actions/setup-r@master
with:
r-version: ${{ matrix.config.r }}
Expand Down
1 change: 1 addition & 0 deletions DESCRIPTION
Expand Up @@ -80,4 +80,5 @@ Collate:
'ui.R'
'utf8.R'
'utils-pipe.R'
'validate_api_spec.R'
'zzz.R'
1 change: 1 addition & 0 deletions NAMESPACE
Expand Up @@ -87,6 +87,7 @@ export(serializer_tsv)
export(serializer_unboxed_json)
export(serializer_yaml)
export(sessionCookie)
export(validate_api_spec)
import(R6)
import(promises)
import(stringi)
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Expand Up @@ -68,6 +68,8 @@ both UIs integration are available from https://github.com/meztez/rapidoc/ and h

* Added `plumb_api()` for standardizing where to locate (`inst/plumber`) and how to run (`plumb_api(package, name)`) plumber apis inside an R package. To view the available Plumber APIs, call `available_apis()`. (#631)

* Added `validate_api_spec()` to validate a Plumber API produces a valid OpenAPI Specification. (Experimental!) (#633)


### Minor new features and improvements

Expand Down
2 changes: 1 addition & 1 deletion R/openapi-spec.R
Expand Up @@ -60,7 +60,7 @@ parametersSpecification <- function(endpointParams, pathParams, funcParams = NUL

params <- list(
parameters = list(),
requestBody = list()
requestBody = NULL
)
inBody <- filterApiTypes("requestBody", "location")
inRaw <- filterApiTypes("binary", "format")
Expand Down
102 changes: 102 additions & 0 deletions R/validate_api_spec.R
@@ -0,0 +1,102 @@

#' @include globals.R
validate_api_spec_folder <- function() {
file.path(tempdir(), "plumber_validate_api_spec")
}


validate_api_spec__install_node_modules <- function() {

if (!nzchar(Sys.which("node"))) {
stop("node not installed")
}
if (!nzchar(Sys.which("npm"))) {
stop("npm not installed")
}

if (dir.exists(validate_api_spec_folder())) {
# assume npm install has completed
return(invisible(TRUE))
}

dir.create(validate_api_spec_folder(), recursive = TRUE, showWarnings = FALSE)

file.copy(
system.file(file.path("validate_api_spec", "package.json"), package = "plumber"),
file.path(validate_api_spec_folder(), "package.json")
)

old_wd <- setwd(validate_api_spec_folder())
on.exit({
setwd(old_wd)
}, add = TRUE)

# install everything. Ignore regular output
status <- system2("npm", c("install", "--loglevel", "warn"), stdout = FALSE)
if (status != 0) {
# delete the folder if it didn't work
unlink(validate_api_spec_folder(), recursive = TRUE)
stop("Could not install npm dependencies required for plumber::validate_api_spec()")
}

invisible(TRUE)
}


#' Validate OpenAPI Spec
#'
#' Validate an OpenAPI Spec using [Swagger CLI](https://github.com/APIDevTools/swagger-cli) which calls [Swagger Parser](https://github.com/APIDevTools/swagger-parser).
#'
#' If the api is deemed invalid, an error will be thrown.
#'
#' This function is VERY experimental and may be altered, changed, or removed in the future.
#'
#' @param pr A Plumber API
#' @param verbose Logical that determines if a "is valid" statement is displayed. Defaults to `TRUE`
#' @export
#' @examples
#' \dontrun{
#' pr <- plumb_api("plumber", "01-append")
#' validate_api_spec(pr)
#' }
validate_api_spec <- function(pr, verbose = TRUE) {

validate_api_spec__install_node_modules()

spec <- jsonlite::toJSON(pr$get_api_spec(), auto_unbox = TRUE, pretty = TRUE)
old_wd <- setwd(validate_api_spec_folder())
on.exit({
setwd(old_wd)
}, add = TRUE)

tmpfile <- tempfile(fileext = ".json")
on.exit({
unlink(tmpfile)
}, add = TRUE)
cat(spec, file = tmpfile)

output <- system2(
"node_modules/.bin/swagger-cli",
c(
"validate",
tmpfile
),
stdout = TRUE,
stderr = TRUE
)

output <- paste0(output, collapse = "\n")

# using expect_equal vs a regex test to have a better error message
is_equal <- sub(tmpfile, "", output, fixed = TRUE) == " is valid"
if (!isTRUE(is_equal)) {
cat("Plumber Spec: \n", as.character(spec), "\nOutput:\n", output)
stop("Plumber OpenAPI Spec is not valid")
}

if (isTRUE(verbose)) {
cat(crayon::green("\u2714"), crayon::silver(": Plumber OpenAPI Spec is valid"), "\n", sep = "")
}

invisible(TRUE)
}
2 changes: 1 addition & 1 deletion inst/plumber/05-static/plumber.R
Expand Up @@ -5,7 +5,7 @@ list()
list()

#* @get /
#* @json(auto_unbox = TRUE)
#* @serializer json list(auto_unbox = TRUE)
function() {
"static file server at './files'"
}
8 changes: 4 additions & 4 deletions inst/plumber/14-future/plumber.R
Expand Up @@ -12,15 +12,15 @@ future::plan("multiprocess") # use all available cores
# /future will not block /sync from being able to be loaded.


#' @json(auto_unbox = TRUE)
#' @serializer json list(auto_unbox = TRUE)
#' @get /sync
function() {
# print route, time, and worker pid
paste0("/sync; ", Sys.time(), "; pid:", Sys.getpid())
}

#' @contentType list(type = "text/html")
#' @json(auto_unbox = TRUE)
#' @serializer json list(auto_unbox = TRUE)
#' @get /future
function() {

Expand All @@ -39,7 +39,7 @@ function() {

# Originally by @antoine-sachet from https://github.com/rstudio/plumber/issues/389
#' @get /divide
#' @json(auto_unbox = TRUE)
#' @serializer json list(auto_unbox = TRUE)
#' @param a number
#' @param b number
function(a = NA, b = NA) {
Expand All @@ -55,7 +55,7 @@ function(a = NA, b = NA) {
}

#' @get /divide-catch
#' @json(auto_unbox = TRUE)
#' @serializer json list(auto_unbox = TRUE)
#' @param a number
#' @param b number
function(a = NA, b = NA) {
Expand Down
4 changes: 2 additions & 2 deletions inst/plumber/15-openapi-spec/entrypoint.R
Expand Up @@ -5,12 +5,12 @@ openapi_func <- function(spec) {
spec$paths[["/sum"]]$get$summary <- "Sum numbers"
spec$paths[["/sum"]]$get$parameters <- list(list(
"description" = "numbers",
"required" = "true",
"required" = TRUE,
"in" = "query",
"name" = "num",
"schema" = list("type" = "array", "items" = list("type" = "integer"), "minItems" = 1),
"style" = "form",
"explode" = "false"
"explode" = FALSE
))
spec
}
Expand Down
6 changes: 6 additions & 0 deletions inst/validate_api_spec/package.json
@@ -0,0 +1,6 @@
{
"private": true,
"devDependencies": {
"swagger-cli": "^4.0.4"
}
}
27 changes: 27 additions & 0 deletions man/validate_api_spec.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions package.json

This file was deleted.

3 changes: 1 addition & 2 deletions pkgdown/_pkgdown.yml
Expand Up @@ -82,8 +82,7 @@ reference:
- 'pr_set_api_spec'
- 'pr_set_ui'
- 'register_ui'
- title: All functions
# desc: description here
- 'validate_api_spec'

- title: POST Body and Query String Parsers
contents:
Expand Down
4 changes: 2 additions & 2 deletions tests/testthat/helper-for-each-plumber-api.R
@@ -1,4 +1,4 @@
for_each_plumber_api <- function(fn) {
for_each_plumber_api <- function(fn, ...) {
lapply(
available_apis("plumber")$name,
function(name) {
Expand All @@ -19,7 +19,7 @@ for_each_plumber_api <- function(fn) {
expect_true(inherits(pr, "plumber"), paste0("plumb_api(\"", package, "\", \"", name, "\")"))


fn(pr)
fn(pr, ...)
}
)
}
34 changes: 4 additions & 30 deletions tests/testthat/test-openapi.R
Expand Up @@ -259,39 +259,13 @@ test_that("api kitchen sink", {
# yarn add swagger-ui

# yarn install
swagger_cli_path <- "../../node_modules/.bin/swagger-cli"
skip_if_not(file.exists(swagger_cli_path))
swagger_cli_path <- normalizePath(swagger_cli_path)

validate_spec <- function(pr) {
spec <- jsonlite::toJSON(pr$get_api_spec(), auto_unbox = TRUE)
tmpfile <- tempfile(fileext = ".json")
on.exit({
unlink(tmpfile)
})
cat(spec, file = tmpfile)

output <- system2(
swagger_cli_path,
c(
"validate",
tmpfile
),
stdout = TRUE,
stderr = TRUE
)

output <- paste0(output, collapse = "\n")

# using expect_equal vs a regex test to have a better error message
expect_equal(sub(tmpfile, "", output, fixed = TRUE), " is valid")
}

for_each_plumber_api(validate_spec)

# TODO test more situations
skip_if_not(nzchar(Sys.which("node")), "node not installed")
skip_if_not(nzchar(Sys.which("npm")), "`npm` system dep not installed")

for_each_plumber_api(validate_api_spec, verbose = FALSE)

# TODO test more situations
})

test_that("multiple variations in function extract correct metadata", {
Expand Down