Skip to content

Commit 8d0a699

Browse files
committed
docs: added rust guide
1 parent 9a49399 commit 8d0a699

4 files changed

Lines changed: 1606 additions & 3 deletions

File tree

docs/getting-started/introduction.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ An action is a service **you** build and run. It connects to the Code0 runtime (
1111
what functions it provides, what events it can fire, and what custom data types it understands.
1212
Users then pick your functions and events inside the flow editor.
1313

14-
Hercules is the TypeScript SDK for building actions.
14+
Hercules is the SDK for building actions, available for TypeScript and Rust.
1515

1616
## What an Action does
1717

docs/tutorials/first-simple-action.mdx

Lines changed: 212 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ title: First Simple Action
33
description: Build a complete action with a function, event, and data type from scratch
44
---
55

6+
import { Tabs, Tab } from 'fumadocs-ui/components/tabs';
7+
68
This tutorial walks through building a complete action that:
79

810
- Exposes a `fibonacci` function
@@ -11,8 +13,11 @@ This tutorial walks through building a complete action that:
1113

1214
## 1. Create the RuntimeFunction
1315

14-
A `RuntimeFunction` contains the actual implementation. Decorate it with `@Identifier`, `@Signature`, and `@Name`,
15-
then implement the `run()` method.
16+
A `RuntimeFunction` contains the actual implementation.
17+
18+
<Tabs items={['TypeScript', 'Rust']}>
19+
<Tab value="TypeScript">
20+
Decorate it with `@Identifier`, `@Signature`, and `@Name`, then implement the `run()` method.
1621

1722
```ts
1823
// src/functions/fibonacciRuntimeFunction.ts
@@ -47,11 +52,62 @@ export class FibonacciRuntimeFunction {
4752

4853
`@OmitRuntimeFunction()` prevents Hercules from auto-generating a public function definition from this class — we'll
4954
define that separately below.
55+
</Tab>
56+
<Tab value="Rust">
57+
Attach `#[hercules::runtime_function(...)]`, giving it an `identifier` and `signature`, then implement
58+
`RuntimeFunctionHandler`. Attaching the macro registers it automatically — there's no separate registration call to
59+
make later.
60+
61+
```rust
62+
// src/functions/fibonacci_runtime_function.rs
63+
use hercules::{
64+
async_trait, Arguments, FunctionContext, PlainValue, Result, RuntimeFunctionHandler,
65+
};
66+
67+
#[hercules::runtime_function(
68+
identifier = "fibonacci_runtime",
69+
signature = "(test: NUMBER): NUMBER",
70+
name(en_US = "Fibonacci (Runtime)"),
71+
display_message(en_US = "Computes the n-th Fibonacci number"),
72+
omit_definition
73+
)]
74+
#[parameter(runtime_name = "test", name(en_US = "N"))]
75+
pub struct FibonacciRuntimeFunction;
76+
77+
#[async_trait]
78+
impl RuntimeFunctionHandler for FibonacciRuntimeFunction {
79+
async fn run(&self, context: &FunctionContext, args: &Arguments) -> Result<PlainValue> {
80+
let n: u64 = args.get("test")?;
81+
82+
log::debug!(
83+
"computing fibonacci({n}) for project={} execution={}",
84+
context.project_id,
85+
context.execution_id
86+
);
87+
Ok(fib(n).into())
88+
}
89+
}
90+
91+
fn fib(n: u64) -> u64 {
92+
if n <= 1 {
93+
n
94+
} else {
95+
fib(n - 1) + fib(n - 2)
96+
}
97+
}
98+
```
99+
100+
`omit_definition` prevents Hercules from auto-publishing this runtime function as its own public function — we'll
101+
define that separately below.
102+
</Tab>
103+
</Tabs>
50104

51105
## 2. Create the Function
52106

53107
A `Function` extends the `RuntimeFunction` and adds public-facing metadata without changing the implementation.
54108

109+
<Tabs items={['TypeScript', 'Rust']}>
110+
<Tab value="TypeScript">
55111
```ts
56112
// src/functions/fibonacciFunction.ts
57113
import { Identifier, Name, Parameter } from '@code0-tech/hercules';
@@ -67,9 +123,31 @@ import { FibonacciRuntimeFunction } from './fibonacciRuntimeFunction.js';
67123
})
68124
export class FibonacciFunction extends FibonacciRuntimeFunction {}
69125
```
126+
</Tab>
127+
<Tab value="Rust">
128+
`#[hercules::function(base = ...)]` plays the same role as `extends` here: it points at the runtime function that
129+
actually executes, and carries only metadata overrides — no `run()` of its own.
130+
131+
```rust
132+
// src/functions/fibonacci_function.rs
133+
use super::fibonacci_runtime_function::FibonacciRuntimeFunction;
134+
135+
#[hercules::function(base = FibonacciRuntimeFunction, identifier = "fibonacci")]
136+
#[parameter(
137+
runtime_name = "test",
138+
name(en_US = "Input Number"),
139+
description(en_US = "The position in the Fibonacci sequence"),
140+
default_value = 10
141+
)]
142+
pub struct FibonacciFunction;
143+
```
144+
</Tab>
145+
</Tabs>
70146

71147
## 3. Create the RuntimeEvent
72148

149+
<Tabs items={['TypeScript', 'Rust']}>
150+
<Tab value="TypeScript">
73151
```ts
74152
// src/events/userCreatedRuntimeEvent.ts
75153
import { Identifier, Signature, Name, EventSetting } from '@code0-tech/hercules';
@@ -85,9 +163,30 @@ import { Identifier, Signature, Name, EventSetting } from '@code0-tech/hercules'
85163
})
86164
export class UserCreatedRuntimeEvent {}
87165
```
166+
</Tab>
167+
<Tab value="Rust">
168+
```rust
169+
// src/events/user_created_runtime_event.rs
170+
#[hercules::runtime_event(
171+
identifier = "user_created",
172+
signature = "(userId: NUMBER): void",
173+
name(en_US = "User Created")
174+
)]
175+
#[setting(
176+
identifier = "FILTER_ROLE",
177+
name(en_US = "Role Filter"),
178+
description(en_US = "Only trigger for users with this role"),
179+
optional
180+
)]
181+
pub struct UserCreatedRuntimeEvent;
182+
```
183+
</Tab>
184+
</Tabs>
88185

89186
## 4. Create the Event
90187

188+
<Tabs items={['TypeScript', 'Rust']}>
189+
<Tab value="TypeScript">
91190
```ts
92191
// src/events/userCreatedEvent.ts
93192
import { Identifier, Name, Editable } from '@code0-tech/hercules';
@@ -98,9 +197,29 @@ import { UserCreatedRuntimeEvent } from './userCreatedRuntimeEvent.js';
98197
@Editable(false)
99198
export class UserCreatedEvent extends UserCreatedRuntimeEvent {}
100199
```
200+
</Tab>
201+
<Tab value="Rust">
202+
```rust
203+
// src/events/user_created_event.rs
204+
use super::user_created_runtime_event::UserCreatedRuntimeEvent;
205+
206+
#[hercules::event(
207+
base = UserCreatedRuntimeEvent,
208+
identifier = "user_created_event",
209+
name(en_US = "On User Created")
210+
)]
211+
pub struct UserCreatedEvent;
212+
```
213+
214+
`editable` defaults to `false`, so there's nothing to set explicitly here — add the bare `editable` flag to turn it
215+
on.
216+
</Tab>
217+
</Tabs>
101218

102219
## 5. Create the DataType
103220

221+
<Tabs items={['TypeScript', 'Rust']}>
222+
<Tab value="TypeScript">
104223
Data types are backed by a Zod schema. Decorate the class with `@Identifier`, `@Name`, and `@Schema`.
105224

106225
```ts
@@ -115,9 +234,28 @@ const EmailSchema = z.string().regex(/^[^@]+@[^@]+\.[^@]+$/);
115234
@Schema(EmailSchema)
116235
export class EmailDataType {}
117236
```
237+
</Tab>
238+
<Tab value="Rust">
239+
Data types are derived from a real Rust type's `schemars::JsonSchema` impl instead of a hand-written schema DSL —
240+
`schemars`' own validation attributes (`#[schemars(regex(...))]`) double as the wire validation rules.
241+
242+
```rust
243+
// src/data_types/email_data_type.rs
244+
use hercules::JsonSchema;
245+
use serde::{Deserialize, Serialize};
246+
247+
#[hercules::data_type(identifier = "email_address", name(en_US = "Email Address"))]
248+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
249+
#[serde(transparent)]
250+
pub struct EmailAddress(#[schemars(regex(pattern = r"^[^@]+@[^@]+\.[^@]+$"))] pub String);
251+
```
252+
</Tab>
253+
</Tabs>
118254

119255
## 6. Wire It All Together
120256

257+
<Tabs items={['TypeScript', 'Rust']}>
258+
<Tab value="TypeScript">
121259
```ts
122260
// src/index.ts
123261
import { Action, CodeZeroEvent } from '@code0-tech/hercules';
@@ -164,14 +302,86 @@ action.connect(process.env.AUTH_TOKEN ?? 'token').catch((err: unknown) => {
164302
process.exit(1);
165303
});
166304
```
305+
</Tab>
306+
<Tab value="Rust">
307+
Every type above already registered itself when its module got linked into the binary, so there's nothing left to
308+
wire up beyond building the `Action` and connecting it:
309+
310+
```rust
311+
// src/main.rs
312+
mod data_types;
313+
mod events;
314+
mod functions;
315+
316+
use hercules::{Action, ConfigurationDefinition, HerculesEvent, Translation};
317+
use tokio_stream::StreamExt;
318+
319+
fn env(key: &str, default: &str) -> String {
320+
std::env::var(key).unwrap_or_else(|_| default.to_string())
321+
}
322+
323+
fn build_action() -> Action {
324+
Action::new(env("ACTION_ID", "example-action"), env("VERSION", "0.0.0"))
325+
.aquila_url(env("AQUILA_URL", "127.0.0.1:8081"))
326+
.author("my-org")
327+
.icon("tabler:bolt")
328+
.documentation("A simple example action")
329+
.name([Translation::new("en-US", "Example Action")])
330+
.configuration(
331+
ConfigurationDefinition::new("EXAMPLE_CONFIG", "string")
332+
.name([Translation::new("en-US", "Example Config")]),
333+
)
334+
}
335+
336+
#[tokio::main]
337+
async fn main() -> hercules::Result<()> {
338+
let action = build_action();
339+
let mut events = action.subscribe();
340+
341+
// The returned `Connected` handle can be discarded immediately: the
342+
// background dispatch task (started inside `connect`) holds its own
343+
// `Arc` to the shared connection state.
344+
action
345+
.connect(env("AUTH_TOKEN", "token"), None)
346+
.await
347+
.unwrap_or_else(|err| panic!("failed to connect to Aquila: {err}"));
348+
349+
// React to connection lifecycle events on the main task for as long as
350+
// the process runs. Panicking here (unlike inside a spawned task) takes
351+
// the whole process down on an unrecoverable stream error.
352+
while let Some(event) = events.next().await {
353+
match event {
354+
HerculesEvent::Connected => log::info!("connected to Aquila"),
355+
HerculesEvent::Error(error) => panic!("Aquila stream error: {error}"),
356+
_ => {}
357+
}
358+
}
359+
360+
Ok(())
361+
}
362+
```
363+
</Tab>
364+
</Tabs>
167365

168366
## Firing an Event
169367

170368
To fire the `user_created` event from within your action (e.g. after a webhook call):
171369

370+
<Tabs items={['TypeScript', 'Rust']}>
371+
<Tab value="TypeScript">
172372
```ts
173373
await action.fire(UserCreatedRuntimeEvent, projectId, { userId: 42 });
174374
```
175375

176376
The second argument is the project ID this event belongs to, and the third is the payload matching the event's
177377
signature.
378+
</Tab>
379+
<Tab value="Rust">
380+
```rust
381+
action.fire("user_created", project_id, serde_json::json!({ "userId": 42 }))?;
382+
```
383+
384+
The second argument is the project ID this event belongs to, and the third is the payload matching the event's
385+
signature.
386+
</Tab>
387+
</Tabs>

0 commit comments

Comments
 (0)