This commit is contained in:
Tom Elliott 2025-01-20 21:13:21 +13:00
parent 7dfe4c3c50
commit 5e84656a89
18 changed files with 276 additions and 57 deletions

View File

@ -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()
}

View File

@ -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()",

View File

@ -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., `<T>(x: T) => T`)
- [ ] Add support for conditional return types
e.g., `const sample = <T, N extends number>(x: T[], n: N) => N extends 1 ? T : T[]`
- [ ] Function overloads? Perhaps just a wrapper around several function definitions...

18
man/ts_character.Rd Normal file
View File

@ -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())}).
}

23
man/ts_compile.Rd Normal file
View File

@ -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
}

View File

@ -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.
}

View File

@ -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!).
}

18
man/ts_integer.Rd Normal file
View File

@ -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)}).
}

View File

@ -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.
}

18
man/ts_logical.Rd Normal file
View File

@ -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)}).
}

14
man/ts_null.Rd Normal file
View File

@ -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}.
}

18
man/ts_numeric.Rd Normal file
View File

@ -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)}).
}

15
man/ts_void.Rd Normal file
View File

@ -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).
}

View File

@ -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(

View File

@ -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<Character<1>)>;
const fn_mean = (x: number | number[]) => Promise<Numeric<1>)>;
c("const sample_one = (x: number | number[]) => Promise<Numeric<1>)>;", "const sample_one = (x: string | string[]) => Promise<Character<1>)>;")
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));

View File

@ -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))
# })
# );

View File

@ -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")
})

View File

@ -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)
})