Skip to content

Commit

Permalink
Generate basic test. (#28)
Browse files Browse the repository at this point in the history
Closes #16.
  • Loading branch information
jonthegeek committed Sep 15, 2023
1 parent ac70c2e commit 1aca0ff
Show file tree
Hide file tree
Showing 13 changed files with 148 additions and 152 deletions.
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Imports:
S7,
stbl,
styler,
testthat,
usethis,
utils,
yaml
Expand All @@ -35,7 +36,6 @@ Suggests:
knitr,
rmarkdown,
stringr,
testthat,
withr
VignetteBuilder:
knitr
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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)
1 change: 1 addition & 0 deletions R/beekeeper-package.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
84 changes: 36 additions & 48 deletions R/generate.R
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
21 changes: 3 additions & 18 deletions R/use_beekeeper.R
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
13 changes: 13 additions & 0 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
48 changes: 21 additions & 27 deletions README.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -24,57 +24,51 @@ 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

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
Expand Down
Loading

0 comments on commit 1aca0ff

Please sign in to comment.