Skip to content

Commit f52d297

Browse files
New struct-based service definition API (#117)
* feat: struct-based service definition API Put #[restate_sdk::service] (or #[object]/#[workflow]) on an impl block, annotate handlers with #[handler], and do dependency injection via &self struct fields. Bind the struct value directly (no .serve()) via the new IntoServiceDefinition trait. Shared/exclusive is inferred from the context type. Generic structs (bounds + where-clauses) are supported. The trait-based API keeps working but is deprecated with a compile-time warning. Examples and test-services migrated to the new API. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: use prelude-reexported macros (drop restate_sdk:: prefix) The service/object/workflow/handler macros are now re-exported from the prelude, so first-party examples, tests and test-services use the bare attribute form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: rename service attr arg `vis` to `client_visibility` Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Polish * Polish * Make sure options get overriden --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 845fe1b commit f52d297

48 files changed

Lines changed: 1634 additions & 667 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,12 @@ Then you're ready to develop your Restate service using Rust:
3131
```rust
3232
use restate_sdk::prelude::*;
3333

34-
#[restate_sdk::service]
35-
trait Greeter {
36-
async fn greet(name: String) -> HandlerResult<String>;
37-
}
38-
39-
struct GreeterImpl;
34+
struct Greeter;
4035

41-
impl Greeter for GreeterImpl {
42-
async fn greet(&self, _: Context<'_>, name: String) -> HandlerResult<String> {
36+
#[service]
37+
impl Greeter {
38+
#[handler]
39+
async fn greet(&self, _ctx: Context<'_>, name: String) -> HandlerResult<String> {
4340
Ok(format!("Greetings {name}"))
4441
}
4542
}
@@ -50,7 +47,7 @@ async fn main() {
5047
// tracing_subscriber::fmt::init();
5148
HttpServer::new(
5249
Endpoint::builder()
53-
.with_service(GreeterImpl.serve())
50+
.bind(Greeter)
5451
.build(),
5552
)
5653
.listen_and_serve("0.0.0.0:9080".parse().unwrap())
@@ -79,15 +76,12 @@ Here's how to create a simple Lambda service:
7976
```rust
8077
use restate_sdk::prelude::*;
8178

82-
#[restate_sdk::service]
83-
trait Greeter {
84-
async fn greet(name: String) -> HandlerResult<String>;
85-
}
86-
87-
struct GreeterImpl;
79+
struct Greeter;
8880

89-
impl Greeter for GreeterImpl {
90-
async fn greet(&self, _: Context<'_>, name: String) -> HandlerResult<String> {
81+
#[service]
82+
impl Greeter {
83+
#[handler]
84+
async fn greet(&self, _ctx: Context<'_>, name: String) -> HandlerResult<String> {
9185
Ok(format!("Greetings {name}"))
9286
}
9387
}
@@ -100,7 +94,7 @@ async fn main() {
10094
// Build and run the Lambda endpoint
10195
LambdaEndpoint::run(
10296
Endpoint::builder()
103-
.bind(GreeterImpl.serve())
97+
.bind(Greeter)
10498
.build(),
10599
)
106100
.await
@@ -144,7 +138,7 @@ async fn test_container() {
144138
.with_max_level(tracing::Level::INFO) // Set the maximum log level
145139
.init();
146140

147-
let endpoint = Endpoint::builder().bind(MyServiceImpl.serve()).build();
141+
let endpoint = Endpoint::builder().bind(MyService).build();
148142

149143
// simple test container intialization with default configuration
150144
//let test_container = TestContainer::default().start(endpoint).await.unwrap();

examples/counter.rs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,30 @@
11
use restate_sdk::prelude::*;
22

3-
#[restate_sdk::object]
4-
trait Counter {
5-
#[shared]
6-
async fn get() -> Result<u64, TerminalError>;
7-
async fn add(val: u64) -> Result<u64, TerminalError>;
8-
async fn increment() -> Result<u64, TerminalError>;
9-
async fn reset() -> Result<(), TerminalError>;
10-
}
11-
12-
struct CounterImpl;
3+
struct Counter;
134

145
const COUNT: &str = "count";
156

16-
impl Counter for CounterImpl {
7+
#[object]
8+
impl Counter {
9+
#[handler(name = "get")]
1710
async fn get(&self, ctx: SharedObjectContext<'_>) -> Result<u64, TerminalError> {
1811
Ok(ctx.get::<u64>(COUNT).await?.unwrap_or(0))
1912
}
2013

14+
#[handler]
2115
async fn add(&self, ctx: ObjectContext<'_>, val: u64) -> Result<u64, TerminalError> {
2216
let current = ctx.get::<u64>(COUNT).await?.unwrap_or(0);
2317
let new = current + val;
2418
ctx.set(COUNT, new);
2519
Ok(new)
2620
}
2721

22+
#[handler]
2823
async fn increment(&self, ctx: ObjectContext<'_>) -> Result<u64, TerminalError> {
2924
self.add(ctx, 1).await
3025
}
3126

27+
#[handler]
3228
async fn reset(&self, ctx: ObjectContext<'_>) -> Result<(), TerminalError> {
3329
ctx.clear(COUNT);
3430
Ok(())
@@ -38,7 +34,7 @@ impl Counter for CounterImpl {
3834
#[tokio::main]
3935
async fn main() {
4036
tracing_subscriber::fmt::init();
41-
HttpServer::new(Endpoint::builder().bind(CounterImpl.serve()).build())
37+
HttpServer::new(Endpoint::builder().bind(Counter).build())
4238
.listen_and_serve("0.0.0.0:9080".parse().unwrap())
4339
.await;
4440
}

examples/cron.rs

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,14 @@ use std::time::Duration;
1313
/// ```shell
1414
/// $ curl -v http://localhost:8080/PeriodicTask/my-periodic-task/start
1515
/// ```
16-
#[restate_sdk::object]
17-
trait PeriodicTask {
18-
/// Schedules the periodic task to start
19-
async fn start() -> Result<(), TerminalError>;
20-
/// Stops the periodic task
21-
async fn stop() -> Result<(), TerminalError>;
22-
/// Business logic of the periodic task
23-
async fn run() -> Result<(), TerminalError>;
24-
}
25-
26-
struct PeriodicTaskImpl;
16+
struct PeriodicTask;
2717

2818
const ACTIVE: &str = "active";
2919

30-
impl PeriodicTask for PeriodicTaskImpl {
20+
#[object]
21+
impl PeriodicTask {
22+
/// Schedules the periodic task to start
23+
#[handler]
3124
async fn start(&self, context: ObjectContext<'_>) -> Result<(), TerminalError> {
3225
if context
3326
.get::<bool>(ACTIVE)
@@ -39,21 +32,25 @@ impl PeriodicTask for PeriodicTaskImpl {
3932
}
4033

4134
// Schedule the periodic task
42-
PeriodicTaskImpl::schedule_next(&context);
35+
PeriodicTask::schedule_next(&context);
4336

4437
// Mark the periodic task as active
4538
context.set(ACTIVE, true);
4639

4740
Ok(())
4841
}
4942

43+
/// Stops the periodic task
44+
#[handler]
5045
async fn stop(&self, context: ObjectContext<'_>) -> Result<(), TerminalError> {
5146
// Remove the active flag
5247
context.clear(ACTIVE);
5348

5449
Ok(())
5550
}
5651

52+
/// Business logic of the periodic task
53+
#[handler]
5754
async fn run(&self, context: ObjectContext<'_>) -> Result<(), TerminalError> {
5855
if context.get::<bool>(ACTIVE).await?.is_none() {
5956
// Task is inactive, do nothing
@@ -64,13 +61,13 @@ impl PeriodicTask for PeriodicTaskImpl {
6461
println!("Triggered the periodic task!");
6562

6663
// Schedule the periodic task
67-
PeriodicTaskImpl::schedule_next(&context);
64+
PeriodicTask::schedule_next(&context);
6865

6966
Ok(())
7067
}
7168
}
7269

73-
impl PeriodicTaskImpl {
70+
impl PeriodicTask {
7471
fn schedule_next(context: &ObjectContext<'_>) {
7572
// To schedule, create a client to the callee handler (in this case, we're calling ourselves)
7673
context
@@ -84,7 +81,7 @@ impl PeriodicTaskImpl {
8481
#[tokio::main]
8582
async fn main() {
8683
tracing_subscriber::fmt::init();
87-
HttpServer::new(Endpoint::builder().bind(PeriodicTaskImpl.serve()).build())
84+
HttpServer::new(Endpoint::builder().bind(PeriodicTask).build())
8885
.listen_and_serve("0.0.0.0:9080".parse().unwrap())
8986
.await;
9087
}

examples/failures.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
use rand::Rng;
22
use restate_sdk::prelude::*;
33

4-
#[restate_sdk::service]
5-
trait FailureExample {
6-
#[name = "doRun"]
7-
async fn do_run() -> Result<(), TerminalError>;
8-
}
9-
10-
struct FailureExampleImpl;
4+
struct FailureExample;
115

126
#[derive(Debug, thiserror::Error)]
137
#[error("I'm very bad, retry me")]
148
struct MyError;
159

16-
impl FailureExample for FailureExampleImpl {
10+
#[service]
11+
impl FailureExample {
12+
#[handler(name = "doRun")]
1713
async fn do_run(&self, context: Context<'_>) -> Result<(), TerminalError> {
1814
context
1915
.run::<_, _, ()>(|| async move {
@@ -32,7 +28,7 @@ impl FailureExample for FailureExampleImpl {
3228
#[tokio::main]
3329
async fn main() {
3430
tracing_subscriber::fmt::init();
35-
HttpServer::new(Endpoint::builder().bind(FailureExampleImpl.serve()).build())
31+
HttpServer::new(Endpoint::builder().bind(FailureExample).build())
3632
.listen_and_serve("0.0.0.0:9080".parse().unwrap())
3733
.await;
3834
}

examples/fan_out.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,11 @@ use std::time::Duration;
1212
/// ```shell
1313
/// $ curl -v http://localhost:8080/FanOut/fan_out
1414
/// ```
15-
#[restate_sdk::service]
16-
trait FanOut {
17-
async fn fan_out() -> Result<String, TerminalError>;
18-
}
19-
20-
struct FanOutImpl;
15+
struct FanOut;
2116

22-
impl FanOut for FanOutImpl {
17+
#[service]
18+
impl FanOut {
19+
#[handler]
2320
async fn fan_out(&self, ctx: Context<'_>) -> Result<String, TerminalError> {
2421
let labels = ["fast", "medium", "slow"];
2522

@@ -43,7 +40,7 @@ impl FanOut for FanOutImpl {
4340
#[tokio::main]
4441
async fn main() {
4542
tracing_subscriber::fmt::init();
46-
HttpServer::new(Endpoint::builder().bind(FanOutImpl.serve()).build())
43+
HttpServer::new(Endpoint::builder().bind(FanOut).build())
4744
.listen_and_serve("0.0.0.0:9080".parse().unwrap())
4845
.await;
4946
}

examples/greeter.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
use restate_sdk::prelude::*;
22
use std::convert::Infallible;
33

4-
#[restate_sdk::service]
5-
trait Greeter {
6-
async fn greet(name: String) -> Result<String, Infallible>;
7-
}
8-
9-
struct GreeterImpl;
4+
struct Greeter;
105

11-
impl Greeter for GreeterImpl {
12-
async fn greet(&self, _: Context<'_>, name: String) -> Result<String, Infallible> {
6+
#[service]
7+
impl Greeter {
8+
#[handler]
9+
async fn greet(&self, _ctx: Context<'_>, name: String) -> Result<String, Infallible> {
1310
Ok(format!("Greetings {name}"))
1411
}
1512
}
1613

1714
#[tokio::main]
1815
async fn main() {
1916
tracing_subscriber::fmt::init();
20-
HttpServer::new(Endpoint::builder().bind(GreeterImpl.serve()).build())
17+
HttpServer::new(Endpoint::builder().bind(Greeter).build())
2118
.listen_and_serve("0.0.0.0:9080".parse().unwrap())
2219
.await;
2320
}

examples/run.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
use restate_sdk::prelude::*;
22
use std::collections::HashMap;
33

4-
#[restate_sdk::service]
5-
trait RunExample {
6-
async fn do_run() -> Result<Json<HashMap<String, String>>, HandlerError>;
7-
}
8-
9-
struct RunExampleImpl(reqwest::Client);
4+
struct RunExample(reqwest::Client);
105

11-
impl RunExample for RunExampleImpl {
6+
#[service]
7+
impl RunExample {
8+
#[handler]
129
async fn do_run(
1310
&self,
1411
context: Context<'_>,
@@ -39,7 +36,7 @@ async fn main() {
3936
tracing_subscriber::fmt::init();
4037
HttpServer::new(
4138
Endpoint::builder()
42-
.bind(RunExampleImpl(reqwest::Client::new()).serve())
39+
.bind(RunExample(reqwest::Client::new()))
4340
.build(),
4441
)
4542
.listen_and_serve("0.0.0.0:9080".parse().unwrap())

examples/schema.rs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,17 @@ use serde::{Deserialize, Serialize};
1010
use std::time::Duration;
1111

1212
#[derive(Serialize, Deserialize, JsonSchema)]
13-
struct Product {
13+
pub struct Product {
1414
id: String,
1515
name: String,
1616
price_cents: u32,
1717
}
1818

19-
#[restate_sdk::service]
20-
trait CatalogService {
21-
async fn get_product_by_id(product_id: String) -> Result<Json<Product>, HandlerError>;
22-
async fn save_product(product: Json<Product>) -> Result<String, HandlerError>;
23-
async fn is_in_stock(product_id: String) -> Result<bool, HandlerError>;
24-
}
25-
26-
struct CatalogServiceImpl;
19+
struct CatalogService;
2720

28-
impl CatalogService for CatalogServiceImpl {
21+
#[service]
22+
impl CatalogService {
23+
#[handler]
2924
async fn get_product_by_id(
3025
&self,
3126
ctx: Context<'_>,
@@ -39,6 +34,7 @@ impl CatalogService for CatalogServiceImpl {
3934
}))
4035
}
4136

37+
#[handler]
4238
async fn save_product(
4339
&self,
4440
_ctx: Context<'_>,
@@ -47,6 +43,7 @@ impl CatalogService for CatalogServiceImpl {
4743
Ok(product.0.id)
4844
}
4945

46+
#[handler]
5047
async fn is_in_stock(
5148
&self,
5249
_ctx: Context<'_>,
@@ -59,7 +56,7 @@ impl CatalogService for CatalogServiceImpl {
5956
#[tokio::main]
6057
async fn main() {
6158
tracing_subscriber::fmt::init();
62-
HttpServer::new(Endpoint::builder().bind(CatalogServiceImpl.serve()).build())
59+
HttpServer::new(Endpoint::builder().bind(CatalogService).build())
6360
.listen_and_serve("0.0.0.0:9080".parse().unwrap())
6461
.await;
6562
}

examples/services/my_service.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
use restate_sdk::prelude::*;
22

3-
#[restate_sdk::service]
4-
pub trait MyService {
5-
async fn my_handler(greeting: String) -> Result<String, HandlerError>;
6-
}
7-
8-
pub struct MyServiceImpl;
3+
pub struct MyService;
94

10-
impl MyService for MyServiceImpl {
5+
#[service]
6+
impl MyService {
7+
#[handler]
118
async fn my_handler(&self, _ctx: Context<'_>, greeting: String) -> Result<String, HandlerError> {
129
Ok(format!("{greeting}!"))
1310
}
@@ -16,7 +13,7 @@ impl MyService for MyServiceImpl {
1613
#[tokio::main]
1714
async fn main() {
1815
tracing_subscriber::fmt::init();
19-
HttpServer::new(Endpoint::builder().bind(MyServiceImpl.serve()).build())
16+
HttpServer::new(Endpoint::builder().bind(MyService).build())
2017
.listen_and_serve("0.0.0.0:9080".parse().unwrap())
2118
.await;
2219
}

0 commit comments

Comments
 (0)