Skip to main content

winterbaume_appsync/
views.rs

1//! Serde-compatible view types for AppSync state snapshots.
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6use winterbaume_core::{StateChangeNotifier, StateViewError, StatefulService};
7
8use crate::handlers::AppSyncService;
9use crate::state::AppSyncState;
10use crate::types::{
11    Api, ApiCacheEntry, ApiKeyEntry, ChannelNamespaceEntry, GraphqlApi, SchemaStatus, TypeEntry,
12};
13
14/// Serializable view of the entire AppSync state for one account/region.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct AppsyncStateView {
17    /// GraphQL APIs (v1) keyed by api_id.
18    #[serde(default)]
19    pub apis: HashMap<String, GraphqlApiView>,
20    /// Event APIs (v2) keyed by api_id.
21    #[serde(default)]
22    pub event_apis: HashMap<String, ApiView>,
23    /// API caches keyed by api_id.
24    #[serde(default)]
25    pub api_caches: HashMap<String, ApiCacheView>,
26    /// API keys keyed by api_id.
27    #[serde(default)]
28    pub api_keys: HashMap<String, Vec<ApiKeyView>>,
29    /// Channel namespaces keyed by api_id.
30    #[serde(default)]
31    pub channel_namespaces: HashMap<String, Vec<ChannelNamespaceView>>,
32    /// Types keyed by api_id.
33    #[serde(default)]
34    pub types: HashMap<String, Vec<TypeView>>,
35    /// Schema statuses keyed by api_id.
36    #[serde(default)]
37    pub schema_statuses: HashMap<String, SchemaStatusView>,
38    /// Resource tags keyed by resource ARN.
39    #[serde(default)]
40    pub resource_tags: HashMap<String, HashMap<String, String>>,
41}
42
43/// Serializable view of a GraphQL API (v1).
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct GraphqlApiView {
46    pub api_id: String,
47    pub name: String,
48    pub authentication_type: String,
49    pub arn: String,
50    #[serde(default)]
51    pub uris: HashMap<String, String>,
52    #[serde(default)]
53    pub tags: HashMap<String, String>,
54    #[serde(default)]
55    pub xray_enabled: bool,
56    #[serde(default)]
57    pub additional_authentication_provider: Option<serde_json::Value>,
58    #[serde(default)]
59    pub lambda_authorizer_config: Option<serde_json::Value>,
60    #[serde(default)]
61    pub user_pool_config: Option<serde_json::Value>,
62    #[serde(default)]
63    pub enhanced_metrics_config: Option<serde_json::Value>,
64}
65
66/// Serializable view of an Event API (v2).
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ApiView {
69    pub api_id: String,
70    pub name: String,
71    pub api_arn: String,
72    pub created: f64,
73    #[serde(default)]
74    pub tags: HashMap<String, String>,
75    pub owner_contact: Option<String>,
76}
77
78/// Serializable view of an API cache.
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct ApiCacheView {
81    pub api_id: String,
82    pub api_caching_behavior: String,
83    pub r#type: String,
84    pub ttl: i64,
85    pub at_rest_encryption_enabled: bool,
86    pub transit_encryption_enabled: bool,
87    pub status: String,
88    pub health_metrics_config: Option<String>,
89}
90
91/// Serializable view of an API key.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct ApiKeyView {
94    pub id: String,
95    pub api_id: String,
96    pub description: Option<String>,
97    pub expires: i64,
98    pub deletes: i64,
99}
100
101/// Serializable view of a channel namespace.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct ChannelNamespaceView {
104    pub api_id: String,
105    pub name: String,
106    pub channel_namespace_arn: String,
107    pub created: f64,
108    pub last_modified: f64,
109    #[serde(default)]
110    pub tags: HashMap<String, String>,
111}
112
113/// Serializable view of a GraphQL type.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct TypeView {
116    pub api_id: String,
117    pub name: String,
118    pub definition: Option<String>,
119    pub format: String,
120    pub arn: String,
121}
122
123/// Serializable view of a schema creation status.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct SchemaStatusView {
126    pub status: String,
127    pub details: Option<String>,
128}
129
130// ---------------------------------------------------------------------------
131// From conversions
132// ---------------------------------------------------------------------------
133
134impl From<&GraphqlApi> for GraphqlApiView {
135    fn from(api: &GraphqlApi) -> Self {
136        GraphqlApiView {
137            api_id: api.api_id.clone(),
138            name: api.name.clone(),
139            authentication_type: api.authentication_type.clone(),
140            arn: api.arn.clone(),
141            uris: api.uris.clone(),
142            tags: api.tags.clone(),
143            xray_enabled: api.xray_enabled,
144            additional_authentication_provider: api.additional_authentication_provider.clone(),
145            lambda_authorizer_config: api.lambda_authorizer_config.clone(),
146            user_pool_config: api.user_pool_config.clone(),
147            enhanced_metrics_config: api.enhanced_metrics_config.clone(),
148        }
149    }
150}
151
152impl From<GraphqlApiView> for GraphqlApi {
153    fn from(v: GraphqlApiView) -> Self {
154        GraphqlApi {
155            api_id: v.api_id,
156            name: v.name,
157            authentication_type: v.authentication_type,
158            arn: v.arn,
159            uris: v.uris,
160            tags: v.tags,
161            xray_enabled: v.xray_enabled,
162            additional_authentication_provider: v.additional_authentication_provider,
163            lambda_authorizer_config: v.lambda_authorizer_config,
164            user_pool_config: v.user_pool_config,
165            enhanced_metrics_config: v.enhanced_metrics_config,
166        }
167    }
168}
169
170impl From<&Api> for ApiView {
171    fn from(api: &Api) -> Self {
172        ApiView {
173            api_id: api.api_id.clone(),
174            name: api.name.clone(),
175            api_arn: api.api_arn.clone(),
176            created: api.created,
177            tags: api.tags.clone(),
178            owner_contact: api.owner_contact.clone(),
179        }
180    }
181}
182
183impl From<ApiView> for Api {
184    fn from(v: ApiView) -> Self {
185        Api {
186            api_id: v.api_id,
187            name: v.name,
188            api_arn: v.api_arn,
189            created: v.created,
190            tags: v.tags,
191            owner_contact: v.owner_contact,
192        }
193    }
194}
195
196impl From<&ApiCacheEntry> for ApiCacheView {
197    fn from(e: &ApiCacheEntry) -> Self {
198        ApiCacheView {
199            api_id: e.api_id.clone(),
200            api_caching_behavior: e.api_caching_behavior.clone(),
201            r#type: e.r#type.clone(),
202            ttl: e.ttl,
203            at_rest_encryption_enabled: e.at_rest_encryption_enabled,
204            transit_encryption_enabled: e.transit_encryption_enabled,
205            status: e.status.clone(),
206            health_metrics_config: e.health_metrics_config.clone(),
207        }
208    }
209}
210
211impl From<ApiCacheView> for ApiCacheEntry {
212    fn from(v: ApiCacheView) -> Self {
213        ApiCacheEntry {
214            api_id: v.api_id,
215            api_caching_behavior: v.api_caching_behavior,
216            r#type: v.r#type,
217            ttl: v.ttl,
218            at_rest_encryption_enabled: v.at_rest_encryption_enabled,
219            transit_encryption_enabled: v.transit_encryption_enabled,
220            status: v.status,
221            health_metrics_config: v.health_metrics_config,
222        }
223    }
224}
225
226impl From<&ApiKeyEntry> for ApiKeyView {
227    fn from(k: &ApiKeyEntry) -> Self {
228        ApiKeyView {
229            id: k.id.clone(),
230            api_id: k.api_id.clone(),
231            description: k.description.clone(),
232            expires: k.expires,
233            deletes: k.deletes,
234        }
235    }
236}
237
238impl From<ApiKeyView> for ApiKeyEntry {
239    fn from(v: ApiKeyView) -> Self {
240        ApiKeyEntry {
241            id: v.id,
242            api_id: v.api_id,
243            description: v.description,
244            expires: v.expires,
245            deletes: v.deletes,
246        }
247    }
248}
249
250impl From<&ChannelNamespaceEntry> for ChannelNamespaceView {
251    fn from(ns: &ChannelNamespaceEntry) -> Self {
252        ChannelNamespaceView {
253            api_id: ns.api_id.clone(),
254            name: ns.name.clone(),
255            channel_namespace_arn: ns.channel_namespace_arn.clone(),
256            created: ns.created,
257            last_modified: ns.last_modified,
258            tags: ns.tags.clone(),
259        }
260    }
261}
262
263impl From<ChannelNamespaceView> for ChannelNamespaceEntry {
264    fn from(v: ChannelNamespaceView) -> Self {
265        ChannelNamespaceEntry {
266            api_id: v.api_id,
267            name: v.name,
268            channel_namespace_arn: v.channel_namespace_arn,
269            created: v.created,
270            last_modified: v.last_modified,
271            tags: v.tags,
272        }
273    }
274}
275
276impl From<&TypeEntry> for TypeView {
277    fn from(t: &TypeEntry) -> Self {
278        TypeView {
279            api_id: t.api_id.clone(),
280            name: t.name.clone(),
281            definition: t.definition.clone(),
282            format: t.format.clone(),
283            arn: t.arn.clone(),
284        }
285    }
286}
287
288impl From<TypeView> for TypeEntry {
289    fn from(v: TypeView) -> Self {
290        TypeEntry {
291            api_id: v.api_id,
292            name: v.name,
293            definition: v.definition,
294            format: v.format,
295            arn: v.arn,
296        }
297    }
298}
299
300impl From<&SchemaStatus> for SchemaStatusView {
301    fn from(s: &SchemaStatus) -> Self {
302        SchemaStatusView {
303            status: s.status.clone(),
304            details: s.details.clone(),
305        }
306    }
307}
308
309impl From<SchemaStatusView> for SchemaStatus {
310    fn from(v: SchemaStatusView) -> Self {
311        SchemaStatus {
312            status: v.status,
313            details: v.details,
314        }
315    }
316}
317
318impl From<&AppSyncState> for AppsyncStateView {
319    fn from(state: &AppSyncState) -> Self {
320        AppsyncStateView {
321            apis: state
322                .apis
323                .iter()
324                .map(|(k, v)| (k.clone(), GraphqlApiView::from(v)))
325                .collect(),
326            event_apis: state
327                .event_apis
328                .iter()
329                .map(|(k, v)| (k.clone(), ApiView::from(v)))
330                .collect(),
331            api_caches: state
332                .api_caches
333                .iter()
334                .map(|(k, v)| (k.clone(), ApiCacheView::from(v)))
335                .collect(),
336            api_keys: state
337                .api_keys
338                .iter()
339                .map(|(k, v)| (k.clone(), v.iter().map(ApiKeyView::from).collect()))
340                .collect(),
341            channel_namespaces: state
342                .channel_namespaces
343                .iter()
344                .map(|(k, v)| {
345                    (
346                        k.clone(),
347                        v.iter().map(ChannelNamespaceView::from).collect(),
348                    )
349                })
350                .collect(),
351            types: state
352                .types
353                .iter()
354                .map(|(k, v)| (k.clone(), v.iter().map(TypeView::from).collect()))
355                .collect(),
356            schema_statuses: state
357                .schema_statuses
358                .iter()
359                .map(|(k, v)| (k.clone(), SchemaStatusView::from(v)))
360                .collect(),
361            resource_tags: state.resource_tags.clone(),
362        }
363    }
364}
365
366impl From<AppsyncStateView> for AppSyncState {
367    fn from(view: AppsyncStateView) -> Self {
368        AppSyncState {
369            apis: view
370                .apis
371                .into_values()
372                .map(|v| {
373                    let api = GraphqlApi::from(v);
374                    (api.api_id.clone(), api)
375                })
376                .collect(),
377            event_apis: view
378                .event_apis
379                .into_values()
380                .map(|v| {
381                    let api = Api::from(v);
382                    (api.api_id.clone(), api)
383                })
384                .collect(),
385            api_caches: view
386                .api_caches
387                .into_values()
388                .map(|v| {
389                    let cache = ApiCacheEntry::from(v);
390                    (cache.api_id.clone(), cache)
391                })
392                .collect(),
393            api_keys: view
394                .api_keys
395                .into_iter()
396                .map(|(k, v)| (k, v.into_iter().map(ApiKeyEntry::from).collect()))
397                .collect(),
398            channel_namespaces: view
399                .channel_namespaces
400                .into_iter()
401                .map(|(k, v)| (k, v.into_iter().map(ChannelNamespaceEntry::from).collect()))
402                .collect(),
403            types: view
404                .types
405                .into_iter()
406                .map(|(k, v)| (k, v.into_iter().map(TypeEntry::from).collect()))
407                .collect(),
408            schema_statuses: view
409                .schema_statuses
410                .into_iter()
411                .map(|(k, v)| (k, SchemaStatus::from(v)))
412                .collect(),
413            resource_tags: view.resource_tags,
414        }
415    }
416}
417
418// ---------------------------------------------------------------------------
419// StatefulService implementation
420// ---------------------------------------------------------------------------
421
422impl StatefulService for AppSyncService {
423    type StateView = AppsyncStateView;
424
425    async fn snapshot(&self, account_id: &str, region: &str) -> Self::StateView {
426        let state = self.state.get(account_id, region);
427        let guard = state.read().await;
428        AppsyncStateView::from(&*guard)
429    }
430
431    async fn restore(
432        &self,
433        account_id: &str,
434        region: &str,
435        view: Self::StateView,
436    ) -> Result<(), StateViewError> {
437        let new_state = AppSyncState::from(view);
438        {
439            let state = self.state.get(account_id, region);
440            *state.write().await = new_state;
441        }
442        self.notify_state_changed(account_id, region).await;
443        Ok(())
444    }
445
446    async fn merge(
447        &self,
448        account_id: &str,
449        region: &str,
450        view: Self::StateView,
451    ) -> Result<(), StateViewError> {
452        let incoming = AppSyncState::from(view);
453        let state = self.state.get(account_id, region);
454        {
455            let mut guard = state.write().await;
456            guard.apis.extend(incoming.apis);
457            guard.event_apis.extend(incoming.event_apis);
458            guard.api_caches.extend(incoming.api_caches);
459            for (k, v) in incoming.api_keys {
460                guard.api_keys.entry(k).or_default().extend(v);
461            }
462            for (k, v) in incoming.channel_namespaces {
463                guard.channel_namespaces.entry(k).or_default().extend(v);
464            }
465            for (k, v) in incoming.types {
466                guard.types.entry(k).or_default().extend(v);
467            }
468            guard.schema_statuses.extend(incoming.schema_statuses);
469            for (k, v) in incoming.resource_tags {
470                guard.resource_tags.entry(k).or_default().extend(v);
471            }
472        }
473        self.notify_state_changed(account_id, region).await;
474        Ok(())
475    }
476
477    fn notifier(&self) -> &StateChangeNotifier<Self::StateView> {
478        &self.notifier
479    }
480}