Skip to content

Commit 35077bc

Browse files
committed
feat: added core sdk
1 parent 4ffa681 commit 35077bc

23 files changed

Lines changed: 2021 additions & 16 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ The action sdk to connect with aquila
33

44
This action sdk is currently implemented in:
55
- [Typescript](./ts/README.md)
6+
- [Rust](./rust/README.md)
67

78
# GRPC Communcation Flow
89

rust/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target

rust/README.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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.

rust/examples/simple-example-rs/src/data_types/email_data_type.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ use hercules::JsonSchema;
22
use serde::{Deserialize, Serialize};
33

44
/// A data type derived from a real Rust type instead of a hand-written
5-
/// schema DSL: `#[derive(JsonSchema)]` gives us both the structural type
6-
/// (`"string"`) and, via `#[schemars(regex(...))]`
5+
/// schema DSL: `#[derive(JsonSchema)]` gives us the structural type
6+
/// (`"string"`), and `#[schemars(regex(...))]` gives us the validation rule
7+
/// Aquila enforces on top of it.
78
#[hercules::data_type(identifier = "email_address", name(en_US = "Email Address"))]
89
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
910
#[serde(transparent)]

rust/hercules-macros/src/lib.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
//! Attribute macros that replace the TS SDK's `reflect-metadata` decorators
2-
//! (`@Identifier`, `@Signature`, `@Parameter`, ...) with compile-time code
3-
//! generation: each macro parses its arguments and any repeated
4-
//! `#[parameter(...)]`/`#[setting(...)]` helper attributes on the annotated
5-
//! struct, then emits a `meta()` associated function implementing the
6-
//! corresponding `hercules` trait. No runtime reflection, no metadata
7-
//! registry — the definition is just a value the macro builds once, at
8-
//! compile time.
1+
//! Attribute macros that turn `identifier = "...", name(en_US = "...")`
2+
//! arguments — plus any repeated `#[parameter(...)]`/`#[setting(...)]` helper
3+
//! attributes on the annotated struct — into a `meta()` associated function
4+
//! implementing the corresponding `hercules` trait. Each macro parses its
5+
//! arguments and generates that function once, at compile time; there's no
6+
//! runtime reflection or metadata registry involved.
97
108
mod data_type;
119
mod event;

rust/hercules-macros/src/parse.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,12 @@ impl AttrArgs {
128128
}
129129
}
130130

131-
/// Extracts and removes every `#[name(...)]` attribute on `item`, in
132-
/// source order, parsing each occurrence's arguments as [`AttrArgs`]. This
133-
/// is how `#[parameter(...)]`/`#[setting(...)]` behave like the TS SDK's
134-
/// repeatable `@Parameter`/`@EventSetting` decorators: rustc never resolves
135-
/// them as real attributes because the outer `#[hercules::...]` macro (which
136-
/// runs first) strips them before re-emitting the struct.
131+
/// Extracts and removes every `#[name(...)]` attribute on `item`, in source
132+
/// order, parsing each occurrence's arguments as [`AttrArgs`]. This is how
133+
/// `#[parameter(...)]`/`#[setting(...)]` can appear multiple times on one
134+
/// struct: rustc never resolves them as real attributes because the outer
135+
/// `#[hercules::...]` macro (which runs first) strips them before
136+
/// re-emitting the struct.
137137
pub fn take_repeated(item: &mut ItemStruct, name: &str) -> syn::Result<Vec<AttrArgs>> {
138138
let mut found = Vec::new();
139139
let mut error = None;

rust/hercules/Cargo.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "hercules"
3+
description = "Rust SDK for the hercules action runner"
4+
version.workspace = true
5+
edition.workspace = true
6+
license.workspace = true
7+
repository.workspace = true
8+
9+
[dependencies]
10+
hercules-macros = { path = "../hercules-macros", version = "0.0.0" }
11+
tucana.workspace = true
12+
tokio.workspace = true
13+
tonic.workspace = true
14+
async-trait.workspace = true
15+
thiserror.workspace = true
16+
serde.workspace = true
17+
serde_json.workspace = true
18+
schemars.workspace = true
19+
tokio-stream.workspace = true
20+
log.workspace = true
21+
inventory.workspace = true
22+
23+
[dev-dependencies]
24+
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "test-util"] }

0 commit comments

Comments
 (0)