Skip to main content

fakecloud_core/
service.rs

1use async_trait::async_trait;
2use bytes::Bytes;
3use http::{HeaderMap, Method, StatusCode};
4use std::collections::HashMap;
5
6/// A parsed AWS request.
7#[derive(Debug)]
8pub struct AwsRequest {
9    pub service: String,
10    pub action: String,
11    pub region: String,
12    pub account_id: String,
13    pub request_id: String,
14    pub headers: HeaderMap,
15    pub query_params: HashMap<String, String>,
16    pub body: Bytes,
17    pub path_segments: Vec<String>,
18    /// The raw URI path, before splitting into segments.
19    pub raw_path: String,
20    /// The raw URI query string (everything after `?`), preserving repeated keys.
21    pub raw_query: String,
22    pub method: Method,
23    /// Whether this request came via Query (form-encoded) or JSON protocol.
24    pub is_query_protocol: bool,
25    /// The access key ID from the SigV4 Authorization header, if present.
26    pub access_key_id: Option<String>,
27}
28
29impl AwsRequest {
30    /// Parse the request body as JSON, returning `Value::Null` on failure.
31    pub fn json_body(&self) -> serde_json::Value {
32        serde_json::from_slice(&self.body).unwrap_or(serde_json::Value::Null)
33    }
34}
35
36/// A response body. Most handlers return [`ResponseBody::Bytes`] built from
37/// an in-memory [`Bytes`] buffer; the [`File`](ResponseBody::File) variant
38/// exists so large disk-backed objects can be streamed straight from the
39/// filesystem to the HTTP body without being materialized into RAM. The file
40/// handle is opened by the service handler while it still holds the
41/// per-bucket read guard, so the reader sees a consistent inode even if a
42/// concurrent PUT/DELETE renames or unlinks the path before dispatch streams
43/// the body.
44#[derive(Debug)]
45pub enum ResponseBody {
46    Bytes(Bytes),
47    File { file: tokio::fs::File, size: u64 },
48}
49
50impl ResponseBody {
51    pub fn len(&self) -> u64 {
52        match self {
53            ResponseBody::Bytes(b) => b.len() as u64,
54            ResponseBody::File { size, .. } => *size,
55        }
56    }
57
58    pub fn is_empty(&self) -> bool {
59        self.len() == 0
60    }
61
62    /// Accessor that returns the bytes of a `Bytes` variant and panics for
63    /// `File`. Used by tests and by callers that know the response was built
64    /// from an in-memory buffer (JSON handlers, cross-service glue).
65    pub fn expect_bytes(&self) -> &[u8] {
66        match self {
67            ResponseBody::Bytes(b) => b,
68            ResponseBody::File { .. } => {
69                panic!("expect_bytes called on ResponseBody::File")
70            }
71        }
72    }
73}
74
75impl Default for ResponseBody {
76    fn default() -> Self {
77        ResponseBody::Bytes(Bytes::new())
78    }
79}
80
81impl From<Bytes> for ResponseBody {
82    fn from(b: Bytes) -> Self {
83        ResponseBody::Bytes(b)
84    }
85}
86
87impl From<Vec<u8>> for ResponseBody {
88    fn from(v: Vec<u8>) -> Self {
89        ResponseBody::Bytes(Bytes::from(v))
90    }
91}
92
93impl From<&'static [u8]> for ResponseBody {
94    fn from(s: &'static [u8]) -> Self {
95        ResponseBody::Bytes(Bytes::from_static(s))
96    }
97}
98
99impl From<String> for ResponseBody {
100    fn from(s: String) -> Self {
101        ResponseBody::Bytes(Bytes::from(s))
102    }
103}
104
105impl From<&'static str> for ResponseBody {
106    fn from(s: &'static str) -> Self {
107        ResponseBody::Bytes(Bytes::from_static(s.as_bytes()))
108    }
109}
110
111impl PartialEq<Bytes> for ResponseBody {
112    fn eq(&self, other: &Bytes) -> bool {
113        match self {
114            ResponseBody::Bytes(b) => b == other,
115            ResponseBody::File { .. } => false,
116        }
117    }
118}
119
120/// A response from a service handler.
121pub struct AwsResponse {
122    pub status: StatusCode,
123    pub content_type: String,
124    pub body: ResponseBody,
125    pub headers: HeaderMap,
126}
127
128impl AwsResponse {
129    pub fn xml(status: StatusCode, body: impl Into<Bytes>) -> Self {
130        Self {
131            status,
132            content_type: "text/xml".to_string(),
133            body: ResponseBody::Bytes(body.into()),
134            headers: HeaderMap::new(),
135        }
136    }
137
138    pub fn json(status: StatusCode, body: impl Into<Bytes>) -> Self {
139        Self {
140            status,
141            content_type: "application/x-amz-json-1.1".to_string(),
142            body: ResponseBody::Bytes(body.into()),
143            headers: HeaderMap::new(),
144        }
145    }
146
147    /// Convenience constructor for a 200 OK JSON response from a `serde_json::Value`.
148    pub fn ok_json(value: serde_json::Value) -> Self {
149        Self::json(StatusCode::OK, serde_json::to_vec(&value).unwrap())
150    }
151}
152
153/// Error returned by service handlers.
154#[derive(Debug, thiserror::Error)]
155pub enum AwsServiceError {
156    #[error("service not found: {service}")]
157    ServiceNotFound { service: String },
158
159    #[error("action {action} not implemented for service {service}")]
160    ActionNotImplemented { service: String, action: String },
161
162    #[error("{code}: {message}")]
163    AwsError {
164        status: StatusCode,
165        code: String,
166        message: String,
167        /// Additional key-value pairs to include in the error XML (e.g., BucketName, Key, Condition).
168        extra_fields: Vec<(String, String)>,
169        /// Additional HTTP headers to include in the error response.
170        headers: Vec<(String, String)>,
171    },
172}
173
174impl AwsServiceError {
175    pub fn action_not_implemented(service: &str, action: &str) -> Self {
176        Self::ActionNotImplemented {
177            service: service.to_string(),
178            action: action.to_string(),
179        }
180    }
181
182    pub fn aws_error(
183        status: StatusCode,
184        code: impl Into<String>,
185        message: impl Into<String>,
186    ) -> Self {
187        Self::AwsError {
188            status,
189            code: code.into(),
190            message: message.into(),
191            extra_fields: Vec::new(),
192            headers: Vec::new(),
193        }
194    }
195
196    pub fn aws_error_with_fields(
197        status: StatusCode,
198        code: impl Into<String>,
199        message: impl Into<String>,
200        extra_fields: Vec<(String, String)>,
201    ) -> Self {
202        Self::AwsError {
203            status,
204            code: code.into(),
205            message: message.into(),
206            extra_fields,
207            headers: Vec::new(),
208        }
209    }
210
211    pub fn aws_error_with_headers(
212        status: StatusCode,
213        code: impl Into<String>,
214        message: impl Into<String>,
215        headers: Vec<(String, String)>,
216    ) -> Self {
217        Self::AwsError {
218            status,
219            code: code.into(),
220            message: message.into(),
221            extra_fields: Vec::new(),
222            headers,
223        }
224    }
225
226    pub fn extra_fields(&self) -> &[(String, String)] {
227        match self {
228            Self::AwsError { extra_fields, .. } => extra_fields,
229            _ => &[],
230        }
231    }
232
233    pub fn status(&self) -> StatusCode {
234        match self {
235            Self::ServiceNotFound { .. } => StatusCode::BAD_REQUEST,
236            Self::ActionNotImplemented { .. } => StatusCode::NOT_IMPLEMENTED,
237            Self::AwsError { status, .. } => *status,
238        }
239    }
240
241    pub fn code(&self) -> &str {
242        match self {
243            Self::ServiceNotFound { .. } => "UnknownService",
244            Self::ActionNotImplemented { .. } => "InvalidAction",
245            Self::AwsError { code, .. } => code,
246        }
247    }
248
249    pub fn message(&self) -> String {
250        match self {
251            Self::ServiceNotFound { service } => format!("service not found: {service}"),
252            Self::ActionNotImplemented { service, action } => {
253                format!("action {action} not implemented for service {service}")
254            }
255            Self::AwsError { message, .. } => message.clone(),
256        }
257    }
258
259    pub fn response_headers(&self) -> &[(String, String)] {
260        match self {
261            Self::AwsError { headers, .. } => headers,
262            _ => &[],
263        }
264    }
265}
266
267/// Trait that every AWS service implements.
268#[async_trait]
269pub trait AwsService: Send + Sync {
270    /// The AWS service identifier (e.g., "sqs", "sns", "sts", "events", "ssm").
271    fn service_name(&self) -> &str;
272
273    /// Handle an incoming request.
274    async fn handle(&self, request: AwsRequest) -> Result<AwsResponse, AwsServiceError>;
275
276    /// List of actions this service supports (for introspection).
277    fn supported_actions(&self) -> &[&str];
278}