# apply

[![Package Version](https://img.shields.io/hexpm/v/apply)](https://hex.pm/packages/apply)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/apply/)

Call **Erlang and JavaScript runtime functions at runtime, by string path** —
without writing an `@external` declaration plus a hand-rolled
`*_ffi.erl` / `*_ffi.mjs` entry for every function you need.

```gleam
import apply

pub fn main() {
  // Erlang target
  let assert Ok(3) = apply.call("erlang:length", #([1, 2, 3]))

  // JavaScript target
  let assert Ok(9) = apply.call("Math.max", #(9, 2))
}
```

The path format depends on the target you build for:

| | Erlang | JavaScript |
| --- | --- | --- |
| Path format | `"module:function"` (bare name defaults to `erlang`) | `"object.property.path"` (resolved on `globalThis`) |
| Example | `"erlang:length"` | `"Math.max"` |

## Installation

```sh
gleam add apply
```

## API at a glance

Pick the level of control you need.

| Function | Module | Purpose |
| --- | --- | --- |
| **`call(path, args)`** | `apply` | Quick call: dispatch by target, errors flattened to `String`. |
| `apply(path, args)` | `apply/erl`, `apply/js` | Same, but keeps the target's structured error type. |
| **`try_catch(func:, args:)`** | `apply/erl`, `apply/js` | Execute a fetched function, capturing throws as structured errors. |
| `get_erlang_function(path, arity)` | `apply/erl` | Fetch a reusable `Function` handle (validates module/atom/arity). |
| `get_function_from_global(path)` | `apply/js` | Fetch a reusable `Function` (keeps the correct `this`). |
| `get_obj_from_global(path)` / `get(obj, attr)` / `has(obj, attr)` | `apply/js` | Fetch / read / test JS values by dotted path. |
| **`classify(value)`** | `apply/erl`, `apply/js` | Branch on the runtime type with precise types (`ErlValue` / `JsValue`). |
| **`classify(value)`** | `apply` | Cross-target classification into a portable `apply.Value`. |
| `tuple_to_list(value)` | `apply/erl`, `apply/js`, `apply/boundary` | Split a tuple (a JS array) into a list of opaque elements (`ErlObj` / `JsObj` / generic `a`). |
| **`tuple_to_values(value)`** | `apply/erl`, `apply/js`, `apply` | Split a tuple/array and `classify` each element → `List(ErlValue)` / `List(JsValue)` / `List(apply.Value)`. |
| `atom_to_string(a)` / `string_to_atom(s)` | `apply/erl` | Round-trip Erlang atoms. |
| `platform()` / `is_tuple()` / `is_function()` / `tuple_size()` / `tuple_to_list()` | `apply/boundary` | Runtime checks / tuple splitting. |
| `to_custom_type(value)` / `identify(value)` / `debug_string(value)` | `apply/boundary` | Low-level casting / tagging / rendering. |

### `call` vs target-specific `apply` vs `try_catch`

- **`apply.call`** is the shortcut: it dispatches by target and flattens errors
  to a readable `String`.
- **`erl.apply` / `js.apply`** are the same, but keep the target's `ErlError` /
  `JsError`, so you can pattern-match the failure.
- **`get_*` then `try_catch`** is the most explicit: fetch a function once, keep
  it around, and separate lookup errors from execution errors. Use it to reuse a
  handle or to inspect a `FuncRunningError` (`error_type` / `error_reason` /
  `error_stack` are structured, not a string).

All of `call` / `apply` / `try_catch` return a **generic** success value, so you
can use the result directly as the concrete type you expect. Only when a
function can return **more than one type** do you need `classify`.

## Quick start

```gleam
import apply

// Erlang
let assert Ok(3) = apply.call("erlang:length", #([1, 2, 3]))
let assert Ok("123") = apply.call("erlang:integer_to_binary", #(123))

// JavaScript
let assert Ok(9) = apply.call("Math.max", #(9, 2))
```

Caveats:

- `args` **must be a tuple**; the arity is taken from its length.
- The success value is untyped (`a`) — nothing is checked at runtime.
- On an unsupported target it returns `Error("unsupported runtime: ...")`.

## Fetch, then execute

Use `apply/erl` / `apply/js` when you want reusable handles or structured
errors.

```gleam
import apply/erl

// Fetch and call separately: `length` is a reusable handle.
let assert Ok(length) = erl.get_erlang_function("erlang:length", 1)
let assert Ok(v) = erl.try_catch(length, #([1, 2, 3]))

// Or in one step
let assert Ok(v2) = erl.apply("erlang:length", #([1, 2, 3]))

// Atoms are opaque values you can round-trip
let assert Ok(atom) = erl.string_to_atom("hello")
let assert Ok("hello") = erl.atom_to_string(atom)
```

```gleam
import apply/js

let assert Ok(math) = js.get_obj_from_global("Math")
let assert Ok(pi) = js.get(math, "PI")
let assert Ok(max) = js.get_function_from_global("Math.max")
let assert Ok(9) = js.try_catch(max, #(9, 2))
```

`get_function_from_global` remembers the correct `this`, so method calls such as
`"console.log"` keep working.

## Branching on the runtime type — `classify`

Because `apply` / `try_catch` return a generic `a`, the common case needs no
extra work:

```gleam
let assert Ok(3) = erl.apply("erlang:length", #([1, 2, 3]))
```

When the function may return **different runtime types**, use `classify`. It
returns a variant that carries the concrete value, so you match on constructors
instead of strings.

### Erlang — `erl.classify`

`ErlValue` variants: `ErlBool`, `ErlInt`, `ErlFloat`, `ErlString`,
`ErlBitArray`, `ErlList(List(Dynamic))`, `ErlDict(Dict(Dynamic, Dynamic))`,
`ErlNil`, `ErlTuple(Dynamic)`, `ErlFunction(Function)`, `ErlAtom(Atom)`,
`ErlLocal(ErlObj)`.

```gleam
import apply/erl

let assert Ok(raw) = erl.apply("erlang:is_atom", #(1))
let assert erl.ErlBool(False) = erl.classify(raw)
```

### JavaScript — `js.classify`

`JsValue` variants: `JsBool`, `JsInt`, `JsFloat`, `JsString`, `JsBitArray`,
`JsList(List(Dynamic))`, `JsDict(Dict(Dynamic, Dynamic))`, `JsNil`,
`JsArray(Dynamic)`, `JsFunction(Function)`, `JsObject(JsObj)`.

```gleam
import apply/js

let assert Ok(raw) = js.apply("Math.max", #(9, 2))
let assert js.JsInt(9) = js.classify(raw)
```

### Erlang example — `{X, Y}` or `false`

Erlang functions often return a tuple on success and an atom (`false`) on miss.
`tuple_to_values` splits the tuple **and** classifies each element, so you can
match on the resulting `List(ErlValue)` directly:

```gleam
import apply/erl

pub fn lookup(key: String, records: List(#(String, Int))) -> Result(Int, Nil) {
  case erl.apply("lists:keyfind", #(key, 1, records)) {
    Ok(raw) ->
      case erl.tuple_to_values(raw) {
        Ok([erl.ErlString(_), erl.ErlInt(v)]) -> Ok(v)
        _ -> Error(Nil) // false: not found, or unexpected shape
      }
    Error(_) -> Error(Nil)
  }
}
```

### JavaScript example — `JSON.parse`

```gleam
import apply/js
import gleam/int
import gleam/list

pub fn describe_json(text: String) -> String {
  case js.apply("JSON.parse", #(text)) {
    Ok(raw) ->
      case js.classify(raw) {
        js.JsInt(n) -> "int " <> int.to_string(n)
        js.JsString(s) -> "string " <> s
        js.JsArray(_) ->
          case js.tuple_to_list(raw) {
            Ok(items) -> "array of " <> int.to_string(list.length(items))
            _ -> "array"
          }
        js.JsObject(_) -> "object"
        _ -> "other"
      }
    Error(_) -> "invalid json"
  }
}
```

> Typing differences: on Erlang `5.0` is a `"float"`; on JavaScript `5.0` is the
> number `5` and identifies as an `"int"`. On Erlang any valid-UTF-8 `binary` is
> a `"string"`, other binaries are `"bit_array"`; on JavaScript `string` and
> `bit_array` are distinct. `"atom"` only occurs on Erlang.

### Cross-target — `apply.classify`

For code that must run on both targets, `apply.classify` returns a portable
`apply.Value` (`VBool`, `VInt`, `VFloat`, `VString`, `VBitArray`,
`VList(List(Dynamic))`, `VDict(Dict(Dynamic, Dynamic))`, `VNil`,
`VTuple(Dynamic)`, `VFunction(Dynamic)`, `VAtom(Dynamic)`, `VLocal(Dynamic)`).
Handle-like values are `Dynamic` because the two runtimes have different
concrete handle types.

```gleam
import apply
import gleam/float

pub fn as_int(value: a) -> Int {
  case apply.classify(value) {
    apply.VInt(n) -> n
    apply.VFloat(f) -> float.round(f)
    _ -> 0
  }
}
```

`apply.tuple_to_values` is the cross-target counterpart of `erl.tuple_to_values` /
`js.tuple_to_values`: it splits a tuple (a JS array on that target) and
classifies each element into `List(apply.Value)`.

```gleam
case apply.tuple_to_values(raw) {
  Ok([apply.VAtom(_), apply.VInt(n)]) -> Ok(n)
  _ -> Error(Nil)
}
```

## Decoding collections — `to_custom_type` and `gleam/dynamic`

`VList` / `VDict` / `ErlList` / `JsDict` elements are `Dynamic`, so they plug
into the standard `gleam/dynamic/decode` decoders:

```gleam
import apply/erl
import gleam/dynamic/decode
import gleam/list
import gleam/result

let assert Ok(raw) = erl.apply("lists:seq", #(1, 3))
let assert erl.ErlList(items) = erl.classify(raw)
let assert Ok([1, 2, 3]) =
  items
  |> list.map(decode.run(_, decode.int))
  |> result.all
```

`boundary.to_custom_type(value)` is an **identity cast** (no runtime work): it
lets you assert a concrete type when you already know it, e.g.
`let #(_, found) = boundary.to_custom_type(tuple)`.

## Error handling

Every error variant has a matching `format_error`, producing messages in a
shared `path/arity + reason` style.

| Scenario | Erlang | JavaScript |
| --- | --- | --- |
| Malformed path | `path "Math.max" is not in "module:function" form` | resolved via `Reflect`, failure surfaces as `property "..." does not exist on ...` |
| Target missing | `module atom "not_a_module" in path "not_a_module:foo" does not exist` | `property "notExist" does not exist on ...` |
| Not exported at that arity | `function "length/2" is not exported by module "erlang"` | — (JS has no arity check) |
| Not callable | `not a callable function: ...` | `path "Math.PI" is not a function` |
| Args not a tuple | `args must be a tuple, got ...` | `args must be a tuple, got ...` |
| Threw while running | `error: badarg` (+ stack lines) | `TypeError: Reflect.get called on non-object` (+ stack lines) |
| Wrong runtime | `needs "erlang" runtime, but running on "javascript"` | `needs "javascript" runtime, but running on "erlang"` |

`FuncRunningError` keeps `error_type` / `error_reason` / `error_stack`
structured, so you can inspect it instead of parsing the rendered message.

## Shared helpers — `apply/boundary`

```gleam
import apply/boundary

boundary.platform()               // OnErlang | OnJavascript
boundary.platform_name()          // "erlang" | "javascript"
boundary.is_tuple(#(1, 2))        // True
boundary.is_function(fn() { 1 })  // True
boundary.tuple_size(#(1, 2))      // Ok(2)
boundary.tuple_to_list(#(1, 2))   // Ok([1, 2])
boundary.identify(value)          // #("int" | "string" | ... , value)
boundary.to_custom_type(raw)      // cast to the type you expect
```

## Development

```sh
gleam test                      # default target
gleam test --target erlang
gleam test --target javascript
gleam format --check src test
```

Tests enable themselves based on the runtime; an Erlang toolchain or Node.js
must be on `PATH` as appropriate.

Further documentation: <https://hexdocs.pm/apply/>.

## License

Apache-2.0 — see [LICENSE](LICENSE).
