From 4b1f414c9a9ab102deb09251164fa7106148ba5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 12:06:00 +0000 Subject: [PATCH 1/5] Initial plan From 9607de3223876ad2a6e344046cb29b41649a5501 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 12:13:53 +0000 Subject: [PATCH 2/5] Add generated parameter validation for supported path args Agent-Logs-Url: https://github.com/api2r/beekeeper/sessions/a41a0262-71f9-490c-bc84-eebe62c2a703 Co-authored-by: jonthegeek <33983824+jonthegeek@users.noreply.github.com> --- R/generate_pkg-paths.R | 38 +++++++++++++++++-- inst/templates/paths.R | 3 ++ .../_fixtures/guru/paths-apis-get_api.R | 2 + .../_fixtures/guru/paths-apis-get_provider.R | 1 + .../guru/paths-apis-get_service_api.R | 3 ++ .../_fixtures/guru/paths-apis-get_services.R | 1 + .../paths-things-search_things.R | 3 ++ tests/testthat/test-generate_pkg-paths.R | 29 ++++++++++++-- 8 files changed, 72 insertions(+), 8 deletions(-) diff --git a/R/generate_pkg-paths.R b/R/generate_pkg-paths.R index 5080bb8..f6b97cf 100644 --- a/R/generate_pkg-paths.R +++ b/R/generate_pkg-paths.R @@ -120,6 +120,7 @@ S7::method(as_bk_data, class_paths) <- function(x) { params_df$allowEmptyValue, params_df$required ) + params_df$to_r <- .param_schema_to_r(params_df$schema) params_df$description <- .paths_fill_descriptions( params_df$description, params_df$schema$description @@ -146,10 +147,11 @@ S7::method(as_bk_data, class_paths) <- function(x) { list( name = params_df$name, class = params_df$class, - description = params_df$description + description = params_df$description, + to_r = params_df$to_r ), - function(name, class, description) { - list(name = name, class = class, description = description) + function(name, class, description, to_r) { + list(name = name, class = class, description = description, to_r = to_r) } ) } @@ -182,6 +184,15 @@ S7::method(as_bk_data, class_paths) <- function(x) { return(.compile_param_class_descriptions(type, allow_empty, required)) } +.param_schema_to_r <- function(params_schema) { + type <- dplyr::left_join( + dplyr::select(params_schema, "type", "format"), + dplyr::select(oas_format_registry, "type", "format", "to_r"), + by = c("type", "format") + ) + type$to_r +} + .compile_param_class_descriptions <- function(type, allow_empty, required) { r_class_descriptions <- glue::glue("length-1 `{type$r_class_name}`") |> .paste0_if( @@ -227,6 +238,7 @@ S7::method(as_bk_data, class_paths) <- function(x) { params_cookie <- .prep_param_args(op$params_cookie_raw, security_arg_names) args <- .params_to_args(params) args_named <- .params_to_named_args(params) + validations <- .params_to_validations(params) c( op, list( @@ -236,7 +248,8 @@ S7::method(as_bk_data, class_paths) <- function(x) { params_cookie = params_cookie, args = args, args_named = args_named, - test_args = args + test_args = args, + validations = validations ) ) }) @@ -279,6 +292,23 @@ S7::method(as_bk_data, class_paths) <- function(x) { .collapse_comma_self_equal(setdiff(params, security_args)) %|"|% character() } +.params_to_validations <- function(params) { + checks <- purrr::keep( + params, + function(param) { + !is.null(param$to_r) && + !is.na(param$to_r) && + !startsWith(param$to_r, "todo_") + } + ) + purrr::map( + checks, + function(param) { + list(name = param$name, to_r = param$to_r) + } + ) +} + .generate_paths_file <- function( path_operation, operation_id, diff --git a/inst/templates/paths.R b/inst/templates/paths.R index e793004..a9c84ce 100644 --- a/inst/templates/paths.R +++ b/inst/templates/paths.R @@ -24,6 +24,9 @@ #' @rdname {{api_abbr}}_{{operation_id}} #' @returns `req_{{api_abbr}}_{{operation_id}}()`: A `httr2_request` request object. req_{{api_abbr}}_{{operation_id}} <- function({{#args}}{{{args}}}{{/args}}{{#has_security}}{{#args}}, {{/args}}{{{security_signature}}}{{/has_security}}) { +{{#validations}} + stbl::{{to_r}}({{name}}) +{{/validations}} {{api_abbr}}_req_prepare( path = {{{path}}}, method = "{{method}}"{{#has_security}}, diff --git a/tests/testthat/_fixtures/guru/paths-apis-get_api.R b/tests/testthat/_fixtures/guru/paths-apis-get_api.R index 9de7ccb..1868f71 100644 --- a/tests/testthat/_fixtures/guru/paths-apis-get_api.R +++ b/tests/testthat/_fixtures/guru/paths-apis-get_api.R @@ -25,6 +25,8 @@ guru_get_api <- function(provider, api, max_reqs = Inf, max_tries_per_req = 3) { #' @rdname guru_get_api #' @returns `req_guru_get_api()`: A `httr2_request` request object. req_guru_get_api <- function(provider, api) { + stbl::to_chr_scalar(provider) + stbl::to_chr_scalar(api) guru_req_prepare( path = c("/specs/{provider}/{api}.json", provider = provider, api = api), method = "get" diff --git a/tests/testthat/_fixtures/guru/paths-apis-get_provider.R b/tests/testthat/_fixtures/guru/paths-apis-get_provider.R index adea4b9..1522781 100644 --- a/tests/testthat/_fixtures/guru/paths-apis-get_provider.R +++ b/tests/testthat/_fixtures/guru/paths-apis-get_provider.R @@ -24,6 +24,7 @@ guru_get_provider <- function(provider, max_reqs = Inf, max_tries_per_req = 3) { #' @rdname guru_get_provider #' @returns `req_guru_get_provider()`: A `httr2_request` request object. req_guru_get_provider <- function(provider) { + stbl::to_chr_scalar(provider) guru_req_prepare( path = c("/{provider}.json", provider = provider), method = "get" diff --git a/tests/testthat/_fixtures/guru/paths-apis-get_service_api.R b/tests/testthat/_fixtures/guru/paths-apis-get_service_api.R index 0003878..f5f5a62 100644 --- a/tests/testthat/_fixtures/guru/paths-apis-get_service_api.R +++ b/tests/testthat/_fixtures/guru/paths-apis-get_service_api.R @@ -26,6 +26,9 @@ guru_get_service_api <- function(provider, service, api, max_reqs = Inf, max_tri #' @rdname guru_get_service_api #' @returns `req_guru_get_service_api()`: A `httr2_request` request object. req_guru_get_service_api <- function(provider, service, api) { + stbl::to_chr_scalar(provider) + stbl::to_chr_scalar(service) + stbl::to_chr_scalar(api) guru_req_prepare( path = c("/specs/{provider}/{service}/{api}.json", provider = provider, service = service, api = api), method = "get" diff --git a/tests/testthat/_fixtures/guru/paths-apis-get_services.R b/tests/testthat/_fixtures/guru/paths-apis-get_services.R index b8d4600..412edb8 100644 --- a/tests/testthat/_fixtures/guru/paths-apis-get_services.R +++ b/tests/testthat/_fixtures/guru/paths-apis-get_services.R @@ -24,6 +24,7 @@ guru_get_services <- function(provider, max_reqs = Inf, max_tries_per_req = 3) { #' @rdname guru_get_services #' @returns `req_guru_get_services()`: A `httr2_request` request object. req_guru_get_services <- function(provider) { + stbl::to_chr_scalar(provider) guru_req_prepare( path = c("/{provider}/services.json", provider = provider), method = "get" diff --git a/tests/testthat/_fixtures/header_cookie/paths-things-search_things.R b/tests/testthat/_fixtures/header_cookie/paths-things-search_things.R index ff325ca..731dfbd 100644 --- a/tests/testthat/_fixtures/header_cookie/paths-things-search_things.R +++ b/tests/testthat/_fixtures/header_cookie/paths-things-search_things.R @@ -26,6 +26,9 @@ test_search_things <- function(x_auth_token, session_id, q, max_reqs = Inf, max_ #' @rdname test_search_things #' @returns `req_test_search_things()`: A `httr2_request` request object. req_test_search_things <- function(x_auth_token, session_id, q) { + stbl::to_chr_scalar(x_auth_token) + stbl::to_chr_scalar(session_id) + stbl::to_chr_scalar(q) test_req_prepare( path = "/things", method = "get", diff --git a/tests/testthat/test-generate_pkg-paths.R b/tests/testthat/test-generate_pkg-paths.R index e635798..e381bbd 100644 --- a/tests/testthat/test-generate_pkg-paths.R +++ b/tests/testthat/test-generate_pkg-paths.R @@ -258,7 +258,20 @@ test_that(".compile_param_class_descriptions() uses class names (#85)", { ) }) -test_that(".generate_paths_file() renders header and cookie params correctly (#84)", { +test_that(".params_to_validations() only includes supported checks (#69)", { + params <- list( + list(name = "q", to_r = "to_chr_scalar"), + list(name = "from", to_r = "todo_to_date_scalar"), + list(name = "x", to_r = NA_character_) + ) + + expect_identical( + .params_to_validations(params), + list(list(name = "q", to_r = "to_chr_scalar")) + ) +}) + +test_that(".generate_paths_file() renders header and cookie params correctly (#84, #69)", { skip_on_cran() expected_content <- readLines( test_path("_fixtures", "header_cookie", "paths-things-search_things.R") @@ -278,17 +291,20 @@ test_that(".generate_paths_file() renders header and cookie params correctly (#8 list( name = "x_auth_token", class = "length-1 `character`", - description = "Authentication token" + description = "Authentication token", + to_r = "to_chr_scalar" ), list( name = "session_id", class = "length-1 `character`", - description = "Session identifier" + description = "Session identifier", + to_r = "to_chr_scalar" ), list( name = "q", class = "length-1 `character`", - description = "Search query" + description = "Search query", + to_r = "to_chr_scalar" ) ), params_query = "q = q", @@ -296,6 +312,11 @@ test_that(".generate_paths_file() renders header and cookie params correctly (#8 params_cookie = "session_id = session_id", args = "x_auth_token, session_id, q", args_named = "x_auth_token = x_auth_token, session_id = session_id, q = q", + validations = list( + list(name = "x_auth_token", to_r = "to_chr_scalar"), + list(name = "session_id", to_r = "to_chr_scalar"), + list(name = "q", to_r = "to_chr_scalar") + ), test_args = "x_auth_token, session_id, q", pagination = FALSE, pagination_fn = "" From 6a0154e0ce1bc0ab1be6ec3bb3f07af778c19a72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 12:31:35 +0000 Subject: [PATCH 3/5] Assign cast values during generated request validation Agent-Logs-Url: https://github.com/api2r/beekeeper/sessions/f658bc2a-d5af-45df-ac02-a79209f0ab59 Co-authored-by: jonthegeek <33983824+jonthegeek@users.noreply.github.com> --- inst/templates/paths.R | 2 +- tests/testthat/_fixtures/guru/paths-apis-get_api.R | 4 ++-- tests/testthat/_fixtures/guru/paths-apis-get_provider.R | 2 +- tests/testthat/_fixtures/guru/paths-apis-get_service_api.R | 6 +++--- tests/testthat/_fixtures/guru/paths-apis-get_services.R | 2 +- .../_fixtures/header_cookie/paths-things-search_things.R | 6 +++--- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/inst/templates/paths.R b/inst/templates/paths.R index a9c84ce..afb8938 100644 --- a/inst/templates/paths.R +++ b/inst/templates/paths.R @@ -25,7 +25,7 @@ #' @returns `req_{{api_abbr}}_{{operation_id}}()`: A `httr2_request` request object. req_{{api_abbr}}_{{operation_id}} <- function({{#args}}{{{args}}}{{/args}}{{#has_security}}{{#args}}, {{/args}}{{{security_signature}}}{{/has_security}}) { {{#validations}} - stbl::{{to_r}}({{name}}) + {{name}} <- stbl::{{to_r}}({{name}}) {{/validations}} {{api_abbr}}_req_prepare( path = {{{path}}}, diff --git a/tests/testthat/_fixtures/guru/paths-apis-get_api.R b/tests/testthat/_fixtures/guru/paths-apis-get_api.R index 1868f71..9139aaa 100644 --- a/tests/testthat/_fixtures/guru/paths-apis-get_api.R +++ b/tests/testthat/_fixtures/guru/paths-apis-get_api.R @@ -25,8 +25,8 @@ guru_get_api <- function(provider, api, max_reqs = Inf, max_tries_per_req = 3) { #' @rdname guru_get_api #' @returns `req_guru_get_api()`: A `httr2_request` request object. req_guru_get_api <- function(provider, api) { - stbl::to_chr_scalar(provider) - stbl::to_chr_scalar(api) + provider <- stbl::to_chr_scalar(provider) + api <- stbl::to_chr_scalar(api) guru_req_prepare( path = c("/specs/{provider}/{api}.json", provider = provider, api = api), method = "get" diff --git a/tests/testthat/_fixtures/guru/paths-apis-get_provider.R b/tests/testthat/_fixtures/guru/paths-apis-get_provider.R index 1522781..8efb4d5 100644 --- a/tests/testthat/_fixtures/guru/paths-apis-get_provider.R +++ b/tests/testthat/_fixtures/guru/paths-apis-get_provider.R @@ -24,7 +24,7 @@ guru_get_provider <- function(provider, max_reqs = Inf, max_tries_per_req = 3) { #' @rdname guru_get_provider #' @returns `req_guru_get_provider()`: A `httr2_request` request object. req_guru_get_provider <- function(provider) { - stbl::to_chr_scalar(provider) + provider <- stbl::to_chr_scalar(provider) guru_req_prepare( path = c("/{provider}.json", provider = provider), method = "get" diff --git a/tests/testthat/_fixtures/guru/paths-apis-get_service_api.R b/tests/testthat/_fixtures/guru/paths-apis-get_service_api.R index f5f5a62..99a0df0 100644 --- a/tests/testthat/_fixtures/guru/paths-apis-get_service_api.R +++ b/tests/testthat/_fixtures/guru/paths-apis-get_service_api.R @@ -26,9 +26,9 @@ guru_get_service_api <- function(provider, service, api, max_reqs = Inf, max_tri #' @rdname guru_get_service_api #' @returns `req_guru_get_service_api()`: A `httr2_request` request object. req_guru_get_service_api <- function(provider, service, api) { - stbl::to_chr_scalar(provider) - stbl::to_chr_scalar(service) - stbl::to_chr_scalar(api) + provider <- stbl::to_chr_scalar(provider) + service <- stbl::to_chr_scalar(service) + api <- stbl::to_chr_scalar(api) guru_req_prepare( path = c("/specs/{provider}/{service}/{api}.json", provider = provider, service = service, api = api), method = "get" diff --git a/tests/testthat/_fixtures/guru/paths-apis-get_services.R b/tests/testthat/_fixtures/guru/paths-apis-get_services.R index 412edb8..92f3245 100644 --- a/tests/testthat/_fixtures/guru/paths-apis-get_services.R +++ b/tests/testthat/_fixtures/guru/paths-apis-get_services.R @@ -24,7 +24,7 @@ guru_get_services <- function(provider, max_reqs = Inf, max_tries_per_req = 3) { #' @rdname guru_get_services #' @returns `req_guru_get_services()`: A `httr2_request` request object. req_guru_get_services <- function(provider) { - stbl::to_chr_scalar(provider) + provider <- stbl::to_chr_scalar(provider) guru_req_prepare( path = c("/{provider}/services.json", provider = provider), method = "get" diff --git a/tests/testthat/_fixtures/header_cookie/paths-things-search_things.R b/tests/testthat/_fixtures/header_cookie/paths-things-search_things.R index 731dfbd..b8c3980 100644 --- a/tests/testthat/_fixtures/header_cookie/paths-things-search_things.R +++ b/tests/testthat/_fixtures/header_cookie/paths-things-search_things.R @@ -26,9 +26,9 @@ test_search_things <- function(x_auth_token, session_id, q, max_reqs = Inf, max_ #' @rdname test_search_things #' @returns `req_test_search_things()`: A `httr2_request` request object. req_test_search_things <- function(x_auth_token, session_id, q) { - stbl::to_chr_scalar(x_auth_token) - stbl::to_chr_scalar(session_id) - stbl::to_chr_scalar(q) + x_auth_token <- stbl::to_chr_scalar(x_auth_token) + session_id <- stbl::to_chr_scalar(session_id) + q <- stbl::to_chr_scalar(q) test_req_prepare( path = "/things", method = "get", From d127d1802f85f8a26f6d1e2689a2759f6259c488 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 13:41:43 +0000 Subject: [PATCH 4/5] Conditionally add stbl import for generated validators Agent-Logs-Url: https://github.com/api2r/beekeeper/sessions/80cf8c79-c03f-4f2a-8965-54e8528f322d Co-authored-by: jonthegeek <33983824+jonthegeek@users.noreply.github.com> --- R/generate_pkg-paths.R | 11 +++++++++++ R/generate_pkg-setup.R | 5 ++++- R/generate_pkg.R | 19 +++++++++++++------ tests/testthat/test-generate_pkg-paths.R | 19 +++++++++++++++++++ tests/testthat/test-generate_pkg-setup.R | 12 ++++++++++++ 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/R/generate_pkg-paths.R b/R/generate_pkg-paths.R index f6b97cf..9e683ea 100644 --- a/R/generate_pkg-paths.R +++ b/R/generate_pkg-paths.R @@ -309,6 +309,17 @@ S7::method(as_bk_data, class_paths) <- function(x) { ) } +.paths_need_stbl <- function(paths, security_arg_names = character()) { + ops <- as_bk_data(paths) + if (!length(ops)) { + return(FALSE) + } + any(purrr::map_lgl(ops, function(op) { + params <- .remove_security_args(op$params, security_arg_names) + length(.params_to_validations(params)) > 0 + })) +} + .generate_paths_file <- function( path_operation, operation_id, diff --git a/R/generate_pkg-setup.R b/R/generate_pkg-setup.R index 035e6a9..0112b70 100644 --- a/R/generate_pkg-setup.R +++ b/R/generate_pkg-setup.R @@ -54,7 +54,7 @@ ) } -.setup_r <- function(pkg_dir) { +.setup_r <- function(pkg_dir, include_stbl = FALSE) { if (as.character(pkg_dir) != ".") { usethis::local_project(pkg_dir, quiet = TRUE) # nocov } @@ -62,5 +62,8 @@ use_testthat() purrr::quietly(use_httptest2)() use_package("nectar") + if (include_stbl) { + use_package("stbl") + } use_package("beekeeper", type = "Suggests") } diff --git a/R/generate_pkg.R b/R/generate_pkg.R index 2cbba5b..28fdb6a 100644 --- a/R/generate_pkg.R +++ b/R/generate_pkg.R @@ -23,16 +23,23 @@ generate_pkg <- function( .assert_is_pkg(pkg_dir) config <- .read_config(config_file) api_definition <- .read_api_definition(pkg_dir, config$rapid_file) - .setup_r(pkg_dir) - touched_files <- .generate_pkg_impl(config, api_definition) - return(invisible(touched_files)) -} - -.generate_pkg_impl <- function(config, api_definition) { security_data <- .generate_security( config$api_abbr, api_definition@components@security_schemes ) + security_arg_names <- security_data$security_arg_names %|0|% character() + .setup_r( + pkg_dir, + include_stbl = .paths_need_stbl( + api_definition@paths, + security_arg_names + ) + ) + touched_files <- .generate_pkg_impl(config, api_definition, security_data) + return(invisible(touched_files)) +} + +.generate_pkg_impl <- function(config, api_definition, security_data) { prep_files <- .generate_prepare(config, api_definition, security_data) pagination_data <- .generate_pagination() path_files <- .generate_paths( diff --git a/tests/testthat/test-generate_pkg-paths.R b/tests/testthat/test-generate_pkg-paths.R index e381bbd..236ddf9 100644 --- a/tests/testthat/test-generate_pkg-paths.R +++ b/tests/testthat/test-generate_pkg-paths.R @@ -271,6 +271,25 @@ test_that(".params_to_validations() only includes supported checks (#69)", { ) }) +test_that(".paths_need_stbl() flags actionable validations", { + api_definition_true <- readRDS(test_path( + "_fixtures", + "guru", + "_beekeeper_rapid.rds" + )) + + expect_true( + .paths_need_stbl( + api_definition_true@paths, + character() + ) + ) +}) + +test_that(".paths_need_stbl() returns FALSE for empty paths", { + expect_false(.paths_need_stbl(rapid::class_paths(), character())) +}) + test_that(".generate_paths_file() renders header and cookie params correctly (#84, #69)", { skip_on_cran() expected_content <- readLines( diff --git a/tests/testthat/test-generate_pkg-setup.R b/tests/testthat/test-generate_pkg-setup.R index 58eefac..dc189f1 100644 --- a/tests/testthat/test-generate_pkg-setup.R +++ b/tests/testthat/test-generate_pkg-setup.R @@ -53,3 +53,15 @@ test_that(".setup_r() sets up dependencies", { "testthat" ) }) + +test_that(".setup_r() can include stbl in imports", { + skip_on_cran() + + create_local_package() + .setup_r(".", include_stbl = TRUE) + + dependencies <- desc::desc()$get_deps() + imports <- dependencies$package[dependencies$type == "Imports"] + expect_contains(imports, "nectar") + expect_contains(imports, "stbl") +}) From 79c7ebbd70b0cc65db6da57e475c929874ecc68d Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Thu, 14 May 2026 09:11:07 -0500 Subject: [PATCH 5/5] Tags tests with issue number Co-authored-by: Jon Harmon --- tests/testthat/test-generate_pkg-paths.R | 4 ++-- tests/testthat/test-generate_pkg-setup.R | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/testthat/test-generate_pkg-paths.R b/tests/testthat/test-generate_pkg-paths.R index 236ddf9..fd78ba8 100644 --- a/tests/testthat/test-generate_pkg-paths.R +++ b/tests/testthat/test-generate_pkg-paths.R @@ -271,7 +271,7 @@ test_that(".params_to_validations() only includes supported checks (#69)", { ) }) -test_that(".paths_need_stbl() flags actionable validations", { +test_that(".paths_need_stbl() flags actionable validations (#69)", { api_definition_true <- readRDS(test_path( "_fixtures", "guru", @@ -286,7 +286,7 @@ test_that(".paths_need_stbl() flags actionable validations", { ) }) -test_that(".paths_need_stbl() returns FALSE for empty paths", { +test_that(".paths_need_stbl() returns FALSE for empty paths (#69)", { expect_false(.paths_need_stbl(rapid::class_paths(), character())) }) diff --git a/tests/testthat/test-generate_pkg-setup.R b/tests/testthat/test-generate_pkg-setup.R index dc189f1..c241281 100644 --- a/tests/testthat/test-generate_pkg-setup.R +++ b/tests/testthat/test-generate_pkg-setup.R @@ -54,7 +54,7 @@ test_that(".setup_r() sets up dependencies", { ) }) -test_that(".setup_r() can include stbl in imports", { +test_that(".setup_r() can include stbl in imports (#69)", { skip_on_cran() create_local_package()