|
| 1 | +# hercules |
| 2 | + |
| 3 | +Rust SDK for building Hercules actions that connect to Aquila. An action |
| 4 | +registers the functions it exposes, the events it can fire, and any custom |
| 5 | +data types it needs, then talks to Aquila over a persistent gRPC stream. |
| 6 | + |
| 7 | +Depends on the [`tucana`](https://crates.io/crates/tucana) crate for the |
| 8 | +underlying protobuf types. |
| 9 | + |
| 10 | +**Not yet included:** the `definitions/` standard library (the ~150 |
| 11 | +`std::*` function/data-type declarations) and an equivalent of the `hercules |
| 12 | +export` CLI. Both are independent of the core framework and can be added |
| 13 | +later. |
| 14 | + |
| 15 | +## Installation |
| 16 | + |
| 17 | +Not yet published to crates.io — until then, link it locally by path: |
| 18 | + |
| 19 | +```toml |
| 20 | +[dependencies] |
| 21 | +hercules = { path = "../hercules" } |
| 22 | +``` |
| 23 | + |
| 24 | +## Quick start |
| 25 | + |
| 26 | +Attach `#[hercules::runtime_function]` to a struct and implement |
| 27 | +`RuntimeFunctionHandler` — that's enough to register it, no separate call |
| 28 | +required: |
| 29 | + |
| 30 | +```rust |
| 31 | +use hercules::{Arguments, FunctionContext, PlainValue, Result, RuntimeFunctionHandler, async_trait}; |
| 32 | + |
| 33 | +#[hercules::runtime_function(identifier = "add", signature = "(a: NUMBER, b: NUMBER): NUMBER")] |
| 34 | +struct Add; |
| 35 | + |
| 36 | +#[async_trait] |
| 37 | +impl RuntimeFunctionHandler for Add { |
| 38 | + async fn run(&self, _ctx: &FunctionContext, args: &Arguments) -> Result<PlainValue> { |
| 39 | + let a: f64 = args.get("a")?; |
| 40 | + let b: f64 = args.get("b")?; |
| 41 | + Ok((a + b).into()) |
| 42 | + } |
| 43 | +} |
| 44 | + |
| 45 | +#[tokio::main] |
| 46 | +async fn main() -> Result<()> { |
| 47 | + let action = hercules::Action::new("my-action", "0.1.0").aquila_url("127.0.0.1:8081"); |
| 48 | + |
| 49 | + let action = action.connect("token", None).await?; |
| 50 | + std::future::pending::<()>().await; // keep dispatching execution requests |
| 51 | + # let _ = action; |
| 52 | + Ok(()) |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +Only `identifier` and `version` are required on `Action::new` — `author`, |
| 57 | +`icon`, `documentation`, `name`, and `configuration` are optional chained |
| 58 | +setters. |
| 59 | + |
| 60 | +See [`examples/simple-example-rs`](./examples/simple-example-rs) for a |
| 61 | +runnable action exercising every registration kind: a runtime function, its |
| 62 | +public variant, a data type, and a runtime event. |
| 63 | + |
| 64 | +```bash |
| 65 | +cd examples/simple-example-rs |
| 66 | +cp .env.example .env && set -a && source .env && set +a |
| 67 | +cargo run |
| 68 | +``` |
| 69 | + |
| 70 | +## Public vs. runtime functions |
| 71 | + |
| 72 | +A `RuntimeFunction` holds the actual implementation. A `Function` is an |
| 73 | +optional, separately-identified public variant of it — same execution |
| 74 | +behavior, its own metadata (identifier, parameter defaults, ...). Use |
| 75 | +`#[hercules::function(base = ...)]` when you want to expose a runtime |
| 76 | +function under different public-facing metadata without duplicating logic: |
| 77 | + |
| 78 | +```rust |
| 79 | +#[hercules::function(base = Add, identifier = "sum")] |
| 80 | +#[parameter(runtime_name = "a", default_value = 0)] |
| 81 | +struct Sum; |
| 82 | +``` |
| 83 | + |
| 84 | +Events follow the same pattern: `#[hercules::runtime_event]` for the |
| 85 | +internal definition, `#[hercules::event(base = ...)]` for a public, |
| 86 | +user-facing variant. |
| 87 | + |
| 88 | +## Data types |
| 89 | + |
| 90 | +Data types are derived from a Rust type's `schemars::JsonSchema` impl |
| 91 | +instead of a hand-written schema DSL — `schemars`' own validation attributes |
| 92 | +double as the wire validation rules: |
| 93 | + |
| 94 | +```rust |
| 95 | +use hercules::JsonSchema; |
| 96 | +use serde::{Deserialize, Serialize}; |
| 97 | + |
| 98 | +#[hercules::data_type(identifier = "email_address", name(en_US = "Email Address"))] |
| 99 | +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] |
| 100 | +#[serde(transparent)] |
| 101 | +pub struct EmailAddress(#[schemars(regex(pattern = r"^[^@]+@[^@]+\.[^@]+$"))] pub String); |
| 102 | +``` |
| 103 | + |
| 104 | +## Firing events |
| 105 | + |
| 106 | +```rust |
| 107 | +action.fire("user_created", project_id, serde_json::json!({ "userId": 42 }))?; |
| 108 | +``` |
| 109 | + |
| 110 | +The second argument is the project ID the event belongs to; the third is |
| 111 | +the payload matching the event's signature. |
| 112 | + |
| 113 | +## Testing without a live Aquila |
| 114 | + |
| 115 | +`Action::build_module()` returns the wire `Module` without connecting, so |
| 116 | +registration logic (macro-generated metadata, `Function`/`Event` merging, |
| 117 | +data type schema resolution) can be unit tested directly — see |
| 118 | +[`examples/simple-example-rs/src/main.rs`](./examples/simple-example-rs/src/main.rs)'s |
| 119 | +`tests` module for a working example. |
0 commit comments