Skip to content

Commit 65d9321

Browse files
Schemas and other payload metadata (#59)
* Enhance discovery with I/O JSON schemas * Implemented empty schema specs for inputs/outputs, improved documentation organization * Reorganized the `WithSchema` trait together with `WithContentType` in a single trait called `PayloadMetadata`. Those logically belong together, and are used really for describing payloads. * schemars feature is implicitly declared by cargo, no need to manually specify it * Enable in test-services the schemars feature, this should give it a good coverage * Enable in schema example the schemars feature, also improve the example a bit --------- Co-authored-by: Hxphsts <hxphsts@kernelx.ai>
1 parent 0e708cd commit 65d9321

File tree

14 files changed

+645
-50
lines changed

14 files changed

+645
-50
lines changed

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ name = "tracing"
1212
path = "examples/tracing.rs"
1313
required-features = ["tracing-span-filter"]
1414

15+
[[example]]
16+
name = "schema"
17+
path = "examples/schema.rs"
18+
required-features = ["schemars"]
19+
1520
[features]
1621
default = ["http_server", "rand", "uuid", "tracing-span-filter"]
1722
hyper = ["dep:hyper", "http-body-util", "restate-sdk-shared-core/http"]
@@ -30,6 +35,7 @@ rand = { version = "0.9", optional = true }
3035
regress = "0.10"
3136
restate-sdk-macros = { version = "0.4", path = "macros" }
3237
restate-sdk-shared-core = { version = "0.3.0", features = ["request_identity", "sha2_random_seed", "http"] }
38+
schemars = { version = "1.0.0-alpha.17", optional = true }
3339
serde = "1.0"
3440
serde_json = "1.0"
3541
thiserror = "2.0"
@@ -44,6 +50,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "registry"] }
4450
trybuild = "1.0"
4551
reqwest = { version = "0.12", features = ["json"] }
4652
rand = "0.9"
53+
schemars = "1.0.0-alpha.17"
4754

4855
[build-dependencies]
4956
jsonptr = "0.5.1"

examples/schema.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//! Run with auto-generated schemas for `Json<Product>` using `schemars`:
2+
//! cargo run --example schema --features schemars
3+
//!
4+
//! Run with primitive schemas only:
5+
//! cargo run --example schema
6+
7+
use restate_sdk::prelude::*;
8+
use schemars::JsonSchema;
9+
use serde::{Deserialize, Serialize};
10+
use std::time::Duration;
11+
12+
#[derive(Serialize, Deserialize, JsonSchema)]
13+
struct Product {
14+
id: String,
15+
name: String,
16+
price_cents: u32,
17+
}
18+
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;
27+
28+
impl CatalogService for CatalogServiceImpl {
29+
async fn get_product_by_id(
30+
&self,
31+
ctx: Context<'_>,
32+
product_id: String,
33+
) -> Result<Json<Product>, HandlerError> {
34+
ctx.sleep(Duration::from_millis(50)).await?;
35+
Ok(Json(Product {
36+
id: product_id,
37+
name: "Sample Product".to_string(),
38+
price_cents: 1995,
39+
}))
40+
}
41+
42+
async fn save_product(
43+
&self,
44+
_ctx: Context<'_>,
45+
product: Json<Product>,
46+
) -> Result<String, HandlerError> {
47+
Ok(product.0.id)
48+
}
49+
50+
async fn is_in_stock(
51+
&self,
52+
_ctx: Context<'_>,
53+
product_id: String,
54+
) -> Result<bool, HandlerError> {
55+
Ok(!product_id.contains("out-of-stock"))
56+
}
57+
}
58+
59+
#[tokio::main]
60+
async fn main() {
61+
tracing_subscriber::fmt::init();
62+
HttpServer::new(Endpoint::builder().bind(CatalogServiceImpl.serve()).build())
63+
.listen_and_serve("0.0.0.0:9080".parse().unwrap())
64+
.await;
65+
}

macros/src/gen.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,32 @@ impl<'a> ServiceGenerator<'a> {
194194
quote! { None }
195195
};
196196

197+
let input_schema = match &handler.arg {
198+
Some(PatType { ty, .. }) => {
199+
quote! {
200+
Some(::restate_sdk::discovery::InputPayload::from_metadata::<#ty>())
201+
}
202+
}
203+
None => quote! {
204+
Some(::restate_sdk::discovery::InputPayload::empty())
205+
}
206+
};
207+
208+
let output_ty = &handler.output_ok;
209+
let output_schema = match output_ty {
210+
syn::Type::Tuple(tuple) if tuple.elems.is_empty() => quote! {
211+
Some(::restate_sdk::discovery::OutputPayload::empty())
212+
},
213+
_ => quote! {
214+
Some(::restate_sdk::discovery::OutputPayload::from_metadata::<#output_ty>())
215+
}
216+
};
217+
197218
quote! {
198219
::restate_sdk::discovery::Handler {
199220
name: ::restate_sdk::discovery::HandlerName::try_from(#handler_literal).expect("Handler name valid"),
200-
input: None,
201-
output: None,
221+
input: #input_schema,
222+
output: #output_schema,
202223
ty: #handler_ty,
203224
}
204225
}

src/discovery.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,43 @@ mod generated {
88
}
99

1010
pub use generated::*;
11+
12+
use crate::serde::PayloadMetadata;
13+
14+
impl InputPayload {
15+
pub fn empty() -> Self {
16+
Self {
17+
content_type: None,
18+
json_schema: None,
19+
required: None,
20+
}
21+
}
22+
23+
pub fn from_metadata<T: PayloadMetadata>() -> Self {
24+
let input_metadata = T::input_metadata();
25+
Self {
26+
content_type: Some(input_metadata.accept_content_type.to_owned()),
27+
json_schema: T::json_schema(),
28+
required: Some(input_metadata.is_required),
29+
}
30+
}
31+
}
32+
33+
impl OutputPayload {
34+
pub fn empty() -> Self {
35+
Self {
36+
content_type: None,
37+
json_schema: None,
38+
set_content_type_if_empty: Some(false),
39+
}
40+
}
41+
42+
pub fn from_metadata<T: PayloadMetadata>() -> Self {
43+
let output_metadata = T::output_metadata();
44+
Self {
45+
content_type: Some(output_metadata.content_type.to_owned()),
46+
json_schema: T::json_schema(),
47+
set_content_type_if_empty: Some(output_metadata.set_content_type_if_empty),
48+
}
49+
}
50+
}

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
//! - [Scheduling & Timers][crate::context::ContextTimers]: Let a handler pause for a certain amount of time. Restate durably tracks the timer across failures.
2828
//! - [Awakeables][crate::context::ContextAwakeables]: Durable Futures to wait for events and the completion of external tasks.
2929
//! - [Error Handling][crate::errors]: Restate retries failures infinitely. Use `TerminalError` to stop retries.
30-
//! - [Serialization][crate::serde]: The SDK serializes results to send them to the Server.
30+
//! - [Serialization][crate::serde]: The SDK serializes results to send them to the Server. Includes [Schema Generation and payload metadata](crate::serde::PayloadMetadata) for documentation & discovery.
3131
//! - [Serving][crate::http_server]: Start an HTTP server to expose services.
3232
//!
3333
//! # SDK Overview

0 commit comments

Comments
 (0)