8.4.633-stable Switch to dev

Calling functions from the command line

greycat call runs an @expose function on a running server and prints its result. The function runs as a task of that server, in its store, with the identity of the token that calls it. Nothing is built and the store is not opened locally, so call works while greycat serve is up, from the project directory or from any other machine.

It replaces the curl request you would write to try an endpoint, and it is the way to start maintenance functions (an import, a migration, a recompute) on a server that must keep serving. greycat run cannot do that: it opens the store itself, which a running server already holds.

A first call

Given this project:

@library("std", "0.0.0");

type Person {
    name: String;
    age: int;
}

@expose
fn add(a: int, b: int): int {
    return a + b;
}

@expose
fn greet(name: String): String {
    return "Hello, ${name}!";
}

@expose
fn people(): Array<Person> {
    return [Person { name: "Ada", age: 36 }, Person { name: "Alan", age: 41 }];
}

Start the server in a terminal:

greycat serve

And call it from another one, in the same directory:

greycat call project::add 2 3
# 5
greycat call project::greet John
# Hello, John!

The function is named with its module, as in its HTTP route (/project::add). Only @expose functions can be called. Any other name fails with unresolved function: project::hidden (only exposed functions can be called).

Arguments

Arguments are positional, one per parameter. Before anything is sent, call downloads the ABI of the server and checks each argument against the signature of the function:

  • A String or char parameter takes its argument verbatim. No quotes are needed, and quotes are kept: greycat call project::greet '"John"' answers Hello, "John"!.
  • Any other parameter reads its argument as JSON of the declared type: 42, 2.5, true, null, [1, 2, 3], {"name":"Ada","age":36}.
  • Where the type is not precise (any, an abstract type), a _type field names the type of an object: '{"_type":"project::Person","name":"Ada","age":36}'.
  • Trailing nullable parameters can be left out. They receive null.
@library("std", "0.0.0");

type Person {
    name: String;
    age: int;
}

@expose
fn describe(p: Person): String {
    return "${p.name} is ${p.age}";
}

@expose
fn mean(values: Array<float>, scale: float?): float {
    var sum = 0.0;
    for (_, v in values) {
        sum = sum + v;
    }
    return sum / values.size() * (scale ?? 1.0);
}
greycat call project::describe '{"name":"Ada","age":36}'
# Ada is 36
greycat call project::mean '[1, 2, 3]'
# 2.0
greycat call project::mean '[1, 2, 3]' 2
# 4.0

Quote JSON arguments for your shell, since it would otherwise split [1, 2, 3] into three words.

The check uses the full types of the ABI, generics included. A wrong argument fails before the call is made, and the message names the parameter:

greycat call project::add 2
# missing required argument 'b' of project::add(a: int, b: int): int
greycat call project::mean '[1]' 2 3
# too many arguments: 3 given to project::mean(values: Array<float>, scale: float?): float
greycat call project::describe '{"name":"Ada"}'
# json missing fields at line 1, column 1 got 'Map' while 'Person' expected for parameter 'p' of project::describe(p: Person): String

Output

The result is printed on stdout in GCL notation, indented:

greycat call project::people
Array<Person> {
  Person {
    name: "Ada",
    age: 36
  },
  Person {
    name: "Alan",
    age: 41
  }
}

--format=json prints JSON instead, which suits a script. --pretty=false prints the value on a single line, in either format:

greycat call project::people --format=json --pretty=false
# [{"name":"Ada","age":36},{"name":"Alan","age":41}]
greycat call project::people --format=json | jq -r '.[].name'
# Ada
# Alan

On a terminal, the result is colored, and a progress line on stderr shows the status of the task and its elapsed time while it runs. A function that declares its steps with Task::expected_steps and Task::add_steps gets a progress bar with a percentage:

@library("std", "0.0.0");

@expose
fn recompute(n: int): int {
    Task::expected_steps(n);
    for (var i = 0; i < n; i++) {
        // ... one unit of work
        Task::add_steps(1);
    }
    return n;
}
project::recompute █████████                      30% running 2.4s

The progress line is only drawn when stderr is a terminal, so a redirected output only holds the result.

Errors and exit code

call exits with code 0 when the task ended and its result was printed. It exits with a non-zero code when the function is unknown, an argument does not read, the token is refused, the server cannot be reached, or the task failed.

When the function throws, the error and its stack are printed on stdout, and the status of the task on stderr:

@library("std", "0.0.0");

@expose
fn fail() {
    throw "something went wrong";
}
greycat call project::fail
Error {
  message: "something went wrong",
  stack: Array<ErrorFrame> {
    ErrorFrame {
      module: "project",
      function: "fail",
      line: 5,
      column: 34
    }
  }
}
project::fail: task 8 error

Ctrl-C cancels the task on the server, not only the command. The task stops with the error cancelled, and call exits with a non-zero code.

Server and token

Run from the project directory, call needs no option. It finds the server and its root token in gcdata/security/, which greycat serve fills when it starts:

  • The server is http://localhost:<port>, with the port the running server wrote to gcdata/security/port, else 8080. --port sets another port, and --url another server.
  • The call is authenticated with the root token in gcdata/security/token. --token sends another token, and the function then runs with the permissions of that user.

From another directory, or for a remote server, give both:

greycat call project::add 2 3 --url=https://twin.example.com --token=<token>

Each option has its environment variable, which is convenient in a script or a CI job:

export GREYCAT_URL=https://twin.example.com
export GREYCAT_TOKEN=<token>
greycat call project::add 2 3

greycat token --user=<name> prints a token for an existing user. Call with it to check what that user is allowed to do: a function its role does not allow is refused with HTTP 403.

greycat user add alice user
greycat call project::add 2 3 --token=$(greycat token --user=alice)
# 5

A server with a certificate from a private authority needs --ca_path, the directory holding the certificates to trust.

Task class and world

The task runs in the medium worker class by default. --task_class=small or --task_class=large picks another one. When the server has no worker in that class, the task runs in the nearest class that has some.

--world=<name> runs the function in a world of the server (see Many Worlds). Without it, or with --world=main, the function runs in main, whatever the default world of the user. A world the server does not have, or that the user may not use, fails the call.

Reserved functions

The functions the server answers itself, such as runtime::Task::running or runtime::Task::history, can be called as well. They return at once instead of running as a task:

greycat call runtime::Task::running
# Array<Task> {}

runtime::Task::events (a stream of events) and runtime::Runtime::abi (a file) have no value to print, so call refuses them.

Options

Option Environment variable Description
--url=<string> GREYCAT_URL Server to call, http://localhost:<port> when unset
--port=<int> GREYCAT_PORT Port of the local server, read from gcdata/security/port when unset
--token=<string> GREYCAT_TOKEN Token sent as Authorization, the root token of gcdata/security/token when unset
--format=<string> GREYCAT_FORMAT Output format, gcl (default) or json
--pretty=<bool> GREYCAT_PRETTY Indent the result, true by default
--task_class=<string> GREYCAT_TASK_CLASS Worker class of the task: small, medium (default) or large
--world=<string> GREYCAT_WORLD World the function runs in, main when unset
--ca_path=<string> GREYCAT_CA_PATH Directory of extra CA certificates to trust for HTTPS