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 from a service handler.
37pub struct AwsResponse {
38    pub status: StatusCode,
39    pub content_type: String,
40    pub body: Bytes,
41    pub headers: HeaderMap,
42}
43
44impl AwsResponse {
45    pub fn xml(status: StatusCode, body: impl Into<Bytes>) -> Self {
46        Self {
47            status,
48            content_type: "text/xml".to_string(),
49            body: body.into(),
50            headers: HeaderMap::new(),
51        }
52    }
53
54    pub fn json(status: StatusCode, body: impl Into<Bytes>) -> Self {
55        Self {
56            status,
57            content_type: "application/x-amz-json-1.1".to_string(),
58            body: body.into(),
59            headers: HeaderMap::new(),
60        }
61    }
62
63    /// Convenience constructor for a 200 OK JSON response from a `serde_json::Value`.
64    pub fn ok_json(value: serde_json::Value) -> Self {
65        Self::json(StatusCode::OK, serde_json::to_vec(&value).unwrap())
66    }
67}
68
69/// Error returned by service handlers.
70#[derive(Debug, thiserror::Error)]
71pub enum AwsServiceError {
72    #[error("service not found: {service}")]
73    ServiceNotFound { service: String },
74
75    #[error("action {action} not implemented for service {service}")]
76    ActionNotImplemented { service: String, action: String },
77
78    #[error("{code}: {message}")]
79    AwsError {
80        status: StatusCode,
81        code: String,
82        message: String,
83        /// Additional key-value pairs to include in the error XML (e.g., BucketName, Key, Condition).
84        extra_fields: Vec<(String, String)>,
85        /// Additional HTTP headers to include in the error response.
86        headers: Vec<(String, String)>,
87    },
88}
89
90impl AwsServiceError {
91    pub fn action_not_implemented(service: &str, action: &str) -> Self {
92        Self::ActionNotImplemented {
93            service: service.to_string(),
94            action: action.to_string(),
95        }
96    }
97
98    pub fn aws_error(
99        status: StatusCode,
100        code: impl Into<String>,
101        message: impl Into<String>,
102    ) -> Self {
103        Self::AwsError {
104            status,
105            code: code.into(),
106            message: message.into(),
107            extra_fields: Vec::new(),
108            headers: Vec::new(),
109        }
110    }
111
112    pub fn aws_error_with_fields(
113        status: StatusCode,
114        code: impl Into<String>,
115        message: impl Into<String>,
116        extra_fields: Vec<(String, String)>,
117    ) -> Self {
118        Self::AwsError {
119            status,
120            code: code.into(),
121            message: message.into(),
122            extra_fields,
123            headers: Vec::new(),
124        }
125    }
126
127    pub fn aws_error_with_headers(
128        status: StatusCode,
129        code: impl Into<String>,
130        message: impl Into<String>,
131        headers: Vec<(String, String)>,
132    ) -> Self {
133        Self::AwsError {
134            status,
135            code: code.into(),
136            message: message.into(),
137            extra_fields: Vec::new(),
138            headers,
139        }
140    }
141
142    pub fn extra_fields(&self) -> &[(String, String)] {
143        match self {
144            Self::AwsError { extra_fields, .. } => extra_fields,
145            _ => &[],
146        }
147    }
148
149    pub fn status(&self) -> StatusCode {
150        match self {
151            Self::ServiceNotFound { .. } => StatusCode::BAD_REQUEST,
152            Self::ActionNotImplemented { .. } => StatusCode::NOT_IMPLEMENTED,
153            Self::AwsError { status, .. } => *status,
154        }
155    }
156
157    pub fn code(&self) -> &str {
158        match self {
159            Self::ServiceNotFound { .. } => "UnknownService",
160            Self::ActionNotImplemented { .. } => "InvalidAction",
161            Self::AwsError { code, .. } => code,
162        }
163    }
164
165    pub fn message(&self) -> String {
166        match self {
167            Self::ServiceNotFound { service } => format!("service not found: {service}"),
168            Self::ActionNotImplemented { service, action } => {
169                format!("action {action} not implemented for service {service}")
170            }
171            Self::AwsError { message, .. } => message.clone(),
172        }
173    }
174
175    pub fn response_headers(&self) -> &[(String, String)] {
176        match self {
177            Self::AwsError { headers, .. } => headers,
178            _ => &[],
179        }
180    }
181}
182
183/// Trait that every AWS service implements.
184#[async_trait]
185pub trait AwsService: Send + Sync {
186    /// The AWS service identifier (e.g., "sqs", "sns", "sts", "events", "ssm").
187    fn service_name(&self) -> &str;
188
189    /// Handle an incoming request.
190    async fn handle(&self, request: AwsRequest) -> Result<AwsResponse, AwsServiceError>;
191
192    /// List of actions this service supports (for introspection).
193    fn supported_actions(&self) -> &[&str];
194}