From e2d5ace226ecc2f41d942d0644180fd15b5c1705 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 00:33:39 +0000 Subject: [PATCH 1/4] Initial plan From 6087ef9253e65847392ef53454b7a33860ee6d85 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 00:38:29 +0000 Subject: [PATCH 2/4] Refactor onet_request to separate path segments from query parameters Co-authored-by: farach <1520139+farach@users.noreply.github.com> --- R/crosswalks.R | 13 ++++++------- R/database.R | 4 ++-- R/occupations.R | 10 +++++----- R/request.R | 24 +++++++++++++++++++----- R/search.R | 11 +++++++++-- 5 files changed, 41 insertions(+), 21 deletions(-) diff --git a/R/crosswalks.R b/R/crosswalks.R index 23bc162..df59e72 100644 --- a/R/crosswalks.R +++ b/R/crosswalks.R @@ -27,7 +27,7 @@ onet_crosswalk_military <- function(keyword, start = 1, end = 20) { } resp <- onet_request("online/crosswalks/military", - keyword = keyword, start = start, end = end) |> + .query = list(keyword = keyword, start = start, end = end)) |> onet_perform() # Define expected schema @@ -83,15 +83,14 @@ onet_taxonomy_map <- function(code, from = c("active", "2010"), to = c("2010", " } # Build the endpoint path based on direction - endpoint <- if (from == "active") { - paste0("taxonomy/active/2010/", code) + if (from == "active") { + resp <- onet_request("taxonomy/active/2010", .path_segments = code) |> + onet_perform() } else { - paste0("taxonomy/2010/active/", code) + resp <- onet_request("taxonomy/2010/active", .path_segments = code) |> + onet_perform() } - resp <- onet_request(endpoint) |> - onet_perform() - # Define expected schema schema <- empty_tibble(code = character(), title = character()) diff --git a/R/database.R b/R/database.R index 80b3f4b..e0986b2 100644 --- a/R/database.R +++ b/R/database.R @@ -57,7 +57,7 @@ onet_table_info <- function(table_id) { cli_abort("{.arg table_id} must be a single character string.") } - resp <- onet_request("database/info", table_id) |> + resp <- onet_request("database/info", .path_segments = table_id) |> onet_perform() # Define expected schema @@ -136,7 +136,7 @@ onet_table <- function(table_id, page_size = 2000, show_progress = TRUE) { #' @return A list with `data` (tibble), `start`, `end`, and `total`. #' @keywords internal onet_table_page <- function(table_id, start = 1, end = 2000) { - resp <- onet_request("database/rows", table_id, start = start, end = end) |> + resp <- onet_request("database/rows", .path_segments = table_id, .query = list(start = start, end = end)) |> onet_perform() data <- if (is.null(resp$row) || length(resp$row) == 0) { diff --git a/R/occupations.R b/R/occupations.R index 1fa094b..e089a9a 100644 --- a/R/occupations.R +++ b/R/occupations.R @@ -18,7 +18,7 @@ #' head(occupations) #' } onet_occupations <- function(start = 1, end = 1000) { - resp <- onet_request("online/occupations", start = start, end = end) |> + resp <- onet_request("online/occupations", .query = list(start = start, end = end)) |> onet_perform() # Define expected schema @@ -51,7 +51,7 @@ onet_occupations <- function(start = 1, end = 1000) { #' } onet_occupation <- function(code) { validate_onet_code(code) - resp <- onet_request("online/occupations", code, "summary") |> + resp <- onet_request("online/occupations", .path_segments = c(code, "summary")) |> onet_perform() resp } @@ -71,7 +71,7 @@ onet_occupation <- function(code) { #' } onet_occupation_details <- function(code) { validate_onet_code(code) - resp <- onet_request("online/occupations", code, "details") |> + resp <- onet_request("online/occupations", .path_segments = c(code, "details")) |> onet_perform() resp } @@ -154,7 +154,7 @@ onet_abilities <- function(code) { #' } onet_technology <- function(code) { validate_onet_code(code) - resp <- onet_request("online/occupations", code, "hot_technology") |> + resp <- onet_request("online/occupations", .path_segments = c(code, "hot_technology")) |> onet_perform() # Define expected schema @@ -194,7 +194,7 @@ onet_occupation_element <- function(code, element) { validate_onet_code(code) # Get summary which contains all elements - resp <- onet_request("online/occupations", code, "summary") |> + resp <- onet_request("online/occupations", .path_segments = c(code, "summary")) |> onet_perform() # Define expected schema diff --git a/R/request.R b/R/request.R index 608deeb..98d48d3 100644 --- a/R/request.R +++ b/R/request.R @@ -6,14 +6,28 @@ onet_base_url <- "https://api-v2.onetcenter.org" #' Creates an httr2 request object configured for the O*NET API. #' #' @param .path Character string specifying the API endpoint path. -#' @param ... Additional path segments and query parameters passed to the API. +#' @param .path_segments Additional path segments to append to the URL path (optional). +#' @param .query Named list or arguments for query parameters (optional). #' #' @return An httr2 request object. #' @keywords internal -onet_request <- function(.path, ...) { - request(onet_base_url) |> - req_url_path_append(.path) |> - req_url_query(...) |> +onet_request <- function(.path, .path_segments = NULL, .query = list()) { + req <- request(onet_base_url) |> + req_url_path_append(.path) + + # Append additional path segments if provided + if (!is.null(.path_segments)) { + for (segment in .path_segments) { + req <- req |> req_url_path_append(segment) + } + } + + # Add query parameters if provided + if (length(.query) > 0) { + req <- req |> req_url_query(!!!.query) + } + + req |> req_headers(`X-API-Key` = onet_api_key()) |> req_retry( max_tries = 3, diff --git a/R/search.R b/R/search.R index 88da416..f02cc01 100644 --- a/R/search.R +++ b/R/search.R @@ -27,7 +27,7 @@ onet_search <- function(keyword, start = 1, end = 20) { cli_abort("{.arg keyword} must be a single character string.") } - resp <- onet_request("online/search", keyword = keyword, start = start, end = end) |> + resp <- onet_request("online/search", .query = list(keyword = keyword, start = start, end = end)) |> onet_perform() # Define expected schema for empty results @@ -44,10 +44,17 @@ onet_search <- function(keyword, start = 1, end = 20) { } results <- map(resp$occupation, \(x) { + # Try multiple field names for relevance score + relevance <- x$relevance_score %||% + x$relevanceScore %||% + x$relevance %||% + x$score %||% + NA_real_ + tibble( code = x$code %||% NA_character_, title = x$title %||% NA_character_, - relevance_score = x$relevance_score %||% NA_real_ + relevance_score = relevance ) }) |> list_rbind() From a708f2072501f628d067a9e98870a8c197d30f16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 00:40:24 +0000 Subject: [PATCH 3/4] Add tests for request construction Co-authored-by: farach <1520139+farach@users.noreply.github.com> --- tests/testthat/test-request-construction.R | 68 ++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/testthat/test-request-construction.R diff --git a/tests/testthat/test-request-construction.R b/tests/testthat/test-request-construction.R new file mode 100644 index 0000000..3f7d083 --- /dev/null +++ b/tests/testthat/test-request-construction.R @@ -0,0 +1,68 @@ +test_that("onet_request builds URL correctly with path only", { + # This test verifies that onet_request can handle path-only endpoints + # without query parameters (which was causing errors before) + + # Mock the API key + old_key <- Sys.getenv("ONET_API_KEY") + on.exit(Sys.setenv(ONET_API_KEY = old_key)) + Sys.setenv(ONET_API_KEY = "test-key") + + # Build a request with only path + req <- onet2r:::onet_request("database") + + expect_s3_class(req, "httr2_request") + expect_match(req$url, "https://api-v2.onetcenter.org/database") +}) + +test_that("onet_request builds URL correctly with path segments", { + old_key <- Sys.getenv("ONET_API_KEY") + on.exit(Sys.setenv(ONET_API_KEY = old_key)) + Sys.setenv(ONET_API_KEY = "test-key") + + # Build a request with path segments + req <- onet2r:::onet_request("online/occupations", .path_segments = c("15-1252.00", "summary")) + + expect_s3_class(req, "httr2_request") + expect_match(req$url, "https://api-v2.onetcenter.org/online/occupations/15-1252.00/summary") +}) + +test_that("onet_request builds URL correctly with query parameters", { + old_key <- Sys.getenv("ONET_API_KEY") + on.exit(Sys.setenv(ONET_API_KEY = old_key)) + Sys.setenv(ONET_API_KEY = "test-key") + + # Build a request with query parameters + req <- onet2r:::onet_request("online/search", .query = list(keyword = "software", start = 1, end = 20)) + + expect_s3_class(req, "httr2_request") + expect_match(req$url, "https://api-v2.onetcenter.org/online/search") + # The query params should be in the URL + expect_match(req$url, "keyword=software") + expect_match(req$url, "start=1") + expect_match(req$url, "end=20") +}) + +test_that("onet_request builds URL correctly with both path segments and query parameters", { + old_key <- Sys.getenv("ONET_API_KEY") + on.exit(Sys.setenv(ONET_API_KEY = old_key)) + Sys.setenv(ONET_API_KEY = "test-key") + + # Build a request with both path segments and query parameters + req <- onet2r:::onet_request("database/rows", .path_segments = "skills", .query = list(start = 1, end = 100)) + + expect_s3_class(req, "httr2_request") + expect_match(req$url, "https://api-v2.onetcenter.org/database/rows/skills") + expect_match(req$url, "start=1") + expect_match(req$url, "end=100") +}) + +test_that("onet_request includes API key header", { + old_key <- Sys.getenv("ONET_API_KEY") + on.exit(Sys.setenv(ONET_API_KEY = old_key)) + test_key <- "test-api-key-123" + Sys.setenv(ONET_API_KEY = test_key) + + req <- onet2r:::onet_request("database") + + expect_equal(req$headers[["X-API-Key"]], test_key) +}) From c8584f1147d864205aa805ee87227bbf9aeee7e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 02:48:20 +0000 Subject: [PATCH 4/4] Fix onet_tables() to handle actual API response format The API returns tables as a top-level list, not nested under a 'table' field. Changed to iterate over resp directly and use table_id field instead of id. Co-authored-by: farach <1520139+farach@users.noreply.github.com> --- R/database.R | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/R/database.R b/R/database.R index e0986b2..3b6c691 100644 --- a/R/database.R +++ b/R/database.R @@ -21,13 +21,14 @@ onet_tables <- function() { # Define expected schema schema <- empty_tibble(id = character(), title = character()) - if (is.null(resp$table) || length(resp$table) == 0) { + # The API returns tables as a top-level list, not nested under 'table' + if (!is.list(resp) || length(resp) == 0) { return(schema) } - map(resp$table, \(x) { + map(resp, \(x) { tibble( - id = x$id %||% NA_character_, + id = x$table_id %||% NA_character_, title = x$title %||% NA_character_ ) }) |> list_rbind()