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    /// Create an IAM admin user in a specific account. Returns credentials
42    /// for the new user. Solves the multi-account bootstrap problem: the
43    /// root bypass only targets the default account, so this endpoint lets
44    /// callers create credentials for any account.
45    pub async fn create_admin(
46        &self,
47        account_id: &str,
48        user_name: &str,
49    ) -> Result<CreateAdminResponse, Error> {
50        let resp = self
51            .client
52            .post(format!("{}/_fakecloud/iam/create-admin", self.base_url))
53            .json(&CreateAdminRequest {
54                account_id: account_id.to_string(),
55                user_name: user_name.to_string(),
56            })
57            .send()
58            .await?;
59        Self::parse(resp).await
60    }
61
62    /// Reset a single service's state.
63    pub async fn reset_service(&self, service: &str) -> Result<ResetServiceResponse, Error> {
64        let resp = self
65            .client
66            .post(format!("{}/_fakecloud/reset/{}", self.base_url, service))
67            .send()
68            .await?;
69        Self::parse(resp).await
70    }
71
72    /// Reset a single service's state for a specific account only.
73    pub async fn reset_service_for_account(
74        &self,
75        service: &str,
76        account_id: &str,
77    ) -> Result<ResetServiceResponse, Error> {
78        let resp = self
79            .client
80            .post(format!(
81                "{}/_fakecloud/reset/{}/{}",
82                self.base_url, service, account_id
83            ))
84            .send()
85            .await?;
86        Self::parse(resp).await
87    }
88
89    // ── Sub-clients ─────────────────────────────────────────────────
90
91    pub fn lambda(&self) -> LambdaClient<'_> {
92        LambdaClient { fc: self }
93    }
94
95    pub fn ses(&self) -> SesClient<'_> {
96        SesClient { fc: self }
97    }
98
99    pub fn sns(&self) -> SnsClient<'_> {
100        SnsClient { fc: self }
101    }
102
103    pub fn sqs(&self) -> SqsClient<'_> {
104        SqsClient { fc: self }
105    }
106
107    pub fn events(&self) -> EventsClient<'_> {
108        EventsClient { fc: self }
109    }
110
111    pub fn s3(&self) -> S3Client<'_> {
112        S3Client { fc: self }
113    }
114
115    pub fn dynamodb(&self) -> DynamoDbClient<'_> {
116        DynamoDbClient { fc: self }
117    }
118
119    pub fn secretsmanager(&self) -> SecretsManagerClient<'_> {
120        SecretsManagerClient { fc: self }
121    }
122
123    pub fn cognito(&self) -> CognitoClient<'_> {
124        CognitoClient { fc: self }
125    }
126
127    pub fn rds(&self) -> RdsClient<'_> {
128        RdsClient { fc: self }
129    }
130
131    pub fn elasticache(&self) -> ElastiCacheClient<'_> {
132        ElastiCacheClient { fc: self }
133    }
134
135    pub fn apigatewayv2(&self) -> ApiGatewayV2Client<'_> {
136        ApiGatewayV2Client { fc: self }
137    }
138
139    pub fn stepfunctions(&self) -> StepFunctionsClient<'_> {
140        StepFunctionsClient { fc: self }
141    }
142
143    pub fn bedrock(&self) -> BedrockClient<'_> {
144        BedrockClient { fc: self }
145    }
146
147    // ── Internal helpers ────────────────────────────────────────────
148
149    async fn parse<T: serde::de::DeserializeOwned>(resp: reqwest::Response) -> Result<T, Error> {
150        let status = resp.status().as_u16();
151        if !resp.status().is_success() {
152            let body = resp.text().await.unwrap_or_default();
153            return Err(Error::Api { status, body });
154        }
155        Ok(resp.json::<T>().await?)
156    }
157}
158
159// ── RDS ─────────────────────────────────────────────────────────────
160
161pub struct RdsClient<'a> {
162    fc: &'a FakeCloud,
163}
164
165impl RdsClient<'_> {
166    /// List fakecloud-managed RDS DB instances with runtime metadata.
167    pub async fn get_instances(&self) -> Result<RdsInstancesResponse, Error> {
168        let resp = self
169            .fc
170            .client
171            .get(format!("{}/_fakecloud/rds/instances", self.fc.base_url))
172            .send()
173            .await?;
174        FakeCloud::parse(resp).await
175    }
176}
177
178// ── ElastiCache ─────────────────────────────────────────────────────
179
180pub struct ElastiCacheClient<'a> {
181    fc: &'a FakeCloud,
182}
183
184impl ElastiCacheClient<'_> {
185    /// List fakecloud-managed ElastiCache cache clusters with runtime metadata.
186    pub async fn get_clusters(&self) -> Result<ElastiCacheClustersResponse, Error> {
187        let resp = self
188            .fc
189            .client
190            .get(format!(
191                "{}/_fakecloud/elasticache/clusters",
192                self.fc.base_url
193            ))
194            .send()
195            .await?;
196        FakeCloud::parse(resp).await
197    }
198
199    /// List fakecloud-managed ElastiCache replication groups with runtime metadata.
200    pub async fn get_replication_groups(
201        &self,
202    ) -> Result<ElastiCacheReplicationGroupsResponse, Error> {
203        let resp = self
204            .fc
205            .client
206            .get(format!(
207                "{}/_fakecloud/elasticache/replication-groups",
208                self.fc.base_url
209            ))
210            .send()
211            .await?;
212        FakeCloud::parse(resp).await
213    }
214
215    /// List fakecloud-managed ElastiCache serverless caches with runtime metadata.
216    pub async fn get_serverless_caches(
217        &self,
218    ) -> Result<ElastiCacheServerlessCachesResponse, Error> {
219        let resp = self
220            .fc
221            .client
222            .get(format!(
223                "{}/_fakecloud/elasticache/serverless-caches",
224                self.fc.base_url
225            ))
226            .send()
227            .await?;
228        FakeCloud::parse(resp).await
229    }
230}
231
232// ── Lambda ──────────────────────────────────────────────────────────
233
234pub struct LambdaClient<'a> {
235    fc: &'a FakeCloud,
236}
237
238impl LambdaClient<'_> {
239    /// List recorded Lambda invocations.
240    pub async fn get_invocations(&self) -> Result<LambdaInvocationsResponse, Error> {
241        let resp = self
242            .fc
243            .client
244            .get(format!(
245                "{}/_fakecloud/lambda/invocations",
246                self.fc.base_url
247            ))
248            .send()
249            .await?;
250        FakeCloud::parse(resp).await
251    }
252
253    /// List warm (cached) Lambda containers.
254    pub async fn get_warm_containers(&self) -> Result<WarmContainersResponse, Error> {
255        let resp = self
256            .fc
257            .client
258            .get(format!(
259                "{}/_fakecloud/lambda/warm-containers",
260                self.fc.base_url
261            ))
262            .send()
263            .await?;
264        FakeCloud::parse(resp).await
265    }
266
267    /// Evict the warm container for a specific function.
268    pub async fn evict_container(
269        &self,
270        function_name: &str,
271    ) -> Result<EvictContainerResponse, Error> {
272        let resp = self
273            .fc
274            .client
275            .post(format!(
276                "{}/_fakecloud/lambda/{}/evict-container",
277                self.fc.base_url, function_name
278            ))
279            .send()
280            .await?;
281        FakeCloud::parse(resp).await
282    }
283}
284
285// ── SES ─────────────────────────────────────────────────────────────
286
287pub struct SesClient<'a> {
288    fc: &'a FakeCloud,
289}
290
291impl SesClient<'_> {
292    /// List all sent emails.
293    pub async fn get_emails(&self) -> Result<SesEmailsResponse, Error> {
294        let resp = self
295            .fc
296            .client
297            .get(format!("{}/_fakecloud/ses/emails", self.fc.base_url))
298            .send()
299            .await?;
300        FakeCloud::parse(resp).await
301    }
302
303    /// Simulate an inbound email (SES receipt rules).
304    pub async fn simulate_inbound(
305        &self,
306        req: &InboundEmailRequest,
307    ) -> Result<InboundEmailResponse, Error> {
308        let resp = self
309            .fc
310            .client
311            .post(format!("{}/_fakecloud/ses/inbound", self.fc.base_url))
312            .json(req)
313            .send()
314            .await?;
315        FakeCloud::parse(resp).await
316    }
317}
318
319// ── SNS ─────────────────────────────────────────────────────────────
320
321pub struct SnsClient<'a> {
322    fc: &'a FakeCloud,
323}
324
325impl SnsClient<'_> {
326    /// List all published SNS messages.
327    pub async fn get_messages(&self) -> Result<SnsMessagesResponse, Error> {
328        let resp = self
329            .fc
330            .client
331            .get(format!("{}/_fakecloud/sns/messages", self.fc.base_url))
332            .send()
333            .await?;
334        FakeCloud::parse(resp).await
335    }
336
337    /// List subscriptions pending confirmation.
338    pub async fn get_pending_confirmations(&self) -> Result<PendingConfirmationsResponse, Error> {
339        let resp = self
340            .fc
341            .client
342            .get(format!(
343                "{}/_fakecloud/sns/pending-confirmations",
344                self.fc.base_url
345            ))
346            .send()
347            .await?;
348        FakeCloud::parse(resp).await
349    }
350
351    /// Confirm a pending subscription.
352    pub async fn confirm_subscription(
353        &self,
354        req: &ConfirmSubscriptionRequest,
355    ) -> Result<ConfirmSubscriptionResponse, Error> {
356        let resp = self
357            .fc
358            .client
359            .post(format!(
360                "{}/_fakecloud/sns/confirm-subscription",
361                self.fc.base_url
362            ))
363            .json(req)
364            .send()
365            .await?;
366        FakeCloud::parse(resp).await
367    }
368}
369
370// ── SQS ─────────────────────────────────────────────────────────────
371
372pub struct SqsClient<'a> {
373    fc: &'a FakeCloud,
374}
375
376impl SqsClient<'_> {
377    /// List all messages across all queues.
378    pub async fn get_messages(&self) -> Result<SqsMessagesResponse, Error> {
379        let resp = self
380            .fc
381            .client
382            .get(format!("{}/_fakecloud/sqs/messages", self.fc.base_url))
383            .send()
384            .await?;
385        FakeCloud::parse(resp).await
386    }
387
388    /// Tick the message expiration processor (expire visibility-timed-out messages).
389    pub async fn tick_expiration(&self) -> Result<ExpirationTickResponse, Error> {
390        let resp = self
391            .fc
392            .client
393            .post(format!(
394                "{}/_fakecloud/sqs/expiration-processor/tick",
395                self.fc.base_url
396            ))
397            .send()
398            .await?;
399        FakeCloud::parse(resp).await
400    }
401
402    /// Force all messages in a queue to its DLQ.
403    pub async fn force_dlq(&self, queue_name: &str) -> Result<ForceDlqResponse, Error> {
404        let resp = self
405            .fc
406            .client
407            .post(format!(
408                "{}/_fakecloud/sqs/{}/force-dlq",
409                self.fc.base_url, queue_name
410            ))
411            .send()
412            .await?;
413        FakeCloud::parse(resp).await
414    }
415}
416
417// ── EventBridge ─────────────────────────────────────────────────────
418
419pub struct EventsClient<'a> {
420    fc: &'a FakeCloud,
421}
422
423impl EventsClient<'_> {
424    /// Get event history and delivery records.
425    pub async fn get_history(&self) -> Result<EventHistoryResponse, Error> {
426        let resp = self
427            .fc
428            .client
429            .get(format!("{}/_fakecloud/events/history", self.fc.base_url))
430            .send()
431            .await?;
432        FakeCloud::parse(resp).await
433    }
434
435    /// Fire a specific EventBridge rule manually.
436    pub async fn fire_rule(&self, req: &FireRuleRequest) -> Result<FireRuleResponse, Error> {
437        let resp = self
438            .fc
439            .client
440            .post(format!("{}/_fakecloud/events/fire-rule", self.fc.base_url))
441            .json(req)
442            .send()
443            .await?;
444        FakeCloud::parse(resp).await
445    }
446}
447
448// ── S3 ──────────────────────────────────────────────────────────────
449
450pub struct S3Client<'a> {
451    fc: &'a FakeCloud,
452}
453
454impl S3Client<'_> {
455    /// List S3 notification events.
456    pub async fn get_notifications(&self) -> Result<S3NotificationsResponse, Error> {
457        let resp = self
458            .fc
459            .client
460            .get(format!("{}/_fakecloud/s3/notifications", self.fc.base_url))
461            .send()
462            .await?;
463        FakeCloud::parse(resp).await
464    }
465
466    /// Tick the S3 lifecycle processor.
467    pub async fn tick_lifecycle(&self) -> Result<LifecycleTickResponse, Error> {
468        let resp = self
469            .fc
470            .client
471            .post(format!(
472                "{}/_fakecloud/s3/lifecycle-processor/tick",
473                self.fc.base_url
474            ))
475            .send()
476            .await?;
477        FakeCloud::parse(resp).await
478    }
479}
480
481// ── DynamoDB ────────────────────────────────────────────────────────
482
483pub struct DynamoDbClient<'a> {
484    fc: &'a FakeCloud,
485}
486
487impl DynamoDbClient<'_> {
488    /// Tick the DynamoDB TTL processor.
489    pub async fn tick_ttl(&self) -> Result<TtlTickResponse, Error> {
490        let resp = self
491            .fc
492            .client
493            .post(format!(
494                "{}/_fakecloud/dynamodb/ttl-processor/tick",
495                self.fc.base_url
496            ))
497            .send()
498            .await?;
499        FakeCloud::parse(resp).await
500    }
501}
502
503// ── SecretsManager ──────────────────────────────────────────────────
504
505pub struct SecretsManagerClient<'a> {
506    fc: &'a FakeCloud,
507}
508
509impl SecretsManagerClient<'_> {
510    /// Tick the SecretsManager rotation scheduler.
511    pub async fn tick_rotation(&self) -> Result<RotationTickResponse, Error> {
512        let resp = self
513            .fc
514            .client
515            .post(format!(
516                "{}/_fakecloud/secretsmanager/rotation-scheduler/tick",
517                self.fc.base_url
518            ))
519            .send()
520            .await?;
521        FakeCloud::parse(resp).await
522    }
523}
524
525// ── Cognito ─────────────────────────────────────────────────────────
526
527pub struct CognitoClient<'a> {
528    fc: &'a FakeCloud,
529}
530
531impl CognitoClient<'_> {
532    /// Get confirmation codes for a specific user.
533    pub async fn get_user_codes(
534        &self,
535        pool_id: &str,
536        username: &str,
537    ) -> Result<UserConfirmationCodes, Error> {
538        let resp = self
539            .fc
540            .client
541            .get(format!(
542                "{}/_fakecloud/cognito/confirmation-codes/{}/{}",
543                self.fc.base_url, pool_id, username
544            ))
545            .send()
546            .await?;
547        FakeCloud::parse(resp).await
548    }
549
550    /// List all confirmation codes across all pools.
551    pub async fn get_confirmation_codes(&self) -> Result<ConfirmationCodesResponse, Error> {
552        let resp = self
553            .fc
554            .client
555            .get(format!(
556                "{}/_fakecloud/cognito/confirmation-codes",
557                self.fc.base_url
558            ))
559            .send()
560            .await?;
561        FakeCloud::parse(resp).await
562    }
563
564    /// Confirm a user (bypass email/phone verification).
565    pub async fn confirm_user(
566        &self,
567        req: &ConfirmUserRequest,
568    ) -> Result<ConfirmUserResponse, Error> {
569        let resp = self
570            .fc
571            .client
572            .post(format!(
573                "{}/_fakecloud/cognito/confirm-user",
574                self.fc.base_url
575            ))
576            .json(req)
577            .send()
578            .await?;
579        let status = resp.status().as_u16();
580        let body: ConfirmUserResponse = resp.json().await?;
581        if status >= 400 {
582            return Err(Error::Api {
583                status,
584                body: body.error.unwrap_or_default(),
585            });
586        }
587        Ok(body)
588    }
589
590    /// List all active tokens.
591    pub async fn get_tokens(&self) -> Result<TokensResponse, Error> {
592        let resp = self
593            .fc
594            .client
595            .get(format!("{}/_fakecloud/cognito/tokens", self.fc.base_url))
596            .send()
597            .await?;
598        FakeCloud::parse(resp).await
599    }
600
601    /// Expire tokens (optionally filtered by pool/user).
602    pub async fn expire_tokens(
603        &self,
604        req: &ExpireTokensRequest,
605    ) -> Result<ExpireTokensResponse, Error> {
606        let resp = self
607            .fc
608            .client
609            .post(format!(
610                "{}/_fakecloud/cognito/expire-tokens",
611                self.fc.base_url
612            ))
613            .json(req)
614            .send()
615            .await?;
616        FakeCloud::parse(resp).await
617    }
618
619    /// List auth events.
620    pub async fn get_auth_events(&self) -> Result<AuthEventsResponse, Error> {
621        let resp = self
622            .fc
623            .client
624            .get(format!(
625                "{}/_fakecloud/cognito/auth-events",
626                self.fc.base_url
627            ))
628            .send()
629            .await?;
630        FakeCloud::parse(resp).await
631    }
632}
633
634// ── API Gateway v2 ──────────────────────────────────────────────────
635
636pub struct ApiGatewayV2Client<'a> {
637    fc: &'a FakeCloud,
638}
639
640impl ApiGatewayV2Client<'_> {
641    /// List all HTTP API requests that were received and processed.
642    pub async fn get_requests(&self) -> Result<ApiGatewayV2RequestsResponse, Error> {
643        let resp = self
644            .fc
645            .client
646            .get(format!(
647                "{}/_fakecloud/apigatewayv2/requests",
648                self.fc.base_url
649            ))
650            .send()
651            .await?;
652        FakeCloud::parse(resp).await
653    }
654}
655
656// ── Step Functions ──────────────────────────────────────────────────
657
658pub struct StepFunctionsClient<'a> {
659    fc: &'a FakeCloud,
660}
661
662impl StepFunctionsClient<'_> {
663    /// List all Step Functions executions with status, input, output, and timestamps.
664    pub async fn get_executions(&self) -> Result<StepFunctionsExecutionsResponse, Error> {
665        let resp = self
666            .fc
667            .client
668            .get(format!(
669                "{}/_fakecloud/stepfunctions/executions",
670                self.fc.base_url
671            ))
672            .send()
673            .await?;
674        FakeCloud::parse(resp).await
675    }
676}
677
678// ── Bedrock ─────────────────────────────────────────────────────────
679
680pub struct BedrockClient<'a> {
681    fc: &'a FakeCloud,
682}
683
684impl BedrockClient<'_> {
685    /// List recorded Bedrock runtime invocations. Each invocation has an optional
686    /// `error` field that is set for calls faulted via [`Self::queue_fault`].
687    pub async fn get_invocations(&self) -> Result<BedrockInvocationsResponse, Error> {
688        let resp = self
689            .fc
690            .client
691            .get(format!(
692                "{}/_fakecloud/bedrock/invocations",
693                self.fc.base_url
694            ))
695            .send()
696            .await?;
697        FakeCloud::parse(resp).await
698    }
699
700    /// Configure a single canned response for a Bedrock model.
701    pub async fn set_model_response(
702        &self,
703        model_id: &str,
704        response: &str,
705    ) -> Result<BedrockModelResponseConfig, Error> {
706        let resp = self
707            .fc
708            .client
709            .post(format!(
710                "{}/_fakecloud/bedrock/models/{}/response",
711                self.fc.base_url, model_id
712            ))
713            .header("content-type", "text/plain")
714            .body(response.to_string())
715            .send()
716            .await?;
717        FakeCloud::parse(resp).await
718    }
719
720    /// Replace the prompt-conditional response rule list for a Bedrock model.
721    pub async fn set_response_rules(
722        &self,
723        model_id: &str,
724        rules: &[BedrockResponseRule],
725    ) -> Result<BedrockModelResponseConfig, Error> {
726        let body = serde_json::json!({ "rules": rules });
727        let resp = self
728            .fc
729            .client
730            .post(format!(
731                "{}/_fakecloud/bedrock/models/{}/responses",
732                self.fc.base_url, model_id
733            ))
734            .json(&body)
735            .send()
736            .await?;
737        FakeCloud::parse(resp).await
738    }
739
740    /// Clear all prompt-conditional response rules for a Bedrock model.
741    pub async fn clear_response_rules(
742        &self,
743        model_id: &str,
744    ) -> Result<BedrockModelResponseConfig, Error> {
745        let resp = self
746            .fc
747            .client
748            .delete(format!(
749                "{}/_fakecloud/bedrock/models/{}/responses",
750                self.fc.base_url, model_id
751            ))
752            .send()
753            .await?;
754        FakeCloud::parse(resp).await
755    }
756
757    /// Queue a fault rule that will cause the next matching Bedrock runtime call(s) to fail.
758    pub async fn queue_fault(
759        &self,
760        rule: &BedrockFaultRule,
761    ) -> Result<BedrockStatusResponse, Error> {
762        let resp = self
763            .fc
764            .client
765            .post(format!("{}/_fakecloud/bedrock/faults", self.fc.base_url))
766            .json(rule)
767            .send()
768            .await?;
769        FakeCloud::parse(resp).await
770    }
771
772    /// List currently queued fault rules.
773    pub async fn get_faults(&self) -> Result<BedrockFaultsResponse, Error> {
774        let resp = self
775            .fc
776            .client
777            .get(format!("{}/_fakecloud/bedrock/faults", self.fc.base_url))
778            .send()
779            .await?;
780        FakeCloud::parse(resp).await
781    }
782
783    /// Clear all queued fault rules.
784    pub async fn clear_faults(&self) -> Result<BedrockStatusResponse, Error> {
785        let resp = self
786            .fc
787            .client
788            .delete(format!("{}/_fakecloud/bedrock/faults", self.fc.base_url))
789            .send()
790            .await?;
791        FakeCloud::parse(resp).await
792    }
793}