diff --git a/NAMESPACE b/NAMESPACE index bda2ac1..28a4d33 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,6 +4,8 @@ S3method(print,ts_object) S3method(ts_compile,character) S3method(ts_compile,default) S3method(ts_compile,ts_function) +S3method(ts_compile,ts_overload) +export(is_overload) export(ts_character) export(ts_compile) export(ts_dataframe) @@ -13,3 +15,4 @@ export(ts_integer) export(ts_list) export(ts_logical) export(ts_numeric) +export(ts_overload) diff --git a/R/compile.R b/R/compile.R index ef8b1c6..0fd22d6 100644 --- a/R/compile.R +++ b/R/compile.R @@ -1,10 +1,10 @@ #' @export -ts_compile <- function(f, file = NULL, ...) { +ts_compile <- function(f, ..., file = NULL) { UseMethod("ts_compile") } #' @export -ts_compile.ts_function <- function(f, file = NULL, name = deparse(substitute(f))) { +ts_compile.ts_function <- function(f, name = deparse(substitute(f)), ...) { inputs <- attr(f, "args") result <- attr(f, "result") @@ -14,10 +14,17 @@ ts_compile.ts_function <- function(f, file = NULL, name = deparse(substitute(f)) sprintf("const %s = (%s) => Promise<%s>;", name, fn_args, result$type_fn) } +#' @export +ts_compile.ts_overload <- function(f, file = NULL, name = deparse(substitute(f))) { + cmt <- sprintf("\n// %s overloads", name) + oloads <- sapply(f, ts_compile, name = name) + paste(cmt, paste(oloads, collapse = "\n"), sep = "\n") +} + #' @export ts_compile.character <- function( f, - file = sprintf("%s.ts", tools::file_path_sans_ext(f))) { + file = sprintf("%s.d.ts", tools::file_path_sans_ext(f))) { if (length(f) > 1) { return(sapply(f, ts_compile)) } @@ -53,6 +60,6 @@ ts_compile.character <- function( } #' @export -ts_compile.default <- function(f) { +ts_compile.default <- function(f, ...) { warning("Not supported") } diff --git a/R/function.R b/R/function.R index a7f94e6..ff2e9b8 100644 --- a/R/function.R +++ b/R/function.R @@ -14,13 +14,27 @@ parse_args <- function(x, mc) { args } -# TS function +#' TS function definition +#' +#' @param f an R function +#' @param ... argument definitions, OR function overloads +#' @param result return type (ignored if overloads are provided) #' @export ts_function <- function(f, ..., result = NULL) { args <- list(...) - if (!is_object(result)) { + if (!is.null(result) && !is_object(result)) { stop("Invalid return type") } + if (any(is_overload(args))) { + if (!all(is_overload(args))) { + stop("Cannot mix overloads with standard arguments") + } + z <- lapply(args, function(x) { + do.call(ts_function, c(list(f), x$args, list(result = x$result))) + }) + class(z) <- "ts_overload" + return(z) + } fn <- function(...) { mc <- match.call(f) @@ -32,3 +46,15 @@ ts_function <- function(f, ..., result = NULL) { class(fn) <- c("ts_function", class(f)) fn } + +#' @export +is_overload <- function(x) { + sapply(x, inherits, what = "ts_overload") +} + +#' @export +ts_overload <- function(..., result = NULL) { + structure(list(args = list(...), result = result), + class = "ts_overload" + ) +} diff --git a/R/types.R b/R/types.R index 7c221fc..e83e883 100644 --- a/R/types.R +++ b/R/types.R @@ -6,12 +6,14 @@ #' @param type_fn The type of the object that Typescript expects to recieve from R. #' @param default The default value of the object. #' @param check A function that checks the object and returns it if it is valid. This operates on the R side and is mostly for development and debugging purposes. It is up to the developer to ensure that all functions return the correct type of object always. +#' @param generic logical, if `TRUE` then the object is a generic type. #' #' @md object <- function(type = "any", type_fn = "any", default = NULL, - check = function() stop("Not implemented")) { + check = function() stop("Not implemented"), + generic = FALSE) { e <- environment() e$attr <- function(name, value) { diff --git a/README.Rmd b/README.Rmd index da80666..9c5ae85 100644 --- a/README.Rmd +++ b/README.Rmd @@ -102,9 +102,9 @@ myfun(1:5) myfun("hello world") -cat(readLines("tests/testthat/app/app.R"), sep = "\n") +cat(readLines("tests/testthat/app.R"), sep = "\n") -ts_compile("tests/testthat/app/app.R", file = "") +ts_compile("tests/testthat/app.R", file = "") ``` ## TODO @@ -114,3 +114,5 @@ ts_compile("tests/testthat/app/app.R", file = "") - [ ] 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/README.md b/README.md index aa1cacb..3cd1bfd 100644 --- a/README.md +++ b/README.md @@ -98,19 +98,35 @@ myfun(1:5) myfun("hello world") #> Error: Expected a number -cat(readLines("tests/testthat/app/app.R"), sep = "\n") +cat(readLines("tests/testthat/app.R"), sep = "\n") #> 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) #> ) +#> +#> sample_one <- ts_function( +#> sample, +#> ts_overload( +#> x = ts_numeric(), +#> result = ts_numeric(1) +#> ), +#> ts_overload( +#> x = ts_character(), +#> result = ts_character(1) +#> ) +#> ) -ts_compile("tests/testthat/app/app.R", file = "") +ts_compile("tests/testthat/app.R", file = "") #> import type { Character, Numeric } from 'rserve-ts'; #> #> const fn_first = (x: string | string[]) => Promise)>; #> const fn_mean = (x: number | number[]) => Promise)>; +#> +#> // sample_one overloads +#> const sample_one = (x: number | number[]) => Promise)>; +#> const sample_one = (x: string | string[]) => Promise)>; ``` ## TODO @@ -123,3 +139,6 @@ ts_compile("tests/testthat/app/app.R", file = "") 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/object.Rd b/man/object.Rd index 1e98170..ce42aaf 100644 --- a/man/object.Rd +++ b/man/object.Rd @@ -8,7 +8,8 @@ object( type = "any", type_fn = "any", default = NULL, - check = function() stop("Not implemented") + check = function() stop("Not implemented"), + generic = FALSE ) } \arguments{ @@ -19,6 +20,8 @@ object( \item{default}{The default value of the object.} \item{check}{A function that checks the object and returns it if it is valid. This operates on the R side and is mostly for development and debugging purposes. It is up to the developer to ensure that all functions return the correct type of object always.} + +\item{generic}{logical, if \code{TRUE} then the object is a generic type.} } \description{ This is the base type for all typed objects. It is not meant to be used directly. diff --git a/man/ts_function.Rd b/man/ts_function.Rd new file mode 100644 index 0000000..61df748 --- /dev/null +++ b/man/ts_function.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/function.R +\name{ts_function} +\alias{ts_function} +\title{TS function definition} +\usage{ +ts_function(f, ..., result = NULL) +} +\arguments{ +\item{f}{an R function} + +\item{...}{argument definitions, OR function overloads} + +\item{result}{return type (ignored if overloads are provided)} +} +\description{ +TS function definition +} diff --git a/tests/testthat/app.R b/tests/testthat/app.R new file mode 100644 index 0000000..1821a31 --- /dev/null +++ b/tests/testthat/app.R @@ -0,0 +1,18 @@ +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) +) + +sample_one <- ts_function( + sample, + ts_overload( + x = ts_numeric(), + result = ts_numeric(1) + ), + ts_overload( + x = ts_character(), + result = ts_character(1) + ) +) diff --git a/tests/testthat/app.d.ts b/tests/testthat/app.d.ts new file mode 100644 index 0000000..3a34907 --- /dev/null +++ b/tests/testthat/app.d.ts @@ -0,0 +1,5 @@ +import type { Character, Numeric, PE.Numeric<1>, } from 'rserve-ts'; + +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)>;") diff --git a/tests/testthat/app/app.R b/tests/testthat/app/app.R deleted file mode 100644 index 08143b4..0000000 --- a/tests/testthat/app/app.R +++ /dev/null @@ -1,6 +0,0 @@ -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) -) diff --git a/tests/testthat/app/app.ts b/tests/testthat/app/app.ts deleted file mode 100644 index 8155de3..0000000 --- a/tests/testthat/app/app.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { types } from 'rserve-ts'; - -const fn_first = (x: string | string[]) => Promise)>; -const fn_mean = (x: number | number[]) => Promise)>; diff --git a/tests/testthat/functions.R b/tests/testthat/functions.R new file mode 100644 index 0000000..26cfc00 --- /dev/null +++ b/tests/testthat/functions.R @@ -0,0 +1,17 @@ +# overload input/return types +sample_one <- ts_function( + sample, + ts_overload( + x = ts_numeric(), + result = ts_numeric(1) + ), + ts_overload( + x = ts_character(), + result = ts_character(1) + ) +) +ts_compile(sample_one) + +# compile to: +# const sample_one = (x: number) => Promise; +# const sample_one = (x: string) => Promise;