From b46cd0a100ffe56efa1791e55fb94dc9ac940ceb Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Thu, 20 Jun 2024 10:35:07 +0100 Subject: [PATCH 1/3] promises implementation --- DESCRIPTION | 4 +- NAMESPACE | 2 + NEWS.md | 5 ++- R/promises.R | 87 ++++++++++++++++++++++++++++++++++++++ man/as.promise.ncurlAio.Rd | 43 +++++++++++++++++++ 5 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 R/promises.R create mode 100644 man/as.promise.ncurlAio.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 1fb422452..cccd04b44 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: nanonext Type: Package Title: NNG (Nanomsg Next Gen) Lightweight Messaging Library -Version: 1.1.0.9003 +Version: 1.1.0.9004 Description: R binding for NNG (Nanomsg Next Gen), a successor to ZeroMQ. NNG is a socket library implementing 'Scalability Protocols', a reliable, high-performance standard for common communications patterns including @@ -30,6 +30,8 @@ SystemRequirements: 'libnng' >= 1.6 and 'libmbedtls' >= 2.5, or 'cmake' and 'xz' to compile NNG and/or Mbed TLS included in package sources Depends: R (>= 3.5) +Enhances: + promises Suggests: knitr, later, diff --git a/NAMESPACE b/NAMESPACE index 97d1a1a78..9793d3875 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -35,6 +35,8 @@ S3method(print,recvAio) S3method(print,sendAio) S3method(print,tlsConfig) S3method(print,unresolvedValue) +S3method(promises::as.promise,ncurlAio) +S3method(promises::is.promising,ncurlAio) S3method(start,nanoDialer) S3method(start,nanoListener) export("%~>%") diff --git a/NEWS.md b/NEWS.md index 99740166e..e01d44c84 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,8 +1,9 @@ -# nanonext 1.1.0.9003 (development) +# nanonext 1.1.0.9004 (development) #### New Features -* The method `x[]` for an Aio `x` is a new equivalent to `collect_aio_(x)`, which waits for and collects the data. +* Adds 'ncurlAio' method for `promises::as.promise()` and `promises::is.promising()` to enable 'ncurlAio' promises. +* Adds method `x[]` for an Aio `x` as a new equivalent to `collect_aio_(x)`, which waits for and collects the data. #### Updates diff --git a/R/promises.R b/R/promises.R new file mode 100644 index 000000000..2e3ffcc87 --- /dev/null +++ b/R/promises.R @@ -0,0 +1,87 @@ +# Copyright (C) 2024 Hibiki AI Limited +# +# This file is part of nanonext. +# +# nanonext is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# nanonext is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# nanonext. If not, see . + +# ncurlAio promises ------------------------------------------------------------ + +#' Make ncurl Promise +#' +#' Creates a \sQuote{promise} from an \sQuote{ncurlAio} object. +#' +#' @param x an object of class \sQuote{ncurlAio}. +#' +#' @return A \sQuote{promise} object. +#' +#' @details This function is an S3 method for the generic \code{as.promise} for +#' class \sQuote{ncurlAio}. +#' +#' Requires the \CRANpkg{promises} package. +#' +#' Allows an \sQuote{ncurlAio} to be used with the promise pipe +#' \code{\%...>\%}, which schedules a function to run upon resolution of the +#' Aio. +#' +#' @examples +#' if (interactive() && requireNamespace("promises", quietly = TRUE)) { +#' +#' library(promises) +#' +#' p <- as.promise(ncurl_aio("https://www.cam.ac.uk/")) +#' print(p) +#' is.promise(p) +#' +#' p2 <- ncurl_aio("https://postman-echo.com/get") %...>% identity() +#' p2$then(cat) +#' is.promise(p2) +#' +#' } +#' +#' @exportS3Method promises::as.promise +#' +as.promise.ncurlAio <- function(x) { + + promise <- .subset2(x, "promise") + + if (is.null(promise)) { + + if (unresolved(x)) { + promise <- promises::then( + promises::promise( + function(resolve, reject) + context <- set_promise_context(x, environment()) + ), + onFulfilled = function(value) + if (value != 200L) + stop(if (value < 100) nng_error(value) else status_code(value)) else + .subset2(x, "value") + ) + } else { + value <- .subset2(x, "result") + promise <- if (value != 200L) + promises::promise_reject(if (value < 100) nng_error(value) else status_code(value)) else + promises::promise_resolve(.subset2(x, "value")) + } + + assign("promise", promise, x) + + } + + promise + +} + +#' @exportS3Method promises::is.promising +#' +is.promising.ncurlAio <- function(x) TRUE diff --git a/man/as.promise.ncurlAio.Rd b/man/as.promise.ncurlAio.Rd new file mode 100644 index 000000000..90b14026f --- /dev/null +++ b/man/as.promise.ncurlAio.Rd @@ -0,0 +1,43 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/promises.R +\name{as.promise.ncurlAio} +\alias{as.promise.ncurlAio} +\title{Make ncurl Promise} +\usage{ +\method{as.promise}{ncurlAio}(x) +} +\arguments{ +\item{x}{an object of class \sQuote{ncurlAio}.} +} +\value{ +A \sQuote{promise} object. +} +\description{ +Creates a \sQuote{promise} from an \sQuote{ncurlAio} object. +} +\details{ +This function is an S3 method for the generic \code{as.promise} for + class \sQuote{ncurlAio}. + + Requires the \CRANpkg{promises} package. + + Allows an \sQuote{ncurlAio} to be used with the promise pipe + \code{\%...>\%}, which schedules a function to run upon resolution of the + Aio. +} +\examples{ +if (interactive() && requireNamespace("promises", quietly = TRUE)) { + +library(promises) + +p <- as.promise(ncurl_aio("https://www.cam.ac.uk/")) +print(p) +is.promise(p) + +p2 <- ncurl_aio("https://postman-echo.com/get") \%...>\% identity() +p2$then(cat) +is.promise(p2) + +} + +} From 0e1163663dfc88be8b1694e56ff972acf3dd8661 Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:20:28 +0100 Subject: [PATCH 2/3] document promises --- R/ncurl.R | 79 ++++++++++++++++++++++++++++++++++ R/promises.R | 87 -------------------------------------- man/as.promise.ncurlAio.Rd | 18 +------- man/ncurl_aio.Rd | 25 +++++++++++ 4 files changed, 105 insertions(+), 104 deletions(-) delete mode 100644 R/promises.R diff --git a/R/ncurl.R b/R/ncurl.R index f06c8aecd..432ddb3cf 100644 --- a/R/ncurl.R +++ b/R/ncurl.R @@ -108,6 +108,18 @@ ncurl <- function(url, #' save as a file). #' } #' +#' @section Promises: +#' +#' \sQuote{ncurlAio} may be used anywhere that accepts a \sQuote{promise} +#' from the \CRANpkg{promises} package through the included +#' \code{as.promise} method. +#' +#' The promises created are completely event-driven and non-polling. +#' +#' If a status code of 200 (OK) is returned then the promise is resolved +#' with the reponse body, otherwise it is rejected with a translation of the +#' status code or \sQuote{errorValue} as the case may be. +#' #' @seealso \code{\link{ncurl_session}} for persistent connections. #' @examples #' nc <- ncurl_aio("https://www.r-project.org/", @@ -118,6 +130,17 @@ ncurl <- function(url, #' nc$headers #' nc$data #' +#' if (interactive() && requireNamespace("promises", quietly = TRUE)) { +#' library(promises) +#' +#' p <- as.promise(ncurl_aio("https://www.cam.ac.uk/")) +#' print(p) +#' +#' p2 <- ncurl_aio("https://postman-echo.com/get") %...>% identity() +#' p2$then(cat) +#' is.promise(p2) +#' } +#' #' @export #' ncurl_aio <- function(url, @@ -189,3 +212,59 @@ transact <- function(session) .Call(rnng_ncurl_transact, session) #' @export #' close.ncurlSession <- function(con, ...) invisible(.Call(rnng_ncurl_session_close, con)) + +#' Make ncurl Promise +#' +#' Creates a \sQuote{promise} from an \sQuote{ncurlAio} object. +#' +#' @param x an object of class \sQuote{ncurlAio}. +#' +#' @return A \sQuote{promise} object. +#' +#' @details This function is an S3 method for the generic \code{as.promise} for +#' class \sQuote{ncurlAio}. +#' +#' Requires the \CRANpkg{promises} package. +#' +#' Allows an \sQuote{ncurlAio} to be used with the promise pipe +#' \code{\%...>\%}, which schedules a function to run upon resolution of the +#' Aio. +#' +#' @exportS3Method promises::as.promise +#' +as.promise.ncurlAio <- function(x) { + + promise <- .subset2(x, "promise") + + if (is.null(promise)) { + + if (unresolved(x)) { + promise <- promises::then( + promises::promise( + function(resolve, reject) + context <- set_promise_context(x, environment()) + ), + onFulfilled = function(value) + if (value != 200L) + stop(if (value < 100) nng_error(value) else status_code(value)) else + .subset2(x, "value") + ) + } else { + value <- .subset2(x, "result") + promise <- if (value != 200L) + promises::promise_reject(if (value < 100) nng_error(value) else status_code(value)) else + promises::promise_resolve(.subset2(x, "value")) + } + + assign("promise", promise, x) + + } + + promise + +} + +#' @exportS3Method promises::is.promising +#' +is.promising.ncurlAio <- function(x) TRUE + diff --git a/R/promises.R b/R/promises.R deleted file mode 100644 index 2e3ffcc87..000000000 --- a/R/promises.R +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (C) 2024 Hibiki AI Limited -# -# This file is part of nanonext. -# -# nanonext is free software: you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# nanonext is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with -# nanonext. If not, see . - -# ncurlAio promises ------------------------------------------------------------ - -#' Make ncurl Promise -#' -#' Creates a \sQuote{promise} from an \sQuote{ncurlAio} object. -#' -#' @param x an object of class \sQuote{ncurlAio}. -#' -#' @return A \sQuote{promise} object. -#' -#' @details This function is an S3 method for the generic \code{as.promise} for -#' class \sQuote{ncurlAio}. -#' -#' Requires the \CRANpkg{promises} package. -#' -#' Allows an \sQuote{ncurlAio} to be used with the promise pipe -#' \code{\%...>\%}, which schedules a function to run upon resolution of the -#' Aio. -#' -#' @examples -#' if (interactive() && requireNamespace("promises", quietly = TRUE)) { -#' -#' library(promises) -#' -#' p <- as.promise(ncurl_aio("https://www.cam.ac.uk/")) -#' print(p) -#' is.promise(p) -#' -#' p2 <- ncurl_aio("https://postman-echo.com/get") %...>% identity() -#' p2$then(cat) -#' is.promise(p2) -#' -#' } -#' -#' @exportS3Method promises::as.promise -#' -as.promise.ncurlAio <- function(x) { - - promise <- .subset2(x, "promise") - - if (is.null(promise)) { - - if (unresolved(x)) { - promise <- promises::then( - promises::promise( - function(resolve, reject) - context <- set_promise_context(x, environment()) - ), - onFulfilled = function(value) - if (value != 200L) - stop(if (value < 100) nng_error(value) else status_code(value)) else - .subset2(x, "value") - ) - } else { - value <- .subset2(x, "result") - promise <- if (value != 200L) - promises::promise_reject(if (value < 100) nng_error(value) else status_code(value)) else - promises::promise_resolve(.subset2(x, "value")) - } - - assign("promise", promise, x) - - } - - promise - -} - -#' @exportS3Method promises::is.promising -#' -is.promising.ncurlAio <- function(x) TRUE diff --git a/man/as.promise.ncurlAio.Rd b/man/as.promise.ncurlAio.Rd index 90b14026f..72d57ccef 100644 --- a/man/as.promise.ncurlAio.Rd +++ b/man/as.promise.ncurlAio.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/promises.R +% Please edit documentation in R/ncurl.R \name{as.promise.ncurlAio} \alias{as.promise.ncurlAio} \title{Make ncurl Promise} @@ -25,19 +25,3 @@ This function is an S3 method for the generic \code{as.promise} for \code{\%...>\%}, which schedules a function to run upon resolution of the Aio. } -\examples{ -if (interactive() && requireNamespace("promises", quietly = TRUE)) { - -library(promises) - -p <- as.promise(ncurl_aio("https://www.cam.ac.uk/")) -print(p) -is.promise(p) - -p2 <- ncurl_aio("https://postman-echo.com/get") \%...>\% identity() -p2$then(cat) -is.promise(p2) - -} - -} diff --git a/man/ncurl_aio.Rd b/man/ncurl_aio.Rd index e1260d641..e2e7a48b3 100644 --- a/man/ncurl_aio.Rd +++ b/man/ncurl_aio.Rd @@ -61,6 +61,20 @@ An 'ncurlAio' (object of class 'ncurlAio' and 'recvAio') (invisibly). \description{ nano cURL - a minimalist http(s) client - async edition. } +\section{Promises}{ + + + \sQuote{ncurlAio} may be used anywhere that accepts a \sQuote{promise} + from the \CRANpkg{promises} package through the included + \code{as.promise} method. + + The promises created are completely event-driven and non-polling. + + If a status code of 200 (OK) is returned then the promise is resolved + with the reponse body, otherwise it is rejected with a translation of the + status code or \sQuote{errorValue} as the case may be. +} + \examples{ nc <- ncurl_aio("https://www.r-project.org/", response = c("date", "server"), @@ -70,6 +84,17 @@ nc$status nc$headers nc$data +if (interactive() && requireNamespace("promises", quietly = TRUE)) { +library(promises) + +p <- as.promise(ncurl_aio("https://www.cam.ac.uk/")) +print(p) + +p2 <- ncurl_aio("https://postman-echo.com/get") \%...>\% identity() +p2$then(cat) +is.promise(p2) +} + } \seealso{ \code{\link{ncurl_session}} for persistent connections. From 7d1d859b27177a75f5ea294fd4d98b6e5fcc1142 Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:22:52 +0100 Subject: [PATCH 3/3] add tests & improve error handling --- R/ncurl.R | 9 ++++----- man/as.promise.ncurlAio.Rd | 2 +- man/ncurl_aio.Rd | 7 +++---- src/utils.c | 20 ++++++++++++-------- tests/tests.R | 6 ++++++ 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/R/ncurl.R b/R/ncurl.R index 432ddb3cf..9504c2716 100644 --- a/R/ncurl.R +++ b/R/ncurl.R @@ -131,14 +131,13 @@ ncurl <- function(url, #' nc$data #' #' if (interactive() && requireNamespace("promises", quietly = TRUE)) { -#' library(promises) #' -#' p <- as.promise(ncurl_aio("https://www.cam.ac.uk/")) +#' p <- as.promise(nc) #' print(p) #' -#' p2 <- ncurl_aio("https://postman-echo.com/get") %...>% identity() -#' p2$then(cat) +#' p2 <- ncurl_aio("https://postman-echo.com/get") %...>% cat #' is.promise(p2) +#' #' } #' #' @export @@ -224,7 +223,7 @@ close.ncurlSession <- function(con, ...) invisible(.Call(rnng_ncurl_session_clos #' @details This function is an S3 method for the generic \code{as.promise} for #' class \sQuote{ncurlAio}. #' -#' Requires the \CRANpkg{promises} package. +#' Requires the \pkg{promises} package. #' #' Allows an \sQuote{ncurlAio} to be used with the promise pipe #' \code{\%...>\%}, which schedules a function to run upon resolution of the diff --git a/man/as.promise.ncurlAio.Rd b/man/as.promise.ncurlAio.Rd index 72d57ccef..d9acf5c42 100644 --- a/man/as.promise.ncurlAio.Rd +++ b/man/as.promise.ncurlAio.Rd @@ -19,7 +19,7 @@ Creates a \sQuote{promise} from an \sQuote{ncurlAio} object. This function is an S3 method for the generic \code{as.promise} for class \sQuote{ncurlAio}. - Requires the \CRANpkg{promises} package. + Requires the \pkg{promises} package. Allows an \sQuote{ncurlAio} to be used with the promise pipe \code{\%...>\%}, which schedules a function to run upon resolution of the diff --git a/man/ncurl_aio.Rd b/man/ncurl_aio.Rd index e2e7a48b3..2e843f6e6 100644 --- a/man/ncurl_aio.Rd +++ b/man/ncurl_aio.Rd @@ -85,14 +85,13 @@ nc$headers nc$data if (interactive() && requireNamespace("promises", quietly = TRUE)) { -library(promises) -p <- as.promise(ncurl_aio("https://www.cam.ac.uk/")) +p <- as.promise(nc) print(p) -p2 <- ncurl_aio("https://postman-echo.com/get") \%...>\% identity() -p2$then(cat) +p2 <- ncurl_aio("https://postman-echo.com/get") \%...>\% cat is.promise(p2) + } } diff --git a/src/utils.c b/src/utils.c index 3c627db5b..9024e48dc 100644 --- a/src/utils.c +++ b/src/utils.c @@ -25,15 +25,19 @@ SEXP mk_error_ncurl(const int xc) { - const char *names[] = {"status", "headers", "data", ""}; - SEXP out = PROTECT(Rf_mkNamed(VECSXP, names)); - SEXP err = Rf_ScalarInteger(xc); + SEXP env; + PROTECT(env = Rf_allocSExp(ENVSXP)); + NANO_CLASS2(env, "ncurlAio", "recvAio"); + SEXP err = PROTECT(Rf_ScalarInteger(xc)); Rf_classgets(err, nano_error); - SET_VECTOR_ELT(out, 0, err); - SET_VECTOR_ELT(out, 1, err); - SET_VECTOR_ELT(out, 2, err); - UNPROTECT(1); - return out; + Rf_defineVar(nano_ResultSymbol, err, env); + Rf_defineVar(nano_StatusSymbol, err, env); + Rf_defineVar(nano_ProtocolSymbol, err, env); + Rf_defineVar(nano_HeadersSymbol, err, env); + Rf_defineVar(nano_ValueSymbol, err, env); + Rf_defineVar(nano_DataSymbol, err, env); + UNPROTECT(2); + return env; } diff --git a/tests/tests.R b/tests/tests.R index 123708404..c708bf897 100644 --- a/tests/tests.R +++ b/tests/tests.R @@ -562,6 +562,12 @@ nanotestw(dial(s, url = "tls+tcp://.", tls = tls, error = FALSE) > 0) nanotestw(listen(s, url = "tls+tcp://.", tls = tls, error = FALSE) > 0) nanotestz(close(s1)) nanotestz(close(s)) +if (requireNamespace("promises", quietly = TRUE)) { + nanotestaio(n <- ncurl_aio("https://postman-echo.com/get")) + nanotest(tryCatch(promises::is.promise(promises::then(n, cat)), error = function(e) TRUE)) + nanotest(promises::is.promise(promises::as.promise(call_aio(n)))) + later::run_now() +} if (Sys.info()[["sysname"]] == "Linux") { rm(list = ls()) gc()