Rib
Rib is the language used by the API Gateway that enables users to write programs capable of manipulating worker responses, which are WebAssembly (WASM) values.
Rib uses the WAVE (WebAssembly Value Encoding) (opens in a new tab) syntax for encoding values.
Rib Grammar
rib-expr below defines the grammar of Rib.
rib_expr ::= rib_expr_untyped (":" type_name)?
rib_expr_untyped ::= simple_expr rib_expr_rest
simple_expr ::= simple_expr_untyped (":" type_name)?
simple_expr_untyped ::=
list_comprehension
| list_reduce
| pattern_match
| let_binding
| conditional
| flag
| record
| code_block
| tuple
| sequence
| boolean_literal
| literal
| not
| option
| result
| identifier
| call
| signed_num
rib_expr_rest ::= binary_rest | method_invocation | select_field_rest | select_index_res | number_rest | range_rest
select_field_rest ::= ("." select_field)*
binary_rest ::= (binary_op simple_expr)*
number_rest ::= "." fraction
method_invocation ::= "." call
call ::= function_name ("(" argument ("," argument)* ")")?
function_name ::= variant | enum | worker_function_call
select_index_rest ::= ("[" select_index "]")*
binary_op ::= ">=" | "<=" | "==" | "<" | ">" | "&&" | "||" | "+" | "-" | "*" | "/"
range_rest ::= ".." ("="? pos_num | pos_num? )
fraction ::= digits (('e' | 'E') ('+' | '-')? digits)?
let_binding ::= "let" let_variable (":" type_name)? "=" rib_expr
conditional ::= "if" rib_expr "then" rib_expr "else" rib_expr
selection_expr ::= select_field | select_index
select_field ::= selection_base_expr "." identifier
select_index ::= pos_num
nested_indices ::= "[" pos_num "]" ("," "[" pos_num "]")*
flag ::= "{" flag_names "}"
flag_names ::= flag_name ("," flag_name)*
flag_name ::= (letter | "_" | digit | "-")+
record ::= "{" record_fields "}"
record_fields ::= field ("," field)*
field ::= field_key ":" rib_expr
field_key ::= (letter | "_" | digit | "-")+
code_block ::= rib_expr (";" rib_expr)*
code_block_unit ::= code_block ";"
selection_base_expr ::= select_index | select_field | identifier | sequence | tuple
tuple ::= "(" ib_expr ("," rib_expr)* ")"
sequence ::= "[" rib_expr ("," rib_expr)* "]"
enum ::= identifier
variant ::= identifier ( "(" rib_expr ")" ) ?
argument ::= rib_expr
list_comprehension ::= "for" identifier_text "in" expr "{"
code_block_unit?
"yield" expr ";"
"}"
list_reduce ::= "reduce" identifier_text"," identifier_text "in" expr "from" expr "{"
code_block_unit?
"yield" expr ";"
"}"
text ::= letter (letter | digit | "_" | "-")*
pos_num ::= digit+
digits ::= [0-9]+
signed_num ::= ("+" | "-")? pos_num
literal ::= "\"" (static_term | dynamic_term)* "\""
static_term ::= (letter | space | digit | "_" | "-" | "." | "/" | ":" | "@")+
dynamic_term ::= "${" rib_expr "}"
boolean_literal ::= "true" | "false"
not ::= "!" rib_expr
option ::= "some" "(" rib_expr ")" | "none"
result ::= "ok" "(" rib_expr ")" | "error" "(" rib_expr ")"
identifier ::= any_text
function_name ::= variant | enum | instance | text
instance ::= "instance"
any_text ::= letter (letter | digit | "_" | "-")*
type_name := basic_type | list_type | tuple_type | option_type | result_type
basic_type ::= "bool" | "s8" | "u8" | "s16" | "u16" | "s32" | "u32" | "s64" | "u64" | "f32" | "f64" | "char" | "string"
list_type ::= "list" "<" (basic_type | list_type | tuple_type | option_type | result_type) ">"
tuple_type ::= "tuple" "<" (basic_type | list_type | tuple_type | option_type | result_type) ("," (basic_type | list_type | tuple_type | option_type | result_type))* ">"
option_type ::= "option" "<" (basic_type | list_type | tuple_type | option_type | result_type) ">"
result_type ::= "result" "<" (basic_type | list_type | tuple_type | option_type | result_type) "," (basic_type | list_type | tuple_type | option_type | result_type) ">"
The grammar is mainly to give a high level overview of the syntax of Rib.
The following sections show some examples of each construct.
Rib scripts can be multiline, and it should be separated by ;.
Return type of a Rib script
The last expression in the Rib script is the return value of the script. The last line in Rib script
shouldn't end with a ;, as this is a syntax error.
Let's see how we can write Rib corresponding to the types supported by the WebAssembly Component Model (opens in a new tab)
Core Language Elements
Identifier
fooHere foo is an identifier. Note that identifiers are not wrapped with quotes.
Rib will fail at compilation if variable foo is not available in the context.
This implies, if you are running Rib specifying a wasm component through api-gateway in golem, then Rib has the access to look at component metadata.
If Rib finds this variable foo in the component metadata,then it tries to infer its types.
If such a variable is not present in the wasm component (i.e, you can cross check this in WIT file of the component),
then technically there are two possibilities. Rib will choose to fail the compilation or consider it as a global variable input.
A global input is nothing but Rib expects foo to be passed in to the evaluator of Rib script (somehow).
Since you are using Rib from the api-gateway part of golem the only valid global variable supported is request and nothing else.
This would mean, it will fail compilation.
Example:
my-worker-function(request)
request here is a global input and Rib knows the type of request if my-worker-function is a valid function in the component (which acts as dependencies to Rib)
Here are a few other examples of identifiers:
foo-bar
FOO-BARMore on global inputs to follow.
Primitives
https://component-model.bytecodealliance.org/design/wit.html#primitive-types (opens in a new tab)
Numbers
1You can annotate a type to Rib expression to make it specific, otherwise, it will get inferred as u64 for a positive integer, s64 for a signed integer, and f64 for a floating point number.
1: u64Boolean
truefalseString
"foo"Lists
# Sequence of numbers
[1, 2, 3]You can annotate the type of the list as follows:
let x: list<s32> = [1, 2, 3];["foo", "bar", "baz"]# Sequence of record
[{a: "foo"}, {b : "bar"}]Options
Option in corresponding to WASM , which can take the shape of Some or None. This is similar to Option type in std Rust.
An Option can be Some of a value, or None.
some(1) noneYou can annotate the type of option as follows, if needed.
let x: option<u32> = none;The syntax is inspired from wasm wave.
Results
Resultin Rib is WASM Result, which can take the shape of ok or err. This is similar to Result type in std Rust.
A Result can be Ok of a value, or an Err of a value.
ok(1) err("error")
{
"user": "ok(Alice)",
"age": 30u32
}Note that there are various possibilities (opens in a new tab) of result in Web Assembly component model.
If there is no data associated with the error case, you can do the following:
let my-result: result<_, string> = err("foo")
my-resultIf there are no data associated with the success case, you can do the following:
let my-result: result<string> = ok("foo")
If you don't care the type of success and error.
let my-result: result = ok("foo")When using rib, we recommend using the mostly used pattern which is to know both the success and error case (i.e, result<u32, string>)
Tuples
A tuple type is an ordered fixed length sequence of values of specified types. It is similar to a record, except that the fields are identified by their order instead of by names.
(1, 20.1, "foo")This is also equivalent to the following in Rib. You can be specific about the types just like in any rib expression.
let my-tuple: tuple<u64, f64, string> = (1, 20.1, "foo");
my-tupleUnlike list, the values in a tuple can be of different types.
Here is another example:
("foo", 1, {a: "bar"})Record
{ name: "John", age: 30 }
{ city: "New York", population: 8000000 }
{ name: "John", age: 30, { country: "France", capital: "Paris" } }This is parsed as a WASM Record. The syntax is inspired from WASM-WAVE.
Note that, sometimes you will need to annotate the type of the number. It depends on the compilation context.
Note that keys are not considered as variables. Also note that keys in a WASM record don't have quotes. Example: {"foo" : "bar"} is wrong.
Variants
Let say your WIT file has the following variant (opens in a new tab)
variant bid-result {
success,
failure(string),
}
bid: func() -> bid-result;
process-result: func(res: bid-result) -> string
Then, in Rib, all you need to is simply use the variant terms directly. Example:
let x = failure("its failed");
xYou can pattern match (more on this patter-match below) on variants as follows:
let x = failure("its failed");
let result = match x { success => "its success", failure(msg) => msg }
resultThis will return "its failed".
Here is the example WIT file and the corresponding rib scripts that shows how to pass variants as arguments to agent methods. More explanation on how to invoke agent methods using rib is explained further down below.
variant bid-result {
success(string),
failure(string)
}
handle: func(res: bid-result) -> string;
let agent = my-agent("something");
agent.handle(failure("it's failed"));let agent = my-agent("something");
agent.handle(success);let agent = my-agent("something");
let bid-result = worker.bid();
let result = agent.handle(bid-result);
result
Note that rib script should have the dependency to the component that defines the variant. Otherwise, Rib will fail with compilation.
failure("its failed")
[invalid script] error in the following rib found at line 1, column 203
`failure("its failed")`
cause: invalid function call `failure`
unknown function
If Rib is used through API gateway and not directly, this dependency is already added automatically as you specify component name when defining API definition.
Enums
Let's say your WIT file has the following enum (opens in a new tab) types,
enum status {
backlog,
in-progress,
done,
}
enum priority {
low,
medium,
high,
}
Then, in Rib, all you need to do is to simply specify the name of the term. Example by typing in backlog, it knows its an enum type
which can be any of backlog, in-progress and done.
let my-status = backlog;
let my-priory = priority;
{s: my-status, p: my-priority}You can pattern match (more on this pattern-match below) on enum values as follows
let my-priority = low;
let result = match my-priority {
low => "its low",
medium => "its medium",
high => "its high"
};
resultThis will return the result "its low".
Here is the example WIT file and the corresponding rib scripts that shows how to pass variants as arguments to agent method. More explanation on how to invoke worker function using rib is explained further down below.
enum status {
backlog,
in-progress,
done,
}
enum priority {
low,
medium,
high,
}
process-user: func(status: status, priority: priority, user-id: string) -> string;
let agent = my-agent("something");
let result = agent.process-user(backlog, low, "jon");
result
let agent = my-agent("something");
let status = in-progress;
let priority = medium;
let result = agent.process-user(status, priority, "jon");
resultNote that rib script should have the dependency to the component that defines the enum. Otherwise, Rib will fail with compilation.
If Rib is used through API gateway and not directly, this dependency is already added automatically as you specify component name when defining API definition.
Flags
{ Foo, Bar, Baz }This is of a flag type.
Expressions and Syntax Features
Assign to a variable
This can be done using let binding which we have already seen in the above examples
let x = 1;We can annotate the type of the variable as well.
let x: u64 = 1;If you are passing this variable to an agent method, then you may not need to specify the type of the variable.
String Interpolation (concatenation)
This is similar to languages like scala where you start with ${ and end with }
let x = "foo";
let y = "bar";
let z = "${x}-and-${y}";Evaluating this Rib will result in "foo-and-bar". The type of z is a string.
Selection of Field
A field can be selected from an Rib expression if it is a Record type. For example, foo.user
is a valid selection given foo is a variable that gets evaluated to a record value.
let foo = { name: "John", age: 30 };
foo.nameSelection of Index
This is selecting an index from a sequence value.
let x = [1, 2, 3];
x[0]You can also inline as given below
[1, 2, 3][0]Comparison (Boolean)
let x: u8 = 1;
let y: u8 = 2;
x == y
Similarly, we can use other comparison operators like >=, <=, ==, < etc.
Both operands should be a valid Rib code that points/evaluated to a number or string.
Arithmetic
let x: u8 = 1;
let y: u8 = 2;
x + y+, -, / and * are supported.
Collections and Control Structures
List Comprehension
let x = ["foo", "bar"];
for p in x {
yield p;
}List Aggregation
let ages: list<u16> = [1, 2, 3];
reduce z, a in ages from 0 {
yield z + a;
}Ranges
Ranges can be right exclusive or right inclusive. The right exclusive range is denoted by .. and right inclusive range is denoted by ..=.
1..10;let initial: u32 = 1;
let final: u32 = 10;
let x = initial..final;
for i in x {
yield i
}Similarly, you can use ..= to include the last number in the range.
1..=10;let initial: u32 = 1;
let final: u32 = 10;
let x = initial..=final;
for i in x {
yield i
}Please note that, you may need to help Rib compiler with type annotations for the numbers involved in the range. This depends on the context. We will improve these aspects as we go.
You can also create infinite range, where you skip the right side of ..
1..;However, note that as of now Rib interpreter (runtime) will spot any infinite loop and will throw an error. Example: The following will throw an error.
let x = 1:u8..;
for i in x {
yield i
}However, you can use infinite ranges to select a segment of the list without worrying about the end index.
let x: list<u32> = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for i in x[1..] {
yield i
}Pattern Matching
let res: result<str, str> = ok("foo");
match res {
ok(v) => v,
err(msg) => msg
}This would probably be your go-to construct when you are dealing with complex data structures like result or option or other custom variant (WASM Variant)
that comes out as the response of an agent method
let agent-result = my-agent.my-method(1, "jon");
match agent-result {
ok(x) => "foo",
err(msg) => "error"
}Exhaustive pattern matching
If the pattern match is not exhaustive, then it will throw compilation errors
Example: The following pattern matching is invalid
match agent-result {
some(x) => "found",
}This will result in following error:
Error: Non-exhaustive pattern match. The following patterns are not covered: `none`.
To ensure a complete match, add these patterns or cover them with a wildcard (`_`) or an identifier.Now, let's say your agent responded with a variant.
Note on variant: A variant statement defines a new type where instances of the type match exactly one of the variants listed for the type.
This is similar to a "sum" type in algebraic datatype (or an enum in Rust if you're familiar with it).
Variants can be thought of as tagged unions as well.
Pattern Match on Variants
Given you are using Rib through worker bridge, which knows about component metadata,
then, let's say we have a variant of type as given below responded from the agent, when invoking a function called foo:
variant my_variant {
bar( {a: u32,b: string }),
foo(list<string>),
}then, the following will work:
let x = foo("abc");
match x {
bar({a: _, b: _}) => "success",
foo(some_list) => some_list[0]
}
Variables in the context of pattern matching
In all of the above, there exist a variable in the context of pattern.
Example x in ok(x) or msg in err(msg) or x in some(x) or x in bar(x) or x in foo(x).
These variables are bound to the value that is being matched.
Example, given the agent response is ok(1), the following match expression will result in 2.
let result = my-agent.my-method("foo");
match result {
ok(x) => x + 1,
err(msg) => 0
}The following match expression will result in "c", if the agent response was variant value foo(["a", "b", "c"]),
and will result in "a" if it was variant value bar({a: 1, b: "a"}).
let result = my-agent.my-method();
match result {
bar(x) => x.b,
foo(x) => x[1]
}
Wild card patterns
In some of the above examples, the binding variables are unused. We can use _ as a wildcard pattern to indicate that we don't care about the value.
match response {
bar(_) => "bar",
foo(_) => "foo"
}Conditional Statement
let id: u32 = 10;
if id > 3 then "higher" else "lower"The structure of the conditional statement is if <condition> then <then-rib-expr> else <else-rib-expr>,
where condition-expr is an expr that should get evaluated to boolean.
The then-rib-expr or else-rib-expr can be an any valid rib code, which could be another if else itself
let id: u32 = request.user.id;
if id > 3 then "higher" else if id == 3 then "equal" else "lower"You must ensure that the branches (then branch and else branch) resolve to the same type. Otherwise, Rib will fail to compile.
External Interactions
HTTP Request Input and Field Selection
Rib is currently mainly used in worker-gateway which will allow us to expose HTTP APIs on top of worker functions.
If you are using Rib in the context of worker-gateway then
you can use the variable request in your Rib script, which allows you to access various parameters of an input HTTP request.
In other words, request is a global input to the Rib script when used in API definitions, and
worker-gateway will ensure to pass the value of request to the rib evaluator internally.
Please refer to worker-gateway documents for more details.
request will hold all the details of an input HTTP request in gateway,
such as headers, path, query parameters and body.
Request Body Parameters
To select the body field in request,
request.body.userRequest Header Parameters
To select a header from the request
request.headers.userRequest Path Parameters
To select the value of a path variable in your HTTP request /v4/{user-id}/get-cart-contents
request.path.user-idRequest Query Parameters
To select the value of a query variable in your HTTP request /v4/{user-id}/get-cart-contents?{country}
request.query.countryThe request.path.* and request.headers.* will be inferred as string unless you specify it using type annotation.
Example:
let user-id: u8 = request.path.user-id;To select the value of a query variable in your HTTP request /v4/{user-id}/get-cart-contents?{country}
let country: string = request.query.country;A typical Rib script in API definition (worker gateway)
Here is a full example of a Rib script that looks up various elements in an HTTP request.
Given the route is /v4/{user-id}/get-cart-contents?{country} , which is a POST request,
which has a body, then the following Rib script can be valid, and has a dependency to a component
that has a function my-function that takes two arguments of type {foo: string, bar: string} and string
let user-id: u8 = request.path.user-id;
let country: string = request.query.country;
let input = request.body;
let agent = my-agent("my-worker-${user-id}");
let result = agent.my-method(input, country);
resultThis Rib script will be part of an API definition (please refer to worker-gateway for more details)
When registering this API definition, it already keeps track of the requirements of the input HTTP request.
In this case, the input request is a POST request with a body, and the body is of type {foo: string, bar: string},
and the path variable is user-id of type u8, and the query parameter is country of type string.
Once the API is deployed and the API is invoked, worker gateway will validate the input request against these requirements, and if they don't satisfy, it results in a BadRequest error.
Please note that the Rib language by itself does not support keywords such as request, path, body, or headers.
The above examples are valid only when using Rib through worker-gateway.
If a Rib script is evaluated in the context of an HTTP request, these values are available in the context of the Rib evaluator.
Currently, there are two ways to use Rib in golem. One is worker-gateway
and the other is golem REPL. In the case of REPL, request is not available, since we are not executing Rib in the context of an HTTP request.
Here is an example of an invalid Rib script
request
request.bodyIn the above case, compiler is not able to infer the type of request or request.body
as it is not passed as an argument to an agent method.
Invoke agent methods
Rib is mainly used to write scripts to manipulate the input and call agent methods. Refer to the worker-gateway documentation for more on how you use Rib to invoke agent methods. This is useful to expose some HTTP APIs on top of these agents running in the Golem platform, and you don't want to keep updating or change the components and expose additional APIs, as you can write a simple Rib script to do the necessary changes to the input and pass it to the agent methods functions. Invoking functions is similar to any other languages.
Agent method invocation
let agent = my-agent("something");
let result = agent.get-cart-contents();
resultRib is evaluated in the context of a particular component (this is taken care by worker-gateway that it evaluates Rib in the context of a wasm component).
In this script, first you create an instance (instance of an agent) using the agent type (my-agent) and specifying values for the constructor parameters. With this instance, the agent methods are available to call.
Agent method invocation with arguments
Let's say there exists a function add-to-cart which takes a product as the argument, where product is a wasm record
let agent = my-agent("something");
let product = {product-id: 1, quantity: 2, name: "mac"};
agent.add-to-cart(input);Similarly you can pass multiple arguments to the agent method and they should be separated by ,.
Say you need to pass the username along with with the product details.
let agent = my-agent("something");
let product = {product-id: 1, quantity: 2, name: "mac"};
let username = "foo";
agent.add-to-cart(username, product);You can inline the arguments as well.
let agent = my-agent("something");
let product = {product-id: 1, quantity: 2, name: "mac"};
agent.add-to-cart("foo", {product-id: 1, quantity: 2, name: "mac"});Multiple invocations
Rib allows you to invoke a function multiple times, or invoke multiple functions across multiple agents in a single script. That said, it is important to note that Rib by itself is not durable and is stateless. The next time you invoke Rib (through worker-gateway for example), these functions will get executed on the agent again and doesn't cache any result in anyway.
Let's say we want to accumulate the contents of a shopping cart from user-id 1 to user-id 5.
let agent = my-agent("something");
let initial = 1: u64;
let final = 5: u64;
let range = initial..final;
let result = for i in range {
yield agent.get-cart-contents("user-id-${i}");
};
result
Type Inference
Rib is mainly used to write script to manipulate the input and call agent methods. In this case, for the most part, most of the types will be automatically inferred. Otherwise, there will be a compilation error, asking the developer to annotate the types and help the compiler.
let x: string = request.body;
xSay the request body is a record in Json, as given below. Rib sees it as a WASM Record type.
{
"user": "Alice",
"age": 30u32
}Then we can use Rib language to select the field user, as it considers this request's body as a WASM Record type.
let x: string = request.body.user;
xLimitations
We recommend the users of golem to not rely on Rib for complex business logic as of now. It's better to write it in standard languages that works with golem such as Rust, Python etc, making sure your logic or workflow is durable. We will be expanding the usability and reliability of Rib as we go.
Issues and Trouble Shooting
If you bump into compilation errors, annotating the types will help the compiler to infer the types properly. If you bump into internal runtime errors, please report to us and we will try to fix it as soon as possible.
We are making improvements in this space continuously.