Skip to content

Commit 72e58fa

Browse files
committed
Add support for _server.yml
1 parent c08fee8 commit 72e58fa

File tree

9 files changed

+236
-28
lines changed

9 files changed

+236
-28
lines changed

DESCRIPTION

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ Imports:
2323
roxygen2,
2424
stringi,
2525
svglite,
26-
webutils
26+
webutils,
27+
yaml
2728
Remotes:
2829
thomasp85/fiery,
2930
thomasp85/reqres,
@@ -34,7 +35,6 @@ Suggests:
3435
htmlwidgets,
3536
nanoparquet,
3637
quarto,
37-
readr,
38-
yaml
38+
readr
3939
VignetteBuilder: quarto
4040
URL: https://laughing-spoon-ywk3ewq.pages.github.io/

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export(api_statics)
7272
export(api_stop)
7373
export(api_trace)
7474
export(api_trace_header)
75+
export(create_server_yml)
7576
export(device_formatter)
7677
export(format_bmp)
7778
export(format_cat)

R/Plumber.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ Plumber <- R6Class(
8888
if (!is.null(doc_type)) {
8989
private$DOC_TYPE <- arg_match0(
9090
doc_type,
91-
c("rapidoc", "redoc", "swagger")
91+
c("rapidoc", "redoc", "swagger", "")
9292
)
9393
}
9494
check_string(doc_path)
@@ -142,7 +142,7 @@ Plumber <- R6Class(
142142
...,
143143
silent = FALSE
144144
) {
145-
if (length(private$OPENAPI) != 0 && !is.null(private$DOC_TYPE)) {
145+
if (length(private$OPENAPI) != 0 && !is.null(private$DOC_TYPE) && private$DOC_TYPE != "") {
146146
openapi_file <- tempfile(fileext = ".json")
147147
write_json(private$OPENAPI, openapi_file, auto_unbox = TRUE)
148148
api_route <- openapi_route(

R/api.R

Lines changed: 104 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
#'
66
#' @param ... plumber files or directories containing plumber files to be parsed
77
#' in the given order. The order of parsing determines the final order of the
8-
#' routes in the stack
8+
#' routes in the stack. If `...` contains a `_server.yml` file then all other
9+
#' files in `...` will be ignored and the `_server.yml` file will be used as the
10+
#' basis for the API
911
#' @param host A string that is a valid IPv4 address that is owned by this
1012
#' server
1113
#' @param port A number or integer that indicates the server port that should be
@@ -61,19 +63,59 @@ api <- function(
6163
compression_limit = get_opts("compressionLimit", 1e3),
6264
env = caller_env()
6365
) {
64-
api <- Plumber$new(
65-
host = host,
66-
port = port,
67-
doc_type = doc_type,
68-
doc_path = doc_path,
69-
reject_missing_methods = reject_missing_methods,
70-
ignore_trailing_slash = ignore_trailing_slash,
71-
max_request_size = max_request_size,
72-
shared_secret = shared_secret,
73-
compression_limit = compression_limit,
74-
env = env
75-
)
76-
api_parse(api, ...)
66+
locations <- dots_to_plumber_files(...)
67+
if (isTRUE(is_plumber2_server_yml(locations))) {
68+
server_yml <- yaml::read_yaml(locations)
69+
if (!is.null(server_yml$constructor)) {
70+
api <- source(
71+
fs::path(
72+
fs::path_dir(locations),
73+
server_yml$constructor
74+
),
75+
verbose = FALSE
76+
)
77+
if (!is_plumber_api(api)) {
78+
cli::cli_abort(
79+
"The constructor file in {.file {locations}} did not produce a plumber2 API"
80+
)
81+
}
82+
} else {
83+
api <- Plumber$new(
84+
host = server_yml$options$host %||% host,
85+
port = server_yml$options$port %||% port,
86+
doc_type = server_yml$options$docType %||% doc_type,
87+
doc_path = server_yml$options$docPath %||% doc_path,
88+
reject_missing_methods = server_yml$options$methodNotAllowed %||%
89+
reject_missing_methods,
90+
ignore_trailing_slash = server_yml$options$ignoreTrailingSlash %||%
91+
ignore_trailing_slash,
92+
max_request_size = server_yml$options$maxRequestSize %||%
93+
max_request_size,
94+
shared_secret = shared_secret,
95+
compression_limit = server_yml$options$compressionLimit %||%
96+
compression_limit,
97+
env = env
98+
)
99+
}
100+
locations <- fs::path(
101+
fs::path_dir(locations),
102+
server_yml$routes
103+
)
104+
} else {
105+
api <- Plumber$new(
106+
host = host,
107+
port = port,
108+
doc_type = doc_type,
109+
doc_path = doc_path,
110+
reject_missing_methods = reject_missing_methods,
111+
ignore_trailing_slash = ignore_trailing_slash,
112+
max_request_size = max_request_size,
113+
shared_secret = shared_secret,
114+
compression_limit = compression_limit,
115+
env = env
116+
)
117+
}
118+
api_parse(api, locations)
77119
}
78120
#' @rdname api
79121
#' @param x An object to test for whether it is a plumber api
@@ -85,14 +127,55 @@ is_plumber_api <- function(x) inherits(x, "Plumber")
85127
#' @param api A plumber2 api object to parse files into
86128
#' @export
87129
api_parse <- function(api, ...) {
88-
locations <- list2(...)
89-
lapply(locations, function(loc) {
130+
locations <- dots_to_plumber_files(..., prefer_yml = FALSE)
131+
for (loc in locations) {
132+
api$parse_file(file)
133+
}
134+
api
135+
}
136+
137+
dots_to_plumber_files <- function(..., prefer_yml = TRUE, call = caller_env()) {
138+
locations <- unlist(lapply(list2(...), function(loc) {
90139
if (fs::is_dir(loc)) {
91-
loc <- fs::dir_ls(loc)
140+
loc <- fs::dir_ls(loc, all = TRUE, recurse = TRUE)
141+
server_yml <- is_plumber2_server_yml(loc)
142+
if (prefer_yml && any(server_yml)) {
143+
loc <- loc[server_yml]
144+
} else {
145+
loc <- loc[fs::path_ext(loc) %in% c("R", "r")]
146+
}
92147
}
93-
for (file in loc) {
94-
api$parse_file(file)
148+
loc
149+
}))
150+
if (length(locations) == 0) return(character())
151+
if (!all(fs::file_exists(locations))) {
152+
cli::cli_abort("{.arg ...} must point to existing files", call = call)
153+
}
154+
server_yml <- is_plumber2_server_yml(locations)
155+
if (prefer_yml && any(server_yml)) {
156+
if (sum(server_yml) != 1) {
157+
cli::cli_abort(
158+
"You can at most use one {.file _server.yml} file to specify your API",
159+
call = call
160+
)
95161
}
96-
})
97-
api
162+
if (length(locations) != 1) {
163+
cli::cli_warn(
164+
"{.file _server.yml} found. Ignoring all other files provided in {.arg ...}",
165+
call = call
166+
)
167+
}
168+
locations[server_yml]
169+
} else {
170+
if (any(server_yml)) {
171+
cli::cli_warn(
172+
"Ignoring {.file _server.yml} files in {.arg ...}",
173+
call = call
174+
)
175+
}
176+
locations[!server_yml]
177+
}
98178
}
179+
180+
# For use by connect etc
181+
create_server <- api

R/create_server_yml.R

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#' Create a _server.yml file to describe your API
2+
#'
3+
#' While you can manually create a plumber2 API by calling [api()], you will
4+
#' often need to deploy the api somewhere else. To facilitate this you can
5+
#' create a `_server.yml` that encapsulates all of your settings and plumber
6+
#' files. If you call [api()] with a path to such a file the API will be
7+
#' constructed according to its content.
8+
#'
9+
#' @param ... path to files and/or directories that contain annotated plumber
10+
#' files to be used by your API
11+
#' @param path The folder to place the generated `_server.yml` file in
12+
#' @param constructor The path to a file that creates a plumber2 API object. Can
13+
#' be omitted in which case an API object will be created for you
14+
#' @param freeze_opt Logical specifying whether any options you currently have
15+
#' locally (either as environment variables or R options) should be written to
16+
#' the `_server.yml` file. Shared secret will never be written to the file and
17+
#' you must find a different way to move that to your deployment server.
18+
#' @param options_prefix The prefix to use for the options when looking for
19+
#' options to freeze
20+
#'
21+
#' @export
22+
#'
23+
create_server_yml <- function(
24+
...,
25+
path = ".",
26+
constructor = NULL,
27+
freeze_opt = TRUE,
28+
options_prefix = "plumber2"
29+
) {
30+
routes <- unlist(list(...))
31+
if (!is.null(routes)) {
32+
check_character(routes)
33+
if (any(!fs::file_exists(routes))) {
34+
cli::cli_abort("{.arg ...} must point to existing files or directories")
35+
}
36+
routes <- fs::path_real(routes)
37+
if (!all(fs::path_has_parent(routes, path))) {
38+
cli::cli_abort(
39+
"{.arg ...} must all point to files placed in subfolders relative to {.arg path}"
40+
)
41+
}
42+
routes <- fs::path_rel(routes, path)
43+
}
44+
if (
45+
!is.null(constructor) &&
46+
(!fs::is_file(constructor) || !fs::path_ext(constructor) %in% c("R", "r"))
47+
) {
48+
cli::cli_abort("{.arg constructor} must point to an existing R file")
49+
}
50+
settings <- list(
51+
engine = "plumber2",
52+
routes = routes,
53+
constructor = constructor,
54+
options = list()
55+
)
56+
57+
if (freeze_opt) {
58+
settings$options <- all_opts(prefix = options_prefix)
59+
}
60+
yaml::write_yaml(settings, fs::path_join(c(path, "_server.yml")))
61+
}
62+
63+
is_plumber2_server_yml <- function(path) {
64+
vapply(path, function(p) {
65+
if (fs::path_file(p) != "_server.yml") {
66+
return(FALSE)
67+
}
68+
isTRUE(tolower(yaml::read_yaml(p)$engine) == "plumber2")
69+
}, logical(1))
70+
}

R/options.R

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
# )
2727
#}
2828

29-
get_opts <- function(x, default = NULL, prefix = c("plumber2", "plumber")) {
29+
get_opts <- function(x, default = NULL, prefix = "plumber2") {
3030
getOption(paste0(prefix[1], ".", x), default = {
3131
env_name <- toupper(paste0(prefix[1], "_", x))
3232
res <- Sys.getenv(env_name)
@@ -44,3 +44,16 @@ get_opts <- function(x, default = NULL, prefix = c("plumber2", "plumber")) {
4444
res
4545
})
4646
}
47+
48+
all_opts <- function(prefix = "plumber2") {
49+
compact(list(
50+
host = get_opts("host", prefix = prefix),
51+
port = get_opts("port", prefix = prefix),
52+
docType = get_opts("docType", prefix = prefix),
53+
docPath = get_opts("docPath", prefix = prefix),
54+
methodNotAllowed = get_opts("methodNotAllowed", prefix = prefix),
55+
ignoreTrailingSlash = get_opts("ignoreTrailingSlash", prefix = prefix),
56+
maxRequestSize = get_opts("maxRequestSize", prefix = prefix),
57+
compressionLimit = get_opts("compressionLimit", prefix = prefix)
58+
))
59+
}

_pkgdown.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ reference:
3636
contents:
3737
- api
3838
- api_run
39+
- create_server_yml
3940
- title: "Adding handlers and routes"
4041
contents:
4142
- api_get

man/api.Rd

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/create_server_yml.Rd

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)