Skip to main content

fakecloud_appsync/
service.rs

1//! AWS AppSync (`appsync`) restJson1 dispatch + operation handlers.
2//!
3//! Every AppSync operation is a RESTful `<METHOD> /v1|/v2/...` route with path
4//! labels, so requests route on HTTP method + `@http` URI template (segment
5//! matching with captured labels). State is account-partitioned and persisted.
6//! Sub-resource operations validate their parent GraphQL API and return the
7//! declared `NotFoundException` when the parent -- or the addressed resource --
8//! is absent, so synthetic conformance ids land on a declared error and real
9//! round-trips against a created API succeed.
10
11use std::sync::Arc;
12
13use async_trait::async_trait;
14use base64::Engine as _;
15use http::{Method, StatusCode};
16use percent_encoding::percent_decode_str;
17use rand::Rng;
18use serde_json::{json, Map, Value};
19use tokio::sync::Mutex as AsyncMutex;
20use uuid::Uuid;
21
22use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
23use fakecloud_persistence::SnapshotStore;
24
25use crate::persistence::save_snapshot;
26use crate::state::{now_epoch, AppSyncData, SchemaState, SharedAppSyncState};
27
28/// Every operation name in the AWS AppSync Smithy model (74 operations).
29pub const APPSYNC_ACTIONS: &[&str] = &[
30    "AssociateApi",
31    "AssociateMergedGraphqlApi",
32    "AssociateSourceGraphqlApi",
33    "CreateApi",
34    "CreateApiCache",
35    "CreateApiKey",
36    "CreateChannelNamespace",
37    "CreateDataSource",
38    "CreateDomainName",
39    "CreateFunction",
40    "CreateGraphqlApi",
41    "CreateResolver",
42    "CreateType",
43    "DeleteApi",
44    "DeleteApiCache",
45    "DeleteApiKey",
46    "DeleteChannelNamespace",
47    "DeleteDataSource",
48    "DeleteDomainName",
49    "DeleteFunction",
50    "DeleteGraphqlApi",
51    "DeleteResolver",
52    "DeleteType",
53    "DisassociateApi",
54    "DisassociateMergedGraphqlApi",
55    "DisassociateSourceGraphqlApi",
56    "EvaluateCode",
57    "EvaluateMappingTemplate",
58    "FlushApiCache",
59    "GetApi",
60    "GetApiAssociation",
61    "GetApiCache",
62    "GetChannelNamespace",
63    "GetDataSource",
64    "GetDataSourceIntrospection",
65    "GetDomainName",
66    "GetFunction",
67    "GetGraphqlApi",
68    "GetGraphqlApiEnvironmentVariables",
69    "GetIntrospectionSchema",
70    "GetResolver",
71    "GetSchemaCreationStatus",
72    "GetSourceApiAssociation",
73    "GetType",
74    "ListApiKeys",
75    "ListApis",
76    "ListChannelNamespaces",
77    "ListDataSources",
78    "ListDomainNames",
79    "ListFunctions",
80    "ListGraphqlApis",
81    "ListResolvers",
82    "ListResolversByFunction",
83    "ListSourceApiAssociations",
84    "ListTagsForResource",
85    "ListTypes",
86    "ListTypesByAssociation",
87    "PutGraphqlApiEnvironmentVariables",
88    "StartDataSourceIntrospection",
89    "StartSchemaCreation",
90    "StartSchemaMerge",
91    "TagResource",
92    "UntagResource",
93    "UpdateApi",
94    "UpdateApiCache",
95    "UpdateApiKey",
96    "UpdateChannelNamespace",
97    "UpdateDataSource",
98    "UpdateDomainName",
99    "UpdateFunction",
100    "UpdateGraphqlApi",
101    "UpdateResolver",
102    "UpdateSourceApiAssociation",
103    "UpdateType",
104];
105
106/// Operations that mutate persisted state on success (so a snapshot is taken).
107/// `GetSchemaCreationStatus` is included because reading it settles the async
108/// schema-creation lifecycle from `Processing` to `Success`.
109const SAVE_AFTER: &[&str] = &[
110    "CreateApi",
111    "CreateApiCache",
112    "CreateApiKey",
113    "CreateChannelNamespace",
114    "CreateDataSource",
115    "CreateDomainName",
116    "CreateFunction",
117    "CreateGraphqlApi",
118    "CreateResolver",
119    "CreateType",
120    "DeleteApi",
121    "DeleteApiCache",
122    "DeleteApiKey",
123    "DeleteChannelNamespace",
124    "DeleteDataSource",
125    "DeleteDomainName",
126    "DeleteFunction",
127    "DeleteGraphqlApi",
128    "DeleteResolver",
129    "DeleteType",
130    "AssociateApi",
131    "AssociateMergedGraphqlApi",
132    "AssociateSourceGraphqlApi",
133    "DisassociateApi",
134    "DisassociateMergedGraphqlApi",
135    "DisassociateSourceGraphqlApi",
136    "FlushApiCache",
137    "PutGraphqlApiEnvironmentVariables",
138    "StartDataSourceIntrospection",
139    "StartSchemaCreation",
140    "StartSchemaMerge",
141    "GetSchemaCreationStatus",
142    "TagResource",
143    "UntagResource",
144    "UpdateApi",
145    "UpdateApiCache",
146    "UpdateApiKey",
147    "UpdateChannelNamespace",
148    "UpdateDataSource",
149    "UpdateDomainName",
150    "UpdateFunction",
151    "UpdateGraphqlApi",
152    "UpdateResolver",
153    "UpdateSourceApiAssociation",
154    "UpdateType",
155];
156
157pub struct AppSyncService {
158    state: SharedAppSyncState,
159    snapshot_store: Option<Arc<dyn SnapshotStore>>,
160    snapshot_lock: Arc<AsyncMutex<()>>,
161}
162
163impl AppSyncService {
164    pub fn new(state: SharedAppSyncState) -> Self {
165        Self {
166            state,
167            snapshot_store: None,
168            snapshot_lock: Arc::new(AsyncMutex::new(())),
169        }
170    }
171
172    pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
173        self.snapshot_store = Some(store);
174        self
175    }
176
177    async fn save(&self) {
178        save_snapshot(
179            &self.state,
180            self.snapshot_store.clone(),
181            &self.snapshot_lock,
182        )
183        .await;
184    }
185
186    /// Persist hook for the CloudFormation provisioner; `None` in memory mode.
187    pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
188        let store = self.snapshot_store.clone()?;
189        let state = self.state.clone();
190        let lock = self.snapshot_lock.clone();
191        Some(Arc::new(move || {
192            let state = state.clone();
193            let store = store.clone();
194            let lock = lock.clone();
195            Box::pin(async move {
196                crate::persistence::save_snapshot(&state, Some(store), &lock).await;
197            })
198        }))
199    }
200
201    /// Route a request to an operation name + captured path labels by HTTP
202    /// method + `@http` URI template. Labels are captured in path order.
203    fn resolve_action(req: &AwsRequest) -> Option<(&'static str, Vec<String>)> {
204        let raw = req.raw_path.split('?').next().unwrap_or(&req.raw_path);
205        let trimmed = raw.strip_prefix('/').unwrap_or(raw);
206        let segs: Vec<String> = if trimmed.is_empty() {
207            Vec::new()
208        } else {
209            trimmed
210                .split('/')
211                .map(|s| percent_decode_str(s).decode_utf8_lossy().into_owned())
212                .collect()
213        };
214        let s: Vec<&str> = segs.iter().map(String::as_str).collect();
215        let m = &req.method;
216        let get = m == Method::GET;
217        let post = m == Method::POST;
218        let put = m == Method::PUT;
219        let del = m == Method::DELETE;
220        let v = |xs: &[&str]| xs.iter().map(|x| x.to_string()).collect::<Vec<_>>();
221        let (action, labels): (&'static str, Vec<String>) = match s.as_slice() {
222            // ---- v2 Event API surface ----
223            ["v2", "apis"] if post => ("CreateApi", vec![]),
224            ["v2", "apis"] if get => ("ListApis", vec![]),
225            ["v2", "apis", id] if get => ("GetApi", v(&[id])),
226            ["v2", "apis", id] if post => ("UpdateApi", v(&[id])),
227            ["v2", "apis", id] if del => ("DeleteApi", v(&[id])),
228            ["v2", "apis", id, "channelNamespaces"] if post => ("CreateChannelNamespace", v(&[id])),
229            ["v2", "apis", id, "channelNamespaces"] if get => ("ListChannelNamespaces", v(&[id])),
230            ["v2", "apis", id, "channelNamespaces", n] if get => {
231                ("GetChannelNamespace", v(&[id, n]))
232            }
233            ["v2", "apis", id, "channelNamespaces", n] if post => {
234                ("UpdateChannelNamespace", v(&[id, n]))
235            }
236            ["v2", "apis", id, "channelNamespaces", n] if del => {
237                ("DeleteChannelNamespace", v(&[id, n]))
238            }
239
240            // ---- v1 GraphQL API surface ----
241            ["v1", "apis"] if post => ("CreateGraphqlApi", vec![]),
242            ["v1", "apis"] if get => ("ListGraphqlApis", vec![]),
243
244            // API caches (before the generic {apiId} arms; distinct trailing literal)
245            ["v1", "apis", id, "ApiCaches", "update"] if post => ("UpdateApiCache", v(&[id])),
246            ["v1", "apis", id, "ApiCaches"] if post => ("CreateApiCache", v(&[id])),
247            ["v1", "apis", id, "ApiCaches"] if get => ("GetApiCache", v(&[id])),
248            ["v1", "apis", id, "ApiCaches"] if del => ("DeleteApiCache", v(&[id])),
249            ["v1", "apis", id, "FlushCache"] if del => ("FlushApiCache", v(&[id])),
250
251            // API keys
252            ["v1", "apis", id, "apikeys"] if post => ("CreateApiKey", v(&[id])),
253            ["v1", "apis", id, "apikeys"] if get => ("ListApiKeys", v(&[id])),
254            ["v1", "apis", id, "apikeys", k] if del => ("DeleteApiKey", v(&[id, k])),
255            ["v1", "apis", id, "apikeys", k] if post => ("UpdateApiKey", v(&[id, k])),
256
257            // Data sources
258            ["v1", "apis", id, "datasources"] if post => ("CreateDataSource", v(&[id])),
259            ["v1", "apis", id, "datasources"] if get => ("ListDataSources", v(&[id])),
260            ["v1", "apis", id, "datasources", n] if get => ("GetDataSource", v(&[id, n])),
261            ["v1", "apis", id, "datasources", n] if post => ("UpdateDataSource", v(&[id, n])),
262            ["v1", "apis", id, "datasources", n] if del => ("DeleteDataSource", v(&[id, n])),
263
264            // Functions
265            ["v1", "apis", id, "functions"] if post => ("CreateFunction", v(&[id])),
266            ["v1", "apis", id, "functions"] if get => ("ListFunctions", v(&[id])),
267            ["v1", "apis", id, "functions", fid, "resolvers"] if get => {
268                ("ListResolversByFunction", v(&[id, fid]))
269            }
270            ["v1", "apis", id, "functions", fid] if get => ("GetFunction", v(&[id, fid])),
271            ["v1", "apis", id, "functions", fid] if post => ("UpdateFunction", v(&[id, fid])),
272            ["v1", "apis", id, "functions", fid] if del => ("DeleteFunction", v(&[id, fid])),
273
274            // Types + resolvers (resolvers nest under types)
275            ["v1", "apis", id, "types", tn, "resolvers"] if post => {
276                ("CreateResolver", v(&[id, tn]))
277            }
278            ["v1", "apis", id, "types", tn, "resolvers"] if get => ("ListResolvers", v(&[id, tn])),
279            ["v1", "apis", id, "types", tn, "resolvers", fn_] if get => {
280                ("GetResolver", v(&[id, tn, fn_]))
281            }
282            ["v1", "apis", id, "types", tn, "resolvers", fn_] if post => {
283                ("UpdateResolver", v(&[id, tn, fn_]))
284            }
285            ["v1", "apis", id, "types", tn, "resolvers", fn_] if del => {
286                ("DeleteResolver", v(&[id, tn, fn_]))
287            }
288            ["v1", "apis", id, "types"] if post => ("CreateType", v(&[id])),
289            ["v1", "apis", id, "types"] if get => ("ListTypes", v(&[id])),
290            ["v1", "apis", id, "types", tn] if get => ("GetType", v(&[id, tn])),
291            ["v1", "apis", id, "types", tn] if post => ("UpdateType", v(&[id, tn])),
292            ["v1", "apis", id, "types", tn] if del => ("DeleteType", v(&[id, tn])),
293
294            // Schema
295            ["v1", "apis", id, "schema"] if get => ("GetIntrospectionSchema", v(&[id])),
296            ["v1", "apis", id, "schemacreation"] if post => ("StartSchemaCreation", v(&[id])),
297            ["v1", "apis", id, "schemacreation"] if get => ("GetSchemaCreationStatus", v(&[id])),
298
299            // Environment variables
300            ["v1", "apis", id, "environmentVariables"] if get => {
301                ("GetGraphqlApiEnvironmentVariables", v(&[id]))
302            }
303            ["v1", "apis", id, "environmentVariables"] if put => {
304                ("PutGraphqlApiEnvironmentVariables", v(&[id]))
305            }
306
307            // Source-API associations listed on a graphql api
308            ["v1", "apis", id, "sourceApiAssociations"] if get => {
309                ("ListSourceApiAssociations", v(&[id]))
310            }
311
312            // Generic graphql api by id (must come after the sub-resource arms)
313            ["v1", "apis", id] if get => ("GetGraphqlApi", v(&[id])),
314            ["v1", "apis", id] if post => ("UpdateGraphqlApi", v(&[id])),
315            ["v1", "apis", id] if del => ("DeleteGraphqlApi", v(&[id])),
316
317            // Domain names + api associations
318            ["v1", "domainnames"] if post => ("CreateDomainName", vec![]),
319            ["v1", "domainnames"] if get => ("ListDomainNames", vec![]),
320            ["v1", "domainnames", dn, "apiassociation"] if post => ("AssociateApi", v(&[dn])),
321            ["v1", "domainnames", dn, "apiassociation"] if get => ("GetApiAssociation", v(&[dn])),
322            ["v1", "domainnames", dn, "apiassociation"] if del => ("DisassociateApi", v(&[dn])),
323            ["v1", "domainnames", dn] if get => ("GetDomainName", v(&[dn])),
324            ["v1", "domainnames", dn] if post => ("UpdateDomainName", v(&[dn])),
325            ["v1", "domainnames", dn] if del => ("DeleteDomainName", v(&[dn])),
326
327            // Merged / source API associations
328            ["v1", "sourceApis", sid, "mergedApiAssociations"] if post => {
329                ("AssociateMergedGraphqlApi", v(&[sid]))
330            }
331            ["v1", "sourceApis", sid, "mergedApiAssociations", aid] if del => {
332                ("DisassociateMergedGraphqlApi", v(&[sid, aid]))
333            }
334            ["v1", "mergedApis", mid, "sourceApiAssociations"] if post => {
335                ("AssociateSourceGraphqlApi", v(&[mid]))
336            }
337            ["v1", "mergedApis", mid, "sourceApiAssociations", aid, "types"] if get => {
338                ("ListTypesByAssociation", v(&[mid, aid]))
339            }
340            ["v1", "mergedApis", mid, "sourceApiAssociations", aid, "merge"] if post => {
341                ("StartSchemaMerge", v(&[mid, aid]))
342            }
343            ["v1", "mergedApis", mid, "sourceApiAssociations", aid] if get => {
344                ("GetSourceApiAssociation", v(&[mid, aid]))
345            }
346            ["v1", "mergedApis", mid, "sourceApiAssociations", aid] if post => {
347                ("UpdateSourceApiAssociation", v(&[mid, aid]))
348            }
349            ["v1", "mergedApis", mid, "sourceApiAssociations", aid] if del => {
350                ("DisassociateSourceGraphqlApi", v(&[mid, aid]))
351            }
352
353            // Data-source introspection
354            ["v1", "datasources", "introspections"] if post => {
355                ("StartDataSourceIntrospection", vec![])
356            }
357            ["v1", "datasources", "introspections", iid] if get => {
358                ("GetDataSourceIntrospection", v(&[iid]))
359            }
360
361            // Evaluation data plane
362            ["v1", "dataplane-evaluatecode"] if post => ("EvaluateCode", vec![]),
363            ["v1", "dataplane-evaluatetemplate"] if post => ("EvaluateMappingTemplate", vec![]),
364
365            // Tags
366            ["v1", "tags", arn] if get => ("ListTagsForResource", v(&[arn])),
367            ["v1", "tags", arn] if post => ("TagResource", v(&[arn])),
368            ["v1", "tags", arn] if del => ("UntagResource", v(&[arn])),
369
370            _ => return None,
371        };
372        Some((action, labels))
373    }
374}
375
376#[async_trait]
377impl AwsService for AppSyncService {
378    fn service_name(&self) -> &str {
379        "appsync"
380    }
381
382    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
383        let Some((action, labels)) = Self::resolve_action(&req) else {
384            return Err(AwsServiceError::aws_error(
385                StatusCode::NOT_FOUND,
386                "UnknownOperationException",
387                format!("Unknown operation: {} {}", req.method, req.raw_path),
388            ));
389        };
390        let result = self.dispatch(action, &labels, &req);
391        let success = matches!(result.as_ref(), Ok(resp) if resp.status.is_success());
392        if SAVE_AFTER.contains(&action) && success {
393            self.save().await;
394        }
395        result
396    }
397
398    fn supported_actions(&self) -> &[&str] {
399        APPSYNC_ACTIONS
400    }
401}
402
403/// Per-request account + region context.
404struct Ctx {
405    account: String,
406    region: String,
407}
408
409impl AppSyncService {
410    fn dispatch(
411        &self,
412        action: &str,
413        labels: &[String],
414        req: &AwsRequest,
415    ) -> Result<AwsResponse, AwsServiceError> {
416        let body = parse_body(req)?;
417        crate::validate::validate_input(action, &body)?;
418        let ctx = Ctx {
419            account: req.account_id.clone(),
420            region: req.region.clone(),
421        };
422        let q = parse_query(&req.raw_query);
423        crate::validate::validate_query(action, &q)?;
424        let l = |i: usize| labels.get(i).map(String::as_str).unwrap_or_default();
425        match action {
426            // ---- GraphQL APIs ----
427            "CreateGraphqlApi" => self.create_graphql_api(&ctx, &body),
428            "GetGraphqlApi" => self.get_graphql_api(&ctx, l(0)),
429            "ListGraphqlApis" => self.list_graphql_apis(&ctx),
430            "UpdateGraphqlApi" => self.update_graphql_api(&ctx, l(0), &body),
431            "DeleteGraphqlApi" => self.delete_graphql_api(&ctx, l(0)),
432
433            // ---- API keys ----
434            "CreateApiKey" => self.create_api_key(&ctx, l(0), &body),
435            "ListApiKeys" => self.list_api_keys(&ctx, l(0)),
436            "UpdateApiKey" => self.update_api_key(&ctx, l(0), l(1), &body),
437            "DeleteApiKey" => self.delete_api_key(&ctx, l(0), l(1)),
438
439            // ---- Data sources ----
440            "CreateDataSource" => self.create_data_source(&ctx, l(0), &body),
441            "GetDataSource" => self.get_data_source(&ctx, l(0), l(1)),
442            "ListDataSources" => self.list_data_sources(&ctx, l(0)),
443            "UpdateDataSource" => self.update_data_source(&ctx, l(0), l(1), &body),
444            "DeleteDataSource" => self.delete_data_source(&ctx, l(0), l(1)),
445
446            // ---- Resolvers ----
447            "CreateResolver" => self.create_resolver(&ctx, l(0), l(1), &body),
448            "GetResolver" => self.get_resolver(&ctx, l(0), l(1), l(2)),
449            "ListResolvers" => self.list_resolvers(&ctx, l(0), l(1)),
450            "ListResolversByFunction" => self.list_resolvers_by_function(&ctx, l(0), l(1)),
451            "UpdateResolver" => self.update_resolver(&ctx, l(0), l(1), l(2), &body),
452            "DeleteResolver" => self.delete_resolver(&ctx, l(0), l(1), l(2)),
453
454            // ---- Functions ----
455            "CreateFunction" => self.create_function(&ctx, l(0), &body),
456            "GetFunction" => self.get_function(&ctx, l(0), l(1)),
457            "ListFunctions" => self.list_functions(&ctx, l(0)),
458            "UpdateFunction" => self.update_function(&ctx, l(0), l(1), &body),
459            "DeleteFunction" => self.delete_function(&ctx, l(0), l(1)),
460
461            // ---- Types + schema ----
462            "CreateType" => self.create_type(&ctx, l(0), &body),
463            "GetType" => self.get_type(&ctx, l(0), l(1), &q),
464            "ListTypes" => self.list_types(&ctx, l(0), &q),
465            "UpdateType" => self.update_type(&ctx, l(0), l(1), &body),
466            "DeleteType" => self.delete_type(&ctx, l(0), l(1)),
467            "StartSchemaCreation" => self.start_schema_creation(&ctx, l(0), &body),
468            "GetSchemaCreationStatus" => self.get_schema_creation_status(&ctx, l(0)),
469            "GetIntrospectionSchema" => self.get_introspection_schema(&ctx, l(0), &q),
470
471            // ---- API caches ----
472            "CreateApiCache" => self.create_api_cache(&ctx, l(0), &body),
473            "UpdateApiCache" => self.update_api_cache(&ctx, l(0), &body),
474            "GetApiCache" => self.get_api_cache(&ctx, l(0)),
475            "DeleteApiCache" => self.delete_api_cache(&ctx, l(0)),
476            "FlushApiCache" => self.flush_api_cache(&ctx, l(0)),
477
478            // ---- Environment variables ----
479            "PutGraphqlApiEnvironmentVariables" => self.put_env_vars(&ctx, l(0), &body),
480            "GetGraphqlApiEnvironmentVariables" => self.get_env_vars(&ctx, l(0)),
481
482            // ---- Domain names + api associations ----
483            "CreateDomainName" => self.create_domain_name(&ctx, &body),
484            "GetDomainName" => self.get_domain_name(&ctx, l(0)),
485            "ListDomainNames" => self.list_domain_names(&ctx),
486            "UpdateDomainName" => self.update_domain_name(&ctx, l(0), &body),
487            "DeleteDomainName" => self.delete_domain_name(&ctx, l(0)),
488            "AssociateApi" => self.associate_api(&ctx, l(0), &body),
489            "GetApiAssociation" => self.get_api_association(&ctx, l(0)),
490            "DisassociateApi" => self.disassociate_api(&ctx, l(0)),
491
492            // ---- Event APIs + channel namespaces ----
493            "CreateApi" => self.create_api(&ctx, &body),
494            "GetApi" => self.get_api(&ctx, l(0)),
495            "ListApis" => self.list_apis(&ctx),
496            "UpdateApi" => self.update_api(&ctx, l(0), &body),
497            "DeleteApi" => self.delete_api(&ctx, l(0)),
498            "CreateChannelNamespace" => self.create_channel_namespace(&ctx, l(0), &body),
499            "GetChannelNamespace" => self.get_channel_namespace(&ctx, l(0), l(1)),
500            "ListChannelNamespaces" => self.list_channel_namespaces(&ctx, l(0)),
501            "UpdateChannelNamespace" => self.update_channel_namespace(&ctx, l(0), l(1), &body),
502            "DeleteChannelNamespace" => self.delete_channel_namespace(&ctx, l(0), l(1)),
503
504            // ---- Source API associations ----
505            "AssociateSourceGraphqlApi" => self.associate_source_api(&ctx, l(0), &body),
506            "AssociateMergedGraphqlApi" => self.associate_merged_api(&ctx, l(0), &body),
507            "GetSourceApiAssociation" => self.get_source_api_association(&ctx, l(1)),
508            "ListSourceApiAssociations" => self.list_source_api_associations(&ctx, l(0)),
509            "UpdateSourceApiAssociation" => self.update_source_api_association(&ctx, l(1), &body),
510            "DisassociateSourceGraphqlApi" => self.disassociate_source_api(&ctx, l(1)),
511            "DisassociateMergedGraphqlApi" => self.disassociate_source_api(&ctx, l(1)),
512            "ListTypesByAssociation" => self.list_types_by_association(&ctx, l(0), l(1), &q),
513            "StartSchemaMerge" => self.start_schema_merge(&ctx, l(1)),
514
515            // ---- Data-source introspection ----
516            "StartDataSourceIntrospection" => Self::start_ds_introspection(),
517            "GetDataSourceIntrospection" => self.get_ds_introspection(&ctx, l(0)),
518
519            // ---- Tags ----
520            "TagResource" => self.tag_resource(&ctx, l(0), &body),
521            "UntagResource" => self.untag_resource(&ctx, l(0), &q),
522            "ListTagsForResource" => self.list_tags_for_resource(&ctx, l(0)),
523
524            // ---- Evaluation data plane ----
525            "EvaluateCode" => Self::evaluate_code(&body),
526            "EvaluateMappingTemplate" => Self::evaluate_template(&body),
527
528            _ => Err(AwsServiceError::action_not_implemented("appsync", action)),
529        }
530    }
531
532    // ===================== GraphQL APIs =====================
533
534    fn create_graphql_api(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
535        let api_id = gen_api_id();
536        let api = self.build_graphql_api(ctx, &api_id, body);
537        let mut guard = self.state.write();
538        let data = guard.get_or_create(&ctx.account);
539        if let Some(tags) = body.get("tags").and_then(Value::as_object) {
540            let arn = graphql_api_arn(&ctx.region, &ctx.account, &api_id);
541            data.tags.insert(api_id.clone(), string_map(tags));
542            data.tags.insert(arn, string_map(tags));
543        }
544        data.graphql_apis.insert(api_id, api.clone());
545        Ok(ok(json!({ "graphqlApi": api })))
546    }
547
548    fn build_graphql_api(&self, ctx: &Ctx, api_id: &str, body: &Value) -> Value {
549        let arn = graphql_api_arn(&ctx.region, &ctx.account, api_id);
550        let mut api = Map::new();
551        api.insert("apiId".into(), json!(api_id));
552        api.insert("arn".into(), json!(arn));
553        api.insert("owner".into(), json!(ctx.account));
554        api.insert(
555            "uris".into(),
556            json!({
557                "GRAPHQL": format!("https://{api_id}.appsync-api.{}.amazonaws.com/graphql", ctx.region),
558                "REALTIME": format!("wss://{api_id}.appsync-realtime-api.{}.amazonaws.com/graphql", ctx.region),
559            }),
560        );
561        api.insert(
562            "dns".into(),
563            json!({
564                "GRAPHQL": format!("{api_id}.appsync-api.{}.amazonaws.com", ctx.region),
565                "REALTIME": format!("{api_id}.appsync-realtime-api.{}.amazonaws.com", ctx.region),
566            }),
567        );
568        // Defaults for members AWS always populates.
569        api.insert("apiType".into(), json!("GRAPHQL"));
570        api.insert("visibility".into(), json!("GLOBAL"));
571        api.insert("xrayEnabled".into(), json!(false));
572        copy_keys(
573            &mut api,
574            body,
575            &[
576                "name",
577                "authenticationType",
578                "logConfig",
579                "userPoolConfig",
580                "openIDConnectConfig",
581                "additionalAuthenticationProviders",
582                "xrayEnabled",
583                "lambdaAuthorizerConfig",
584                "apiType",
585                "mergedApiExecutionRoleArn",
586                "visibility",
587                "ownerContact",
588                "introspectionConfig",
589                "queryDepthLimit",
590                "resolverCountLimit",
591                "enhancedMetricsConfig",
592                "tags",
593            ],
594        );
595        Value::Object(api)
596    }
597
598    fn get_graphql_api(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
599        require_label(api_id, "apiId")?;
600        let guard = self.state.read();
601        let api = guard
602            .get(&ctx.account)
603            .and_then(|d| d.graphql_apis.get(api_id))
604            .cloned()
605            .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
606        Ok(ok(json!({ "graphqlApi": api })))
607    }
608
609    fn list_graphql_apis(&self, ctx: &Ctx) -> Result<AwsResponse, AwsServiceError> {
610        let guard = self.state.read();
611        let apis: Vec<Value> = guard
612            .get(&ctx.account)
613            .map(|d| d.graphql_apis.values().cloned().collect())
614            .unwrap_or_default();
615        Ok(ok(json!({ "graphqlApis": apis })))
616    }
617
618    fn update_graphql_api(
619        &self,
620        ctx: &Ctx,
621        api_id: &str,
622        body: &Value,
623    ) -> Result<AwsResponse, AwsServiceError> {
624        require_label(api_id, "apiId")?;
625        let mut guard = self.state.write();
626        let data = guard.get_or_create(&ctx.account);
627        if !data.graphql_apis.contains_key(api_id) {
628            return Err(not_found(&format!("GraphQL API {api_id} not found.")));
629        }
630        // Rebuild preserving apiId/arn/uris identity, applying the update body.
631        let api = self.build_graphql_api(ctx, api_id, body);
632        data.graphql_apis.insert(api_id.to_string(), api.clone());
633        Ok(ok(json!({ "graphqlApi": api })))
634    }
635
636    fn delete_graphql_api(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
637        require_label(api_id, "apiId")?;
638        let mut guard = self.state.write();
639        let data = guard.get_or_create(&ctx.account);
640        if data.graphql_apis.remove(api_id).is_none() {
641            return Err(not_found(&format!("GraphQL API {api_id} not found.")));
642        }
643        data.api_keys.remove(api_id);
644        data.data_sources.remove(api_id);
645        data.resolvers.remove(api_id);
646        data.functions.remove(api_id);
647        data.types.remove(api_id);
648        data.api_caches.remove(api_id);
649        data.schemas.remove(api_id);
650        data.env_vars.remove(api_id);
651        Ok(ok(json!({})))
652    }
653
654    /// Return `NotFoundException` unless the GraphQL API exists in this account.
655    fn require_api(&self, data: &AppSyncData, api_id: &str) -> Result<(), AwsServiceError> {
656        require_label(api_id, "apiId")?;
657        if data.graphql_apis.contains_key(api_id) {
658            Ok(())
659        } else {
660            Err(not_found(&format!("GraphQL API {api_id} not found.")))
661        }
662    }
663
664    // ===================== API keys =====================
665
666    fn create_api_key(
667        &self,
668        ctx: &Ctx,
669        api_id: &str,
670        body: &Value,
671    ) -> Result<AwsResponse, AwsServiceError> {
672        let mut guard = self.state.write();
673        let data = guard.get_or_create(&ctx.account);
674        self.require_api(data, api_id)?;
675        let id = gen_hex(8);
676        let expires = body
677            .get("expires")
678            .and_then(Value::as_i64)
679            .unwrap_or_else(|| now_epoch() as i64 + 7 * 24 * 3600);
680        let mut key = Map::new();
681        key.insert("id".into(), json!(id));
682        key.insert("expires".into(), json!(expires));
683        key.insert("deletes".into(), json!(expires + 60 * 24 * 3600));
684        copy_keys(&mut key, body, &["description"]);
685        let key = Value::Object(key);
686        data.api_keys
687            .entry(api_id.to_string())
688            .or_default()
689            .insert(id, key.clone());
690        Ok(ok(json!({ "apiKey": key })))
691    }
692
693    fn list_api_keys(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
694        let guard = self.state.read();
695        let data = guard
696            .get(&ctx.account)
697            .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
698        self.require_api(data, api_id)?;
699        let keys: Vec<Value> = data
700            .api_keys
701            .get(api_id)
702            .map(|m| m.values().cloned().collect())
703            .unwrap_or_default();
704        Ok(ok(json!({ "apiKeys": keys })))
705    }
706
707    fn update_api_key(
708        &self,
709        ctx: &Ctx,
710        api_id: &str,
711        id: &str,
712        body: &Value,
713    ) -> Result<AwsResponse, AwsServiceError> {
714        require_label(id, "id")?;
715        let mut guard = self.state.write();
716        let data = guard.get_or_create(&ctx.account);
717        self.require_api(data, api_id)?;
718        let key = data
719            .api_keys
720            .get_mut(api_id)
721            .and_then(|m| m.get_mut(id))
722            .ok_or_else(|| not_found(&format!("API key {id} not found.")))?;
723        if let Some(obj) = key.as_object_mut() {
724            if let Some(d) = body.get("description") {
725                obj.insert("description".into(), d.clone());
726            }
727            if let Some(e) = body.get("expires") {
728                obj.insert("expires".into(), e.clone());
729            }
730        }
731        Ok(ok(json!({ "apiKey": key })))
732    }
733
734    fn delete_api_key(
735        &self,
736        ctx: &Ctx,
737        api_id: &str,
738        id: &str,
739    ) -> Result<AwsResponse, AwsServiceError> {
740        require_label(id, "id")?;
741        let mut guard = self.state.write();
742        let data = guard.get_or_create(&ctx.account);
743        self.require_api(data, api_id)?;
744        match data.api_keys.get_mut(api_id).and_then(|m| m.remove(id)) {
745            Some(_) => Ok(ok(json!({}))),
746            None => Err(not_found(&format!("API key {id} not found."))),
747        }
748    }
749
750    // ===================== Data sources =====================
751
752    fn create_data_source(
753        &self,
754        ctx: &Ctx,
755        api_id: &str,
756        body: &Value,
757    ) -> Result<AwsResponse, AwsServiceError> {
758        let name = require_body_str(body, "name")?.to_string();
759        let mut guard = self.state.write();
760        let data = guard.get_or_create(&ctx.account);
761        self.require_api(data, api_id)?;
762        let ds = build_data_source(ctx, api_id, &name, body);
763        data.data_sources
764            .entry(api_id.to_string())
765            .or_default()
766            .insert(name, ds.clone());
767        Ok(ok(json!({ "dataSource": ds })))
768    }
769
770    fn get_data_source(
771        &self,
772        ctx: &Ctx,
773        api_id: &str,
774        name: &str,
775    ) -> Result<AwsResponse, AwsServiceError> {
776        require_label(name, "name")?;
777        let guard = self.state.read();
778        let ds = guard
779            .get(&ctx.account)
780            .and_then(|d| d.data_sources.get(api_id))
781            .and_then(|m| m.get(name))
782            .cloned()
783            .ok_or_else(|| not_found(&format!("Data source {name} not found.")))?;
784        Ok(ok(json!({ "dataSource": ds })))
785    }
786
787    fn list_data_sources(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
788        let guard = self.state.read();
789        let data = guard
790            .get(&ctx.account)
791            .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
792        self.require_api(data, api_id)?;
793        let items: Vec<Value> = data
794            .data_sources
795            .get(api_id)
796            .map(|m| m.values().cloned().collect())
797            .unwrap_or_default();
798        Ok(ok(json!({ "dataSources": items })))
799    }
800
801    fn update_data_source(
802        &self,
803        ctx: &Ctx,
804        api_id: &str,
805        name: &str,
806        body: &Value,
807    ) -> Result<AwsResponse, AwsServiceError> {
808        require_label(name, "name")?;
809        let mut guard = self.state.write();
810        let data = guard.get_or_create(&ctx.account);
811        self.require_api(data, api_id)?;
812        if !data
813            .data_sources
814            .get(api_id)
815            .map(|m| m.contains_key(name))
816            .unwrap_or(false)
817        {
818            return Err(not_found(&format!("Data source {name} not found.")));
819        }
820        let ds = build_data_source(ctx, api_id, name, body);
821        data.data_sources
822            .entry(api_id.to_string())
823            .or_default()
824            .insert(name.to_string(), ds.clone());
825        Ok(ok(json!({ "dataSource": ds })))
826    }
827
828    fn delete_data_source(
829        &self,
830        ctx: &Ctx,
831        api_id: &str,
832        name: &str,
833    ) -> Result<AwsResponse, AwsServiceError> {
834        require_label(name, "name")?;
835        let mut guard = self.state.write();
836        let data = guard.get_or_create(&ctx.account);
837        self.require_api(data, api_id)?;
838        match data
839            .data_sources
840            .get_mut(api_id)
841            .and_then(|m| m.remove(name))
842        {
843            Some(_) => Ok(ok(json!({}))),
844            None => Err(not_found(&format!("Data source {name} not found."))),
845        }
846    }
847
848    // ===================== Resolvers =====================
849
850    fn create_resolver(
851        &self,
852        ctx: &Ctx,
853        api_id: &str,
854        type_name: &str,
855        body: &Value,
856    ) -> Result<AwsResponse, AwsServiceError> {
857        require_label(type_name, "typeName")?;
858        let field = require_body_str(body, "fieldName")?.to_string();
859        let mut guard = self.state.write();
860        let data = guard.get_or_create(&ctx.account);
861        self.require_api(data, api_id)?;
862        let resolver = build_resolver(ctx, api_id, type_name, &field, body);
863        data.resolvers
864            .entry(api_id.to_string())
865            .or_default()
866            .insert(resolver_key(type_name, &field), resolver.clone());
867        Ok(ok(json!({ "resolver": resolver })))
868    }
869
870    fn get_resolver(
871        &self,
872        ctx: &Ctx,
873        api_id: &str,
874        type_name: &str,
875        field: &str,
876    ) -> Result<AwsResponse, AwsServiceError> {
877        require_label(type_name, "typeName")?;
878        require_label(field, "fieldName")?;
879        let guard = self.state.read();
880        let resolver = guard
881            .get(&ctx.account)
882            .and_then(|d| d.resolvers.get(api_id))
883            .and_then(|m| m.get(&resolver_key(type_name, field)))
884            .cloned()
885            .ok_or_else(|| not_found(&format!("Resolver {type_name}.{field} not found.")))?;
886        Ok(ok(json!({ "resolver": resolver })))
887    }
888
889    fn list_resolvers(
890        &self,
891        ctx: &Ctx,
892        api_id: &str,
893        type_name: &str,
894    ) -> Result<AwsResponse, AwsServiceError> {
895        require_label(type_name, "typeName")?;
896        let guard = self.state.read();
897        let data = guard
898            .get(&ctx.account)
899            .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
900        self.require_api(data, api_id)?;
901        let prefix = format!("{type_name}::");
902        let items: Vec<Value> = data
903            .resolvers
904            .get(api_id)
905            .map(|m| {
906                m.iter()
907                    .filter(|(k, _)| k.starts_with(&prefix))
908                    .map(|(_, v)| v.clone())
909                    .collect()
910            })
911            .unwrap_or_default();
912        Ok(ok(json!({ "resolvers": items })))
913    }
914
915    fn list_resolvers_by_function(
916        &self,
917        ctx: &Ctx,
918        api_id: &str,
919        function_id: &str,
920    ) -> Result<AwsResponse, AwsServiceError> {
921        require_label(function_id, "functionId")?;
922        let guard = self.state.read();
923        let data = guard
924            .get(&ctx.account)
925            .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
926        self.require_api(data, api_id)?;
927        let items: Vec<Value> = data
928            .resolvers
929            .get(api_id)
930            .map(|m| {
931                m.values()
932                    .filter(|r| {
933                        r.get("pipelineConfig")
934                            .and_then(|p| p.get("functions"))
935                            .and_then(Value::as_array)
936                            .map(|fs| fs.iter().any(|f| f.as_str() == Some(function_id)))
937                            .unwrap_or(false)
938                    })
939                    .cloned()
940                    .collect()
941            })
942            .unwrap_or_default();
943        Ok(ok(json!({ "resolvers": items })))
944    }
945
946    fn update_resolver(
947        &self,
948        ctx: &Ctx,
949        api_id: &str,
950        type_name: &str,
951        field: &str,
952        body: &Value,
953    ) -> Result<AwsResponse, AwsServiceError> {
954        require_label(type_name, "typeName")?;
955        require_label(field, "fieldName")?;
956        let mut guard = self.state.write();
957        let data = guard.get_or_create(&ctx.account);
958        self.require_api(data, api_id)?;
959        let key = resolver_key(type_name, field);
960        if !data
961            .resolvers
962            .get(api_id)
963            .map(|m| m.contains_key(&key))
964            .unwrap_or(false)
965        {
966            return Err(not_found(&format!(
967                "Resolver {type_name}.{field} not found."
968            )));
969        }
970        let resolver = build_resolver(ctx, api_id, type_name, field, body);
971        data.resolvers
972            .entry(api_id.to_string())
973            .or_default()
974            .insert(key, resolver.clone());
975        Ok(ok(json!({ "resolver": resolver })))
976    }
977
978    fn delete_resolver(
979        &self,
980        ctx: &Ctx,
981        api_id: &str,
982        type_name: &str,
983        field: &str,
984    ) -> Result<AwsResponse, AwsServiceError> {
985        require_label(type_name, "typeName")?;
986        require_label(field, "fieldName")?;
987        let mut guard = self.state.write();
988        let data = guard.get_or_create(&ctx.account);
989        self.require_api(data, api_id)?;
990        match data
991            .resolvers
992            .get_mut(api_id)
993            .and_then(|m| m.remove(&resolver_key(type_name, field)))
994        {
995            Some(_) => Ok(ok(json!({}))),
996            None => Err(not_found(&format!(
997                "Resolver {type_name}.{field} not found."
998            ))),
999        }
1000    }
1001
1002    // ===================== Functions =====================
1003
1004    fn create_function(
1005        &self,
1006        ctx: &Ctx,
1007        api_id: &str,
1008        body: &Value,
1009    ) -> Result<AwsResponse, AwsServiceError> {
1010        let mut guard = self.state.write();
1011        let data = guard.get_or_create(&ctx.account);
1012        self.require_api(data, api_id)?;
1013        let function_id = gen_hex(12);
1014        let func = build_function(ctx, api_id, &function_id, body);
1015        data.functions
1016            .entry(api_id.to_string())
1017            .or_default()
1018            .insert(function_id, func.clone());
1019        Ok(ok(json!({ "functionConfiguration": func })))
1020    }
1021
1022    fn get_function(
1023        &self,
1024        ctx: &Ctx,
1025        api_id: &str,
1026        function_id: &str,
1027    ) -> Result<AwsResponse, AwsServiceError> {
1028        require_label(function_id, "functionId")?;
1029        let guard = self.state.read();
1030        let func = guard
1031            .get(&ctx.account)
1032            .and_then(|d| d.functions.get(api_id))
1033            .and_then(|m| m.get(function_id))
1034            .cloned()
1035            .ok_or_else(|| not_found(&format!("Function {function_id} not found.")))?;
1036        Ok(ok(json!({ "functionConfiguration": func })))
1037    }
1038
1039    fn list_functions(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
1040        let guard = self.state.read();
1041        let data = guard
1042            .get(&ctx.account)
1043            .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
1044        self.require_api(data, api_id)?;
1045        let items: Vec<Value> = data
1046            .functions
1047            .get(api_id)
1048            .map(|m| m.values().cloned().collect())
1049            .unwrap_or_default();
1050        Ok(ok(json!({ "functions": items })))
1051    }
1052
1053    fn update_function(
1054        &self,
1055        ctx: &Ctx,
1056        api_id: &str,
1057        function_id: &str,
1058        body: &Value,
1059    ) -> Result<AwsResponse, AwsServiceError> {
1060        require_label(function_id, "functionId")?;
1061        let mut guard = self.state.write();
1062        let data = guard.get_or_create(&ctx.account);
1063        self.require_api(data, api_id)?;
1064        if !data
1065            .functions
1066            .get(api_id)
1067            .map(|m| m.contains_key(function_id))
1068            .unwrap_or(false)
1069        {
1070            return Err(not_found(&format!("Function {function_id} not found.")));
1071        }
1072        let func = build_function(ctx, api_id, function_id, body);
1073        data.functions
1074            .entry(api_id.to_string())
1075            .or_default()
1076            .insert(function_id.to_string(), func.clone());
1077        Ok(ok(json!({ "functionConfiguration": func })))
1078    }
1079
1080    fn delete_function(
1081        &self,
1082        ctx: &Ctx,
1083        api_id: &str,
1084        function_id: &str,
1085    ) -> Result<AwsResponse, AwsServiceError> {
1086        require_label(function_id, "functionId")?;
1087        let mut guard = self.state.write();
1088        let data = guard.get_or_create(&ctx.account);
1089        self.require_api(data, api_id)?;
1090        match data
1091            .functions
1092            .get_mut(api_id)
1093            .and_then(|m| m.remove(function_id))
1094        {
1095            Some(_) => Ok(ok(json!({}))),
1096            None => Err(not_found(&format!("Function {function_id} not found."))),
1097        }
1098    }
1099
1100    // ===================== Types + schema =====================
1101
1102    fn create_type(
1103        &self,
1104        ctx: &Ctx,
1105        api_id: &str,
1106        body: &Value,
1107    ) -> Result<AwsResponse, AwsServiceError> {
1108        let definition = require_body_str(body, "definition")?.to_string();
1109        let format = require_body_str(body, "format")?.to_string();
1110        let mut guard = self.state.write();
1111        let data = guard.get_or_create(&ctx.account);
1112        self.require_api(data, api_id)?;
1113        let name = parse_type_name(&definition).unwrap_or_else(|| gen_hex(6));
1114        let ty = build_type(ctx, api_id, &name, &definition, &format);
1115        data.types
1116            .entry(api_id.to_string())
1117            .or_default()
1118            .insert(name, ty.clone());
1119        Ok(ok(json!({ "type": ty })))
1120    }
1121
1122    fn get_type(
1123        &self,
1124        ctx: &Ctx,
1125        api_id: &str,
1126        type_name: &str,
1127        q: &[(String, String)],
1128    ) -> Result<AwsResponse, AwsServiceError> {
1129        require_label(type_name, "typeName")?;
1130        require_query(q, "format")?;
1131        let guard = self.state.read();
1132        let ty = guard
1133            .get(&ctx.account)
1134            .and_then(|d| d.types.get(api_id))
1135            .and_then(|m| m.get(type_name))
1136            .cloned()
1137            .ok_or_else(|| not_found(&format!("Type {type_name} not found.")))?;
1138        Ok(ok(json!({ "type": ty })))
1139    }
1140
1141    fn list_types(
1142        &self,
1143        ctx: &Ctx,
1144        api_id: &str,
1145        q: &[(String, String)],
1146    ) -> Result<AwsResponse, AwsServiceError> {
1147        require_query(q, "format")?;
1148        let guard = self.state.read();
1149        let data = guard
1150            .get(&ctx.account)
1151            .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
1152        self.require_api(data, api_id)?;
1153        let items: Vec<Value> = data
1154            .types
1155            .get(api_id)
1156            .map(|m| m.values().cloned().collect())
1157            .unwrap_or_default();
1158        Ok(ok(json!({ "types": items })))
1159    }
1160
1161    fn update_type(
1162        &self,
1163        ctx: &Ctx,
1164        api_id: &str,
1165        type_name: &str,
1166        body: &Value,
1167    ) -> Result<AwsResponse, AwsServiceError> {
1168        require_label(type_name, "typeName")?;
1169        let format = require_body_str(body, "format")?.to_string();
1170        let mut guard = self.state.write();
1171        let data = guard.get_or_create(&ctx.account);
1172        self.require_api(data, api_id)?;
1173        let existing = data
1174            .types
1175            .get(api_id)
1176            .and_then(|m| m.get(type_name))
1177            .cloned()
1178            .ok_or_else(|| not_found(&format!("Type {type_name} not found.")))?;
1179        let definition = body
1180            .get("definition")
1181            .and_then(Value::as_str)
1182            .map(str::to_string)
1183            .or_else(|| {
1184                existing
1185                    .get("definition")
1186                    .and_then(Value::as_str)
1187                    .map(str::to_string)
1188            })
1189            .unwrap_or_default();
1190        let ty = build_type(ctx, api_id, type_name, &definition, &format);
1191        data.types
1192            .entry(api_id.to_string())
1193            .or_default()
1194            .insert(type_name.to_string(), ty.clone());
1195        Ok(ok(json!({ "type": ty })))
1196    }
1197
1198    fn delete_type(
1199        &self,
1200        ctx: &Ctx,
1201        api_id: &str,
1202        type_name: &str,
1203    ) -> Result<AwsResponse, AwsServiceError> {
1204        require_label(type_name, "typeName")?;
1205        let mut guard = self.state.write();
1206        let data = guard.get_or_create(&ctx.account);
1207        self.require_api(data, api_id)?;
1208        match data.types.get_mut(api_id).and_then(|m| m.remove(type_name)) {
1209            Some(_) => Ok(ok(json!({}))),
1210            None => Err(not_found(&format!("Type {type_name} not found."))),
1211        }
1212    }
1213
1214    fn start_schema_creation(
1215        &self,
1216        ctx: &Ctx,
1217        api_id: &str,
1218        body: &Value,
1219    ) -> Result<AwsResponse, AwsServiceError> {
1220        let def_b64 = require_body_str(body, "definition")?;
1221        let sdl = decode_blob(def_b64);
1222        let mut guard = self.state.write();
1223        let data = guard.get_or_create(&ctx.account);
1224        self.require_api(data, api_id)?;
1225        data.schemas.insert(
1226            api_id.to_string(),
1227            SchemaState {
1228                definition: sdl,
1229                status: "Processing".to_string(),
1230                details: "Schema creation in progress.".to_string(),
1231            },
1232        );
1233        Ok(ok(json!({ "status": "PROCESSING" })))
1234    }
1235
1236    fn get_schema_creation_status(
1237        &self,
1238        ctx: &Ctx,
1239        api_id: &str,
1240    ) -> Result<AwsResponse, AwsServiceError> {
1241        require_label(api_id, "apiId")?;
1242        let mut guard = self.state.write();
1243        let data = guard.get_or_create(&ctx.account);
1244        self.require_api(data, api_id)?;
1245        let schema = data.schemas.entry(api_id.to_string()).or_default();
1246        // Settle the async lifecycle: first status read after creation flips
1247        // Processing -> Success, mirroring other async ops.
1248        if schema.status.is_empty() || schema.status == "NotApplicable" {
1249            schema.status = "NotApplicable".to_string();
1250        } else {
1251            schema.status = "Success".to_string();
1252            schema.details = "Schema is ready.".to_string();
1253        }
1254        Ok(ok(json!({
1255            "status": schema_status_wire(&schema.status),
1256            "details": schema.details,
1257        })))
1258    }
1259
1260    fn get_introspection_schema(
1261        &self,
1262        ctx: &Ctx,
1263        api_id: &str,
1264        q: &[(String, String)],
1265    ) -> Result<AwsResponse, AwsServiceError> {
1266        require_label(api_id, "apiId")?;
1267        let format = require_query(q, "format")?.to_string();
1268        let guard = self.state.read();
1269        let data = guard
1270            .get(&ctx.account)
1271            .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
1272        self.require_api(data, api_id)?;
1273        let schema = data.schemas.get(api_id).map(|s| s.definition.clone());
1274        match schema {
1275            Some(sdl) if !sdl.is_empty() => {
1276                let bytes = crate::schema::introspection_bytes(&sdl, &format);
1277                let b64 = base64::engine::general_purpose::STANDARD.encode(bytes);
1278                Ok(ok(json!({ "schema": b64 })))
1279            }
1280            _ => Err(AwsServiceError::aws_error(
1281                StatusCode::BAD_REQUEST,
1282                "GraphQLSchemaException",
1283                "No schema found. Provide a valid schema via StartSchemaCreation first.",
1284            )),
1285        }
1286    }
1287
1288    // ===================== API caches =====================
1289
1290    fn create_api_cache(
1291        &self,
1292        ctx: &Ctx,
1293        api_id: &str,
1294        body: &Value,
1295    ) -> Result<AwsResponse, AwsServiceError> {
1296        let mut guard = self.state.write();
1297        let data = guard.get_or_create(&ctx.account);
1298        self.require_api(data, api_id)?;
1299        if data.api_caches.contains_key(api_id) {
1300            return Err(bad("An API cache already exists for this API."));
1301        }
1302        let cache = build_api_cache(body);
1303        data.api_caches.insert(api_id.to_string(), cache.clone());
1304        Ok(ok(json!({ "apiCache": cache })))
1305    }
1306
1307    fn update_api_cache(
1308        &self,
1309        ctx: &Ctx,
1310        api_id: &str,
1311        body: &Value,
1312    ) -> Result<AwsResponse, AwsServiceError> {
1313        let mut guard = self.state.write();
1314        let data = guard.get_or_create(&ctx.account);
1315        self.require_api(data, api_id)?;
1316        if !data.api_caches.contains_key(api_id) {
1317            return Err(not_found("API cache not found."));
1318        }
1319        let cache = build_api_cache(body);
1320        data.api_caches.insert(api_id.to_string(), cache.clone());
1321        Ok(ok(json!({ "apiCache": cache })))
1322    }
1323
1324    fn get_api_cache(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
1325        require_label(api_id, "apiId")?;
1326        let guard = self.state.read();
1327        let cache = guard
1328            .get(&ctx.account)
1329            .and_then(|d| d.api_caches.get(api_id))
1330            .cloned()
1331            .ok_or_else(|| not_found("API cache not found."))?;
1332        Ok(ok(json!({ "apiCache": cache })))
1333    }
1334
1335    fn delete_api_cache(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
1336        require_label(api_id, "apiId")?;
1337        let mut guard = self.state.write();
1338        let data = guard.get_or_create(&ctx.account);
1339        self.require_api(data, api_id)?;
1340        match data.api_caches.remove(api_id) {
1341            Some(_) => Ok(ok(json!({}))),
1342            None => Err(not_found("API cache not found.")),
1343        }
1344    }
1345
1346    fn flush_api_cache(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
1347        require_label(api_id, "apiId")?;
1348        let guard = self.state.read();
1349        let data = guard
1350            .get(&ctx.account)
1351            .ok_or_else(|| not_found("API cache not found."))?;
1352        if data.api_caches.contains_key(api_id) {
1353            Ok(ok(json!({})))
1354        } else {
1355            Err(not_found("API cache not found."))
1356        }
1357    }
1358
1359    // ===================== Environment variables =====================
1360
1361    fn put_env_vars(
1362        &self,
1363        ctx: &Ctx,
1364        api_id: &str,
1365        body: &Value,
1366    ) -> Result<AwsResponse, AwsServiceError> {
1367        let mut guard = self.state.write();
1368        let data = guard.get_or_create(&ctx.account);
1369        self.require_api(data, api_id)?;
1370        let vars = body
1371            .get("environmentVariables")
1372            .and_then(Value::as_object)
1373            .map(string_map)
1374            .unwrap_or_default();
1375        data.env_vars.insert(api_id.to_string(), vars.clone());
1376        Ok(ok(json!({ "environmentVariables": vars })))
1377    }
1378
1379    fn get_env_vars(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
1380        require_label(api_id, "apiId")?;
1381        let guard = self.state.read();
1382        let data = guard
1383            .get(&ctx.account)
1384            .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
1385        self.require_api(data, api_id)?;
1386        let vars = data.env_vars.get(api_id).cloned().unwrap_or_default();
1387        Ok(ok(json!({ "environmentVariables": vars })))
1388    }
1389
1390    // ===================== Domain names =====================
1391
1392    fn create_domain_name(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
1393        let domain = require_body_str(body, "domainName")?.to_string();
1394        let mut guard = self.state.write();
1395        let data = guard.get_or_create(&ctx.account);
1396        let cfg = build_domain_name(ctx, &domain, body);
1397        data.domain_names.insert(domain, cfg.clone());
1398        Ok(ok(json!({ "domainNameConfig": cfg })))
1399    }
1400
1401    fn get_domain_name(&self, ctx: &Ctx, domain: &str) -> Result<AwsResponse, AwsServiceError> {
1402        require_label(domain, "domainName")?;
1403        let guard = self.state.read();
1404        let cfg = guard
1405            .get(&ctx.account)
1406            .and_then(|d| d.domain_names.get(domain))
1407            .cloned()
1408            .ok_or_else(|| not_found(&format!("Domain name {domain} not found.")))?;
1409        Ok(ok(json!({ "domainNameConfig": cfg })))
1410    }
1411
1412    fn list_domain_names(&self, ctx: &Ctx) -> Result<AwsResponse, AwsServiceError> {
1413        let guard = self.state.read();
1414        let items: Vec<Value> = guard
1415            .get(&ctx.account)
1416            .map(|d| d.domain_names.values().cloned().collect())
1417            .unwrap_or_default();
1418        Ok(ok(json!({ "domainNameConfigs": items })))
1419    }
1420
1421    fn update_domain_name(
1422        &self,
1423        ctx: &Ctx,
1424        domain: &str,
1425        body: &Value,
1426    ) -> Result<AwsResponse, AwsServiceError> {
1427        require_label(domain, "domainName")?;
1428        let mut guard = self.state.write();
1429        let data = guard.get_or_create(&ctx.account);
1430        let mut cfg = data
1431            .domain_names
1432            .get(domain)
1433            .cloned()
1434            .ok_or_else(|| not_found(&format!("Domain name {domain} not found.")))?;
1435        if let (Some(obj), Some(desc)) = (cfg.as_object_mut(), body.get("description")) {
1436            obj.insert("description".into(), desc.clone());
1437        }
1438        data.domain_names.insert(domain.to_string(), cfg.clone());
1439        Ok(ok(json!({ "domainNameConfig": cfg })))
1440    }
1441
1442    fn delete_domain_name(&self, ctx: &Ctx, domain: &str) -> Result<AwsResponse, AwsServiceError> {
1443        require_label(domain, "domainName")?;
1444        let mut guard = self.state.write();
1445        let data = guard.get_or_create(&ctx.account);
1446        match data.domain_names.remove(domain) {
1447            Some(_) => {
1448                data.api_associations.remove(domain);
1449                Ok(ok(json!({})))
1450            }
1451            None => Err(not_found(&format!("Domain name {domain} not found."))),
1452        }
1453    }
1454
1455    fn associate_api(
1456        &self,
1457        ctx: &Ctx,
1458        domain: &str,
1459        body: &Value,
1460    ) -> Result<AwsResponse, AwsServiceError> {
1461        require_label(domain, "domainName")?;
1462        let api_id = require_body_str(body, "apiId")?.to_string();
1463        let mut guard = self.state.write();
1464        let data = guard.get_or_create(&ctx.account);
1465        if !data.domain_names.contains_key(domain) {
1466            return Err(not_found(&format!("Domain name {domain} not found.")));
1467        }
1468        let assoc = json!({
1469            "domainName": domain,
1470            "apiId": api_id,
1471            "associationStatus": "SUCCESS",
1472        });
1473        data.api_associations
1474            .insert(domain.to_string(), assoc.clone());
1475        Ok(ok(json!({ "apiAssociation": assoc })))
1476    }
1477
1478    fn get_api_association(&self, ctx: &Ctx, domain: &str) -> Result<AwsResponse, AwsServiceError> {
1479        require_label(domain, "domainName")?;
1480        let guard = self.state.read();
1481        let assoc = guard
1482            .get(&ctx.account)
1483            .and_then(|d| d.api_associations.get(domain))
1484            .cloned()
1485            .ok_or_else(|| not_found(&format!("No API association for {domain}.")))?;
1486        Ok(ok(json!({ "apiAssociation": assoc })))
1487    }
1488
1489    fn disassociate_api(&self, ctx: &Ctx, domain: &str) -> Result<AwsResponse, AwsServiceError> {
1490        require_label(domain, "domainName")?;
1491        let mut guard = self.state.write();
1492        let data = guard.get_or_create(&ctx.account);
1493        match data.api_associations.remove(domain) {
1494            Some(_) => Ok(ok(json!({}))),
1495            None => Err(not_found(&format!("No API association for {domain}."))),
1496        }
1497    }
1498
1499    // ===================== Event APIs =====================
1500
1501    fn create_api(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
1502        let api_id = gen_api_id();
1503        let api = build_event_api(ctx, &api_id, body);
1504        let mut guard = self.state.write();
1505        let data = guard.get_or_create(&ctx.account);
1506        data.apis.insert(api_id, api.clone());
1507        Ok(ok(json!({ "api": api })))
1508    }
1509
1510    fn get_api(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
1511        require_label(api_id, "apiId")?;
1512        let guard = self.state.read();
1513        let api = guard
1514            .get(&ctx.account)
1515            .and_then(|d| d.apis.get(api_id))
1516            .cloned()
1517            .ok_or_else(|| not_found(&format!("API {api_id} not found.")))?;
1518        Ok(ok(json!({ "api": api })))
1519    }
1520
1521    fn list_apis(&self, ctx: &Ctx) -> Result<AwsResponse, AwsServiceError> {
1522        let guard = self.state.read();
1523        let items: Vec<Value> = guard
1524            .get(&ctx.account)
1525            .map(|d| d.apis.values().cloned().collect())
1526            .unwrap_or_default();
1527        Ok(ok(json!({ "apis": items })))
1528    }
1529
1530    fn update_api(
1531        &self,
1532        ctx: &Ctx,
1533        api_id: &str,
1534        body: &Value,
1535    ) -> Result<AwsResponse, AwsServiceError> {
1536        require_label(api_id, "apiId")?;
1537        let mut guard = self.state.write();
1538        let data = guard.get_or_create(&ctx.account);
1539        if !data.apis.contains_key(api_id) {
1540            return Err(not_found(&format!("API {api_id} not found.")));
1541        }
1542        let api = build_event_api(ctx, api_id, body);
1543        data.apis.insert(api_id.to_string(), api.clone());
1544        Ok(ok(json!({ "api": api })))
1545    }
1546
1547    fn delete_api(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
1548        require_label(api_id, "apiId")?;
1549        let mut guard = self.state.write();
1550        let data = guard.get_or_create(&ctx.account);
1551        match data.apis.remove(api_id) {
1552            Some(_) => {
1553                data.channel_namespaces.remove(api_id);
1554                Ok(ok(json!({})))
1555            }
1556            None => Err(not_found(&format!("API {api_id} not found."))),
1557        }
1558    }
1559
1560    fn create_channel_namespace(
1561        &self,
1562        ctx: &Ctx,
1563        api_id: &str,
1564        body: &Value,
1565    ) -> Result<AwsResponse, AwsServiceError> {
1566        let name = require_body_str(body, "name")?.to_string();
1567        let mut guard = self.state.write();
1568        let data = guard.get_or_create(&ctx.account);
1569        if !data.apis.contains_key(api_id) {
1570            return Err(not_found(&format!("API {api_id} not found.")));
1571        }
1572        if data
1573            .channel_namespaces
1574            .get(api_id)
1575            .map(|m| m.contains_key(&name))
1576            .unwrap_or(false)
1577        {
1578            return Err(AwsServiceError::aws_error(
1579                StatusCode::CONFLICT,
1580                "ConflictException",
1581                format!("Channel namespace {name} already exists."),
1582            ));
1583        }
1584        let ns = build_channel_namespace(ctx, api_id, &name, body);
1585        data.channel_namespaces
1586            .entry(api_id.to_string())
1587            .or_default()
1588            .insert(name, ns.clone());
1589        Ok(ok(json!({ "channelNamespace": ns })))
1590    }
1591
1592    fn get_channel_namespace(
1593        &self,
1594        ctx: &Ctx,
1595        api_id: &str,
1596        name: &str,
1597    ) -> Result<AwsResponse, AwsServiceError> {
1598        require_label(name, "name")?;
1599        let guard = self.state.read();
1600        let ns = guard
1601            .get(&ctx.account)
1602            .and_then(|d| d.channel_namespaces.get(api_id))
1603            .and_then(|m| m.get(name))
1604            .cloned()
1605            .ok_or_else(|| not_found(&format!("Channel namespace {name} not found.")))?;
1606        Ok(ok(json!({ "channelNamespace": ns })))
1607    }
1608
1609    fn list_channel_namespaces(
1610        &self,
1611        ctx: &Ctx,
1612        api_id: &str,
1613    ) -> Result<AwsResponse, AwsServiceError> {
1614        require_label(api_id, "apiId")?;
1615        let guard = self.state.read();
1616        let data = guard
1617            .get(&ctx.account)
1618            .ok_or_else(|| not_found(&format!("API {api_id} not found.")))?;
1619        if !data.apis.contains_key(api_id) {
1620            return Err(not_found(&format!("API {api_id} not found.")));
1621        }
1622        let items: Vec<Value> = data
1623            .channel_namespaces
1624            .get(api_id)
1625            .map(|m| m.values().cloned().collect())
1626            .unwrap_or_default();
1627        Ok(ok(json!({ "channelNamespaces": items })))
1628    }
1629
1630    fn update_channel_namespace(
1631        &self,
1632        ctx: &Ctx,
1633        api_id: &str,
1634        name: &str,
1635        body: &Value,
1636    ) -> Result<AwsResponse, AwsServiceError> {
1637        require_label(name, "name")?;
1638        let mut guard = self.state.write();
1639        let data = guard.get_or_create(&ctx.account);
1640        if !data
1641            .channel_namespaces
1642            .get(api_id)
1643            .map(|m| m.contains_key(name))
1644            .unwrap_or(false)
1645        {
1646            return Err(not_found(&format!("Channel namespace {name} not found.")));
1647        }
1648        let ns = build_channel_namespace(ctx, api_id, name, body);
1649        data.channel_namespaces
1650            .entry(api_id.to_string())
1651            .or_default()
1652            .insert(name.to_string(), ns.clone());
1653        Ok(ok(json!({ "channelNamespace": ns })))
1654    }
1655
1656    fn delete_channel_namespace(
1657        &self,
1658        ctx: &Ctx,
1659        api_id: &str,
1660        name: &str,
1661    ) -> Result<AwsResponse, AwsServiceError> {
1662        require_label(name, "name")?;
1663        let mut guard = self.state.write();
1664        let data = guard.get_or_create(&ctx.account);
1665        match data
1666            .channel_namespaces
1667            .get_mut(api_id)
1668            .and_then(|m| m.remove(name))
1669        {
1670            Some(_) => Ok(ok(json!({}))),
1671            None => Err(not_found(&format!("Channel namespace {name} not found."))),
1672        }
1673    }
1674
1675    // ===================== Source API associations =====================
1676
1677    fn associate_source_api(
1678        &self,
1679        ctx: &Ctx,
1680        merged_id: &str,
1681        body: &Value,
1682    ) -> Result<AwsResponse, AwsServiceError> {
1683        require_label(merged_id, "mergedApiIdentifier")?;
1684        let source_id = require_body_str(body, "sourceApiIdentifier")?.to_string();
1685        self.create_source_assoc(ctx, merged_id, &source_id, body)
1686    }
1687
1688    fn associate_merged_api(
1689        &self,
1690        ctx: &Ctx,
1691        source_id: &str,
1692        body: &Value,
1693    ) -> Result<AwsResponse, AwsServiceError> {
1694        require_label(source_id, "sourceApiIdentifier")?;
1695        let merged_id = require_body_str(body, "mergedApiIdentifier")?.to_string();
1696        self.create_source_assoc(ctx, &merged_id, source_id, body)
1697    }
1698
1699    fn create_source_assoc(
1700        &self,
1701        ctx: &Ctx,
1702        merged_id: &str,
1703        source_id: &str,
1704        body: &Value,
1705    ) -> Result<AwsResponse, AwsServiceError> {
1706        {
1707            // Both the merged and source APIs must exist. Synthetic ids (and an
1708            // unsubstituted `{...}` label) resolve to nothing -> NotFoundException,
1709            // which every association op declares.
1710            let guard = self.state.read();
1711            let data = guard.get(&ctx.account);
1712            if !api_ref_exists(data, merged_id) {
1713                return Err(not_found(&format!("Merged API {merged_id} not found.")));
1714            }
1715            if !api_ref_exists(data, source_id) {
1716                return Err(not_found(&format!("Source API {source_id} not found.")));
1717            }
1718        }
1719        let assoc_id = gen_hex(16);
1720        let mut assoc = Map::new();
1721        assoc.insert("associationId".into(), json!(assoc_id));
1722        assoc.insert(
1723            "associationArn".into(),
1724            json!(format!(
1725                "arn:aws:appsync:{}:{}:apis/{merged_id}/sourceApiAssociations/{assoc_id}",
1726                ctx.region, ctx.account
1727            )),
1728        );
1729        assoc.insert("sourceApiId".into(), json!(source_id));
1730        assoc.insert("mergedApiId".into(), json!(merged_id));
1731        assoc.insert("sourceApiAssociationStatus".into(), json!("MERGE_SUCCESS"));
1732        copy_keys(
1733            &mut assoc,
1734            body,
1735            &["description", "sourceApiAssociationConfig"],
1736        );
1737        let assoc = Value::Object(assoc);
1738        let mut guard = self.state.write();
1739        let data = guard.get_or_create(&ctx.account);
1740        data.source_api_associations.insert(assoc_id, assoc.clone());
1741        Ok(ok(json!({ "sourceApiAssociation": assoc })))
1742    }
1743
1744    fn get_source_api_association(
1745        &self,
1746        ctx: &Ctx,
1747        assoc_id: &str,
1748    ) -> Result<AwsResponse, AwsServiceError> {
1749        require_label(assoc_id, "associationId")?;
1750        let guard = self.state.read();
1751        let assoc = guard
1752            .get(&ctx.account)
1753            .and_then(|d| d.source_api_associations.get(assoc_id))
1754            .cloned()
1755            .ok_or_else(|| not_found(&format!("Association {assoc_id} not found.")))?;
1756        Ok(ok(json!({ "sourceApiAssociation": assoc })))
1757    }
1758
1759    fn list_source_api_associations(
1760        &self,
1761        ctx: &Ctx,
1762        api_id: &str,
1763    ) -> Result<AwsResponse, AwsServiceError> {
1764        require_label(api_id, "apiId")?;
1765        let guard = self.state.read();
1766        let data = guard
1767            .get(&ctx.account)
1768            .ok_or_else(|| not_found(&format!("Merged API {api_id} not found.")))?;
1769        self.require_api(data, api_id)?;
1770        let items: Vec<Value> = data
1771            .source_api_associations
1772            .values()
1773            .filter(|a| {
1774                a.get("mergedApiId").and_then(Value::as_str) == Some(api_id)
1775                    || a.get("sourceApiId").and_then(Value::as_str) == Some(api_id)
1776            })
1777            .map(|a| {
1778                json!({
1779                    "associationId": a.get("associationId").cloned().unwrap_or(Value::Null),
1780                    "associationArn": a.get("associationArn").cloned().unwrap_or(Value::Null),
1781                    "sourceApiId": a.get("sourceApiId").cloned().unwrap_or(Value::Null),
1782                    "mergedApiId": a.get("mergedApiId").cloned().unwrap_or(Value::Null),
1783                    "description": a.get("description").cloned().unwrap_or(Value::Null),
1784                })
1785            })
1786            .collect();
1787        Ok(ok(json!({ "sourceApiAssociationSummaries": items })))
1788    }
1789
1790    fn list_types_by_association(
1791        &self,
1792        ctx: &Ctx,
1793        merged_id: &str,
1794        assoc_id: &str,
1795        q: &[(String, String)],
1796    ) -> Result<AwsResponse, AwsServiceError> {
1797        require_label(merged_id, "mergedApiIdentifier")?;
1798        require_label(assoc_id, "associationId")?;
1799        require_query(q, "format")?;
1800        let guard = self.state.read();
1801        let exists = guard
1802            .get(&ctx.account)
1803            .map(|d| d.source_api_associations.contains_key(assoc_id))
1804            .unwrap_or(false);
1805        if !exists {
1806            return Err(not_found(&format!("Association {assoc_id} not found.")));
1807        }
1808        Ok(ok(json!({ "types": [] })))
1809    }
1810
1811    fn update_source_api_association(
1812        &self,
1813        ctx: &Ctx,
1814        assoc_id: &str,
1815        body: &Value,
1816    ) -> Result<AwsResponse, AwsServiceError> {
1817        require_label(assoc_id, "associationId")?;
1818        let mut guard = self.state.write();
1819        let data = guard.get_or_create(&ctx.account);
1820        let assoc = data
1821            .source_api_associations
1822            .get_mut(assoc_id)
1823            .ok_or_else(|| not_found(&format!("Association {assoc_id} not found.")))?;
1824        if let Some(obj) = assoc.as_object_mut() {
1825            for k in ["description", "sourceApiAssociationConfig"] {
1826                if let Some(v) = body.get(k) {
1827                    obj.insert(k.to_string(), v.clone());
1828                }
1829            }
1830        }
1831        Ok(ok(json!({ "sourceApiAssociation": assoc })))
1832    }
1833
1834    fn disassociate_source_api(
1835        &self,
1836        ctx: &Ctx,
1837        assoc_id: &str,
1838    ) -> Result<AwsResponse, AwsServiceError> {
1839        require_label(assoc_id, "associationId")?;
1840        let mut guard = self.state.write();
1841        let data = guard.get_or_create(&ctx.account);
1842        match data.source_api_associations.remove(assoc_id) {
1843            Some(_) => Ok(ok(
1844                json!({ "sourceApiAssociationStatus": "DELETION_IN_PROGRESS" }),
1845            )),
1846            None => Err(not_found(&format!("Association {assoc_id} not found."))),
1847        }
1848    }
1849
1850    fn start_schema_merge(
1851        &self,
1852        ctx: &Ctx,
1853        assoc_id: &str,
1854    ) -> Result<AwsResponse, AwsServiceError> {
1855        require_label(assoc_id, "associationId")?;
1856        let guard = self.state.read();
1857        let exists = guard
1858            .get(&ctx.account)
1859            .map(|d| d.source_api_associations.contains_key(assoc_id))
1860            .unwrap_or(false);
1861        if exists {
1862            Ok(ok(
1863                json!({ "sourceApiAssociationStatus": "MERGE_IN_PROGRESS" }),
1864            ))
1865        } else {
1866            Err(not_found(&format!("Association {assoc_id} not found.")))
1867        }
1868    }
1869
1870    // ===================== Data-source introspection =====================
1871
1872    fn start_ds_introspection() -> Result<AwsResponse, AwsServiceError> {
1873        let id = gen_hex(20);
1874        Ok(ok(json!({
1875            "introspectionId": id,
1876            "introspectionStatus": "PROCESSING",
1877            "introspectionStatusDetail": "Introspection started.",
1878        })))
1879    }
1880
1881    fn get_ds_introspection(
1882        &self,
1883        _ctx: &Ctx,
1884        introspection_id: &str,
1885    ) -> Result<AwsResponse, AwsServiceError> {
1886        require_label(introspection_id, "introspectionId")?;
1887        // Introspection jobs are ephemeral; a synthetic/unknown id is a
1888        // declared NotFoundException. Real jobs would settle here.
1889        Err(not_found(&format!(
1890            "Introspection {introspection_id} not found."
1891        )))
1892    }
1893
1894    // ===================== Tags =====================
1895
1896    fn tag_resource(
1897        &self,
1898        ctx: &Ctx,
1899        arn: &str,
1900        body: &Value,
1901    ) -> Result<AwsResponse, AwsServiceError> {
1902        require_resource_arn(arn)?;
1903        let mut guard = self.state.write();
1904        let data = guard.get_or_create(&ctx.account);
1905        let entry = data.tags.entry(arn.to_string()).or_default();
1906        if let Some(tags) = body.get("tags").and_then(Value::as_object) {
1907            for (k, v) in tags {
1908                if let Some(s) = v.as_str() {
1909                    entry.insert(k.clone(), s.to_string());
1910                }
1911            }
1912        }
1913        Ok(ok(json!({})))
1914    }
1915
1916    fn untag_resource(
1917        &self,
1918        ctx: &Ctx,
1919        arn: &str,
1920        q: &[(String, String)],
1921    ) -> Result<AwsResponse, AwsServiceError> {
1922        require_resource_arn(arn)?;
1923        let mut guard = self.state.write();
1924        let data = guard.get_or_create(&ctx.account);
1925        if let Some(entry) = data.tags.get_mut(arn) {
1926            for (_, v) in q.iter().filter(|(k, _)| k == "tagKeys") {
1927                entry.remove(v);
1928            }
1929        }
1930        Ok(ok(json!({})))
1931    }
1932
1933    fn list_tags_for_resource(&self, ctx: &Ctx, arn: &str) -> Result<AwsResponse, AwsServiceError> {
1934        require_resource_arn(arn)?;
1935        let guard = self.state.read();
1936        let tags = guard
1937            .get(&ctx.account)
1938            .and_then(|d| d.tags.get(arn))
1939            .cloned()
1940            .unwrap_or_default();
1941        Ok(ok(json!({ "tags": tags })))
1942    }
1943
1944    // ===================== Evaluation data plane =====================
1945
1946    fn evaluate_code(body: &Value) -> Result<AwsResponse, AwsServiceError> {
1947        let code = body.get("code").and_then(Value::as_str).unwrap_or_default();
1948        let context = body
1949            .get("context")
1950            .and_then(Value::as_str)
1951            .unwrap_or_default();
1952        let ev = crate::evaluate::evaluate(code, context);
1953        Ok(ok(build_evaluation(ev)))
1954    }
1955
1956    fn evaluate_template(body: &Value) -> Result<AwsResponse, AwsServiceError> {
1957        let template = body
1958            .get("template")
1959            .and_then(Value::as_str)
1960            .unwrap_or_default();
1961        let context = body
1962            .get("context")
1963            .and_then(Value::as_str)
1964            .unwrap_or_default();
1965        let ev = crate::evaluate::evaluate(template, context);
1966        Ok(ok(build_evaluation(ev)))
1967    }
1968}
1969
1970// ===================== builders =====================
1971
1972fn build_evaluation(ev: crate::evaluate::Evaluation) -> Value {
1973    let mut out = Map::new();
1974    if let Some(r) = ev.result {
1975        out.insert("evaluationResult".into(), json!(r));
1976    }
1977    if let Some(s) = ev.stash {
1978        out.insert("stash".into(), json!(s));
1979    }
1980    if let Some(e) = ev.error {
1981        out.insert("error".into(), json!({ "message": e }));
1982    }
1983    out.insert("logs".into(), json!([]));
1984    Value::Object(out)
1985}
1986
1987fn build_data_source(ctx: &Ctx, api_id: &str, name: &str, body: &Value) -> Value {
1988    let mut ds = Map::new();
1989    ds.insert(
1990        "dataSourceArn".into(),
1991        json!(format!(
1992            "arn:aws:appsync:{}:{}:apis/{api_id}/datasources/{name}",
1993            ctx.region, ctx.account
1994        )),
1995    );
1996    ds.insert("name".into(), json!(name));
1997    copy_keys(
1998        &mut ds,
1999        body,
2000        &[
2001            "description",
2002            "type",
2003            "serviceRoleArn",
2004            "dynamodbConfig",
2005            "lambdaConfig",
2006            "elasticsearchConfig",
2007            "openSearchServiceConfig",
2008            "httpConfig",
2009            "relationalDatabaseConfig",
2010            "eventBridgeConfig",
2011            "metricsConfig",
2012        ],
2013    );
2014    Value::Object(ds)
2015}
2016
2017fn build_resolver(ctx: &Ctx, api_id: &str, type_name: &str, field: &str, body: &Value) -> Value {
2018    let mut r = Map::new();
2019    r.insert("typeName".into(), json!(type_name));
2020    r.insert("fieldName".into(), json!(field));
2021    r.insert(
2022        "resolverArn".into(),
2023        json!(format!(
2024            "arn:aws:appsync:{}:{}:apis/{api_id}/types/{type_name}/resolvers/{field}",
2025            ctx.region, ctx.account
2026        )),
2027    );
2028    r.insert(
2029        "kind".into(),
2030        json!(body.get("kind").and_then(Value::as_str).unwrap_or("UNIT")),
2031    );
2032    copy_keys(
2033        &mut r,
2034        body,
2035        &[
2036            "dataSourceName",
2037            "requestMappingTemplate",
2038            "responseMappingTemplate",
2039            "pipelineConfig",
2040            "syncConfig",
2041            "cachingConfig",
2042            "maxBatchSize",
2043            "runtime",
2044            "code",
2045            "metricsConfig",
2046        ],
2047    );
2048    Value::Object(r)
2049}
2050
2051fn build_function(ctx: &Ctx, api_id: &str, function_id: &str, body: &Value) -> Value {
2052    let mut f = Map::new();
2053    f.insert("functionId".into(), json!(function_id));
2054    f.insert(
2055        "functionArn".into(),
2056        json!(format!(
2057            "arn:aws:appsync:{}:{}:apis/{api_id}/functions/{function_id}",
2058            ctx.region, ctx.account
2059        )),
2060    );
2061    f.insert(
2062        "functionVersion".into(),
2063        json!(body
2064            .get("functionVersion")
2065            .and_then(Value::as_str)
2066            .unwrap_or("2018-05-29")),
2067    );
2068    copy_keys(
2069        &mut f,
2070        body,
2071        &[
2072            "name",
2073            "description",
2074            "dataSourceName",
2075            "requestMappingTemplate",
2076            "responseMappingTemplate",
2077            "syncConfig",
2078            "maxBatchSize",
2079            "runtime",
2080            "code",
2081        ],
2082    );
2083    Value::Object(f)
2084}
2085
2086fn build_type(ctx: &Ctx, api_id: &str, name: &str, definition: &str, format: &str) -> Value {
2087    json!({
2088        "name": name,
2089        "arn": format!("arn:aws:appsync:{}:{}:apis/{api_id}/types/{name}", ctx.region, ctx.account),
2090        "definition": definition,
2091        "format": format,
2092    })
2093}
2094
2095fn build_api_cache(body: &Value) -> Value {
2096    let mut c = Map::new();
2097    c.insert("status".into(), json!("AVAILABLE"));
2098    copy_keys(
2099        &mut c,
2100        body,
2101        &[
2102            "ttl",
2103            "apiCachingBehavior",
2104            "transitEncryptionEnabled",
2105            "atRestEncryptionEnabled",
2106            "type",
2107            "healthMetricsConfig",
2108        ],
2109    );
2110    Value::Object(c)
2111}
2112
2113fn build_domain_name(ctx: &Ctx, domain: &str, body: &Value) -> Value {
2114    let mut c = Map::new();
2115    c.insert("domainName".into(), json!(domain));
2116    c.insert(
2117        "appsyncDomainName".into(),
2118        json!(format!(
2119            "{}.appsync-api.{}.amazonaws.com",
2120            gen_hex(14),
2121            ctx.region
2122        )),
2123    );
2124    c.insert("hostedZoneId".into(), json!("Z2FDTNDATAQYW2"));
2125    c.insert(
2126        "domainNameArn".into(),
2127        json!(format!(
2128            "arn:aws:appsync:{}:{}:domainnames/{domain}",
2129            ctx.region, ctx.account
2130        )),
2131    );
2132    copy_keys(&mut c, body, &["description", "certificateArn", "tags"]);
2133    Value::Object(c)
2134}
2135
2136fn build_event_api(ctx: &Ctx, api_id: &str, body: &Value) -> Value {
2137    let mut api = Map::new();
2138    api.insert("apiId".into(), json!(api_id));
2139    api.insert(
2140        "apiArn".into(),
2141        json!(format!(
2142            "arn:aws:appsync:{}:{}:apis/{api_id}",
2143            ctx.region, ctx.account
2144        )),
2145    );
2146    api.insert("created".into(), json!(now_epoch()));
2147    api.insert("xrayEnabled".into(), json!(false));
2148    api.insert(
2149        "dns".into(),
2150        json!({
2151            "REALTIME": format!("{api_id}.ddpg.appsync-api.{}.amazonaws.com", ctx.region),
2152            "HTTP": format!("{api_id}.appsync-api.{}.amazonaws.com", ctx.region),
2153        }),
2154    );
2155    copy_keys(
2156        &mut api,
2157        body,
2158        &["name", "ownerContact", "tags", "eventConfig"],
2159    );
2160    Value::Object(api)
2161}
2162
2163fn build_channel_namespace(ctx: &Ctx, api_id: &str, name: &str, body: &Value) -> Value {
2164    let mut ns = Map::new();
2165    ns.insert("apiId".into(), json!(api_id));
2166    ns.insert("name".into(), json!(name));
2167    ns.insert(
2168        "channelNamespaceArn".into(),
2169        json!(format!(
2170            "arn:aws:appsync:{}:{}:apis/{api_id}/channelNamespace/{name}",
2171            ctx.region, ctx.account
2172        )),
2173    );
2174    ns.insert("created".into(), json!(now_epoch()));
2175    ns.insert("lastModified".into(), json!(now_epoch()));
2176    copy_keys(
2177        &mut ns,
2178        body,
2179        &[
2180            "subscribeAuthModes",
2181            "publishAuthModes",
2182            "codeHandlers",
2183            "tags",
2184            "handlerConfigs",
2185        ],
2186    );
2187    Value::Object(ns)
2188}
2189
2190// ===================== helpers =====================
2191
2192fn ok(v: Value) -> AwsResponse {
2193    AwsResponse::json_value(StatusCode::OK, v)
2194}
2195
2196fn parse_body(req: &AwsRequest) -> Result<Value, AwsServiceError> {
2197    if req.body.is_empty() {
2198        return Ok(json!({}));
2199    }
2200    serde_json::from_slice(&req.body).map_err(|e| bad(&format!("Request body is malformed: {e}")))
2201}
2202
2203fn bad(msg: &str) -> AwsServiceError {
2204    AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "BadRequestException", msg)
2205}
2206
2207fn not_found(msg: &str) -> AwsServiceError {
2208    AwsServiceError::aws_error(StatusCode::NOT_FOUND, "NotFoundException", msg)
2209}
2210
2211/// Reject an empty path label (a too-short probe URI) with `BadRequestException`
2212/// before resource lookup.
2213fn require_label(value: &str, field: &str) -> Result<(), AwsServiceError> {
2214    if value.is_empty() {
2215        Err(bad(&format!("{field} is required.")))
2216    } else {
2217        Ok(())
2218    }
2219}
2220
2221/// Validate an AppSync `resourceArn` path label: it must be a well-formed ARN
2222/// (`arn:...:appsync:...`) within the declared length bounds. AppSync's
2223/// `ResourceArn` shape constrains it to `^arn:aws:appsync:...` (length 70..=75);
2224/// rejecting a non-ARN (or an unsubstituted `{resourceArn}` placeholder) with
2225/// `BadRequestException` matches the service's own label validation.
2226fn require_resource_arn(arn: &str) -> Result<(), AwsServiceError> {
2227    require_label(arn, "resourceArn")?;
2228    let len = arn.chars().count();
2229    if !arn.starts_with("arn:") || arn.contains('{') || !(1..=1011).contains(&len) {
2230        return Err(bad(&format!("resourceArn '{arn}' is not a valid ARN.")));
2231    }
2232    Ok(())
2233}
2234
2235fn require_body_str<'a>(body: &'a Value, field: &str) -> Result<&'a str, AwsServiceError> {
2236    body.get(field)
2237        .and_then(Value::as_str)
2238        .filter(|s| !s.is_empty())
2239        .ok_or_else(|| bad(&format!("{field} is required.")))
2240}
2241
2242fn require_query<'a>(q: &'a [(String, String)], key: &str) -> Result<&'a str, AwsServiceError> {
2243    q.iter()
2244        .find(|(k, _)| k == key)
2245        .map(|(_, v)| v.as_str())
2246        .filter(|v| !v.is_empty())
2247        .ok_or_else(|| bad(&format!("{key} query parameter is required.")))
2248}
2249
2250fn parse_query(raw: &str) -> Vec<(String, String)> {
2251    raw.split('&')
2252        .filter(|p| !p.is_empty())
2253        .map(|pair| {
2254            let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
2255            (
2256                percent_decode_str(k).decode_utf8_lossy().into_owned(),
2257                percent_decode_str(v).decode_utf8_lossy().into_owned(),
2258            )
2259        })
2260        .collect()
2261}
2262
2263/// Copy each present, non-null key from `src` into `dst`.
2264fn copy_keys(dst: &mut Map<String, Value>, src: &Value, keys: &[&str]) {
2265    for k in keys {
2266        if let Some(v) = src.get(*k) {
2267            if !v.is_null() {
2268                dst.insert((*k).to_string(), v.clone());
2269            }
2270        }
2271    }
2272}
2273
2274fn string_map(obj: &Map<String, Value>) -> std::collections::BTreeMap<String, String> {
2275    obj.iter()
2276        .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
2277        .collect()
2278}
2279
2280fn graphql_api_arn(region: &str, account: &str, api_id: &str) -> String {
2281    format!("arn:aws:appsync:{region}:{account}:apis/{api_id}")
2282}
2283
2284/// Whether an API reference (a bare `apiId` or an `...:apis/<id>` ARN) names a
2285/// GraphQL API that exists in this account. Used by the association ops, which
2286/// reference their source/merged APIs by identifier.
2287fn api_ref_exists(data: Option<&AppSyncData>, ident: &str) -> bool {
2288    let Some(data) = data else {
2289        return false;
2290    };
2291    let id = ident
2292        .rsplit_once("apis/")
2293        .map(|(_, tail)| tail.split('/').next().unwrap_or(tail))
2294        .unwrap_or(ident);
2295    data.graphql_apis.contains_key(id)
2296}
2297
2298fn resolver_key(type_name: &str, field: &str) -> String {
2299    format!("{type_name}::{field}")
2300}
2301
2302/// Extract the top-level type name declared by an SDL `type X { ... }` snippet.
2303fn parse_type_name(definition: &str) -> Option<String> {
2304    for line in definition.lines() {
2305        let t = line.trim();
2306        for kw in [
2307            "type ",
2308            "input ",
2309            "enum ",
2310            "interface ",
2311            "union ",
2312            "scalar ",
2313        ] {
2314            if let Some(rest) = t.strip_prefix(kw) {
2315                if let Some(name) = rest
2316                    .split(|c: char| c.is_whitespace() || c == '{' || c == '(' || c == '=')
2317                    .next()
2318                {
2319                    if !name.is_empty() {
2320                        return Some(name.to_string());
2321                    }
2322                }
2323            }
2324        }
2325    }
2326    None
2327}
2328
2329/// Decode a base64 restJson1 blob to a UTF-8 string, tolerating raw (already
2330/// decoded) input for robustness.
2331fn decode_blob(b64: &str) -> String {
2332    match base64::engine::general_purpose::STANDARD.decode(b64) {
2333        Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
2334        Err(_) => b64.to_string(),
2335    }
2336}
2337
2338/// Map internal schema status to the `SchemaStatus` wire enum.
2339fn schema_status_wire(status: &str) -> &str {
2340    match status {
2341        "Processing" => "PROCESSING",
2342        "Success" => "SUCCESS",
2343        "Active" => "ACTIVE",
2344        "Failed" => "FAILED",
2345        "Deleting" => "DELETING",
2346        _ => "NOT_APPLICABLE",
2347    }
2348}
2349
2350fn gen_api_id() -> String {
2351    const CH: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567";
2352    let mut r = rand::thread_rng();
2353    (0..26)
2354        .map(|_| CH[r.gen_range(0..CH.len())] as char)
2355        .collect()
2356}
2357
2358fn gen_hex(n: usize) -> String {
2359    Uuid::new_v4().simple().to_string()[..n.min(32)].to_string()
2360}
2361
2362#[cfg(test)]
2363mod tests {
2364    use super::*;
2365
2366    fn svc() -> AppSyncService {
2367        AppSyncService::new(Arc::new(parking_lot::RwLock::new(
2368            fakecloud_core::multi_account::MultiAccountState::new("000000000000", "us-east-1", ""),
2369        )))
2370    }
2371
2372    fn ctx() -> Ctx {
2373        Ctx {
2374            account: "000000000000".to_string(),
2375            region: "us-east-1".to_string(),
2376        }
2377    }
2378
2379    fn body_json(resp: &AwsResponse) -> Value {
2380        serde_json::from_slice(resp.body.expect_bytes()).expect("json response body")
2381    }
2382
2383    fn err_of(r: Result<AwsResponse, AwsServiceError>) -> AwsServiceError {
2384        r.err().expect("expected error")
2385    }
2386
2387    fn make_api(s: &AppSyncService) -> String {
2388        let created = body_json(
2389            &s.create_graphql_api(
2390                &ctx(),
2391                &json!({ "name": "demo", "authenticationType": "API_KEY" }),
2392            )
2393            .unwrap(),
2394        );
2395        created["graphqlApi"]["apiId"].as_str().unwrap().to_string()
2396    }
2397
2398    #[test]
2399    fn create_get_list_delete_graphql_api() {
2400        let s = svc();
2401        let api_id = make_api(&s);
2402        let got = body_json(&s.get_graphql_api(&ctx(), &api_id).unwrap());
2403        assert_eq!(got["graphqlApi"]["name"], json!("demo"));
2404        assert!(got["graphqlApi"]["uris"]["GRAPHQL"].is_string());
2405        let list = body_json(&s.list_graphql_apis(&ctx()).unwrap());
2406        assert_eq!(list["graphqlApis"].as_array().unwrap().len(), 1);
2407        s.delete_graphql_api(&ctx(), &api_id).unwrap();
2408        let err = err_of(s.get_graphql_api(&ctx(), &api_id));
2409        assert_eq!(err.status(), StatusCode::NOT_FOUND);
2410    }
2411
2412    #[test]
2413    fn missing_api_is_not_found() {
2414        let s = svc();
2415        let err = err_of(s.get_graphql_api(&ctx(), "nope"));
2416        assert_eq!(err.status(), StatusCode::NOT_FOUND);
2417    }
2418
2419    #[test]
2420    fn api_key_lifecycle() {
2421        let s = svc();
2422        let api_id = make_api(&s);
2423        let key = body_json(&s.create_api_key(&ctx(), &api_id, &json!({})).unwrap());
2424        let id = key["apiKey"]["id"].as_str().unwrap().to_string();
2425        assert!(key["apiKey"]["expires"].is_i64());
2426        let list = body_json(&s.list_api_keys(&ctx(), &api_id).unwrap());
2427        assert_eq!(list["apiKeys"].as_array().unwrap().len(), 1);
2428        s.delete_api_key(&ctx(), &api_id, &id).unwrap();
2429    }
2430
2431    #[test]
2432    fn data_source_and_resolver_roundtrip() {
2433        let s = svc();
2434        let api_id = make_api(&s);
2435        let ds = body_json(
2436            &s.create_data_source(&ctx(), &api_id, &json!({ "name": "src", "type": "NONE" }))
2437                .unwrap(),
2438        );
2439        assert_eq!(ds["dataSource"]["type"], json!("NONE"));
2440        let r = body_json(
2441            &s.create_resolver(
2442                &ctx(),
2443                &api_id,
2444                "Query",
2445                &json!({ "fieldName": "hello", "dataSourceName": "src" }),
2446            )
2447            .unwrap(),
2448        );
2449        assert_eq!(r["resolver"]["fieldName"], json!("hello"));
2450        let got = body_json(&s.get_resolver(&ctx(), &api_id, "Query", "hello").unwrap());
2451        assert_eq!(got["resolver"]["typeName"], json!("Query"));
2452        let listed = body_json(&s.list_resolvers(&ctx(), &api_id, "Query").unwrap());
2453        assert_eq!(listed["resolvers"].as_array().unwrap().len(), 1);
2454    }
2455
2456    #[test]
2457    fn resolver_on_missing_api_is_not_found() {
2458        let s = svc();
2459        let err =
2460            err_of(s.create_resolver(&ctx(), "ghost", "Query", &json!({ "fieldName": "hello" })));
2461        assert_eq!(err.status(), StatusCode::NOT_FOUND);
2462    }
2463
2464    #[test]
2465    fn schema_creation_settles_success() {
2466        let s = svc();
2467        let api_id = make_api(&s);
2468        let sdl = base64::engine::general_purpose::STANDARD.encode("type Query { hello: String }");
2469        s.start_schema_creation(&ctx(), &api_id, &json!({ "definition": sdl }))
2470            .unwrap();
2471        let status = body_json(&s.get_schema_creation_status(&ctx(), &api_id).unwrap());
2472        assert_eq!(status["status"], json!("SUCCESS"));
2473        let schema = body_json(
2474            &s.get_introspection_schema(
2475                &ctx(),
2476                &api_id,
2477                &[("format".to_string(), "SDL".to_string())],
2478            )
2479            .unwrap(),
2480        );
2481        assert!(schema["schema"].is_string());
2482    }
2483
2484    #[test]
2485    fn function_lifecycle() {
2486        let s = svc();
2487        let api_id = make_api(&s);
2488        let f = body_json(
2489            &s.create_function(
2490                &ctx(),
2491                &api_id,
2492                &json!({ "name": "fn1", "dataSourceName": "src" }),
2493            )
2494            .unwrap(),
2495        );
2496        let fid = f["functionConfiguration"]["functionId"]
2497            .as_str()
2498            .unwrap()
2499            .to_string();
2500        let got = body_json(&s.get_function(&ctx(), &api_id, &fid).unwrap());
2501        assert_eq!(got["functionConfiguration"]["name"], json!("fn1"));
2502    }
2503
2504    #[test]
2505    fn tagging_roundtrip() {
2506        let s = svc();
2507        let arn = "arn:aws:appsync:us-east-1:000000000000:apis/abc";
2508        s.tag_resource(&ctx(), arn, &json!({ "tags": { "team": "obs" } }))
2509            .unwrap();
2510        let listed = body_json(&s.list_tags_for_resource(&ctx(), arn).unwrap());
2511        assert_eq!(listed["tags"]["team"], json!("obs"));
2512    }
2513
2514    #[test]
2515    fn evaluate_template_renders() {
2516        let out = body_json(
2517            &AppSyncService::evaluate_template(&json!({
2518                "template": "$util.toJson($ctx.arguments)",
2519                "context": "{\"arguments\":{\"id\":\"1\"}}"
2520            }))
2521            .unwrap(),
2522        );
2523        assert!(out["evaluationResult"].is_string());
2524    }
2525
2526    fn req(method: Method, path: &str) -> AwsRequest {
2527        AwsRequest {
2528            service: "appsync".to_string(),
2529            action: String::new(),
2530            region: "us-east-1".to_string(),
2531            account_id: "000000000000".to_string(),
2532            request_id: "rid".to_string(),
2533            headers: http::HeaderMap::new(),
2534            query_params: std::collections::HashMap::new(),
2535            body: bytes::Bytes::new(),
2536            body_stream: parking_lot::Mutex::new(None),
2537            path_segments: vec![],
2538            raw_path: path.to_string(),
2539            raw_query: String::new(),
2540            method,
2541            is_query_protocol: false,
2542            access_key_id: None,
2543            principal: None,
2544        }
2545    }
2546
2547    #[test]
2548    fn routing_covers_key_ops() {
2549        assert_eq!(
2550            AppSyncService::resolve_action(&req(Method::POST, "/v1/apis"))
2551                .unwrap()
2552                .0,
2553            "CreateGraphqlApi"
2554        );
2555        assert_eq!(
2556            AppSyncService::resolve_action(&req(Method::GET, "/v1/apis/abc"))
2557                .unwrap()
2558                .0,
2559            "GetGraphqlApi"
2560        );
2561        assert_eq!(
2562            AppSyncService::resolve_action(&req(
2563                Method::GET,
2564                "/v1/apis/abc/types/Query/resolvers/hello"
2565            ))
2566            .unwrap()
2567            .0,
2568            "GetResolver"
2569        );
2570        assert_eq!(
2571            AppSyncService::resolve_action(&req(Method::POST, "/v1/apis/abc/ApiCaches/update"))
2572                .unwrap()
2573                .0,
2574            "UpdateApiCache"
2575        );
2576        assert_eq!(
2577            AppSyncService::resolve_action(&req(Method::POST, "/v2/apis"))
2578                .unwrap()
2579                .0,
2580            "CreateApi"
2581        );
2582        assert!(AppSyncService::resolve_action(&req(Method::GET, "/v1/nope")).is_none());
2583    }
2584}