[R] Add httr2 support (work in progress) (#13005)

* add httr2 support to r client gen

* fix headers

* add accepts, content-types

* update samples

* fix req

* update samples

* various fixes

* add data file test

* fix streaming, add tests
This commit is contained in:
William Cheng
2022-08-01 00:58:19 +08:00
committed by GitHub
parent 6b6403b2bf
commit 4635dda518
128 changed files with 17842 additions and 24 deletions

View File

@@ -64,6 +64,8 @@ public class RClientCodegen extends DefaultCodegen implements CodegenConfig {
public static final String USE_RLANG_EXCEPTION = "useRlangExceptionHandling";
public static final String DEFAULT = "default";
public static final String RLANG = "rlang";
public static final String HTTR = "httr";
public static final String HTTR2 = "httr2";
// naming convention for operationId (function names in the API)
public static final String OPERATIONID_NAMING = "operationIdNaming";
@@ -199,6 +201,16 @@ public class RClientCodegen extends DefaultCodegen implements CodegenConfig {
cliOptions.add(exceptionPackage);
cliOptions.add(CliOption.newString(CodegenConstants.ERROR_OBJECT_TYPE, "Error object type."));
supportedLibraries.put(HTTR2, "httr2 (https://httr2.r-lib.org/)");
supportedLibraries.put(HTTR, "httr (https://cran.r-project.org/web/packages/httr/index.html)");
CliOption libraryOption = new CliOption(CodegenConstants.LIBRARY, "HTTP library template (sub-template) to use");
libraryOption.setEnum(supportedLibraries);
// set httr as the default
libraryOption.setDefault(HTTR);
cliOptions.add(libraryOption);
setLibrary(HTTR);
}
@Override
@@ -274,6 +286,17 @@ public class RClientCodegen extends DefaultCodegen implements CodegenConfig {
supportingFiles.add(new SupportingFile("r-client.mustache", File.separator + ".github" + File.separator + "workflows", "r-client.yaml"));
supportingFiles.add(new SupportingFile("lintr.mustache", "", ".lintr"));
if (HTTR.equals(getLibrary())) {
// for httr
setLibrary(HTTR);
} else if (HTTR2.equals(getLibrary())) {
// for httr2
setLibrary(HTTR2);
additionalProperties.put("isHttr2", Boolean.TRUE);
} else {
throw new IllegalArgumentException("Invalid HTTP library " + getLibrary() + ". Only httr, httr2 are supported.");
}
// add lambda for mustache templates to fix license field
additionalProperties.put("lambdaLicense", new Mustache.Lambda() {
@Override

View File

@@ -3,7 +3,12 @@
import(R6)
import(jsonlite)
{{^isHttr2}}
import(httr)
{{/isHttr2}}
{{#isHttr2}}
import(httr2)
{{/isHttr2}}
import(base64enc)
import(stringr)

View File

@@ -233,7 +233,7 @@
#' @export
{{{operationId}}}{{WithHttpInfo}} = function({{#requiredParams}}{{paramName}}, {{/requiredParams}}{{#optionalParams}}{{paramName}} = {{^defaultValue}}NULL{{/defaultValue}}{{{defaultValue}}}, {{/optionalParams}}{{#vendorExtensions.x-streaming}}stream_callback = NULL, {{/vendorExtensions.x-streaming}}{{#returnType}}data_file = NULL, {{/returnType}}...) {
args <- list(...)
query_params <- list()
query_params <- c()
header_params <- c()
{{#requiredParams}}
@@ -265,7 +265,12 @@
"{{baseName}}" = {{paramName}}{{^-last}},{{/-last}}
{{/isFile}}
{{#isFile}}
{{^isHttr2}}
"{{baseName}}" = httr::upload_file({{paramName}}){{^-last}},{{/-last}}
{{/isHttr2}}
{{#isHttr2}}
"{{baseName}}" = {{paramName}}{{^-last}},{{/-last}}
{{/isHttr2}}
{{/isFile}}
{{/formParams}}
)

View File

@@ -11,5 +11,5 @@ Encoding: UTF-8
License: {{#lambdaLicense}}{{licenseInfo}}{{/lambdaLicense}}{{^licenseInfo}}Unlicense{{/licenseInfo}}
LazyData: true
Suggests: testthat
Imports: jsonlite, httr, R6, base64enc, stringr
Imports: jsonlite, httr{{#isHttr2}}2{{/isHttr2}}, R6, base64enc, stringr
RoxygenNote: 7.2.0

View File

@@ -0,0 +1,360 @@
{{>partial_header}}
#' ApiClient Class
#'
#' Generic API client for OpenAPI client library builds.
#' OpenAPI generic API client. This client handles the client-
#' server communication, and is invariant across implementations. Specifics of
#' the methods and models for each application are generated from the OpenAPI Generator
#' templates.
#'
#' NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
#' Ref: https://openapi-generator.tech
#' Do not edit the class manually.
#'
#' @docType class
#' @title ApiClient
#' @description ApiClient Class
#' @format An \code{R6Class} generator object
#' @field base_path Base url
#' @field user_agent Default user agent
#' @field default_headers Default headers
#' @field username Username for HTTP basic authentication
#' @field password Password for HTTP basic authentication
#' @field api_keys API keys
#' @field access_token Access token
#' @field bearer_token Bearer token
#' @field timeout Default timeout in seconds
#' @field retry_status_codes vector of status codes to retry
#' @field max_retry_attempts maximum number of retries for the status codes
#' @importFrom httr add_headers accept timeout content
{{#useRlangExceptionHandling}}
#' @importFrom rlang abort
{{/useRlangExceptionHandling}}
#' @export
ApiClient <- R6::R6Class(
"ApiClient",
public = list(
# base path of all requests
base_path = "{{{basePath}}}",
# user agent in the HTTP request
user_agent = "{{{httpUserAgent}}}{{^httpUserAgent}}OpenAPI-Generator/{{{packageVersion}}}/r{{/httpUserAgent}}",
# default headers in the HTTP request
default_headers = NULL,
# username (HTTP basic authentication)
username = NULL,
# password (HTTP basic authentication)
password = NULL,
# API keys
api_keys = NULL,
# Access token
access_token = NULL,
# Bearer token
bearer_token = NULL,
# Time Out (seconds)
timeout = NULL,
# Vector of status codes to retry
retry_status_codes = NULL,
# Maximum number of retry attempts for the retry status codes
max_retry_attempts = NULL,
#' Initialize a new ApiClient.
#'
#' @description
#' Initialize a new ApiClient.
#'
#' @param base_path Base path.
#' @param user_agent User agent.
#' @param default_headers Default headers.
#' @param username User name.
#' @param password Password.
#' @param api_keys API keys.
#' @param access_token Access token.
#' @param bearer_token Bearer token.
#' @param timeout Timeout.
#' @param retry_status_codes Status codes for retry.
#' @param max_retry_attempts Maxmium number of retry.
#' @export
initialize = function(base_path = NULL, user_agent = NULL,
default_headers = NULL,
username = NULL, password = NULL, api_keys = NULL,
access_token = NULL, bearer_token = NULL, timeout = NULL,
retry_status_codes = NULL, max_retry_attempts = NULL) {
if (!is.null(base_path)) {
self$base_path <- base_path
}
if (!is.null(default_headers)) {
self$default_headers <- default_headers
}
if (!is.null(username)) {
self$username <- username
}
if (!is.null(password)) {
self$password <- password
}
if (!is.null(access_token)) {
self$access_token <- access_token
}
if (!is.null(bearer_token)) {
self$bearer_token <- bearer_token
}
if (!is.null(api_keys)) {
self$api_keys <- api_keys
} else {
self$api_keys <- list()
}
if (!is.null(user_agent)) {
self$`user_agent` <- user_agent
}
if (!is.null(timeout)) {
self$timeout <- timeout
}
if (!is.null(retry_status_codes)) {
self$retry_status_codes <- retry_status_codes
}
if (!is.null(max_retry_attempts)) {
self$max_retry_attempts <- max_retry_attempts
}
},
#' Prepare to make an API call with the retry logic.
#'
#' @description
#' Prepare to make an API call with the retry logic.
#'
#' @param url URL.
#' @param method HTTP method.
#' @param query_params The query parameters.
#' @param header_params The header parameters.
#' @param body The HTTP request body.
#' @param stream_callback Callback function to process the data stream
#' @param ... Other optional arguments.
#' @return HTTP response
#' @export
CallApi = function(url, method, query_params, header_params, accepts,
content_types, body, stream_callback = NULL, ...) {
resp <- self$Execute(url, method, query_params, header_params,
accepts, content_types,
body, stream_callback = stream_callback, ...)
#status_code <- httr::status_code(resp)
#if (is.null(self$max_retry_attempts)) {
# self$req_retry(max_tries <- max_retry_attempts)
#}
#if (!is.null(self$retry_status_codes)) {
# for (i in 1 : self$max_retry_attempts) {
# if (status_code %in% self$retry_status_codes) {
# Sys.sleep((2 ^ i) + stats::runif(n = 1, min = 0, max = 1))
# resp <- self$Execute(url, method, query_params, header_params, body, stream_callback = stream_callback, ...)
# status_code <- httr::status_code(resp)
# } else {
# break
# }
# }
#}
resp
},
#' Make an API call
#'
#' @description
#' Make an API call
#'
#' @param url URL.
#' @param method HTTP method.
#' @param query_params The query parameters.
#' @param header_params The header parameters.
#' @param body The HTTP request body.
#' @param stream_callback Callback function to process data stream
#' @param ... Other optional arguments.
#' @return HTTP response
#' @export
Execute = function(url, method, query_params, header_params, accepts, content_types,
body, stream_callback = NULL, ...) {
req <- request(url)
## add headers and default headers
if (!is.null(header_params) && length(header_params) != 0) {
for (http_header in names(header_params)) {
req <- req %>% req_headers(http_header = header_params[http_header])
}
}
if (!is.null(self$default_headers) && length(self$default_headers) != 0) {
for (http_header in names(header_params)) {
req <- req %>% req_headers(http_header = self$default_headers[http_header])
}
}
# set HTTP accept header
accept = self$select_header(accepts)
if (!is.null(accept)) {
req <- req %>% req_headers("Accept" = accept)
}
# set HTTP content-type header
content_type = self$select_header(content_types)
if (!is.null(content_type)) {
req <- req %>% req_headers("Content-Type" = content_type)
}
## add query parameters
if (length(query_params) != 0) {
for (query_param in names(query_params)) {
req <- req %>% req_url_query(query_param = query_params[query_param])
}
}
# add body parameters
if (!is.null(body)) {
req <- req %>% req_body_raw(body)
}
# set timeout
{{! Adding timeout that can be set at the apiClient object level}}
if (!is.null(self$timeout)) {
req <- req %>% req_timeout(self$timeout)
}
# set retry
if (!is.null(self$max_retry_attempts)) {
req <- req %>% retry_max_tries(self$timeout)
req <- req %>% retry_max_seconds(self$timeout)
}
# set user agent
if (!is.null(self$user_agent)) {
req <- req %>% req_user_agent(self$user_agent)
}
# set HTTP verb
req <- req %>% req_method(method)
# stream data
if (typeof(stream_callback) == "closure") {
req %>% req_stream(stream_callback)
} else {
# perform the HTTP request
resp <- req %>%
req_error(is_error = function(resp) FALSE) %>%
req_perform()
# return ApiResponse
api_response <- ApiResponse$new()
api_response$status_code <- resp %>% resp_status()
api_response$status_code_desc <- resp %>% resp_status_desc()
api_response$response <- resp %>% resp_body_string()
api_response$headers <- resp %>% resp_headers()
api_response
}
},
#' Deserialize the content of API response to the given type.
#'
#' @description
#' Deserialize the content of API response to the given type.
#'
#' @param raw_response Raw response.
#' @param return_type R return type.
#' @param pkg_env Package environment.
#' @return Deserialized object.
#' @export
deserialize = function(raw_response, return_type, pkg_env) {
resp_obj <- jsonlite::fromJSON(raw_response)
self$deserializeObj(resp_obj, return_type, pkg_env)
},
#' Deserialize the response from jsonlite object based on the given type
#'
#' @description
#' Deserialize the response from jsonlite object based on the given type
#' by handling complex and nested types by iterating recursively
#' Example return_types will be like "array[integer]", "map(Pet)", "array[map(Tag)]", etc.,
#'
#' @param obj Response object.
#' @param return_type R return type.
#' @param pkg_env Package environment.
#' @return Deserialized object.
#' @export
deserializeObj = function(obj, return_type, pkg_env) {
return_obj <- NULL
primitive_types <- c("character", "numeric", "integer", "logical", "complex")
# To handle the "map" type
if (startsWith(return_type, "map(")) {
inner_return_type <- regmatches(return_type,
regexec(pattern = "map\\((.*)\\)", return_type))[[1]][2]
return_obj <- lapply(names(obj), function(name) {
self$deserializeObj(obj[[name]], inner_return_type, pkg_env)
})
names(return_obj) <- names(obj)
} else if (startsWith(return_type, "array[")) {
# To handle the "array" type
inner_return_type <- regmatches(return_type,
regexec(pattern = "array\\[(.*)\\]", return_type))[[1]][2]
if (c(inner_return_type) %in% primitive_types) {
return_obj <- vector("list", length = length(obj))
if (length(obj) > 0) {
for (row in 1:length(obj)) {
return_obj[[row]] <- self$deserializeObj(obj[row], inner_return_type, pkg_env)
}
}
} else {
if (!is.null(nrow(obj))) {
return_obj <- vector("list", length = nrow(obj))
if (nrow(obj) > 0) {
for (row in 1:nrow(obj)) {
return_obj[[row]] <- self$deserializeObj(obj[row, , drop = FALSE],
inner_return_type, pkg_env)
}
}
}
}
} else if (exists(return_type, pkg_env) && !(c(return_type) %in% primitive_types)) {
# To handle model objects which are not array or map containers. Ex:"Pet"
return_type <- get(return_type, envir = as.environment(pkg_env))
return_obj <- return_type$new()
return_obj$fromJSON(
jsonlite::toJSON(obj, digits = NA, auto_unbox = TRUE)
)
} else {
# To handle primitive type
return_obj <- obj
}
return_obj
},
#' Return a propery header (for accept or content-type). If JSON-related MIME is found,
#' return it. Otherwise, return the first one, if any.
#'
#' @description
#' Return a propery header (for accept or content-type). If JSON-related MIME is found,
#' return it. Otherwise, return the first one, if any.
#'
#' @param headers A list of headers
#' @return A header (e.g. 'application/json')
#' @export
select_header = function(headers) {
if (length(headers) == 0) {
return(invisible(NULL))
} else {
for (header in headers) {
if (str_detect(header, "(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$")) {
# return JSON-related MIME
return(header)
}
}
# not json mime type, simply return the first one
return(headers[1])
}
}
)
)