diff --git a/DESCRIPTION b/DESCRIPTION index 9a65e1f..da487ca 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -27,6 +27,7 @@ Imports: S7, stbl, styler, + testthat, usethis, utils, yaml @@ -35,7 +36,6 @@ Suggests: knitr, rmarkdown, stringr, - testthat, withr VignetteBuilder: knitr diff --git a/NAMESPACE b/NAMESPACE index f33b2d5..07c367e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -7,5 +7,6 @@ if (getRversion() < "4.3.0") importFrom("S7", "@") importFrom(glue,glue) importFrom(nectar,call_api) importFrom(rapid,as_rapid) +importFrom(testthat,test_that) importFrom(usethis,use_build_ignore) importFrom(usethis,use_package) diff --git a/R/beekeeper-package.R b/R/beekeeper-package.R index 52dc61b..7a09ea8 100644 --- a/R/beekeeper-package.R +++ b/R/beekeeper-package.R @@ -5,6 +5,7 @@ #' @importFrom glue glue #' @importFrom nectar call_api #' @importFrom rapid as_rapid +#' @importFrom testthat test_that #' @importFrom usethis use_package ## usethis namespace: end NULL diff --git a/R/generate.R b/R/generate.R index a055019..436c604 100644 --- a/R/generate.R +++ b/R/generate.R @@ -8,78 +8,66 @@ #' @param pkg_agent A string to identify this package, for use in the #' `user_agent` argument of [nectar::call_api()]. #' -#' @return `TRUE` invisibly. +#' @return A character vector of paths to files that were added or updated, +#' invisibly. #' @export generate_pkg <- function(config_file = "_beekeeper.yml", pkg_agent = generate_pkg_agent(config_file)) { .assert_is_pkg() config <- .read_config(config_file) - api_definition <- readRDS( - fs::path(fs::path_dir(config_file), config$rapid_file) - ) + api_definition <- .read_api_definition(config_file, config$rapid_file) + .prepare_r() - # This will be a series of functions, each of which generates one or more - # files. - .generate_call( + touched_files <- .generate_call( api_title = config$api_title, api_abbr = config$api_abbr, base_url = api_definition@servers@url, pkg_agent = pkg_agent ) - return(invisible(TRUE)) + return(invisible(touched_files)) +} + +.read_api_definition <- function(config_file, rapid_file) { + readRDS( + fs::path(fs::path_dir(config_file), rapid_file) + ) } .generate_call <- function(api_title, api_abbr, base_url, pkg_agent) { - api_title <- stbl::stabilize_chr_scalar( - api_title, - allow_null = FALSE, - allow_zero_length = FALSE, - allow_na = FALSE - ) - api_abbr <- stbl::stabilize_chr_scalar( - api_abbr, - allow_null = FALSE, - allow_zero_length = FALSE, - allow_na = FALSE - ) - base_url <- stbl::stabilize_chr_scalar( - base_url, - allow_null = FALSE, - allow_zero_length = FALSE, - allow_na = FALSE - ) - pkg_agent <- stbl::stabilize_chr_scalar( - pkg_agent, - allow_null = FALSE, - allow_zero_length = FALSE, - allow_na = FALSE + api_title <- .stabilize_chr_scalar_nonempty(api_title) + api_abbr <- .stabilize_chr_scalar_nonempty(api_abbr) + base_url <- .stabilize_chr_scalar_nonempty(base_url) + pkg_agent <- .stabilize_chr_scalar_nonempty(pkg_agent) + + touched_files <- c( + .bk_use_template( + template = "010-call.R", + data = list( + api_title = api_title, + api_abbr = api_abbr, + base_url = base_url, + pkg_agent = pkg_agent + ) + ), + .bk_use_template( + template = "test-010-call.R", + dir = "tests/testthat", + data = list(api_abbr = api_abbr) + ) ) + return(invisible(touched_files)) +} +.prepare_r <- function() { usethis::use_directory("R") + usethis::use_testthat() usethis::use_package("nectar") - - data_for_call <- list( - api_title = api_title, - api_abbr = api_abbr, - base_url = base_url, - pkg_agent = pkg_agent - ) - - template <- "010-call.R" - - # Eventually this will also include a test-generator. - .bk_use_template( - template = template, - data = data_for_call - ) - return(invisible(TRUE)) } - #' Use a template in this package #' #' @param template The name of the template. diff --git a/R/use_beekeeper.R b/R/use_beekeeper.R index 1a9fd94..56bdf4c 100644 --- a/R/use_beekeeper.R +++ b/R/use_beekeeper.R @@ -36,24 +36,9 @@ S7::method(use_beekeeper, rapid::rapid) <- ..., config_file = "_beekeeper.yml", rapid_file = "_beekeeper_rapid.rds") { - api_abbr <- stbl::stabilize_chr_scalar( - api_abbr, - allow_null = FALSE, - allow_zero_length = FALSE, - allow_na = FALSE - ) - config_file <- stbl::stabilize_chr_scalar( - config_file, - allow_null = FALSE, - allow_zero_length = FALSE, - allow_na = FALSE - ) - rapid_file <- stbl::stabilize_chr_scalar( - rapid_file, - allow_null = FALSE, - allow_zero_length = FALSE, - allow_na = FALSE - ) + api_abbr <- .stabilize_chr_scalar_nonempty(api_abbr) + config_file <- .stabilize_chr_scalar_nonempty(config_file) + rapid_file <- .stabilize_chr_scalar_nonempty(rapid_file) saveRDS(x, rapid_file) use_build_ignore(c(config_file, rapid_file)) diff --git a/R/utils.R b/R/utils.R index c41202a..c21a264 100644 --- a/R/utils.R +++ b/R/utils.R @@ -29,3 +29,16 @@ x = "{.path {base_path}} is not inside a package." )) } + +.stabilize_chr_scalar_nonempty <- function(x, + x_arg = rlang::caller_arg(x), + call = rlang::caller_env()) { + stbl::stabilize_chr_scalar( + x, + allow_null = FALSE, + allow_zero_length = FALSE, + allow_na = FALSE, + x_arg = x_arg, + call = call + ) +} diff --git a/README.Rmd b/README.Rmd index df60298..3947d16 100644 --- a/README.Rmd +++ b/README.Rmd @@ -24,7 +24,9 @@ knitr::opts_chunk$set( Use beekeeper to create and maintain R packages that wrap APIs. The generated packages follow best practices, including documentation and testing. -This package forcuses on APIs that follow the [OpenAPI Specification (OAS)](https://spec.openapis.org/oas/v3.1.0). +This package focuses on APIs that follow the [OpenAPI Specification (OAS)](https://spec.openapis.org/oas/v3.1.0). +Currently versions 3.0 and 3.1 are supported, with support for swagger 2.0 to come. +The APIs must have an API document in yaml format. The package skeletons generated by beekeeper implement best practices to streamline package development. ## The Plan @@ -32,49 +34,41 @@ The package skeletons generated by beekeeper implement best practices to streaml This project has been accepted by the [R Consortium 2023 ISC Grant Program](https://www.r-consortium.org/all-projects/awarded-projects/2023-group-1#api2r:%20An%20R%20Package%20for%20Auto-Generating%20R%20API%20Clients)! This is a rough outline of the planned development milestones of this package, working toward a stable version 1.0.0. -I had not re-read this plan in a while, and realized I've been skipping to 0.4.0! -I plan to refocus and get 0.1.0 released asap. - -- 0.1.0: Basic authentication and endpoint calls. - - Export a function to generate `R/*.R` and `tests/testthat/*.R` files to authenticate the user and make a call to the API, given the URL of an OpenAPI spec in YAML format. The generated files will follow and encourage best practices, and will serve as the core around which the rest of a package would be built. - - Produce a vignette about configuring authentication. +Most of the outline was included in the grant proposal. + +- [x] **pre-0.1.0: Infrastructure.** + - [x] I split the OpenAPI-parsing functionality into a separate package, [{rapid}](https://rapid.api2r.org). That package is being developed parallel to this one, and contains all of the *R API D*efinition-specific functionality. + - [x] I also realized I need a package for wrapping {httr2} calls. That package is called [{nectar}](https://nectar.api2r.org), and is also being developed parallel to this one. +- [ ] **0.1.0: Basic authentication and endpoint calls.** + - [x] Export a function or functions to generate `R/*.R` files to call an API, given the URL of an OpenAPI spec in YAML format (or a `rapid::rapid()` object). The generated files will follow and encourage best practices, and will serve as the core around which the rest of a package would be built. + - [x] Also generate a `tests/testthat/*.R` file for that generated function. + - [ ] Generate `R/*.R` and `tests/testthat/*.R` files to authenticate the user. + - [ ] Produce a vignette about configuring authentication. - **Potential challenges:** Authentication is a complex and delicate subject. Some APIs require registration of special apps to "catch" authentication requests, while others simply provide an API key. I will need to carefully navigate these complexities in the vignette. -- 0.2.0: OAS definition discovery. +- **0.2.0: OAS definition discovery.** - Add support for APIs using the OAS json format. - Streamline discovery of API definitions (with associated error handling). - - **Potential challenges:** There does not appear to be a set standard of where API definitions are posted on a given site. It might be difficult to help users find the right place. Interestingly, https://APIs.guru itself has a (simple) API to aid in API discovery, which might provide an opportunity to use beekeeper to generate parts of itself. -- 0.3.0: Batching and rate limiting. + - **Potential challenges:** There does not appear to be a set standard of where API definitions are posted on a given site. It might be difficult to help users find the right place. Interestingly, https://APIs.guru itself has a (simple) API to aid in API discovery, which might provide an opportunity to use beekeeper to generate parts of itself. **UPDATE:** I intend to put much of this functionality in a separate package, [{anyapi}](https://anyapi.api2r.org). +- **0.3.0: Batching and rate limiting.** - Add documentation for implementing batching and rate-limiting. - If possible, export functionality to help implement these processes, but standards seem to vary widely. - - **Potential challenges:** This step will involve more reading and documenting than code, to gather examples of how different APIs implement limits and batching. It's possible systems will be so different that it will be difficult to summarize them. For example, Slack has two separate batching systems in its API, with some functions moved to the newer system, and others not. -- 0.4.0: Endpoint function scaffolding. + - **Potential challenges:** This step will involve more reading and documenting than code, to gather examples of how different APIs implement limits and batching. It's possible systems will be so different that it will be difficult to summarize them. For example, Slack has two separate batching systems in its API, with some functions moved to the newer system, and others not. **UPDATE:** The [development version of {httr2}](https://github.com/r-lib/httr2/) has functionality to help with this quite a lot, thankfully! +- **0.4.0: Endpoint function scaffolding.** - Generate R/*.R and tests/testthat/*.R files for all endpoints ("paths") described in the given API specification. - The generated functions will work, but error checking, documentation, and tests will be minimal. - **Potential challenges:** I'll need to strike a balance here between getting a basic working system and producing something that can be easily expanded later. -- 0.5.0: More robust scaffolding. +- **0.5.0: More robust scaffolding.** - Add parameter documentation. - Also add parameter type checking. - **Potential challenges:** By this point I'll need an OAS definition document to use for testing that includes all of the possible parameter types. I'll likely need to generate a fake API specification that goes beyond a typical individual example. -- 0.6.0: Expected results. +- **0.6.0: Expected results.** - Add response (return value) documentation. - Use expected responses to generate better test scaffolds. - **Potential challenges:** Testing the generation of tests might present unique challenges. I'll need to look into how testthat tests itself. -- 0.7.0: Error messaging. +- **0.7.0: Error messaging.** - Add more robust error messaging for non-standard responses. - **Potential challenges:** Mocking cases where things fail can be tricky. Ideally this step will involve pushing the package to a stable 1.0.0, but that will require enough usage to feel confident that the core function definitions are stable. -### Endpoint function scaffolding - -This is the anticipated path to full functionality, as of 2023-07-06: - -- Use {tibblify} to create an object that specifies an API. The structure of this object will be based on the OpenAPI Specification, and will contain all of the information used by this package (or placeholders indicating that such information is missing). This partially exists in the [dev version of tibblify](https://github.com/mgirlich/tibblify/pull/187), but needs more work before it will be fully ready. My intention is to create one or a few bespoke versions of this object in order to move forward with beekeeper, but I'll simultaneously be working on making it work in tibblify. I currently think this object will be a list of lists/tibbles, but it's possible it will be a single (nested) tibble. -- Create a wrapper in beekeeper that creates such objects and gives them an explicit class, `api_spec`. The idea will be that *any* API spec would eventually be parsable into that object (regardless of whether it started as an OpenAPI spec), and then that object is what we'll work with within beekeeper. -- Create a function that takes an `api_spec` and creates a package. Build backward from here; determine which parts of the spec are useful, and how they need to be parsed to be easy to work with. - -That general framework will make it cleaner to add different types of spec or different implementations of the OpenAPI spec (not all specs are perfect). -That way the process is divided more cleanly into "use the spec to generate the expected `api_spec`" and then "use the `api_spec` to generate a package." -That also means I can guide users through steps to manually make changes to their not-quite-perfect input in order to generate a valid `api_spec`. - ## Installation ::: .pkgdown-release diff --git a/README.md b/README.md index 904d415..7ea8ef4 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,11 @@ Use beekeeper to create and maintain R packages that wrap APIs. The generated packages follow best practices, including documentation and testing. -This package forcuses on APIs that follow the [OpenAPI Specification -(OAS)](https://spec.openapis.org/oas/v3.1.0). The package skeletons -generated by beekeeper implement best practices to streamline package -development. +This package focuses on APIs that follow the [OpenAPI Specification +(OAS)](https://spec.openapis.org/oas/v3.1.0). Currently versions 3.0 and +3.1 are supported, with support for swagger 2.0 to come. The APIs must +have an API document in yaml format. The package skeletons generated by +beekeeper implement best practices to streamline package development. ## The Plan @@ -29,22 +30,33 @@ This project has been accepted by the [R Consortium 2023 ISC Grant Program](https://www.r-consortium.org/all-projects/awarded-projects/2023-group-1#api2r:%20An%20R%20Package%20for%20Auto-Generating%20R%20API%20Clients)! This is a rough outline of the planned development milestones of this -package, working toward a stable version 1.0.0. I had not re-read this -plan in a while, and realized I’ve been skipping to 0.4.0! I plan to -refocus and get 0.1.0 released asap. - -- 0.1.0: Basic authentication and endpoint calls. - - Export a function to generate `R/*.R` and `tests/testthat/*.R` files - to authenticate the user and make a call to the API, given the URL - of an OpenAPI spec in YAML format. The generated files will follow - and encourage best practices, and will serve as the core around - which the rest of a package would be built. - - Produce a vignette about configuring authentication. +package, working toward a stable version 1.0.0. Most of the outline was +included in the grant proposal. + +- [x] **pre-0.1.0: Infrastructure.** + - [x] I split the OpenAPI-parsing functionality into a separate + package, [{rapid}](https://rapid.api2r.org). That package is being + developed parallel to this one, and contains all of the *R API + D*efinition-specific functionality. + - [x] I also realized I need a package for wrapping {httr2} calls. + That package is called [{nectar}](https://nectar.api2r.org), and is + also being developed parallel to this one. +- [ ] **0.1.0: Basic authentication and endpoint calls.** + - [x] Export a function or functions to generate `R/*.R` files to call + an API, given the URL of an OpenAPI spec in YAML format (or a + `rapid::rapid()` object). The generated files will follow and + encourage best practices, and will serve as the core around which + the rest of a package would be built. + - [x] Also generate a `tests/testthat/*.R` file for that generated + function. + - [ ] Generate `R/*.R` and `tests/testthat/*.R` files to authenticate + the user. + - [ ] Produce a vignette about configuring authentication. - **Potential challenges:** Authentication is a complex and delicate subject. Some APIs require registration of special apps to “catch” authentication requests, while others simply provide an API key. I will need to carefully navigate these complexities in the vignette. -- 0.2.0: OAS definition discovery. +- **0.2.0: OAS definition discovery.** - Add support for APIs using the OAS json format. - Streamline discovery of API definitions (with associated error handling). @@ -53,8 +65,10 @@ refocus and get 0.1.0 released asap. difficult to help users find the right place. Interestingly, itself has a (simple) API to aid in API discovery, which might provide an opportunity to use beekeeper to - generate parts of itself. -- 0.3.0: Batching and rate limiting. + generate parts of itself. **UPDATE:** I intend to put much of this + functionality in a separate package, + [{anyapi}](https://anyapi.api2r.org). +- **0.3.0: Batching and rate limiting.** - Add documentation for implementing batching and rate-limiting. - If possible, export functionality to help implement these processes, but standards seem to vary widely. @@ -63,8 +77,10 @@ refocus and get 0.1.0 released asap. implement limits and batching. It’s possible systems will be so different that it will be difficult to summarize them. For example, Slack has two separate batching systems in its API, with some - functions moved to the newer system, and others not. -- 0.4.0: Endpoint function scaffolding. + functions moved to the newer system, and others not. **UPDATE:** The + [development version of {httr2}](https://github.com/r-lib/httr2/) + has functionality to help with this quite a lot, thankfully! +- **0.4.0: Endpoint function scaffolding.** - Generate R/*.R and tests/testthat/*.R files for all endpoints (“paths”) described in the given API specification. - The generated functions will work, but error checking, @@ -72,58 +88,26 @@ refocus and get 0.1.0 released asap. - **Potential challenges:** I’ll need to strike a balance here between getting a basic working system and producing something that can be easily expanded later. -- 0.5.0: More robust scaffolding. +- **0.5.0: More robust scaffolding.** - Add parameter documentation. - Also add parameter type checking. - **Potential challenges:** By this point I’ll need an OAS definition document to use for testing that includes all of the possible parameter types. I’ll likely need to generate a fake API specification that goes beyond a typical individual example. -- 0.6.0: Expected results. +- **0.6.0: Expected results.** - Add response (return value) documentation. - Use expected responses to generate better test scaffolds. - **Potential challenges:** Testing the generation of tests might present unique challenges. I’ll need to look into how testthat tests itself. -- 0.7.0: Error messaging. +- **0.7.0: Error messaging.** - Add more robust error messaging for non-standard responses. - **Potential challenges:** Mocking cases where things fail can be tricky. Ideally this step will involve pushing the package to a stable 1.0.0, but that will require enough usage to feel confident that the core function definitions are stable. -### Endpoint function scaffolding - -This is the anticipated path to full functionality, as of 2023-07-06: - -- Use {tibblify} to create an object that specifies an API. The - structure of this object will be based on the OpenAPI Specification, - and will contain all of the information used by this package (or - placeholders indicating that such information is missing). This - partially exists in the [dev version of - tibblify](https://github.com/mgirlich/tibblify/pull/187), but needs - more work before it will be fully ready. My intention is to create one - or a few bespoke versions of this object in order to move forward with - beekeeper, but I’ll simultaneously be working on making it work in - tibblify. I currently think this object will be a list of - lists/tibbles, but it’s possible it will be a single (nested) tibble. -- Create a wrapper in beekeeper that creates such objects and gives them - an explicit class, `api_spec`. The idea will be that *any* API spec - would eventually be parsable into that object (regardless of whether - it started as an OpenAPI spec), and then that object is what we’ll - work with within beekeeper. -- Create a function that takes an `api_spec` and creates a package. - Build backward from here; determine which parts of the spec are - useful, and how they need to be parsed to be easy to work with. - -That general framework will make it cleaner to add different types of -spec or different implementations of the OpenAPI spec (not all specs are -perfect). That way the process is divided more cleanly into “use the -spec to generate the expected `api_spec`” and then “use the `api_spec` -to generate a package.” That also means I can guide users through steps -to manually make changes to their not-quite-perfect input in order to -generate a valid `api_spec`. - ## Installation
diff --git a/inst/templates/test-010-call.R b/inst/templates/test-010-call.R new file mode 100644 index 0000000..593526c --- /dev/null +++ b/inst/templates/test-010-call.R @@ -0,0 +1,10 @@ +httptest2::with_mock_dir("api/01-call/valid", { + test_that("Can call an endpoint without errors", { + # A path will be auto-filled starting in beekeeper version 0.4.0. + fail( + "Provide any path for this API in PROVIDED_PATH, then delete this fail." + ) + PROVIDED_PATH <- "path/to/endpoint" + expect_no_error({{api_abbr}}_call_api(PROVIDED_PATH)) + }) +}) diff --git a/man/generate_pkg.Rd b/man/generate_pkg.Rd index 004172f..83ec626 100644 --- a/man/generate_pkg.Rd +++ b/man/generate_pkg.Rd @@ -16,7 +16,8 @@ generate_pkg( \code{user_agent} argument of \code{\link[nectar:call_api]{nectar::call_api()}}.} } \value{ -\code{TRUE} invisibly. +A character vector of paths to files that were added or updated, +invisibly. } \description{ Creates or updates package files based on the information in a beekeeper diff --git a/tests/testthat/_fixtures/guru-test-010-call.R b/tests/testthat/_fixtures/guru-test-010-call.R new file mode 100644 index 0000000..ce218b6 --- /dev/null +++ b/tests/testthat/_fixtures/guru-test-010-call.R @@ -0,0 +1,10 @@ +httptest2::with_mock_dir("api/01-call/valid", { + test_that("Can call an endpoint without errors", { + # A path will be auto-filled starting in beekeeper version 0.4.0. + fail( + "Provide any path for this API in PROVIDED_PATH, then delete this fail." + ) + PROVIDED_PATH <- "path/to/endpoint" + expect_no_error(guru_call_api(PROVIDED_PATH)) + }) +}) diff --git a/tests/testthat/test-generate.R b/tests/testthat/test-generate.R index 721d62b..5ee699b 100644 --- a/tests/testthat/test-generate.R +++ b/tests/testthat/test-generate.R @@ -4,6 +4,7 @@ test_that("Applying a 1-server config generates the expected R files.", { # Need to grab the fixtures *before* switching to the local package. config <- readLines(test_path("_fixtures", "guru_beekeeper.yml")) call_expected <- readLines(test_path("_fixtures", "guru-010-call.R")) + t_call_expected <- readLines(test_path("_fixtures", "guru-test-010-call.R")) guru_rapid <- readRDS(test_path("_fixtures", "guru_rapid.rds")) create_local_package() @@ -12,9 +13,16 @@ test_that("Applying a 1-server config generates the expected R files.", { generate_pkg(pkg_agent = "TESTPKG (https://example.com)") call_result <- readLines("R/010-call.R") call_result <- scrub_testpkg(call_result) + t_call_result <- readLines("tests/testthat/test-010-call.R") expect_identical(call_result, call_expected) + expect_identical(t_call_result, t_call_expected) + dependencies <- desc::desc()$get_deps() expect_identical( - desc::desc()$get_deps()$package, + dependencies$package[dependencies$type == "Imports"], "nectar" ) + expect_contains( + dependencies$package[dependencies$type == "Suggests"], + "testthat" + ) }) diff --git a/vignettes/beekeeper.Rmd b/vignettes/beekeeper.Rmd index cee258b..157396e 100644 --- a/vignettes/beekeeper.Rmd +++ b/vignettes/beekeeper.Rmd @@ -63,7 +63,8 @@ With a valid `_beekeeper.yml` file, you can generate the rest of the package. Right now the package will only export a function to call the API, but eventually this process will also generate functions for the endpoints specified in the API's OpenAPI document. ```{r generate} -# Note: Running this command will create or overwrite files in your R directory. +# Note: Running this command will create or overwrite files in your R and +# tests/testthat directories. generate_pkg() ```