Skip to main content

fakecloud_sdk/
client.rs

1use crate::error::Error;
2use crate::types::*;
3
4/// Client for the fakecloud introspection and simulation API (`/_fakecloud/*`).
5pub struct FakeCloud {
6    base_url: String,
7    client: reqwest::Client,
8}
9
10impl FakeCloud {
11    /// Create a new client pointing at the given fakecloud base URL (e.g. `http://localhost:4566`).
12    pub fn new(base_url: &str) -> Self {
13        Self {
14            base_url: base_url.trim_end_matches('/').to_string(),
15            client: reqwest::Client::new(),
16        }
17    }
18
19    // ── Health & Reset ──────────────────────────────────────────────
20
21    /// Check server health.
22    pub async fn health(&self) -> Result<HealthResponse, Error> {
23        let resp = self
24            .client
25            .get(format!("{}/_fakecloud/health", self.base_url))
26            .send()
27            .await?;
28        Self::parse(resp).await
29    }
30
31    /// Reset all service state. Uses the legacy `/_reset` endpoint.
32    pub async fn reset(&self) -> Result<ResetResponse, Error> {
33        let resp = self
34            .client
35            .post(format!("{}/_reset", self.base_url))
36            .send()
37            .await?;
38        Self::parse(resp).await
39    }
40
41    /// Reset a single service's state.
42    pub async fn reset_service(&self, service: &str) -> Result<ResetServiceResponse, Error> {
43        let resp = self
44            .client
45            .post(format!("{}/_fakecloud/reset/{}", self.base_url, service))
46            .send()
47            .await?;
48        Self::parse(resp).await
49    }
50
51    // ── Sub-clients ─────────────────────────────────────────────────
52
53    pub fn lambda(&self) -> LambdaClient<'_> {
54        LambdaClient { fc: self }
55    }
56
57    pub fn ses(&self) -> SesClient<'_> {
58        SesClient { fc: self }
59    }
60
61    pub fn sns(&self) -> SnsClient<'_> {
62        SnsClient { fc: self }
63    }
64
65    pub fn sqs(&self) -> SqsClient<'_> {
66        SqsClient { fc: self }
67    }
68
69    pub fn events(&self) -> EventsClient<'_> {
70        EventsClient { fc: self }
71    }
72
73    pub fn s3(&self) -> S3Client<'_> {
74        S3Client { fc: self }
75    }
76
77    pub fn dynamodb(&self) -> DynamoDbClient<'_> {
78        DynamoDbClient { fc: self }
79    }
80
81    pub fn secretsmanager(&self) -> SecretsManagerClient<'_> {
82        SecretsManagerClient { fc: self }
83    }
84
85    pub fn cognito(&self) -> CognitoClient<'_> {
86        CognitoClient { fc: self }
87    }
88
89    pub fn rds(&self) -> RdsClient<'_> {
90        RdsClient { fc: self }
91    }
92
93    pub fn elasticache(&self) -> ElastiCacheClient<'_> {
94        ElastiCacheClient { fc: self }
95    }
96
97    // ── Internal helpers ────────────────────────────────────────────
98
99    async fn parse<T: serde::de::DeserializeOwned>(resp: reqwest::Response) -> Result<T, Error> {
100        let status = resp.status().as_u16();
101        if !resp.status().is_success() {
102            let body = resp.text().await.unwrap_or_default();
103            return Err(Error::Api { status, body });
104        }
105        Ok(resp.json::<T>().await?)
106    }
107}
108
109// ── RDS ─────────────────────────────────────────────────────────────
110
111pub struct RdsClient<'a> {
112    fc: &'a FakeCloud,
113}
114
115impl RdsClient<'_> {
116    /// List fakecloud-managed RDS DB instances with runtime metadata.
117    pub async fn get_instances(&self) -> Result<RdsInstancesResponse, Error> {
118        let resp = self
119            .fc
120            .client
121            .get(format!("{}/_fakecloud/rds/instances", self.fc.base_url))
122            .send()
123            .await?;
124        FakeCloud::parse(resp).await
125    }
126}
127
128// ── ElastiCache ─────────────────────────────────────────────────────
129
130pub struct ElastiCacheClient<'a> {
131    fc: &'a FakeCloud,
132}
133
134impl ElastiCacheClient<'_> {
135    /// List fakecloud-managed ElastiCache cache clusters with runtime metadata.
136    pub async fn get_clusters(&self) -> Result<ElastiCacheClustersResponse, Error> {
137        let resp = self
138            .fc
139            .client
140            .get(format!(
141                "{}/_fakecloud/elasticache/clusters",
142                self.fc.base_url
143            ))
144            .send()
145            .await?;
146        FakeCloud::parse(resp).await
147    }
148
149    /// List fakecloud-managed ElastiCache replication groups with runtime metadata.
150    pub async fn get_replication_groups(
151        &self,
152    ) -> Result<ElastiCacheReplicationGroupsResponse, Error> {
153        let resp = self
154            .fc
155            .client
156            .get(format!(
157                "{}/_fakecloud/elasticache/replication-groups",
158                self.fc.base_url
159            ))
160            .send()
161            .await?;
162        FakeCloud::parse(resp).await
163    }
164
165    /// List fakecloud-managed ElastiCache serverless caches with runtime metadata.
166    pub async fn get_serverless_caches(
167        &self,
168    ) -> Result<ElastiCacheServerlessCachesResponse, Error> {
169        let resp = self
170            .fc
171            .client
172            .get(format!(
173                "{}/_fakecloud/elasticache/serverless-caches",
174                self.fc.base_url
175            ))
176            .send()
177            .await?;
178        FakeCloud::parse(resp).await
179    }
180}
181
182// ── Lambda ──────────────────────────────────────────────────────────
183
184pub struct LambdaClient<'a> {
185    fc: &'a FakeCloud,
186}
187
188impl LambdaClient<'_> {
189    /// List recorded Lambda invocations.
190    pub async fn get_invocations(&self) -> Result<LambdaInvocationsResponse, Error> {
191        let resp = self
192            .fc
193            .client
194            .get(format!(
195                "{}/_fakecloud/lambda/invocations",
196                self.fc.base_url
197            ))
198            .send()
199            .await?;
200        FakeCloud::parse(resp).await
201    }
202
203    /// List warm (cached) Lambda containers.
204    pub async fn get_warm_containers(&self) -> Result<WarmContainersResponse, Error> {
205        let resp = self
206            .fc
207            .client
208            .get(format!(
209                "{}/_fakecloud/lambda/warm-containers",
210                self.fc.base_url
211            ))
212            .send()
213            .await?;
214        FakeCloud::parse(resp).await
215    }
216
217    /// Evict the warm container for a specific function.
218    pub async fn evict_container(
219        &self,
220        function_name: &str,
221    ) -> Result<EvictContainerResponse, Error> {
222        let resp = self
223            .fc
224            .client
225            .post(format!(
226                "{}/_fakecloud/lambda/{}/evict-container",
227                self.fc.base_url, function_name
228            ))
229            .send()
230            .await?;
231        FakeCloud::parse(resp).await
232    }
233}
234
235// ── SES ─────────────────────────────────────────────────────────────
236
237pub struct SesClient<'a> {
238    fc: &'a FakeCloud,
239}
240
241impl SesClient<'_> {
242    /// List all sent emails.
243    pub async fn get_emails(&self) -> Result<SesEmailsResponse, Error> {
244        let resp = self
245            .fc
246            .client
247            .get(format!("{}/_fakecloud/ses/emails", self.fc.base_url))
248            .send()
249            .await?;
250        FakeCloud::parse(resp).await
251    }
252
253    /// Simulate an inbound email (SES receipt rules).
254    pub async fn simulate_inbound(
255        &self,
256        req: &InboundEmailRequest,
257    ) -> Result<InboundEmailResponse, Error> {
258        let resp = self
259            .fc
260            .client
261            .post(format!("{}/_fakecloud/ses/inbound", self.fc.base_url))
262            .json(req)
263            .send()
264            .await?;
265        FakeCloud::parse(resp).await
266    }
267}
268
269// ── SNS ─────────────────────────────────────────────────────────────
270
271pub struct SnsClient<'a> {
272    fc: &'a FakeCloud,
273}
274
275impl SnsClient<'_> {
276    /// List all published SNS messages.
277    pub async fn get_messages(&self) -> Result<SnsMessagesResponse, Error> {
278        let resp = self
279            .fc
280            .client
281            .get(format!("{}/_fakecloud/sns/messages", self.fc.base_url))
282            .send()
283            .await?;
284        FakeCloud::parse(resp).await
285    }
286
287    /// List subscriptions pending confirmation.
288    pub async fn get_pending_confirmations(&self) -> Result<PendingConfirmationsResponse, Error> {
289        let resp = self
290            .fc
291            .client
292            .get(format!(
293                "{}/_fakecloud/sns/pending-confirmations",
294                self.fc.base_url
295            ))
296            .send()
297            .await?;
298        FakeCloud::parse(resp).await
299    }
300
301    /// Confirm a pending subscription.
302    pub async fn confirm_subscription(
303        &self,
304        req: &ConfirmSubscriptionRequest,
305    ) -> Result<ConfirmSubscriptionResponse, Error> {
306        let resp = self
307            .fc
308            .client
309            .post(format!(
310                "{}/_fakecloud/sns/confirm-subscription",
311                self.fc.base_url
312            ))
313            .json(req)
314            .send()
315            .await?;
316        FakeCloud::parse(resp).await
317    }
318}
319
320// ── SQS ─────────────────────────────────────────────────────────────
321
322pub struct SqsClient<'a> {
323    fc: &'a FakeCloud,
324}
325
326impl SqsClient<'_> {
327    /// List all messages across all queues.
328    pub async fn get_messages(&self) -> Result<SqsMessagesResponse, Error> {
329        let resp = self
330            .fc
331            .client
332            .get(format!("{}/_fakecloud/sqs/messages", self.fc.base_url))
333            .send()
334            .await?;
335        FakeCloud::parse(resp).await
336    }
337
338    /// Tick the message expiration processor (expire visibility-timed-out messages).
339    pub async fn tick_expiration(&self) -> Result<ExpirationTickResponse, Error> {
340        let resp = self
341            .fc
342            .client
343            .post(format!(
344                "{}/_fakecloud/sqs/expiration-processor/tick",
345                self.fc.base_url
346            ))
347            .send()
348            .await?;
349        FakeCloud::parse(resp).await
350    }
351
352    /// Force all messages in a queue to its DLQ.
353    pub async fn force_dlq(&self, queue_name: &str) -> Result<ForceDlqResponse, Error> {
354        let resp = self
355            .fc
356            .client
357            .post(format!(
358                "{}/_fakecloud/sqs/{}/force-dlq",
359                self.fc.base_url, queue_name
360            ))
361            .send()
362            .await?;
363        FakeCloud::parse(resp).await
364    }
365}
366
367// ── EventBridge ─────────────────────────────────────────────────────
368
369pub struct EventsClient<'a> {
370    fc: &'a FakeCloud,
371}
372
373impl EventsClient<'_> {
374    /// Get event history and delivery records.
375    pub async fn get_history(&self) -> Result<EventHistoryResponse, Error> {
376        let resp = self
377            .fc
378            .client
379            .get(format!("{}/_fakecloud/events/history", self.fc.base_url))
380            .send()
381            .await?;
382        FakeCloud::parse(resp).await
383    }
384
385    /// Fire a specific EventBridge rule manually.
386    pub async fn fire_rule(&self, req: &FireRuleRequest) -> Result<FireRuleResponse, Error> {
387        let resp = self
388            .fc
389            .client
390            .post(format!("{}/_fakecloud/events/fire-rule", self.fc.base_url))
391            .json(req)
392            .send()
393            .await?;
394        FakeCloud::parse(resp).await
395    }
396}
397
398// ── S3 ──────────────────────────────────────────────────────────────
399
400pub struct S3Client<'a> {
401    fc: &'a FakeCloud,
402}
403
404impl S3Client<'_> {
405    /// List S3 notification events.
406    pub async fn get_notifications(&self) -> Result<S3NotificationsResponse, Error> {
407        let resp = self
408            .fc
409            .client
410            .get(format!("{}/_fakecloud/s3/notifications", self.fc.base_url))
411            .send()
412            .await?;
413        FakeCloud::parse(resp).await
414    }
415
416    /// Tick the S3 lifecycle processor.
417    pub async fn tick_lifecycle(&self) -> Result<LifecycleTickResponse, Error> {
418        let resp = self
419            .fc
420            .client
421            .post(format!(
422                "{}/_fakecloud/s3/lifecycle-processor/tick",
423                self.fc.base_url
424            ))
425            .send()
426            .await?;
427        FakeCloud::parse(resp).await
428    }
429}
430
431// ── DynamoDB ────────────────────────────────────────────────────────
432
433pub struct DynamoDbClient<'a> {
434    fc: &'a FakeCloud,
435}
436
437impl DynamoDbClient<'_> {
438    /// Tick the DynamoDB TTL processor.
439    pub async fn tick_ttl(&self) -> Result<TtlTickResponse, Error> {
440        let resp = self
441            .fc
442            .client
443            .post(format!(
444                "{}/_fakecloud/dynamodb/ttl-processor/tick",
445                self.fc.base_url
446            ))
447            .send()
448            .await?;
449        FakeCloud::parse(resp).await
450    }
451}
452
453// ── SecretsManager ──────────────────────────────────────────────────
454
455pub struct SecretsManagerClient<'a> {
456    fc: &'a FakeCloud,
457}
458
459impl SecretsManagerClient<'_> {
460    /// Tick the SecretsManager rotation scheduler.
461    pub async fn tick_rotation(&self) -> Result<RotationTickResponse, Error> {
462        let resp = self
463            .fc
464            .client
465            .post(format!(
466                "{}/_fakecloud/secretsmanager/rotation-scheduler/tick",
467                self.fc.base_url
468            ))
469            .send()
470            .await?;
471        FakeCloud::parse(resp).await
472    }
473}
474
475// ── Cognito ─────────────────────────────────────────────────────────
476
477pub struct CognitoClient<'a> {
478    fc: &'a FakeCloud,
479}
480
481impl CognitoClient<'_> {
482    /// Get confirmation codes for a specific user.
483    pub async fn get_user_codes(
484        &self,
485        pool_id: &str,
486        username: &str,
487    ) -> Result<UserConfirmationCodes, Error> {
488        let resp = self
489            .fc
490            .client
491            .get(format!(
492                "{}/_fakecloud/cognito/confirmation-codes/{}/{}",
493                self.fc.base_url, pool_id, username
494            ))
495            .send()
496            .await?;
497        FakeCloud::parse(resp).await
498    }
499
500    /// List all confirmation codes across all pools.
501    pub async fn get_confirmation_codes(&self) -> Result<ConfirmationCodesResponse, Error> {
502        let resp = self
503            .fc
504            .client
505            .get(format!(
506                "{}/_fakecloud/cognito/confirmation-codes",
507                self.fc.base_url
508            ))
509            .send()
510            .await?;
511        FakeCloud::parse(resp).await
512    }
513
514    /// Confirm a user (bypass email/phone verification).
515    pub async fn confirm_user(
516        &self,
517        req: &ConfirmUserRequest,
518    ) -> Result<ConfirmUserResponse, Error> {
519        let resp = self
520            .fc
521            .client
522            .post(format!(
523                "{}/_fakecloud/cognito/confirm-user",
524                self.fc.base_url
525            ))
526            .json(req)
527            .send()
528            .await?;
529        // This endpoint returns 404 for missing users but still has a JSON body
530        let status = resp.status().as_u16();
531        let body: ConfirmUserResponse = resp.json().await?;
532        if status == 404 {
533            return Err(Error::Api {
534                status,
535                body: body.error.unwrap_or_default(),
536            });
537        }
538        Ok(body)
539    }
540
541    /// List all active tokens.
542    pub async fn get_tokens(&self) -> Result<TokensResponse, Error> {
543        let resp = self
544            .fc
545            .client
546            .get(format!("{}/_fakecloud/cognito/tokens", self.fc.base_url))
547            .send()
548            .await?;
549        FakeCloud::parse(resp).await
550    }
551
552    /// Expire tokens (optionally filtered by pool/user).
553    pub async fn expire_tokens(
554        &self,
555        req: &ExpireTokensRequest,
556    ) -> Result<ExpireTokensResponse, Error> {
557        let resp = self
558            .fc
559            .client
560            .post(format!(
561                "{}/_fakecloud/cognito/expire-tokens",
562                self.fc.base_url
563            ))
564            .json(req)
565            .send()
566            .await?;
567        FakeCloud::parse(resp).await
568    }
569
570    /// List auth events.
571    pub async fn get_auth_events(&self) -> Result<AuthEventsResponse, Error> {
572        let resp = self
573            .fc
574            .client
575            .get(format!(
576                "{}/_fakecloud/cognito/auth-events",
577                self.fc.base_url
578            ))
579            .send()
580            .await?;
581        FakeCloud::parse(resp).await
582    }
583}