From dc2e9350fc753661b3a4ca546e4de2db0eea68a5 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Tue, 26 Mar 2024 13:30:38 -0500 Subject: [PATCH 1/4] Better read. --- R/urls.R | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/R/urls.R b/R/urls.R index 979fd1f..0b41648 100644 --- a/R/urls.R +++ b/R/urls.R @@ -4,7 +4,12 @@ .url_fetch <- function(x) { rlang::try_fetch( - jsonlite::read_json(x), + jsonlite::read_json( + x, + simplifyVector = TRUE, + simplifyDataFrame = FALSE, + simplifyMatrix = FALSE + ), error = function(e) { yaml::read_yaml(url(x)) } From 39662d556d93c80e4d4fb9187d075724de0e56f0 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Tue, 26 Mar 2024 15:19:59 -0500 Subject: [PATCH 2/4] Implement tibblified paths. --- DESCRIPTION | 4 +- R/urls.R | 2 +- R/zz-rapid.R | 60 +++++++++- tests/testthat/_snaps/zz-rapid.md | 181 +++++++++++++++--------------- tests/testthat/test-zz-rapid.R | 165 ++++++++++++++++++++++----- 5 files changed, 288 insertions(+), 124 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 8f90f09..a9b8b20 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -24,12 +24,14 @@ Imports: S7 (>= 0.1.1), snakecase, stbl, + tibblify, xml2, yaml Suggests: testthat (>= 3.0.0) Remotes: - jonthegeek/stbl + jonthegeek/stbl, + mgirlich/tibblify#191 Config/testthat/edition: 3 Config/testthat/parallel: true Encoding: UTF-8 diff --git a/R/urls.R b/R/urls.R index 0b41648..4915c53 100644 --- a/R/urls.R +++ b/R/urls.R @@ -11,7 +11,7 @@ simplifyMatrix = FALSE ), error = function(e) { - yaml::read_yaml(url(x)) + yaml::read_yaml(url(x)) # nocov } ) } diff --git a/R/zz-rapid.R b/R/zz-rapid.R index 5c916c2..0dbe463 100644 --- a/R/zz-rapid.R +++ b/R/zz-rapid.R @@ -58,12 +58,14 @@ class_rapid <- S7::new_class( info = class_info, servers = class_servers, components = class_components, + paths = S7::class_data.frame, security = class_security ), constructor = function(info = class_info(), ..., servers = class_servers(), components = class_components(), + paths = data.frame(), security = class_security()) { check_dots_empty() S7::new_object( @@ -71,6 +73,7 @@ class_rapid <- S7::new_class( info = as_info(info), servers = as_servers(servers), components = as_components(components), + paths = paths, security = as_security(security) ) }, @@ -79,7 +82,7 @@ class_rapid <- S7::new_class( validate_lengths( self, key_name = "info", - optional_any = c("components", "security", "servers") + optional_any = c("components", "paths", "security", "servers") ), validate_in_specific( values = self@security@name, @@ -127,6 +130,61 @@ S7::method(as_rapid, S7::new_S3_class("url")) <- function(x, as_rapid(x, ..., arg = arg, call = call) } +S7::method(as_rapid, class_list) <- function(x, + ..., + arg = caller_arg(x), + call = caller_env()) { + x$paths <- .parse_paths(x$paths, x$openapi, x, call) + rlang::try_fetch( + { + x <- as_api_object(x, class_rapid, ..., arg = arg, call = call) + expand_servers(x) + }, + rapid_error_missing_names = function(cnd) { + cli::cli_abort( + "{.arg x} must be comprised of properly formed, supported elements.", + class = "rapid_error_unsupported_elements", + parent = cnd + ) + } + ) +} + +.parse_paths <- S7::new_generic(".parse_paths", "paths") + +S7::method(.parse_paths, S7::class_data.frame) <- function(paths, ...) { + paths +} + +S7::method(.parse_paths, class_list) <- function(paths, + openapi, + x, + call = caller_env()) { + if (!is.null(openapi) && openapi >= "3") { + return(.parse_openapi_spec(x, call = call)) + } + return(data.frame()) +} + +.parse_openapi_spec <- function(x, call = caller_env()) { # nocov start + rlang::try_fetch( + { + tibblify::parse_openapi_spec(x) + }, + error = function(cnd) { + cli::cli_abort( + "Failed to parse paths from OpenAPI spec.", + class = "rapid_error_bad_tibblify", + call = call + ) + } + ) +} # nocov end + +S7::method(.parse_paths, class_any) <- function(paths, ...) { + return(data.frame()) +} + S7::method(as_rapid, class_any) <- function(x, ..., arg = caller_arg(x), diff --git a/tests/testthat/_snaps/zz-rapid.md b/tests/testthat/_snaps/zz-rapid.md index 6a638e9..176222f 100644 --- a/tests/testthat/_snaps/zz-rapid.md +++ b/tests/testthat/_snaps/zz-rapid.md @@ -67,6 +67,7 @@ .. .. @ name : chr(0) .. .. @ details : list() .. .. @ description: chr(0) + @ paths :'data.frame': 0 obs. of 0 variables @ security : .. @ name : chr(0) .. @ required_scopes : list() @@ -104,7 +105,7 @@ Error: ! `x` must be comprised of properly formed, supported elements. Caused by error in `as_rapid()`: - ! `list(letters)` must have names "info", "servers", "components", or "security". + ! `` must have names "info", "servers", "components", "paths", or "security". * Any other names are ignored. --- @@ -115,7 +116,7 @@ Error: ! `x` must be comprised of properly formed, supported elements. Caused by error in `as_rapid()`: - ! `list(list("https://example.com", "A cool server."))` must have names "info", "servers", "components", or "security". + ! `` must have names "info", "servers", "components", "paths", or "security". * Any other names are ignored. # as_rapid() works for yaml urls @@ -125,62 +126,55 @@ Output @ info : - .. @ title : chr "AWS Migration Hub" - .. @ version : chr "2017-05-31" + .. @ title : chr "OpenFEC" + .. @ version : chr "1.0" .. @ contact : - .. .. @ name : chr "Mike Ralphson" - .. .. @ email: chr "mike.ralphson@gmail.com" - .. .. @ url : chr "https://github.com/mermade/aws2openapi" - .. @ description : chr "

The AWS Migration Hub API methods help to obtain server and application migration status and integrate your "| __truncated__ + .. .. @ name : chr(0) + .. .. @ email: chr(0) + .. .. @ url : chr(0) + .. @ description : chr "This application programming interface (API) allows you to explore the way candidates and committees fund their"| __truncated__ .. @ license : - .. .. @ name : chr "Apache 2.0 License" + .. .. @ name : chr(0) .. .. @ identifier: chr(0) - .. .. @ url : chr "http://www.apache.org/licenses/" + .. .. @ url : chr(0) .. @ summary : chr(0) - .. @ terms_of_service: chr "https://aws.amazon.com/service-terms/" + .. @ terms_of_service: chr(0) .. @ origin : - .. .. @ url : chr "https://raw.githubusercontent.com/aws/aws-sdk-js/master/apis/AWSMigrationHub-2017-05-31.normal.json" - .. .. @ format : chr(0) - .. .. @ version: chr(0) + .. .. @ url : chr "https://api.open.fec.gov/swagger/" + .. .. @ format : chr "openapi" + .. .. @ version: chr "3.0" @ servers : - .. @ url : chr [1:4] "http://mgh.{region}.amazonaws.com" ... - .. @ description: chr [1:4] "The AWS Migration Hub multi-region endpoint" ... - .. @ variables : List of 4 - .. .. $ : - .. .. ..@ name : chr "region" - .. .. ..@ default : chr "us-east-1" - .. .. ..@ enum :List of 1 - .. .. .. .. $ : chr [1:23] "us-east-1" "us-east-2" "us-west-1" "us-west-2" ... - .. .. ..@ description: chr "The AWS region" - .. .. $ : - .. .. ..@ name : chr "region" - .. .. ..@ default : chr "us-east-1" - .. .. ..@ enum :List of 1 - .. .. .. .. $ : chr [1:23] "us-east-1" "us-east-2" "us-west-1" "us-west-2" ... - .. .. ..@ description: chr "The AWS region" - .. .. $ : - .. .. ..@ name : chr "region" - .. .. ..@ default : chr "cn-north-1" - .. .. ..@ enum :List of 1 - .. .. .. .. $ : chr [1:2] "cn-north-1" "cn-northwest-1" - .. .. ..@ description: chr "The AWS region" - .. .. $ : - .. .. ..@ name : chr "region" - .. .. ..@ default : chr "cn-north-1" - .. .. ..@ enum :List of 1 - .. .. .. .. $ : chr [1:2] "cn-north-1" "cn-northwest-1" - .. .. ..@ description: chr "The AWS region" + .. @ url : chr "https://api.open.fec.gov/v1" + .. @ description: chr(0) + .. @ variables : list() @ components: .. @ security_schemes: - .. .. @ name : chr "hmac" - .. .. @ details : List of 1 + .. .. @ name : chr [1:3] "ApiKeyHeaderAuth" "ApiKeyQueryAuth" "apiKey" + .. .. @ details : List of 3 .. .. .. $ : - .. .. .. ..@ parameter_name: chr "Authorization" + .. .. .. ..@ parameter_name: chr "X-Api-Key" .. .. .. ..@ location : chr "header" - .. .. @ description: chr "Amazon Signature authorization v4" + .. .. .. $ : + .. .. .. ..@ parameter_name: chr "api_key" + .. .. .. ..@ location : chr "query" + .. .. .. $ : + .. .. .. ..@ parameter_name: chr "api_key" + .. .. .. ..@ location : chr "query" + .. .. @ description: chr(0) + @ paths : tibble [3 x 2] (S3: tbl_df/tbl/data.frame) + $ endpoint : chr [1:3] "a" "b" "c" + $ operations:List of 3 + ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + Named list() + ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + Named list() + ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + Named list() @ security : - .. @ name : chr "hmac" - .. @ required_scopes :List of 1 + .. @ name : chr [1:3] "ApiKeyHeaderAuth" "ApiKeyQueryAuth" "apiKey" + .. @ required_scopes :List of 3 + .. .. $ : chr(0) + .. .. $ : chr(0) .. .. $ : chr(0) .. @ rapid_class_requirement: chr "security_scheme" @@ -191,62 +185,55 @@ Output @ info : - .. @ title : chr "AWS Migration Hub" - .. @ version : chr "2017-05-31" + .. @ title : chr "OpenFEC" + .. @ version : chr "1.0" .. @ contact : - .. .. @ name : chr "Mike Ralphson" - .. .. @ email: chr "mike.ralphson@gmail.com" - .. .. @ url : chr "https://github.com/mermade/aws2openapi" - .. @ description : chr "

The AWS Migration Hub API methods help to obtain server and application migration status and integrate your "| __truncated__ + .. .. @ name : chr(0) + .. .. @ email: chr(0) + .. .. @ url : chr(0) + .. @ description : chr "This application programming interface (API) allows you to explore the way candidates and committees fund their"| __truncated__ .. @ license : - .. .. @ name : chr "Apache 2.0 License" + .. .. @ name : chr(0) .. .. @ identifier: chr(0) - .. .. @ url : chr "http://www.apache.org/licenses/" + .. .. @ url : chr(0) .. @ summary : chr(0) - .. @ terms_of_service: chr "https://aws.amazon.com/service-terms/" + .. @ terms_of_service: chr(0) .. @ origin : - .. .. @ url : chr "https://raw.githubusercontent.com/aws/aws-sdk-js/master/apis/AWSMigrationHub-2017-05-31.normal.json" - .. .. @ format : chr(0) - .. .. @ version: chr(0) + .. .. @ url : chr "https://api.open.fec.gov/swagger/" + .. .. @ format : chr "openapi" + .. .. @ version: chr "3.0" @ servers : - .. @ url : chr [1:4] "http://mgh.{region}.amazonaws.com" ... - .. @ description: chr [1:4] "The AWS Migration Hub multi-region endpoint" ... - .. @ variables : List of 4 - .. .. $ : - .. .. ..@ name : chr "region" - .. .. ..@ default : chr "us-east-1" - .. .. ..@ enum :List of 1 - .. .. .. .. $ : chr [1:23] "us-east-1" "us-east-2" "us-west-1" "us-west-2" ... - .. .. ..@ description: chr "The AWS region" - .. .. $ : - .. .. ..@ name : chr "region" - .. .. ..@ default : chr "us-east-1" - .. .. ..@ enum :List of 1 - .. .. .. .. $ : chr [1:23] "us-east-1" "us-east-2" "us-west-1" "us-west-2" ... - .. .. ..@ description: chr "The AWS region" - .. .. $ : - .. .. ..@ name : chr "region" - .. .. ..@ default : chr "cn-north-1" - .. .. ..@ enum :List of 1 - .. .. .. .. $ : chr [1:2] "cn-north-1" "cn-northwest-1" - .. .. ..@ description: chr "The AWS region" - .. .. $ : - .. .. ..@ name : chr "region" - .. .. ..@ default : chr "cn-north-1" - .. .. ..@ enum :List of 1 - .. .. .. .. $ : chr [1:2] "cn-north-1" "cn-northwest-1" - .. .. ..@ description: chr "The AWS region" + .. @ url : chr "https://api.open.fec.gov/v1" + .. @ description: chr(0) + .. @ variables : list() @ components: .. @ security_schemes: - .. .. @ name : chr "hmac" - .. .. @ details : List of 1 + .. .. @ name : chr [1:3] "ApiKeyHeaderAuth" "ApiKeyQueryAuth" "apiKey" + .. .. @ details : List of 3 .. .. .. $ : - .. .. .. ..@ parameter_name: chr "Authorization" + .. .. .. ..@ parameter_name: chr "X-Api-Key" .. .. .. ..@ location : chr "header" - .. .. @ description: chr "Amazon Signature authorization v4" + .. .. .. $ : + .. .. .. ..@ parameter_name: chr "api_key" + .. .. .. ..@ location : chr "query" + .. .. .. $ : + .. .. .. ..@ parameter_name: chr "api_key" + .. .. .. ..@ location : chr "query" + .. .. @ description: chr(0) + @ paths : tibble [3 x 2] (S3: tbl_df/tbl/data.frame) + $ endpoint : chr [1:3] "a" "b" "c" + $ operations:List of 3 + ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + Named list() + ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + Named list() + ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + Named list() @ security : - .. @ name : chr "hmac" - .. @ required_scopes :List of 1 + .. @ name : chr [1:3] "ApiKeyHeaderAuth" "ApiKeyQueryAuth" "apiKey" + .. @ required_scopes :List of 3 + .. .. $ : chr(0) + .. .. $ : chr(0) .. .. $ : chr(0) .. @ rapid_class_requirement: chr "security_scheme" @@ -283,6 +270,7 @@ .. .. @ name : chr(0) .. .. @ details : list() .. .. @ description: chr(0) + @ paths :'data.frame': 0 obs. of 0 variables @ security : .. @ name : chr [1:3] "ApiKeyHeaderAuth" "ApiKeyQueryAuth" "apiKey" .. @ required_scopes :List of 3 @@ -333,6 +321,15 @@ .. .. .. ..@ parameter_name: chr "api_key" .. .. .. ..@ location : chr "query" .. .. @ description: chr(0) + @ paths : tibble [3 x 2] (S3: tbl_df/tbl/data.frame) + $ endpoint : chr [1:3] "a" "b" "c" + $ operations:List of 3 + ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + Named list() + ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + Named list() + ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + Named list() @ security : .. @ name : chr [1:3] "ApiKeyHeaderAuth" "ApiKeyQueryAuth" "apiKey" .. @ required_scopes :List of 3 diff --git a/tests/testthat/test-zz-rapid.R b/tests/testthat/test-zz-rapid.R index 409bb15..19f2ca5 100644 --- a/tests/testthat/test-zz-rapid.R +++ b/tests/testthat/test-zz-rapid.R @@ -60,7 +60,7 @@ test_that("class_rapid() returns an empty rapid", { ) expect_identical( S7::prop_names(test_result), - c("info", "servers", "components", "security") + c("info", "servers", "components", "paths", "security") ) }) @@ -215,65 +215,98 @@ test_that("as_rapid() works for lists", { test_that("as_rapid() works for yaml urls", { skip_if_not(Sys.getenv("RAPID_TEST_DL") == "true") - yaml_url <- "https://api.apis.guru/v2/specs/amazonaws.com/AWSMigrationHub/2017-05-31/openapi.yaml" - expect_warning(expect_warning( + local_mocked_bindings( + .parse_openapi_spec = function(x, call = NULL) { + return( + tibble::tibble( + endpoint = c("a", "b", "c"), + operations = list( + tibble::tibble(), + tibble::tibble(), + tibble::tibble() + ) + ) + ) + } + ) + yaml_url <- "https://api.apis.guru/v2/specs/fec.gov/1.0/openapi.json" + expect_warning( expect_warning( expect_warning( expect_warning( - expect_warning( - { - test_result <- as_rapid(url(yaml_url)) - }, - "x_has_equivalent_paths", - class = "rapid_warning_extra_names" - ), - "x_release", + { + test_result <- as_rapid(url(yaml_url)) + }, + "schemas", class = "rapid_warning_extra_names" ), "x_twitter", class = "rapid_warning_extra_names" ), - "parameters", + "x_apisguru_categories", class = "rapid_warning_extra_names" ), - "x_amazon_apigateway_authtype", + "openapi", class = "rapid_warning_extra_names" - ), "x_apisguru_driver", class = "rapid_warning_extra_names") + ) expect_snapshot(test_result) }) test_that("as_rapid() works for json urls", { skip_if_not(Sys.getenv("RAPID_TEST_DL") == "true") - - json_url <- "https://api.apis.guru/v2/specs/amazonaws.com/AWSMigrationHub/2017-05-31/openapi.json" - expect_warning(expect_warning( + local_mocked_bindings( + .parse_openapi_spec = function(x, call = NULL) { + return( + tibble::tibble( + endpoint = c("a", "b", "c"), + operations = list( + tibble::tibble(), + tibble::tibble(), + tibble::tibble() + ) + ) + ) + } + ) + json_url <- "https://api.apis.guru/v2/specs/fec.gov/1.0/openapi.json" + expect_warning( expect_warning( expect_warning( expect_warning( - expect_warning( - { - test_result <- as_rapid(url(json_url)) - }, - "x_has_equivalent_paths", - class = "rapid_warning_extra_names" - ), - "x_release", + { + test_result <- as_rapid(url(json_url)) + }, + "schemas", class = "rapid_warning_extra_names" ), "x_twitter", class = "rapid_warning_extra_names" ), - "parameters", + "x_apisguru_categories", class = "rapid_warning_extra_names" ), - "x_amazon_apigateway_authtype", + "openapi", class = "rapid_warning_extra_names" - ), "x_apisguru_driver", class = "rapid_warning_extra_names") + ) expect_snapshot(test_result) }) test_that("as_rapid() stores origin info for urls", { skip_if_not(Sys.getenv("RAPID_TEST_DL") == "true") + local_mocked_bindings( + .parse_openapi_spec = function(x, call = NULL) { + return( + tibble::tibble( + endpoint = c("a", "b", "c"), + operations = list( + tibble::tibble(), + tibble::tibble(), + tibble::tibble() + ) + ) + ) + } + ) test_url <- "https://api.open.fec.gov/swagger/" expect_warning( { @@ -294,7 +327,20 @@ test_that("as_rapid() works for empty optional fields", { # yaml::read_yaml() # saveRDS(x, test_path("fixtures", "fec.rds")) x <- readRDS(test_path("fixtures", "fec.rds")) - + local_mocked_bindings( + .parse_openapi_spec = function(x, call = NULL) { + return( + tibble::tibble( + endpoint = c("a", "b", "c"), + operations = list( + tibble::tibble(), + tibble::tibble(), + tibble::tibble() + ) + ) + ) + } + ) expect_warning( expect_warning( expect_warning( @@ -317,3 +363,64 @@ test_that("as_rapid() works for empty optional fields", { expect_snapshot(test_result) }) + +# This pair breaks with tibblify but has more info. Save for when tibblify works. + +# test_that("as_rapid() works for yaml urls", { +# skip_if_not(Sys.getenv("RAPID_TEST_DL") == "true") +# yaml_url <- "https://api.apis.guru/v2/specs/amazonaws.com/AWSMigrationHub/2017-05-31/openapi.yaml" +# expect_warning(expect_warning( +# expect_warning( +# expect_warning( +# expect_warning( +# expect_warning( +# { +# test_result <- as_rapid(url(yaml_url)) +# }, +# "x_has_equivalent_paths", +# class = "rapid_warning_extra_names" +# ), +# "x_release", +# class = "rapid_warning_extra_names" +# ), +# "x_twitter", +# class = "rapid_warning_extra_names" +# ), +# "parameters", +# class = "rapid_warning_extra_names" +# ), +# "x_amazon_apigateway_authtype", +# class = "rapid_warning_extra_names" +# ), "x_apisguru_driver", class = "rapid_warning_extra_names") +# expect_snapshot(test_result) +# }) +# +# test_that("as_rapid() works for json urls", { +# skip_if_not(Sys.getenv("RAPID_TEST_DL") == "true") +# +# json_url <- "https://api.apis.guru/v2/specs/amazonaws.com/AWSMigrationHub/2017-05-31/openapi.json" +# expect_warning(expect_warning( +# expect_warning( +# expect_warning( +# expect_warning( +# expect_warning( +# { +# test_result <- as_rapid(url(json_url)) +# }, +# "x_has_equivalent_paths", +# class = "rapid_warning_extra_names" +# ), +# "x_release", +# class = "rapid_warning_extra_names" +# ), +# "x_twitter", +# class = "rapid_warning_extra_names" +# ), +# "parameters", +# class = "rapid_warning_extra_names" +# ), +# "x_amazon_apigateway_authtype", +# class = "rapid_warning_extra_names" +# ), "x_apisguru_driver", class = "rapid_warning_extra_names") +# expect_snapshot(test_result) +# }) From 6b6cc1849a3c2bcf9e1457b49663610455602a8a Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Tue, 26 Mar 2024 16:34:30 -0500 Subject: [PATCH 3/4] Toward a real class_paths. --- DESCRIPTION | 12 ++-- NAMESPACE | 3 + R/components-security_schemes.R | 5 +- R/paths.R | 105 ++++++++++++++++++++++++++++++ R/rapid-package.R | 1 + R/zz-rapid.R | 54 ++++----------- man/as_paths.Rd | 27 ++++++++ man/as_rapid.Rd | 9 +-- man/class_paths.Rd | 34 ++++++++++ man/class_rapid.Rd | 5 +- tests/testthat/_snaps/paths.md | 32 +++++++++ tests/testthat/_snaps/zz-rapid.md | 45 +++++++------ tests/testthat/test-paths.R | 52 +++++++++++++++ tests/testthat/test-zz-rapid.R | 3 +- 14 files changed, 310 insertions(+), 77 deletions(-) create mode 100644 R/paths.R create mode 100644 man/as_paths.Rd create mode 100644 man/class_paths.Rd create mode 100644 tests/testthat/_snaps/paths.md create mode 100644 tests/testthat/test-paths.R diff --git a/DESCRIPTION b/DESCRIPTION index a9b8b20..181fd4d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,20 +1,22 @@ Package: rapid Title: R 'API' Descriptions -Version: 0.0.0.9000 +Version: 0.0.0.9003 Authors@R: c( person("Jon", "Harmon", , "jonthegeek@gmail.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0003-4781-4346")), person("The Linux Foundation", role = "cph", comment = "OpenAPI Specification") ) -Description: Convert an 'API' description ('APID'), such as one that follows - the 'OpenAPI Specification', to an R 'API' description object (a - "rapid"). The rapid object follows the 'OpenAPI Specification' to +Description: Convert an 'API' description ('APID'), such as one that + follows the 'OpenAPI Specification', to an R 'API' description object + (a "rapid"). The rapid object follows the 'OpenAPI Specification' to make it easy to convert to and from 'API' documents. License: MIT + file LICENSE URL: https://jonthegeek.github.io/rapid/, https://github.com/jonthegeek/rapid BugReports: https://github.com/jonthegeek/rapid/issues +Depends: + R (>= 3.5.0) Imports: cli, glue, @@ -24,6 +26,7 @@ Imports: S7 (>= 0.1.1), snakecase, stbl, + tibble, tibblify, xml2, yaml @@ -40,6 +43,7 @@ RoxygenNote: 7.3.1 Collate: 'properties.R' 'security.R' + 'paths.R' 'components-security_scheme_details.R' 'components-security_schemes.R' 'components.R' diff --git a/NAMESPACE b/NAMESPACE index 1a6452d..6b72a85 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -11,6 +11,7 @@ export(as_oauth2_implicit_flow) export(as_oauth2_security_scheme) export(as_oauth2_token_flow) export(as_origin) +export(as_paths) export(as_rapid) export(as_reference) export(as_schema) @@ -34,6 +35,7 @@ export(class_oauth2_implicit_flow) export(class_oauth2_security_scheme) export(class_oauth2_token_flow) export(class_origin) +export(class_paths) export(class_rapid) export(class_reference) export(class_schema) @@ -50,6 +52,7 @@ importFrom(S7,"prop<-") importFrom(S7,S7_inherits) importFrom(S7,class_any) importFrom(S7,class_character) +importFrom(S7,class_data.frame) importFrom(S7,class_factor) importFrom(S7,class_list) importFrom(S7,class_logical) diff --git a/R/components-security_schemes.R b/R/components-security_schemes.R index 2eebd15..1ee1828 100644 --- a/R/components-security_schemes.R +++ b/R/components-security_schemes.R @@ -140,10 +140,7 @@ S7::method(length, class_security_schemes) <- function(x) { #' ) #' ) #' ) -as_security_schemes <- S7::new_generic( - "as_security_schemes", - "x" -) +as_security_schemes <- S7::new_generic("as_security_schemes", "x") S7::method( as_security_schemes, diff --git a/R/paths.R b/R/paths.R new file mode 100644 index 0000000..3e0e3c2 --- /dev/null +++ b/R/paths.R @@ -0,0 +1,105 @@ +#' The available paths and operations for the API +#' +#' Holds the relative paths to the individual endpoints and their operations. +#' The path is appended to the URL from the [class_servers()] object in order to +#' construct the full URL. The paths may be empty. +#' +#' @param ... A data.frame, or arguments to pass to [tibble::tibble()]. +#' +#' @return A `paths` S7 object with details about API endpoints. +#' @export +#' +#' @seealso [as_paths()] for coercing objects to `paths`. +#' +#' @examples +#' class_paths() +#' class_paths( +#' tibble::tibble( +#' endpoint = c("/endpoint1", "/endpoint2"), +#' operations = list( +#' tibble::tibble(operation_properties = 1:2), +#' tibble::tibble(operation_properties = 3:5) +#' ) +#' ) +#' ) +class_paths <- S7::new_class( + "paths", + package = "rapid", + parent = class_data.frame, + constructor = function(...) { + if (...length() == 1 && is.data.frame(..1)) { + return(S7::new_object(tibble::as_tibble(..1))) + } + S7::new_object(tibble::tibble(...)) + } +) + +#' Coerce objects to paths +#' +#' `as_paths()` turns an existing object into a `paths` object. This is in +#' contrast with [class_paths()], which builds a `paths` object from individual +#' properties. In practice, [class_paths()] and `as_paths()` are currently +#' functionally identical. However, in the future, `as_paths()` will coerce +#' other valid objects to the expected shape. +#' +#' @inheritParams rlang::args_dots_empty +#' @inheritParams rlang::args_error_context +#' @param x The object to coerce. Must be empty or be a `data.frame()`. +#' +#' @return A `paths` object as returned by [class_paths()]. +#' @export +#' +#' @examples +#' as_paths() +#' as_paths(mtcars) +as_paths <- S7::new_generic("as_paths", "x") + +S7::method(as_paths, class_data.frame) <- function(x, + ..., + arg = caller_arg(x), + call = caller_env()) { + class_paths(x) +} + +S7::method(as_paths, class_any) <- function(x, + ..., + arg = caller_arg(x), + call = caller_env()) { + as_api_object(x, class_paths, ..., arg = arg, call = call) +} + +.parse_paths <- S7::new_generic(".parse_paths", "paths") + +S7::method(.parse_paths, class_data.frame | class_paths) <- function(paths, + ...) { + paths +} + +S7::method(.parse_paths, class_list) <- function(paths, + openapi, + x, + call = caller_env()) { + if (!is.null(openapi) && openapi >= "3") { + return(.parse_openapi_spec(x, call = call)) + } + return(tibble::tibble()) +} + +.parse_openapi_spec <- function(x, call = caller_env()) { # nocov start + rlang::try_fetch( + { + tibblify::parse_openapi_spec(x) + }, + error = function(cnd) { + cli::cli_abort( + "Failed to parse paths from OpenAPI spec.", + class = "rapid_error_bad_tibblify", + call = call + ) + } + ) +} # nocov end + +S7::method(.parse_paths, class_any) <- function(paths, ...) { + return(tibble::tibble()) +} diff --git a/R/rapid-package.R b/R/rapid-package.R index 43baa04..c4bd03f 100644 --- a/R/rapid-package.R +++ b/R/rapid-package.R @@ -5,6 +5,7 @@ #' @importFrom rlang check_dots_empty #' @importFrom S7 class_any #' @importFrom S7 class_character +#' @importFrom S7 class_data.frame #' @importFrom S7 class_factor #' @importFrom S7 class_list #' @importFrom S7 class_logical diff --git a/R/zz-rapid.R b/R/zz-rapid.R index 0dbe463..bde5ca0 100644 --- a/R/zz-rapid.R +++ b/R/zz-rapid.R @@ -1,6 +1,7 @@ #' @include info.R #' @include servers.R #' @include components.R +#' @include paths.R #' @include security.R NULL @@ -12,10 +13,11 @@ NULL #' @param info An `info` object defined by [class_info()]. #' @param servers A `servers` object defined by [class_servers()]. #' @param components A `components` object defined by [class_components()]. +#' @param paths A `paths` object defined by [class_paths()]. #' @param security A `security` object defined by [class_security()]. #' #' @return A `rapid` S7 object, with properties `info`, `servers`, `components`, -#' and `security`. +#' `paths`, and `security`. #' @export #' #' @seealso [as_rapid()] for coercing objects to `rapid`. @@ -58,14 +60,14 @@ class_rapid <- S7::new_class( info = class_info, servers = class_servers, components = class_components, - paths = S7::class_data.frame, + paths = class_paths, security = class_security ), constructor = function(info = class_info(), ..., servers = class_servers(), components = class_components(), - paths = data.frame(), + paths = class_paths(), security = class_security()) { check_dots_empty() S7::new_object( @@ -73,7 +75,7 @@ class_rapid <- S7::new_class( info = as_info(info), servers = as_servers(servers), components = as_components(components), - paths = paths, + paths = as_paths(paths), security = as_security(security) ) }, @@ -106,10 +108,11 @@ S7::method(length, class_rapid) <- function(x) { #' #' @inheritParams rlang::args_dots_empty #' @inheritParams rlang::args_error_context -#' @param x The object to coerce. Must be empty or have names "info" and/or -#' "servers", or names that can be coerced to those names via -#' [snakecase::to_snake_case()]. Extra names are ignored. [url()] objects are -#' read with [jsonlite::fromJSON()] or [yaml::read_yaml()] before conversion. +#' @param x The object to coerce. Must be empty or have names "info", "servers", +#' "components", "paths", and/or "security", or names that can be coerced to +#' those names via [snakecase::to_snake_case()]. Extra names are ignored. +#' [url()] objects are read with [jsonlite::fromJSON()] or [yaml::read_yaml()] +#' before conversion. #' #' @return A `rapid` object as returned by [class_rapid()]. #' @export @@ -150,41 +153,6 @@ S7::method(as_rapid, class_list) <- function(x, ) } -.parse_paths <- S7::new_generic(".parse_paths", "paths") - -S7::method(.parse_paths, S7::class_data.frame) <- function(paths, ...) { - paths -} - -S7::method(.parse_paths, class_list) <- function(paths, - openapi, - x, - call = caller_env()) { - if (!is.null(openapi) && openapi >= "3") { - return(.parse_openapi_spec(x, call = call)) - } - return(data.frame()) -} - -.parse_openapi_spec <- function(x, call = caller_env()) { # nocov start - rlang::try_fetch( - { - tibblify::parse_openapi_spec(x) - }, - error = function(cnd) { - cli::cli_abort( - "Failed to parse paths from OpenAPI spec.", - class = "rapid_error_bad_tibblify", - call = call - ) - } - ) -} # nocov end - -S7::method(.parse_paths, class_any) <- function(paths, ...) { - return(data.frame()) -} - S7::method(as_rapid, class_any) <- function(x, ..., arg = caller_arg(x), diff --git a/man/as_paths.Rd b/man/as_paths.Rd new file mode 100644 index 0000000..6d681f1 --- /dev/null +++ b/man/as_paths.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/paths.R +\name{as_paths} +\alias{as_paths} +\title{Coerce objects to paths} +\usage{ +as_paths(x, ...) +} +\arguments{ +\item{x}{The object to coerce. Must be empty or be a \code{data.frame()}.} + +\item{...}{These dots are for future extensions and must be empty.} +} +\value{ +A \code{paths} object as returned by \code{\link[=class_paths]{class_paths()}}. +} +\description{ +\code{as_paths()} turns an existing object into a \code{paths} object. This is in +contrast with \code{\link[=class_paths]{class_paths()}}, which builds a \code{paths} object from individual +properties. In practice, \code{\link[=class_paths]{class_paths()}} and \code{as_paths()} are currently +functionally identical. However, in the future, \code{as_paths()} will coerce +other valid objects to the expected shape. +} +\examples{ +as_paths() +as_paths(mtcars) +} diff --git a/man/as_rapid.Rd b/man/as_rapid.Rd index 19b9373..5632a27 100644 --- a/man/as_rapid.Rd +++ b/man/as_rapid.Rd @@ -7,10 +7,11 @@ as_rapid(x, ...) } \arguments{ -\item{x}{The object to coerce. Must be empty or have names "info" and/or -"servers", or names that can be coerced to those names via -\code{\link[snakecase:caseconverter]{snakecase::to_snake_case()}}. Extra names are ignored. \code{\link[=url]{url()}} objects are -read with \code{\link[jsonlite:fromJSON]{jsonlite::fromJSON()}} or \code{\link[yaml:read_yaml]{yaml::read_yaml()}} before conversion.} +\item{x}{The object to coerce. Must be empty or have names "info", "servers", +"components", "paths", and/or "security", or names that can be coerced to +those names via \code{\link[snakecase:caseconverter]{snakecase::to_snake_case()}}. Extra names are ignored. +\code{\link[=url]{url()}} objects are read with \code{\link[jsonlite:fromJSON]{jsonlite::fromJSON()}} or \code{\link[yaml:read_yaml]{yaml::read_yaml()}} +before conversion.} \item{...}{These dots are for future extensions and must be empty.} } diff --git a/man/class_paths.Rd b/man/class_paths.Rd new file mode 100644 index 0000000..d1159e5 --- /dev/null +++ b/man/class_paths.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/paths.R +\name{class_paths} +\alias{class_paths} +\title{The available paths and operations for the API} +\usage{ +class_paths(...) +} +\arguments{ +\item{...}{A data.frame, or arguments to pass to \code{\link[tibble:tibble]{tibble::tibble()}}.} +} +\value{ +A \code{paths} S7 object with details about API endpoints. +} +\description{ +Holds the relative paths to the individual endpoints and their operations. +The path is appended to the URL from the \code{\link[=class_servers]{class_servers()}} object in order to +construct the full URL. The paths may be empty. +} +\examples{ +class_paths() +class_paths( + tibble::tibble( + endpoint = c("/endpoint1", "/endpoint2"), + operations = list( + tibble::tibble(operation_properties = 1:2), + tibble::tibble(operation_properties = 3:5) + ) + ) +) +} +\seealso{ +\code{\link[=as_paths]{as_paths()}} for coercing objects to \code{paths}. +} diff --git a/man/class_rapid.Rd b/man/class_rapid.Rd index 39ae461..dfcb061 100644 --- a/man/class_rapid.Rd +++ b/man/class_rapid.Rd @@ -9,6 +9,7 @@ class_rapid( ..., servers = class_servers(), components = class_components(), + paths = class_paths(), security = class_security() ) } @@ -21,11 +22,13 @@ class_rapid( \item{components}{A \code{components} object defined by \code{\link[=class_components]{class_components()}}.} +\item{paths}{A \code{paths} object defined by \code{\link[=class_paths]{class_paths()}}.} + \item{security}{A \code{security} object defined by \code{\link[=class_security]{class_security()}}.} } \value{ A \code{rapid} S7 object, with properties \code{info}, \code{servers}, \code{components}, -and \code{security}. +\code{paths}, and \code{security}. } \description{ An object that represents an API. diff --git a/tests/testthat/_snaps/paths.md b/tests/testthat/_snaps/paths.md new file mode 100644 index 0000000..a11da16 --- /dev/null +++ b/tests/testthat/_snaps/paths.md @@ -0,0 +1,32 @@ +# class_paths() returns an empty paths + + Code + test_result <- class_paths() + test_result + Output + data frame with 0 columns and 0 rows + +# as_paths() errors informatively for bad classes + + Code + as_paths(1:2) + Condition + Error in `as_paths()`: + ! Can't coerce `1:2` to . + +--- + + Code + as_paths(mean) + Condition + Error in `as_paths()`: + ! Can't coerce `mean` to . + +--- + + Code + as_paths(TRUE) + Condition + Error in `as_paths()`: + ! Can't coerce `TRUE` to . + diff --git a/tests/testthat/_snaps/zz-rapid.md b/tests/testthat/_snaps/zz-rapid.md index 176222f..47f73f5 100644 --- a/tests/testthat/_snaps/zz-rapid.md +++ b/tests/testthat/_snaps/zz-rapid.md @@ -67,7 +67,8 @@ .. .. @ name : chr(0) .. .. @ details : list() .. .. @ description: chr(0) - @ paths :'data.frame': 0 obs. of 0 variables + @ paths :Classes 'rapid::paths', 'S7_object' and 'data.frame': 0 obs. of 0 variables + Named list() @ security : .. @ name : chr(0) .. @ required_scopes : list() @@ -161,14 +162,15 @@ .. .. .. ..@ parameter_name: chr "api_key" .. .. .. ..@ location : chr "query" .. .. @ description: chr(0) - @ paths : tibble [3 x 2] (S3: tbl_df/tbl/data.frame) - $ endpoint : chr [1:3] "a" "b" "c" - $ operations:List of 3 - ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + @ paths :Classes 'rapid::paths', 'S7_object' and 'data.frame': 3 obs. of 2 variables: + List of 2 + .. $ endpoint : chr [1:3] "a" "b" "c" + .. $ operations:List of 3 + .. ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) Named list() - ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + .. ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) Named list() - ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + .. ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) Named list() @ security : .. @ name : chr [1:3] "ApiKeyHeaderAuth" "ApiKeyQueryAuth" "apiKey" @@ -220,14 +222,15 @@ .. .. .. ..@ parameter_name: chr "api_key" .. .. .. ..@ location : chr "query" .. .. @ description: chr(0) - @ paths : tibble [3 x 2] (S3: tbl_df/tbl/data.frame) - $ endpoint : chr [1:3] "a" "b" "c" - $ operations:List of 3 - ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + @ paths :Classes 'rapid::paths', 'S7_object' and 'data.frame': 3 obs. of 2 variables: + List of 2 + .. $ endpoint : chr [1:3] "a" "b" "c" + .. $ operations:List of 3 + .. ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) Named list() - ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + .. ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) Named list() - ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + .. ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) Named list() @ security : .. @ name : chr [1:3] "ApiKeyHeaderAuth" "ApiKeyQueryAuth" "apiKey" @@ -270,7 +273,8 @@ .. .. @ name : chr(0) .. .. @ details : list() .. .. @ description: chr(0) - @ paths :'data.frame': 0 obs. of 0 variables + @ paths :Classes 'rapid::paths', 'S7_object' and 'data.frame': 0 obs. of 0 variables + Named list() @ security : .. @ name : chr [1:3] "ApiKeyHeaderAuth" "ApiKeyQueryAuth" "apiKey" .. @ required_scopes :List of 3 @@ -321,14 +325,15 @@ .. .. .. ..@ parameter_name: chr "api_key" .. .. .. ..@ location : chr "query" .. .. @ description: chr(0) - @ paths : tibble [3 x 2] (S3: tbl_df/tbl/data.frame) - $ endpoint : chr [1:3] "a" "b" "c" - $ operations:List of 3 - ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + @ paths :Classes 'rapid::paths', 'S7_object' and 'data.frame': 3 obs. of 2 variables: + List of 2 + .. $ endpoint : chr [1:3] "a" "b" "c" + .. $ operations:List of 3 + .. ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) Named list() - ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + .. ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) Named list() - ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) + .. ..$ : tibble [0 x 0] (S3: tbl_df/tbl/data.frame) Named list() @ security : .. @ name : chr [1:3] "ApiKeyHeaderAuth" "ApiKeyQueryAuth" "apiKey" diff --git a/tests/testthat/test-paths.R b/tests/testthat/test-paths.R new file mode 100644 index 0000000..feb8111 --- /dev/null +++ b/tests/testthat/test-paths.R @@ -0,0 +1,52 @@ +test_that("class_paths() returns an empty paths", { + expect_snapshot({ + test_result <- class_paths() + test_result + }) + expect_s3_class( + test_result, + class = c("rapid::paths", "data.frame", "S7_object"), + exact = TRUE + ) +}) + +test_that("as_paths() errors informatively for bad classes", { + expect_snapshot( + as_paths(1:2), + error = TRUE + ) + expect_snapshot( + as_paths(mean), + error = TRUE + ) + expect_snapshot( + as_paths(TRUE), + error = TRUE + ) +}) + +test_that("as_paths() returns expected objects", { + expect_identical( + as_paths(mtcars), + class_paths(mtcars) + ) + expect_identical( + as_paths(data.frame()), + class_paths() + ) + expect_identical( + as_paths(tibble::tibble()), + class_paths() + ) + expect_identical( + as_paths(), + class_paths() + ) +}) + +test_that("as_paths() works for paths", { + expect_identical( + as_paths(class_paths()), + class_paths() + ) +}) diff --git a/tests/testthat/test-zz-rapid.R b/tests/testthat/test-zz-rapid.R index 19f2ca5..699ba91 100644 --- a/tests/testthat/test-zz-rapid.R +++ b/tests/testthat/test-zz-rapid.R @@ -364,7 +364,8 @@ test_that("as_rapid() works for empty optional fields", { expect_snapshot(test_result) }) -# This pair breaks with tibblify but has more info. Save for when tibblify works. +# This pair breaks with tibblify but has more info. Save for when tibblify +# works. # test_that("as_rapid() works for yaml urls", { # skip_if_not(Sys.getenv("RAPID_TEST_DL") == "true") From 424c91bf5b732c8c97d3f20cdfc7121041299042 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Tue, 26 Mar 2024 16:58:21 -0500 Subject: [PATCH 4/4] Add pkgdown info. --- _pkgdown.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/_pkgdown.yml b/_pkgdown.yml index 8fbc7ac..1d87f5f 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -48,6 +48,10 @@ reference: - as_oauth2_token_flow - class_scopes - as_scopes + - title: paths class + contents: + - class_paths + - as_paths - title: security class contents: - class_security