title: Rust
description: Write custom APIs, hooks, schedulers and event listeners of API Maker in Rust. pub fn main(g: &mut G) -> Result
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-slimunless the settings name another version. - Refuse a request with
Err(am::fail(400, message)).
| hello.rs | |
|---|---|
| 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().
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.
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).
- 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.dbtoo (get_all_gen…). The whole answer of a call (success,status_code,data,total_count,errors) :g.sys.db.full.get_all(…)?, anApiResponse<Vec<Value>>.
Calls at the same time are threads, with std::thread::scope :
Rows as they arrive¶
| Sum a large table | |
|---|---|
The function stays on the thread of the request : it can borrow and change your variables.
System APIs and cache¶
Errors¶
| Refuse a request | |
|---|---|
?on any error type answers500with its message :let n: i64 = text.parse()?;givesinvalid digit found in string.- A panic answers
500withpanic: …and the lines of your code (at main.rs:8 in helper).
Logs, printing and shared values¶
Hooks¶
| Pre hook : only my rows | |
|---|---|
| Post hook : hide a column | |
|---|---|
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.
use crate::Round::cents; // another utility class
pub const RATE: f64 = 0.2;
pub fn add(amount: f64) -> f64 {
cents(amount * (1.0 + RATE))
}
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 ofcode,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
staticis kept per piece of code. Useg.sharedfor what all code shares. - In the Git repository the class is
src/utility_classes/<folder>---<name>/<name>.rs, declaredpub mod <name>;inam/rust/project.rs:cargo checkcompiles 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.
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 | |
|---|---|
serde_jsonandserde(withderive) 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. staticvalues live as long as the runner;g.sharedis 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.
- After
GetAll::the editor offers what makes the type (new,default), afterGetAll::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_allthe columns are offered inside the quotes of an index (rows[0][",row[") and ofget(row.get("), in aforloop, in a closure (.map(|row| row["…"])) and in the function given toget_all_by_stream.changed.(anupdate_many) offersupdated_rows_count,saved.offersrow,rowsandis_list, the whole answer ofg.sys.db.fullofferssuccess,data,errors… am::offersfailand what the crate has.g.req.andg.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].thenfirst_namewritesrows[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.