Skip to content
This page View Markdown Open in ChatGPT Open in Claude

title: Rust description: Write custom APIs, hooks, schedulers and event listeners of API Maker in Rust. pub fn main(g: &mut G) -> Result, g.req, g.res, g.sys.db, am::fail, crates from crates.io, with many examples. keywords: rust, custom api in rust, pub fn main, am::G, serde_json, g.sys.db.get_all, crates.io, cargo, API Maker


Rust

Code in Rust defines pub fn main(g: &mut G) -> Result<Value, Errors>. It is built once as a library the runner loads, on the official image of Rust (rust:1-slim unless the settings name another version), with only the crates you add.

In short

  • Write pub fn main(g: &mut G) -> Result<Value, Errors>.
  • It is built once, on rust:1-slim unless the settings name another version.
  • Refuse a request with Err(am::fail(400, message)).
hello.rs
1
2
3
4
5
6
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let name = g.req.body()["name"].as_str().unwrap_or("world");
    Ok(json!({ "hello": name }))
}
Runtime stable Rust, 1.88 or later : a version of rust:<version>-slim (edition 2024)
main pub fn main(g: &mut G) -> Result<Value, Errors> : Ok(value) answers, Value::Null lets the API go on
Calls of g.sys typed : what a call takes is a type made key after key (GetAll::new("mysql_8", "inventory", "customers").limit(10)), what it answers has its type (Vec<Value>, Option<Value>, i64, UpdateManyResult)
Values serde_json::Value (am::Value, am::json!) : g.req.body()["id"] ; a row as a struct of yours with .to()
? works on every error : the calls of g.sys, parsing, serde_json, your own errors
Crates crates.io : serde[derive] 1 is serde with the feature derive
Printing println! and eprintln! go to the console of the account

The request and the answer

g.req and g.res have a method for each key they are known to have : g.req.body(), g.req.query(), g.req.params(), g.req.headers() read (…_mut() to change one where it is), g.res.set_status_code(201) sets. Each gives a Value : reading a key which is not there gives Value::Null; writing one makes it. The request and the answer are values of JSON themselves too : g.req["added"] = json!(1).

A body which is a list ([{…}, {…}], several rows to save) is that list : g.req.body().as_array().

Read the request, shape the answer
1
2
3
4
5
6
7
8
9
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let status = g.req.query()["status"].as_str().unwrap_or("open").to_owned();
    let signed = !g.req.headers()["x-am-authorization"].is_null();
    g.res.set_status_code(201);
    g.res.headers_mut()["X-Request-Kind"] = json!("orders");
    Ok(json!({ "customer": g.req.params()["id"], "status": status, "signed": signed }))
}

Bytes as the answer

A file, a picture, a PDF : return am::bytes(…) of its bytes, or give that to g.res.set_output, and name their kind of content. A value of JSON has nothing which stands for bytes : am::bytes is the form they travel in. The answer is those bytes as they are, whatever the kind of content, with the headers of the code.

A picture as the answer
1
2
3
4
5
6
7
8
9
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    // The bytes of a picture : read from a file, downloaded, or made by the code.
    let picture: &[u8] = b"GIF89a\x01\0\x01\0\x80\0\0\xff\xff\xff\xff\xff\xff!\xf9\x04\x01\n\0\x01\0,\0\0\0\0\x01\0\x01\0\0\x02\x02L\x01\0;\0";
    g.res.set_content_type("image/gif");
    g.res.headers_mut()["Content-Disposition"] = json!("inline; filename=\"pixel.gif\"");
    Ok(am::bytes(picture))
}

Calling API Maker : g.sys

Each method is a call of the server, with its types : it waits for the answer and gives a Result. What it takes is a type of am made with what it is about, then key after key (GetAll, GetById, Count…) ; what it answers is typed too : rows are a Vec<Value>, a row an Option<Value>, a count an i64, what an API describes a struct (UpdateManyResult).

Database calls
1
2
3
4
5
6
7
8
use am::{Count, Errors, G, GetAll, GetById, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let rows = g.sys.db.get_all(GetAll::new("mysql_8", "inventory", "customers").limit(10).sort("-customer_id"))?;
    let two = g.sys.db.get_by_id(GetById::new("mysql_8", "inventory", "customers", 2))?;
    let count = g.sys.db.count(Count::new("mysql_8", "inventory", "customers").find(json!({ "country": "IN" })))?;
    Ok(json!({ "first": rows.first(), "name": two.map(|row| row["first_name"].clone()), "in_india": count }))
}
Save, update, remove
1
2
3
4
5
6
7
8
9
use am::{Errors, G, RemoveById, Save, UpdateById, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let saved = g.sys.db.save_single_or_multiple(Save::new("mysql_8", "inventory", "customers", json!({ "first_name": "Ada", "last_name": "Lovelace" })))?;
    g.sys.db.update_by_id(UpdateById::new("mysql_8", "inventory", "customers", 2).update_data(json!({ "first_name": "MALLORY" })))?;
    g.sys.db.remove_by_id(RemoveById::new("mysql_8", "inventory", "customers", 101))?;
    // What a save answers : `saved.row()`, `saved.rows()`, `saved.is_list()`.
    Ok(json!({ "saved": saved.row() }))
}
  • A key the type has no method for is set as it is : GetAll::new("mysql_8", "inventory", "customers").with("cursor", "abc"). A type is made of JSON too : GetAll::from(json!({ … })).
  • A row as a struct of your own (#[derive(serde::Deserialize)]) : let customers: Vec<Customer> = rows.to()?;.
  • The generated APIs are methods of g.sys.db too (get_all_gen…). The whole answer of a call (success, status_code, data, total_count, errors) : g.sys.db.full.get_all(…)?, an ApiResponse<Vec<Value>>.

Calls at the same time are threads, with std::thread::scope :

Calls at the same time
use am::{Count, Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let db = &g.sys.db;
    let (orders, products) = std::thread::scope(|scope| {
        let orders = scope.spawn(|| db.count(Count::new("mongodb", "shop", "orders")));
        let products = scope.spawn(|| db.count(Count::new("mongodb", "shop", "products")));
        (orders.join(), products.join())
    });
    let orders = orders.unwrap_or_else(|_| Ok(0))?;
    let products = products.unwrap_or_else(|_| Ok(0))?;
    Ok(json!({ "orders": orders, "products": products }))
}

Rows as they arrive

Sum a large table
use am::{Errors, G, GetAll, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let mut total = 0.0;
    g.sys.db.get_all_by_stream(GetAll::new("mysql_8", "inventory", "orders"), |row| {
        total += row["amount"].as_f64().unwrap_or(0.0);
        Ok(())
    })?;
    Ok(json!({ "amount": total }))
}

The function stays on the thread of the request : it can borrow and change your variables.

System APIs and cache

Encrypt, hash, events and cache keys
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let sealed = g.sys.system.encrypt(json!({ "card": "4111" }))?;
    let digest = g.sys.system.hash("my text")?;
    g.sys.system.emit_event("order_created", json!({ "id": 7 }), &["notify_shop"])?;
    g.sys.cache.set_key("last_order", "7", Some(600))?;
    let last = g.sys.cache.get_key("last_order")?;
    Ok(json!({ "sealed": sealed, "digest": digest, "last": last }))
}

Errors

Refuse a request
1
2
3
4
5
6
7
8
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let Some(quantity) = g.req.body()["quantity"].as_u64() else {
        return Err(am::fail(400, "'quantity' is missing."));
    };
    Ok(json!({ "quantity": quantity }))
}
Catch the errors of a call
use am::{Errors, G, RemoveById, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    match g.sys.db.remove_by_id(RemoveById::new("mysql_8", "inventory", "customers", 404)) {
        Ok(_) => Ok(json!({ "removed": true })),
        Err(refused) => {
            g.logger.warn(format!("not removed : {}", refused.message()));
            Ok(json!({ "removed": false, "code": refused.code() }))
        }
    }
}
  • ? on any error type answers 500 with its message : let n: i64 = text.parse()?; gives invalid digit found in string.
  • A panic answers 500 with panic: … and the lines of your code (at main.rs:8 in helper).

Logs, printing and shared values

Logs and shared values
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    g.logger.log(format!("order {} paid", 7));
    g.logger.error(json!({ "step": "payment" }));   // values as indented JSON
    println!("kept in the console of the account");
    // Shared by every piece of code of the sandbox : update changes a value while no other request does.
    let calls = g.shared.update("calls", |old| json!(old.and_then(|n| n.as_i64()).unwrap_or(0) + 1));
    Ok(calls)
}

Hooks

Pre hook : only my rows
1
2
3
4
5
6
7
use am::{Errors, G, Value};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let owner = g.req.auth()["authAMDB"]["id"].clone();
    g.req.query_mut()["find"]["owner_id"] = owner;
    Ok(Value::Null)
}
Post hook : hide a column
use am::{Errors, G, Value};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    if let Some(rows) = g.res.output_mut().as_array_mut() {
        for row in rows {
            if let Some(row) = row.as_object_mut() {
                row.remove("password");
            }
        }
    }
    Ok(Value::Null)
}

Utility classes

A utility class written in Rust is a module of its name in the crate of every piece of Rust code of the account.

The utility class Tax
use crate::Round::cents; // another utility class

pub const RATE: f64 = 0.2;

pub fn add(amount: f64) -> f64 {
    cents(amount * (1.0 + RATE))
}
A custom API which uses it
use am::{Errors, G, Value, json};

use crate::Tax;

pub fn main(g: &mut G) -> Result<Value, Errors> {
    Ok(json!({ "total": Tax::add(g.req.body()["amount"].as_f64().unwrap_or(0.0)) }))
}
  • The name of the class is the name of the module : letters of a to z, digits and _, no word of Rust (type, match…) and none of code, am, std, core, alloc, main. Name it from the crate : crate::Tax.
  • The modules are compiled into each piece of code which runs : what a class keeps in a static is kept per piece of code. Use g.shared for what all code shares.
  • In the Git repository the class is src/utility_classes/<folder>---<name>/<name>.rs, declared pub mod <name>; in am/rust/project.rs : cargo check compiles the same paths.

Test cases

A test case written in Rust names its tests in am::tests!, in the place of a main. A test is a fn(g: &mut G) -> Result<(), Errors> : the errors it returns, or what it panics with (assert_eq!), are its failure.

A test case
use am::{Errors, G, Mock, RunCustomApi, json};

fn the_cost_of_a_parcel(g: &mut G) -> Result<(), Errors> {
    g.sys.test.mock(Mock::new("db.getAll").when(json!({ "collection": "rates" })).returns(json!([{ "per_kg": 2.5 }])))?;
    let out = g.sys.test.run_custom_api(RunCustomApi::new("shippingCost").body(json!({ "weight": 4 })))?;
    assert_eq!(out["cost"].as_f64(), Some(10.0));
    Ok(())
}

am::tests!(the_cost_of_a_parcel);
  • g.sys.test : run_custom_api(RunCustomApi), run_custom_api_full(RunCustomApi) for the whole answer (data, status_code, headers, logs), mock(Mock), mocks(Vec<Mock>), clear_mocks(Option<&str>), calls(Option<&str>).
  • The custom API under test can be written in any language.

Crates

Add uuid[v4] 1 and serde[derive] 1 in Sandbox Settings, then :

Crates of crates.io
use am::{Errors, G, Value};
use serde::Serialize;

#[derive(Serialize)]
struct Ticket {
    id: String,
    seats: u32,
}

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let ticket = Ticket { id: uuid::Uuid::new_v4().to_string(), seats: 2 };
    Ok(serde_json::to_value(ticket)?)
}
  • serde_json and serde (with derive) are always there; add any other crate with its version requirement (1, ^1.40, >=1.2, <2) and features (tokio[rt-multi-thread,macros]).
  • The crates are built with the runner when the image is built; while code runs nothing is downloaded.

How it runs

  • Each piece of code is built once (cargo build --release, about a second or two once the crates are built) as a library of its own, loaded into the runner. The runner and your code exchange JSON only, through functions of C : no type of one ever crosses into the other. Saving starts the build in the background; code which does not compile is reported in the console of the account (main.rs:4:8: error[E0425]: cannot find value missing).
  • Every request runs on a thread of the runner; requests run at the same time, on every core. A thread takes the next request when its own is done : a thread_local! value of your code may be the one an earlier request left there.
  • static values live as long as the runner; g.shared is shared with every other piece of code of the sandbox.

In the code editor

The editor completes g as Rust writes it, the names of your instances, databases and tables, and the columns of a table on the rows a call answers. More in the code editor in every language.

Where the editor helps
use am::{Errors, G, GetAll, Value};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    // g.  →  req, res, sys, logger, shared      g.sys.db.  →  get_all, get_by_id, query, get_all_gen, full…
    g.logger.log(&g.req.body()["name"]);  // g.req.  →  body, query, headers, auth…      g.res.  →  set_status_code, headers_mut…
    let rows = g.sys.db.get_all(GetAll::new("mysql_8", "inventory", "customers").limit(10))?;
    //         GetAll::  →  new, default       GetAll::new("  →  your instances, then its databases, then their tables
    //         GetAll::new(…).  →  find, limit, sort, select…
    for row in &rows {
        g.logger.log(&row["first_name"]);  // row["  →  the columns of customers
    }
    Ok(rows[0]["first_name"].clone())
}
  • After GetAll:: the editor offers what makes the type (new, default), after GetAll::new(…). the methods which set its keys, each with what it is for. Inside .find(json!({ " it offers the columns of the table, inside .select(" and .sort(" too.
  • What a call answers has its type : on the rows of a get_all the columns are offered inside the quotes of an index (rows[0][", row[") and of get (row.get("), in a for loop, in a closure (.map(|row| row["…"])) and in the function given to get_all_by_stream. changed. (an update_many) offers updated_rows_count, saved. offers row, rows and is_list, the whole answer of g.sys.db.full offers success, data, errors…
  • am:: offers fail and what the crate has.
  • g.req. and g.res. offer a method for each key of the request and of the answer ; g.req.auth(). the opened tokens, written as an index (g.req.auth()["authAMUser"]).
  • The columns follow a row the same way : rows[0]. then first_name writes rows[0]["first_name"].

Reference

g. Rust
req body(), query(), params(), headers(), auth(), req_info(), event_data() (each a &Value, …_mut() to change it, set_… to replace it), is_api_request_from_user() ; a Value itself
res set_status_code, set_content_type, headers_mut(), shared_mut(), output(), output_mut(), set_output, errors(), warnings() ; a Value itself
sys.db get_all(GetAll), get_by_id(GetById), query(Query), save_single_or_multiple(Save), master_save(Save), array_operations(ArrayOperations), update_by_id(UpdateById), update_many(UpdateMany), replace_by_id(UpdateById), remove_by_id(RemoveById), remove_by_query(RemoveByQuery), aggregate(Aggregate), count(Count), distinct(Distinct), distinct_query(Distinct), get_all_by_stream, query_by_stream, and the generated APIs get_all_gen…
sys.db.full, sys.system.full, sys.cache.full the same calls, each with its whole answer : ApiResponse<T> (success, status_code, data, total_count, errors, warnings)
sys.system encrypt, encrypt_with, decrypt, decrypt_with, hash, get_secret, get_secrets, call_external_api, call_external_apis, get_token, get_tokens, execute_query, get_table_meta, get_table_metas, create_indexes, drop_indexes, drop_indexes_of_many, get_indexes, get_indexes_of_many, emit_event, emit_event_ws, is_valid_data_for_table(s), is_valid_data_for_custom_api(s), multi_tenant_instance(s)_updated, is_valid_connection_string(s), get_lock, release_lock
sys.cache get_key, get_keys, set_key, set_keys, remove_key, remove_keys, reset_cache_db, reset_cache_dbs, reset_cache_custom_apis, reset_cache_custom_apis_of_tenant, reset_cache_system_apis, reset_cache_system_apis_of_tenant
sys.call("db.getAll", …) any API by its name in JavaScript, with its arguments as they are
logger log, debug, info, warn, error : a text or a Value
shared get, set, update
errors am::fail(code, message), Errors { list }, .message(), .code()

Debugging

  • The debug runner of Rust builds your code without optimizations, with its symbols : lldb-dap attaches to it for the Debug tab of the API testing page.
  • A panic stops the code when Pause on exceptions is on.
  • Code on the native process of a Mac needs the developer mode of the Mac (sudo DevToolsSecurity -enable) : lldb-dap works through the debugserver of Xcode.
  • Learn more Debugging.