Skip to content

Commit de698d2

Browse files
committed
feat: added example action
1 parent 8d0a699 commit de698d2

10 files changed

Lines changed: 216 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
ACTION_ID=example-action
2+
VERSION=0.0.0
3+
AQUILA_URL=127.0.0.1:8081
4+
AUTH_TOKEN=token
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "simple-example-rs"
3+
version = "0.0.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[dependencies]
8+
hercules = { path = "../../hercules" }
9+
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
10+
tokio-stream.workspace = true
11+
serde = { workspace = true }
12+
serde_json.workspace = true
13+
schemars.workspace = true
14+
log.workspace = true
15+
env_logger = "0.11"
16+
17+
[dev-dependencies]
18+
tucana.workspace = true
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use hercules::JsonSchema;
2+
use serde::{Deserialize, Serialize};
3+
4+
/// 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(...))]`
7+
#[hercules::data_type(identifier = "email_address", name(en_US = "Email Address"))]
8+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
9+
#[serde(transparent)]
10+
pub struct EmailAddress(#[schemars(regex(pattern = r"^[^@]+@[^@]+\.[^@]+$"))] pub String);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod email_data_type;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod user_created_runtime_event;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#[hercules::runtime_event(
2+
identifier = "user_created",
3+
signature = "(): { userId: NUMBER }",
4+
name(en_US = "User created event"),
5+
display_message(en_US = "Triggers on user creation"),
6+
description(
7+
en_US = "Triggers on user creation and has a payload including the user database id"
8+
)
9+
)]
10+
pub struct UserCreatedRuntimeEvent;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use super::fibonacci_runtime_function::FibonacciRuntimeFunction;
2+
3+
/// The public, user-facing variant of [`FibonacciRuntimeFunction`]: same
4+
/// execution behavior, its own identifier and a default input value.
5+
/// Execution always dispatches through the runtime function — this struct
6+
/// carries no `run()` of its own, only metadata overrides.
7+
#[hercules::function(base = FibonacciRuntimeFunction, identifier = "fibonacci")]
8+
#[parameter(
9+
runtime_name = "test",
10+
name(en_US = "Input Number"),
11+
description(en_US = "The position in the Fibonacci sequence"),
12+
default_value = 10
13+
)]
14+
pub struct FibonacciFunction;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use hercules::{
2+
async_trait, Arguments, FunctionContext, PlainValue, Result, RuntimeFunctionHandler,
3+
};
4+
5+
#[hercules::runtime_function(
6+
identifier = "fibonacci_runtime",
7+
signature = "(test: NUMBER): NUMBER",
8+
name(en_US = "Fibonacci (Runtime)"),
9+
display_message(en_US = "Computes the n-th Fibonacci number"),
10+
omit_definition
11+
)]
12+
#[parameter(runtime_name = "test", name(en_US = "N"))]
13+
pub struct FibonacciRuntimeFunction;
14+
15+
#[async_trait]
16+
impl RuntimeFunctionHandler for FibonacciRuntimeFunction {
17+
async fn run(&self, context: &FunctionContext, args: &Arguments) -> Result<PlainValue> {
18+
// Missing or malformed, `args.get` reports it back to Aquila as a
19+
// runtime error on its own — nothing to check by hand here.
20+
let n: u64 = args.get("test")?;
21+
22+
log::debug!(
23+
"computing fibonacci({n}) for project={} execution={}",
24+
context.project_id,
25+
context.execution_id
26+
);
27+
Ok(fib(n).into())
28+
}
29+
}
30+
31+
fn fib(n: u64) -> u64 {
32+
if n <= 1 {
33+
n
34+
} else {
35+
fib(n - 1) + fib(n - 2)
36+
}
37+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod fibonacci_function;
2+
pub mod fibonacci_runtime_function;
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
mod data_types;
2+
mod events;
3+
mod functions;
4+
5+
use hercules::{Action, ConfigurationDefinition, HerculesEvent, Translation};
6+
use tokio_stream::StreamExt;
7+
8+
fn env(key: &str, default: &str) -> String {
9+
std::env::var(key).unwrap_or_else(|_| default.to_string())
10+
}
11+
12+
/// `FibonacciRuntimeFunction`, `FibonacciFunction`, `EmailAddress` and
13+
/// `UserCreatedRuntimeEvent` never appear below — attaching
14+
/// `#[hercules::runtime_function]` / `function` / `data_type` /
15+
/// `runtime_event` registered each of them automatically as part of
16+
/// `Action::new` (see `hercules::registration`). There's simply nothing left
17+
/// to wire up by hand for this example.
18+
fn build_action() -> Action {
19+
Action::new(env("ACTION_ID", "example-action"), env("VERSION", "0.0.0"))
20+
.aquila_url(env("AQUILA_URL", "127.0.0.1:8081"))
21+
.author("code0-tech")
22+
.icon("tabler:bolt")
23+
.documentation("A simple example action")
24+
.name([Translation::new("en-US", "Example Action")])
25+
.configuration(
26+
ConfigurationDefinition::new("EXAMPLE_CONFIG", "string")
27+
.name([Translation::new("en-US", "Example Config")]),
28+
)
29+
}
30+
31+
#[tokio::main]
32+
async fn main() -> hercules::Result<()> {
33+
// RUST_LOG=hercules=debug,simple_example_rs=debug cargo run
34+
env_logger::init();
35+
36+
let action = build_action();
37+
let mut events = action.subscribe();
38+
39+
// The returned `Connected` handle can be discarded immediately: the
40+
// background dispatch task (started inside `connect`) holds its own
41+
// `Arc` to the shared connection state, so nothing here needs to keep
42+
// it alive.
43+
action
44+
.connect(env("AUTH_TOKEN", "token"), None)
45+
.await
46+
.unwrap_or_else(|err| panic!("failed to connect to Aquila: {err}"));
47+
48+
// React to connection lifecycle events on the main task for as long as
49+
// the process runs. Panicking here (unlike inside a spawned task) takes
50+
// the whole process down on an unrecoverable stream error.
51+
while let Some(event) = events.next().await {
52+
match event {
53+
HerculesEvent::Connected => log::info!("connected to Aquila"),
54+
HerculesEvent::Error(error) => panic!("Aquila stream error: {error}"),
55+
_ => {}
56+
}
57+
}
58+
59+
Ok(())
60+
}
61+
62+
#[cfg(test)]
63+
mod tests {
64+
use hercules::wire::Module;
65+
66+
use super::build_action;
67+
68+
/// Exercises the real registration path end to end — link-time
69+
/// `inventory` auto-registration, `Function`/`RuntimeFunction` merging,
70+
/// and `schemars`-driven data type resolution — by building the wire
71+
/// `Module` without connecting.
72+
#[test]
73+
fn builds_a_well_formed_module() {
74+
let module: Module = build_action().build_module();
75+
76+
assert_eq!(module.identifier, "example-action");
77+
78+
let email = module
79+
.definition_data_types
80+
.iter()
81+
.find(|dt| dt.identifier == "email_address")
82+
.expect("email_address data type");
83+
assert_eq!(email.r#type, "string");
84+
assert!(matches!(
85+
email.rules.first().and_then(|r| r.config.clone()),
86+
Some(tucana::shared::definition_data_type_rule::Config::Regex(_))
87+
));
88+
89+
let fibonacci = module
90+
.function_definitions
91+
.iter()
92+
.find(|f| f.runtime_name == "fibonacci")
93+
.expect("fibonacci function");
94+
assert_eq!(fibonacci.runtime_definition_name, "fibonacci_runtime");
95+
let param = &fibonacci.parameter_definitions[0];
96+
assert_eq!(param.runtime_name, "test");
97+
assert_eq!(
98+
param.default_value.as_ref().and_then(|v| v.kind.clone()),
99+
Some(tucana::shared::value::Kind::NumberValue(
100+
tucana::shared::NumberValue {
101+
number: Some(tucana::shared::number_value::Number::Integer(10))
102+
}
103+
))
104+
);
105+
106+
// omit_definition on the runtime function means it isn't separately
107+
// published under its own identifier.
108+
assert!(module
109+
.function_definitions
110+
.iter()
111+
.all(|f| f.runtime_name != "fibonacci_runtime"));
112+
assert_eq!(
113+
module.runtime_function_definitions[0].runtime_name,
114+
"fibonacci_runtime"
115+
);
116+
117+
assert_eq!(module.runtime_flow_types[0].identifier, "user_created");
118+
}
119+
}

0 commit comments

Comments
 (0)