little_durable_objects/
state_transport.rs1use anyhow::{Context, Result, ensure};
2use async_trait::async_trait;
3use bytes::Bytes;
4
5use crate::storage_urls::STATE_CONTENT_TYPE;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub enum StateWrite {
9 Written,
10 AlreadyExists,
11}
12
13#[async_trait]
14pub trait StateTransport: Send + Sync {
15 async fn read(&self, signed_url: &str) -> Result<Bytes>;
16 async fn write(&self, signed_url: &str, bytes: Vec<u8>) -> Result<StateWrite>;
17}
18
19#[derive(Clone, Default)]
20pub struct HttpStateTransport {
21 client: reqwest::Client,
22}
23
24impl HttpStateTransport {
25 pub fn new() -> Self {
26 Self {
27 client: reqwest::Client::new(),
28 }
29 }
30}
31
32#[async_trait]
33impl StateTransport for HttpStateTransport {
34 async fn read(&self, signed_url: &str) -> Result<Bytes> {
35 validate_url(signed_url)?;
36 let response = self
37 .client
38 .get(signed_url)
39 .send()
40 .await
41 .context("read actor state through signed URL")?;
42 ensure!(
43 response.status().is_success(),
44 "actor-state read failed with HTTP {}",
45 response.status()
46 );
47 response
48 .bytes()
49 .await
50 .context("read actor-state response body")
51 }
52
53 async fn write(&self, signed_url: &str, bytes: Vec<u8>) -> Result<StateWrite> {
54 validate_url(signed_url)?;
55 let response = self
56 .client
57 .put(signed_url)
58 .header(reqwest::header::CONTENT_TYPE, STATE_CONTENT_TYPE)
59 .body(bytes)
60 .send()
61 .await
62 .context("write actor state through signed URL")?;
63 if response.status() == reqwest::StatusCode::PRECONDITION_FAILED {
64 return Ok(StateWrite::AlreadyExists);
65 }
66 ensure!(
67 response.status().is_success(),
68 "actor-state write failed with HTTP {}",
69 response.status()
70 );
71 Ok(StateWrite::Written)
72 }
73}
74
75fn validate_url(url: &str) -> Result<()> {
76 let url = reqwest::Url::parse(url).context("parse signed actor-state URL")?;
77 ensure!(
78 matches!(url.scheme(), "http" | "https") && url.host_str().is_some(),
79 "signed actor-state URL must be HTTP or HTTPS"
80 );
81 Ok(())
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn rejects_non_http_state_urls() {
90 assert!(validate_url("file:///tmp/state").is_err());
91 }
92}