Skip to main content

alien_core/bindings/
kv.rs

1//! KV binding definitions for key-value storage across different platforms
2//!
3//! This module defines the binding parameters for different KV services:
4//! - AWS DynamoDB
5//! - GCP Firestore
6//! - Azure Table Storage
7//! - Redis (for Kubernetes/local)
8
9use super::BindingValue;
10use serde::{Deserialize, Serialize};
11
12/// Represents a KV binding for key-value storage across platforms
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
15#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
16#[serde(tag = "service", rename_all = "lowercase")]
17pub enum KvBinding {
18    /// AWS DynamoDB binding
19    Dynamodb(DynamodbKvBinding),
20    /// GCP Firestore binding
21    Firestore(FirestoreKvBinding),
22    /// Azure Table Storage binding
23    TableStorage(TableStorageKvBinding),
24    /// Redis binding (for Kubernetes/local)
25    Redis(RedisKvBinding),
26    /// Local development KV (for testing)
27    #[serde(rename = "local-kv")]
28    Local(LocalKvBinding),
29}
30
31/// AWS DynamoDB KV binding configuration
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
34#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
35#[serde(rename_all = "camelCase")]
36pub struct DynamodbKvBinding {
37    /// The DynamoDB table name
38    pub table_name: BindingValue<String>,
39    /// The AWS region where the table is located
40    pub region: BindingValue<String>,
41    /// Optional endpoint URL for local testing or custom endpoints
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub endpoint_url: Option<BindingValue<String>>,
44}
45
46/// GCP Firestore KV binding configuration
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
49#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
50#[serde(rename_all = "camelCase")]
51pub struct FirestoreKvBinding {
52    /// The GCP project ID
53    pub project_id: BindingValue<String>,
54    /// The Firestore database ID (default "(default)")
55    pub database_id: BindingValue<String>,
56    /// The collection name for KV storage
57    pub collection_name: BindingValue<String>,
58}
59
60/// Azure Table Storage KV binding configuration
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
63#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
64#[serde(rename_all = "camelCase")]
65pub struct TableStorageKvBinding {
66    /// The Azure resource group name
67    pub resource_group_name: BindingValue<String>,
68    /// The storage account name
69    pub account_name: BindingValue<String>,
70    /// The table name
71    pub table_name: BindingValue<String>,
72}
73
74/// Redis KV binding configuration
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
77#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
78#[serde(rename_all = "camelCase")]
79pub struct RedisKvBinding {
80    /// Redis connection URL (e.g., "redis://localhost:6379" or "rediss://...")
81    pub connection_url: BindingValue<String>,
82    /// Optional key prefix for namespacing
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub key_prefix: Option<BindingValue<String>>,
85    /// Optional database number (0-15 for standard Redis)
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub database: Option<BindingValue<u8>>,
88}
89
90/// Local development KV binding configuration
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
93#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
94#[serde(rename_all = "camelCase")]
95pub struct LocalKvBinding {
96    /// The base data directory for local storage
97    pub data_dir: BindingValue<String>,
98    /// Optional key prefix for namespacing
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub key_prefix: Option<BindingValue<String>>,
101}
102
103impl KvBinding {
104    /// Creates a DynamoDB KV binding
105    pub fn dynamodb(
106        table_name: impl Into<BindingValue<String>>,
107        region: impl Into<BindingValue<String>>,
108    ) -> Self {
109        Self::Dynamodb(DynamodbKvBinding {
110            table_name: table_name.into(),
111            region: region.into(),
112            endpoint_url: None,
113        })
114    }
115
116    /// Creates a DynamoDB KV binding with custom endpoint
117    pub fn dynamodb_with_endpoint(
118        table_name: impl Into<BindingValue<String>>,
119        region: impl Into<BindingValue<String>>,
120        endpoint_url: impl Into<BindingValue<String>>,
121    ) -> Self {
122        Self::Dynamodb(DynamodbKvBinding {
123            table_name: table_name.into(),
124            region: region.into(),
125            endpoint_url: Some(endpoint_url.into()),
126        })
127    }
128
129    /// Creates a Firestore KV binding
130    pub fn firestore(
131        project_id: impl Into<BindingValue<String>>,
132        database_id: impl Into<BindingValue<String>>,
133        collection_name: impl Into<BindingValue<String>>,
134    ) -> Self {
135        Self::Firestore(FirestoreKvBinding {
136            project_id: project_id.into(),
137            database_id: database_id.into(),
138            collection_name: collection_name.into(),
139        })
140    }
141
142    /// Creates an Azure Table Storage KV binding
143    pub fn table_storage(
144        resource_group_name: impl Into<BindingValue<String>>,
145        account_name: impl Into<BindingValue<String>>,
146        table_name: impl Into<BindingValue<String>>,
147    ) -> Self {
148        Self::TableStorage(TableStorageKvBinding {
149            resource_group_name: resource_group_name.into(),
150            account_name: account_name.into(),
151            table_name: table_name.into(),
152        })
153    }
154
155    /// Creates a Redis KV binding
156    pub fn redis(connection_url: impl Into<BindingValue<String>>) -> Self {
157        Self::Redis(RedisKvBinding {
158            connection_url: connection_url.into(),
159            key_prefix: None,
160            database: None,
161        })
162    }
163
164    /// Creates a Redis KV binding with prefix and database
165    pub fn redis_with_options(
166        connection_url: impl Into<BindingValue<String>>,
167        key_prefix: Option<impl Into<BindingValue<String>>>,
168        database: Option<u8>,
169    ) -> Self {
170        Self::Redis(RedisKvBinding {
171            connection_url: connection_url.into(),
172            key_prefix: key_prefix.map(|p| p.into()),
173            database: database.map(BindingValue::value),
174        })
175    }
176
177    /// Creates a local KV binding
178    pub fn local(data_dir: impl Into<BindingValue<String>>) -> Self {
179        Self::Local(LocalKvBinding {
180            data_dir: data_dir.into(),
181            key_prefix: None,
182        })
183    }
184
185    /// Creates a local KV binding with prefix
186    pub fn local_with_prefix(
187        data_dir: impl Into<BindingValue<String>>,
188        key_prefix: impl Into<BindingValue<String>>,
189    ) -> Self {
190        Self::Local(LocalKvBinding {
191            data_dir: data_dir.into(),
192            key_prefix: Some(key_prefix.into()),
193        })
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use serde_json::json;
201
202    #[test]
203    fn test_dynamodb_binding() {
204        let binding = KvBinding::dynamodb("my-table", "us-east-1");
205
206        let json = serde_json::to_string(&binding).unwrap();
207        assert!(json.contains(r#""service":"dynamodb""#));
208
209        let deserialized: KvBinding = serde_json::from_str(&json).unwrap();
210        assert_eq!(binding, deserialized);
211    }
212
213    #[test]
214    fn test_firestore_binding() {
215        let binding = KvBinding::firestore("my-project", "(default)", "kv");
216
217        let json = serde_json::to_string(&binding).unwrap();
218        assert!(json.contains(r#""service":"firestore""#));
219
220        let deserialized: KvBinding = serde_json::from_str(&json).unwrap();
221        assert_eq!(binding, deserialized);
222    }
223
224    #[test]
225    fn test_table_storage_binding() {
226        let binding = KvBinding::table_storage("myresourcegroup", "myaccount", "mytable");
227
228        let json = serde_json::to_string(&binding).unwrap();
229        assert!(json.contains(r#""service":"tablestorage""#));
230
231        let deserialized: KvBinding = serde_json::from_str(&json).unwrap();
232        assert_eq!(binding, deserialized);
233    }
234
235    #[test]
236    fn test_redis_binding() {
237        let binding = KvBinding::redis("redis://localhost:6379");
238
239        let json = serde_json::to_string(&binding).unwrap();
240        assert!(json.contains(r#""service":"redis""#));
241
242        let deserialized: KvBinding = serde_json::from_str(&json).unwrap();
243        assert_eq!(binding, deserialized);
244    }
245
246    #[test]
247    fn test_local_binding() {
248        let binding = KvBinding::local("/tmp/kv");
249
250        let json = serde_json::to_string(&binding).unwrap();
251        assert!(json.contains(r#""service":"local-kv""#));
252
253        let deserialized: KvBinding = serde_json::from_str(&json).unwrap();
254        assert_eq!(binding, deserialized);
255    }
256
257    #[test]
258    fn test_binding_value_expressions() {
259        let binding = KvBinding::Dynamodb(DynamodbKvBinding {
260            table_name: BindingValue::expression(json!({"Ref": "MyTable"})),
261            region: BindingValue::value("us-east-1".to_string()),
262            endpoint_url: None,
263        });
264
265        let json = serde_json::to_string(&binding).unwrap();
266        let deserialized: KvBinding = serde_json::from_str(&json).unwrap();
267        assert_eq!(binding, deserialized);
268    }
269}