Skip to main content

winterbaume_core/
service.rs

1//! Service trait for pluggable AWS service backends.
2
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6
7use bytes::Bytes;
8use http::HeaderMap;
9
10/// An incoming mock AWS request, parsed from the SDK's HTTP request.
11#[derive(Debug, Clone)]
12pub struct MockRequest {
13    pub method: String,
14    pub uri: String,
15    pub headers: HeaderMap,
16    pub body: Bytes,
17}
18
19/// A mock AWS response to return to the SDK.
20#[derive(Debug, Clone)]
21pub struct MockResponse {
22    pub status: u16,
23    pub headers: HeaderMap,
24    pub body: Bytes,
25}
26
27impl MockResponse {
28    /// Create an XML response.
29    pub fn xml(status: u16, body: impl Into<Bytes>) -> Self {
30        let mut headers = HeaderMap::new();
31        headers.insert(http::header::CONTENT_TYPE, "text/xml".parse().unwrap());
32        Self {
33            status,
34            headers,
35            body: body.into(),
36        }
37    }
38
39    /// Create a JSON response (awsJson1.0/1.1 protocol).
40    pub fn json(status: u16, body: impl Into<Bytes>) -> Self {
41        let mut headers = HeaderMap::new();
42        headers.insert(
43            http::header::CONTENT_TYPE,
44            "application/x-amz-json-1.0".parse().unwrap(),
45        );
46        Self {
47            status,
48            headers,
49            body: body.into(),
50        }
51    }
52
53    /// Create a REST JSON response (restJson1 protocol).
54    /// Uses `application/json` content type instead of `application/x-amz-json-1.0`.
55    pub fn rest_json(status: u16, body: impl Into<Bytes>) -> Self {
56        let mut headers = HeaderMap::new();
57        headers.insert(
58            http::header::CONTENT_TYPE,
59            "application/json".parse().unwrap(),
60        );
61        Self {
62            status,
63            headers,
64            body: body.into(),
65        }
66    }
67
68    /// Create a CBOR response (rpc-v2-cbor protocol).
69    /// Sets `Content-Type: application/cbor` and `smithy-protocol: rpc-v2-cbor`.
70    pub fn cbor(status: u16, body: impl Into<Bytes>) -> Self {
71        let mut headers = HeaderMap::new();
72        headers.insert(
73            http::header::CONTENT_TYPE,
74            "application/cbor".parse().unwrap(),
75        );
76        headers.insert("smithy-protocol", "rpc-v2-cbor".parse().unwrap());
77        Self {
78            status,
79            headers,
80            body: body.into(),
81        }
82    }
83
84    /// Create an error response (generic, no service-specific namespace).
85    pub fn error(status: u16, code: &str, message: &str) -> Self {
86        let body = format!(
87            r#"<ErrorResponse>
88  <Error>
89    <Type>Sender</Type>
90    <Code>{code}</Code>
91    <Message>{message}</Message>
92  </Error>
93  <RequestId>00000000-0000-0000-0000-000000000000</RequestId>
94</ErrorResponse>"#
95        );
96        Self::xml(status, body)
97    }
98}
99
100/// A generic stub service that returns 501 Not Implemented for all requests.
101///
102/// Used to register services that are recognized (routable) but not yet
103/// fully implemented. This ensures the router returns a proper "not implemented"
104/// error instead of "unknown service".
105pub struct StubService {
106    name: String,
107    patterns: Vec<String>,
108}
109
110impl StubService {
111    /// Create a new stub service with the given service name and URL patterns.
112    pub fn new(name: impl Into<String>, patterns: Vec<String>) -> Self {
113        Self {
114            name: name.into(),
115            patterns,
116        }
117    }
118}
119
120impl MockService for StubService {
121    fn service_name(&self) -> &str {
122        &self.name
123    }
124
125    fn url_patterns(&self) -> Vec<&str> {
126        self.patterns.iter().map(|s| s.as_str()).collect()
127    }
128
129    fn handle(
130        &self,
131        _request: MockRequest,
132    ) -> Pin<Box<dyn Future<Output = MockResponse> + Send + '_>> {
133        let name = self.name.clone();
134        Box::pin(async move {
135            MockResponse::json(
136                501,
137                format!(
138                    r#"{{"__type":"NotImplementedException","message":"Service '{}' is recognized but not yet implemented in winterbaume"}}"#,
139                    name
140                ),
141            )
142        })
143    }
144}
145
146/// Trait for a mock AWS service backend.
147///
148/// Each service (STS, IAM, S3, etc.) implements this trait.
149pub trait MockService: Send + Sync + 'static {
150    /// The service identifier (e.g., "sts", "iam", "s3").
151    fn service_name(&self) -> &str;
152
153    /// URL patterns this service handles (as regex strings).
154    fn url_patterns(&self) -> Vec<&str>;
155
156    /// Handle an incoming request and produce a response.
157    fn handle(
158        &self,
159        request: MockRequest,
160    ) -> Pin<Box<dyn Future<Output = MockResponse> + Send + '_>>;
161}
162
163/// Allow `Arc<S>` to be registered as a service so callers can keep a
164/// shared handle (e.g. for inspecting state via `StatefulService::snapshot`)
165/// while still passing the service to `MockAws::builder().with_service(...)`.
166impl<T: MockService> MockService for Arc<T> {
167    fn service_name(&self) -> &str {
168        (**self).service_name()
169    }
170
171    fn url_patterns(&self) -> Vec<&str> {
172        (**self).url_patterns()
173    }
174
175    fn handle(
176        &self,
177        request: MockRequest,
178    ) -> Pin<Box<dyn Future<Output = MockResponse> + Send + '_>> {
179        (**self).handle(request)
180    }
181}