From 5e84656a89dec1ee5557dd905f22382836b9da5c Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Mon, 20 Jan 2025 21:13:21 +1300 Subject: [PATCH] docs --- R/compile.R | 54 +++++++++++++++++++-------------- R/types.R | 50 +++++++++++++++++++++++++++++- README.Rmd | 10 ------ man/ts_character.Rd | 18 +++++++++++ man/ts_compile.Rd | 23 ++++++++++++++ man/ts_dataframe.Rd | 6 ++++ man/ts_factor.Rd | 3 ++ man/ts_integer.Rd | 18 +++++++++++ man/ts_list.Rd | 3 ++ man/ts_logical.Rd | 18 +++++++++++ man/ts_null.Rd | 14 +++++++++ man/ts_numeric.Rd | 18 +++++++++++ man/ts_void.Rd | 15 +++++++++ tests/testthat/app.R | 4 +-- tests/testthat/app.d.ts | 17 ++++++++--- tests/testthat/functions.R | 14 --------- tests/testthat/test-compile.R | 44 +++++++++++++++++++++++++++ tests/testthat/test-functions.R | 4 --- 18 files changed, 276 insertions(+), 57 deletions(-) create mode 100644 man/ts_character.Rd create mode 100644 man/ts_compile.Rd create mode 100644 man/ts_integer.Rd create mode 100644 man/ts_logical.Rd create mode 100644 man/ts_null.Rd create mode 100644 man/ts_numeric.Rd create mode 100644 man/ts_void.Rd delete mode 100644 tests/testthat/functions.R create mode 100644 tests/testthat/test-compile.R diff --git a/R/compile.R b/R/compile.R index 9363d6d..5402dfb 100644 --- a/R/compile.R +++ b/R/compile.R @@ -1,17 +1,27 @@ +#' Compile R functions to TypeScript schemas +#' @param f A function or file path +#' @param name The name of the function +#' @param ... Additional arguments +#' @param file The file path to write the TypeScript schema (optional). If `""`, the output is printed to the standard output console (see `cat`). +#' @return Character vector of TypeScript schema, or NULL if writing to file +#' @md #' @export -ts_compile <- function(f, ..., file = NULL) { +ts_compile <- function(f, ..., name, file) { o <- UseMethod("ts_compile") } #' @export -ts_compile.ts_function <- function(f, name = deparse(substitute(f)), ...) { - inputs <- attr(f, "args") - result <- attr(f, "result") +ts_compile.ts_function <- function(f, ..., name = deparse(substitute(f))) { + inputs <- f$args + result <- f$result - inputs <- sapply(inputs, \(x) x$zod_type) - fn_args <- paste(inputs) |> - paste(collapse = ", ") - sprintf("const %s = R.ocap([%s], %s]);", name, fn_args, result$r_type) + inputs <- sapply(inputs, \(x) x$input_type) + fn_args <- paste(paste(inputs), collapse = ", ") + + sprintf( + "const %s = Robj.ocap([%s], %s);", name, fn_args, + result$return_type + ) } #' @export @@ -32,22 +42,22 @@ ts_compile.character <- function( x <- sapply(ls(e), \(x) ts_compile(e[[x]], file = file, name = x)) # find any RTYPE.[type] and grab types - types <- unique( - gsub( - "RTYPE\\.(\\w+)", "\\1", - unlist(regmatches(x, gregexpr("RTYPE\\.\\w+", x))) - ) - ) - x <- gsub("RTYPE\\.", "", x) + # types <- unique( + # gsub( + # "RTYPE\\.(\\w+)", "\\1", + # unlist(regmatches(x, gregexpr("RTYPE\\.\\w+", x))) + # ) + # ) + # x <- gsub("RTYPE\\.", "", x) - cat( - sprintf( - "import { %s } from 'rserve-ts';\n\n", - paste(types, collapse = ", ") - ), - file = file + src <- c( + "import { Robj } from 'rserve-ts';", + "import { z } from 'zod';", + "\n", + x ) - cat(x, sep = "\n", file = file, append = TRUE) + + writeLines(src, file) invisible() } diff --git a/R/types.R b/R/types.R index ddf9182..4485440 100644 --- a/R/types.R +++ b/R/types.R @@ -136,7 +136,15 @@ n_type_fun <- function(n, type) { sprintf("%s(%s)", type, ifelse(n < 0, "", n)) } +#' Logical or boolean type +#' +#' Booleans are represented in Zod schema as either a boolean (`z.boolean()`), +#' or a typed Uint8Array (`z.instanceof(Uint8Array)`). +#' +#' @param n The length of the boolean vector. If `n = 1` then a single boolean is expected. If `n = 0` then any length is expected. If `n > 1` then a boolean vector of length `n` is expected. +#' @return A ts object that accepts logical scalars or vectors of length `n`. #' @export +#' @md ts_logical <- function(n = -1L) { ts_object( n_type(n, "z.boolean()"), @@ -151,10 +159,18 @@ ts_logical <- function(n = -1L) { ) } +#' Integer type +#' +#' Integers are represented in Zod schema as either a number (`z.number()`), +#' or a Int32Array (`z.instanceof(Int32Array)`). +#' +#' @param n The length of the integer vector. If `n = 1` then a single integer is expected. If `n = 0` then any length is expected. If `n > 1` then an integer vector of length `n` is expected. +#' @return A ts object that accepts integer scalars or vectors of length `n`. #' @export +#' @md ts_integer <- function(n = -1L) { ts_object( - n_type(n, "z.number()"), + n_type(n, "z.number()", "z.instanceof(Int32Array)"), n_type_fun(n, "Robj.integer"), check = function(x) { if (!is.integer(x)) stop("Expected an integer") @@ -166,7 +182,15 @@ ts_integer <- function(n = -1L) { ) } +#' Numeric type +#' +#' Numbers are represented in Zod schema as either a number (`z.number()`), +#' or a Float64Array (`z.instanceof(Float64Array)`). +#' +#' @param n The length of the numeric vector. If `n = 1` then a single number is expected. If `n = 0` then any length is expected. If `n > 1` then a numeric vector of length `n` is expected. +#' @return A ts object that accepts numeric scalars or vectors of length `n`. #' @export +#' @md ts_numeric <- function(n = -1L) { ts_object( n_type(n, "z.number()"), @@ -181,7 +205,14 @@ ts_numeric <- function(n = -1L) { ) } +#' Character or string type +#' +#' Strings are represented in Zod schema as either a string (`z.string()`), +#' or a string array (`z.array(z.string())`). +#' @param n The length of the string vector. If `n = 1` then a single string is expected. If `n = 0` then any length is expected. If `n > 1` then a string vector of length `n` is expected. +#' @return A ts object that accepts strings or string vectors of length `n`. #' @export +#' @md ts_character <- function(n = -1L) { ts_object( n_type(n, "z.string()"), @@ -203,6 +234,7 @@ vector_as_ts_array <- function(x) { #' Factors are integers with labels. On the JS side, these are *always* represented as a string array (even if only one value - yay!). #' #' @param levels A character vector of levels (optional). +#' @return A ts object that accepts factors with the specified levels. #' #' @export #' @md @@ -236,6 +268,7 @@ ts_factor <- function(levels = NULL) { #' #' A list is a vector of other robjects, which may or may not be named. #' @param ... A list of types, named or unnamed. +#' @return A ts object that accepts lists with the specified types. #' #' @export #' @md @@ -292,6 +325,9 @@ ts_list <- function(...) { #' #' This is essentially a list, but the elements must have names and are all the same length. #' +#' @param ... Named types. +#' @return A ts object that accepts data frames with the specified types. +#' #' @export #' @md ts_dataframe <- function(...) { @@ -323,7 +359,13 @@ ts_dataframe <- function(...) { ) } +#' Null type +#' +#' This is a type that only accepts `NULL`. +#' +#' @return A ts object that only accepts `NULL`. #' @export +#' ts_null <- function() { ts_object( "z.null()", @@ -335,7 +377,13 @@ ts_null <- function() { ) } +#' Void type +#' +#' This is a type that accepts null values (this would typically be used for +#' functions that return nothing). +#' @return A ts object that accepts `NULL`. #' @export +#' @md ts_void <- function() { ts_object( "z.void()", diff --git a/README.Rmd b/README.Rmd index e0725f2..ce68f40 100644 --- a/README.Rmd +++ b/README.Rmd @@ -107,13 +107,3 @@ cat(readLines("tests/testthat/app.R"), sep = "\n") ts_compile("tests/testthat/app.R", file = "") ``` - -## TODO - -- [ ] Add support for more types -- [ ] Allow generic types (e.g., `(x: T) => T`) -- [ ] Add support for conditional return types - - e.g., `const sample = (x: T[], n: N) => N extends 1 ? T : T[]` - -- [ ] Function overloads? Perhaps just a wrapper around several function definitions... diff --git a/man/ts_character.Rd b/man/ts_character.Rd new file mode 100644 index 0000000..f18322f --- /dev/null +++ b/man/ts_character.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/types.R +\name{ts_character} +\alias{ts_character} +\title{Character or string type} +\usage{ +ts_character(n = -1L) +} +\arguments{ +\item{n}{The length of the string vector. If \code{n = 1} then a single string is expected. If \code{n = 0} then any length is expected. If \code{n > 1} then a string vector of length \code{n} is expected.} +} +\value{ +A ts object that accepts strings or string vectors of length \code{n}. +} +\description{ +Strings are represented in Zod schema as either a string (\code{z.string()}), +or a string array (\code{z.array(z.string())}). +} diff --git a/man/ts_compile.Rd b/man/ts_compile.Rd new file mode 100644 index 0000000..4a00147 --- /dev/null +++ b/man/ts_compile.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/compile.R +\name{ts_compile} +\alias{ts_compile} +\title{Compile R functions to TypeScript schemas} +\usage{ +ts_compile(f, ..., name, file) +} +\arguments{ +\item{f}{A function or file path} + +\item{...}{Additional arguments} + +\item{name}{The name of the function} + +\item{file}{The file path to write the TypeScript schema (optional). If \code{""}, the output is printed to the standard output console (see \code{cat}).} +} +\value{ +Character vector of TypeScript schema, or NULL if writing to file +} +\description{ +Compile R functions to TypeScript schemas +} diff --git a/man/ts_dataframe.Rd b/man/ts_dataframe.Rd index 7b5dc44..eef97b1 100644 --- a/man/ts_dataframe.Rd +++ b/man/ts_dataframe.Rd @@ -6,6 +6,12 @@ \usage{ ts_dataframe(...) } +\arguments{ +\item{...}{Named types.} +} +\value{ +A ts object that accepts data frames with the specified types. +} \description{ This is essentially a list, but the elements must have names and are all the same length. } diff --git a/man/ts_factor.Rd b/man/ts_factor.Rd index 1085832..eedbf24 100644 --- a/man/ts_factor.Rd +++ b/man/ts_factor.Rd @@ -9,6 +9,9 @@ ts_factor(levels = NULL) \arguments{ \item{levels}{A character vector of levels (optional).} } +\value{ +A ts object that accepts factors with the specified levels. +} \description{ Factors are integers with labels. On the JS side, these are \emph{always} represented as a string array (even if only one value - yay!). } diff --git a/man/ts_integer.Rd b/man/ts_integer.Rd new file mode 100644 index 0000000..77a5063 --- /dev/null +++ b/man/ts_integer.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/types.R +\name{ts_integer} +\alias{ts_integer} +\title{Integer type} +\usage{ +ts_integer(n = -1L) +} +\arguments{ +\item{n}{The length of the integer vector. If \code{n = 1} then a single integer is expected. If \code{n = 0} then any length is expected. If \code{n > 1} then an integer vector of length \code{n} is expected.} +} +\value{ +A ts object that accepts integer scalars or vectors of length \code{n}. +} +\description{ +Integers are represented in Zod schema as either a number (\code{z.number()}), +or a Int32Array (\code{z.instanceof(Int32Array)}). +} diff --git a/man/ts_list.Rd b/man/ts_list.Rd index dd6cb7a..7bfab76 100644 --- a/man/ts_list.Rd +++ b/man/ts_list.Rd @@ -9,6 +9,9 @@ ts_list(...) \arguments{ \item{...}{A list of types, named or unnamed.} } +\value{ +A ts object that accepts lists with the specified types. +} \description{ A list is a vector of other robjects, which may or may not be named. } diff --git a/man/ts_logical.Rd b/man/ts_logical.Rd new file mode 100644 index 0000000..30e608a --- /dev/null +++ b/man/ts_logical.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/types.R +\name{ts_logical} +\alias{ts_logical} +\title{Logical or boolean type} +\usage{ +ts_logical(n = -1L) +} +\arguments{ +\item{n}{The length of the boolean vector. If \code{n = 1} then a single boolean is expected. If \code{n = 0} then any length is expected. If \code{n > 1} then a boolean vector of length \code{n} is expected.} +} +\value{ +A ts object that accepts logical scalars or vectors of length \code{n}. +} +\description{ +Booleans are represented in Zod schema as either a boolean (\code{z.boolean()}), +or a typed Uint8Array (\code{z.instanceof(Uint8Array)}). +} diff --git a/man/ts_null.Rd b/man/ts_null.Rd new file mode 100644 index 0000000..26a5f67 --- /dev/null +++ b/man/ts_null.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/types.R +\name{ts_null} +\alias{ts_null} +\title{Null type} +\usage{ +ts_null() +} +\value{ +A ts object that only accepts \code{NULL}. +} +\description{ +This is a type that only accepts \code{NULL}. +} diff --git a/man/ts_numeric.Rd b/man/ts_numeric.Rd new file mode 100644 index 0000000..ab18d85 --- /dev/null +++ b/man/ts_numeric.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/types.R +\name{ts_numeric} +\alias{ts_numeric} +\title{Numeric type} +\usage{ +ts_numeric(n = -1L) +} +\arguments{ +\item{n}{The length of the numeric vector. If \code{n = 1} then a single number is expected. If \code{n = 0} then any length is expected. If \code{n > 1} then a numeric vector of length \code{n} is expected.} +} +\value{ +A ts object that accepts numeric scalars or vectors of length \code{n}. +} +\description{ +Numbers are represented in Zod schema as either a number (\code{z.number()}), +or a Float64Array (\code{z.instanceof(Float64Array)}). +} diff --git a/man/ts_void.Rd b/man/ts_void.Rd new file mode 100644 index 0000000..6d5d40e --- /dev/null +++ b/man/ts_void.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/types.R +\name{ts_void} +\alias{ts_void} +\title{Void type} +\usage{ +ts_void() +} +\value{ +A ts object that accepts \code{NULL}. +} +\description{ +This is a type that accepts null values (this would typically be used for +functions that return nothing). +} diff --git a/tests/testthat/app.R b/tests/testthat/app.R index 3e4fcbf..2e208a8 100644 --- a/tests/testthat/app.R +++ b/tests/testthat/app.R @@ -1,8 +1,8 @@ library(ts) fn_mean <- ts_function(mean, x = ts_numeric(), result = ts_numeric(1)) -fn_first <- ts_function(function(x) x[1], - x = ts_character(-1), result = ts_character(1) +fn_first <- ts_function(function(x = ts_character(-1)) x[1], + result = ts_character(1) ) sample_num <- ts_function( diff --git a/tests/testthat/app.d.ts b/tests/testthat/app.d.ts index 3a34907..3a7ec01 100644 --- a/tests/testthat/app.d.ts +++ b/tests/testthat/app.d.ts @@ -1,5 +1,14 @@ -import type { Character, Numeric, PE.Numeric<1>, } from 'rserve-ts'; +import { Robj } from "rserve-ts"; +import { z } from "zod"; -const fn_first = (x: string | string[]) => Promise)>; -const fn_mean = (x: number | number[]) => Promise)>; -c("const sample_one = (x: number | number[]) => Promise)>;", "const sample_one = (x: string | string[]) => Promise)>;") +const fn_first = Robj.ocap( + [z.union([z.string(), Robj.character(0)])], + Robj.character(1) +); + +const fn_mean = Robj.ocap( + [z.union([z.number(), z.instanceof(Float64Array)])], + Robj.numeric(1) +); + +const sample_num = Robj.ocap([z.instanceof(Float64Array)], Robj.numeric(1)); diff --git a/tests/testthat/functions.R b/tests/testthat/functions.R deleted file mode 100644 index f6216c8..0000000 --- a/tests/testthat/functions.R +++ /dev/null @@ -1,14 +0,0 @@ -# overload input/return types - - - - -# ts_compile(d_normal) - -# compile to: -# const sampler = R.ocap( -# [], -# R.list({ -# num: R.ocap([], R.numeric(1)) -# }) -# ); diff --git a/tests/testthat/test-compile.R b/tests/testthat/test-compile.R new file mode 100644 index 0000000..1470bbc --- /dev/null +++ b/tests/testthat/test-compile.R @@ -0,0 +1,44 @@ +test_that("anonomous functions", { + add <- ts_function( + function(a = ts_numeric(1), b = ts_numeric(1)) a + b, + result = ts_numeric(1) + ) + + ts_compile(add) + + # expect_equal(add$call(1, 2), 3) + # expect_error(add$call("a", 2)) +}) + +test_that("Complex functions", { + get_sample <- ts_function( + function(n = ts_numeric(1)) { + sample(values, n) + }, + result = ts_numeric() + ) + + sampler <- ts_function( + function(values = ts_numeric()) { + list( + get = get_sample$copy(), + set = ts_function( + function(value = ts_numeric()) { + values <<- value + } + ) + ) + }, + result = ts_list( + get = get_sample, + set = ts_function(NULL, value = ts_numeric()) + ) + ) + + ts_compile(sampler) +}) + +test_that("Compile files", { + on.exit(if (file.exists("app.d.ts")) unlink("app.d.ts")) + res <- ts_compile("app.R") +}) diff --git a/tests/testthat/test-functions.R b/tests/testthat/test-functions.R index 369fd80..0bde1cd 100644 --- a/tests/testthat/test-functions.R +++ b/tests/testthat/test-functions.R @@ -65,8 +65,4 @@ test_that("function with complex return types", { expect_silent(s$set$call(100:200)) expect_gte(s$get$call(1), 100) - - # you would then 'deploy' this as an App that - # doesn't require the $call methods - # e.g., sampler(1:10)$get(5) })