Skip to main content

ig_client/storage/
utils.rs

1use crate::error::AppError;
2use crate::presentation::transaction::StoreTransaction;
3use crate::storage::config::DatabaseConfig;
4use serde::Serialize;
5use serde::de::DeserializeOwned;
6use serde_json;
7use sqlx::{Executor, PgPool};
8use tracing::info;
9
10/// Stores a list of transactions in the database
11///
12/// # Arguments
13/// * `pool` - PostgreSQL connection pool
14/// * `txs` - List of transactions to store
15///
16/// # Returns
17/// * `Result<usize, AppError>` - Number of transactions inserted or an error
18pub async fn store_transactions(
19    pool: &sqlx::PgPool,
20    txs: &[StoreTransaction],
21) -> Result<usize, AppError> {
22    let mut tx = pool.begin().await?;
23    let mut inserted = 0;
24
25    for t in txs {
26        let result = tx
27            .execute(
28                sqlx::query(
29                    r#"
30                    INSERT INTO ig_options (
31                        reference, deal_date, underlying, strike,
32                        option_type, expiry, transaction_type, pnl_eur, is_fee, raw
33                    )
34                    VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
35                    ON CONFLICT (raw_hash) DO NOTHING
36                    "#,
37                )
38                .bind(&t.reference)
39                .bind(t.deal_date)
40                .bind(&t.underlying)
41                .bind(t.strike)
42                .bind(&t.option_type)
43                .bind(t.expiry)
44                .bind(&t.transaction_type)
45                .bind(t.pnl_eur)
46                .bind(t.is_fee)
47                .bind(&t.raw_json),
48            )
49            .await?;
50
51        inserted += result.rows_affected() as usize;
52    }
53
54    tx.commit().await?;
55    Ok(inserted)
56}
57
58/// Serializes a value to a JSON string
59pub fn serialize_to_json<T: Serialize>(value: &T) -> Result<String, serde_json::Error> {
60    serde_json::to_string(value)
61}
62
63/// Deserializes a JSON string into a value
64pub fn deserialize_from_json<T: DeserializeOwned>(s: &str) -> Result<T, serde_json::Error> {
65    serde_json::from_str(s)
66}
67
68/// Creates a PostgreSQL connection pool from database configuration
69///
70/// # Arguments
71/// * `config` - Database configuration containing URL and max connections
72///
73/// # Returns
74/// * `Result<PgPool, AppError>` - Connection pool or an error
75pub async fn create_connection_pool(config: &DatabaseConfig) -> Result<PgPool, AppError> {
76    info!(
77        "Creating PostgreSQL connection pool with max {} connections",
78        config.max_connections
79    );
80
81    let pool = sqlx::postgres::PgPoolOptions::new()
82        .max_connections(config.max_connections)
83        .connect(&config.url)
84        .await
85        .map_err(AppError::Db)?;
86
87    info!("PostgreSQL connection pool created successfully");
88    Ok(pool)
89}
90
91/// Creates a database configuration from environment variables
92///
93/// # Returns
94/// * `Result<DatabaseConfig, AppError>` - Database configuration or an error
95pub fn create_database_config_from_env() -> Result<DatabaseConfig, AppError> {
96    dotenv::dotenv().ok();
97    let url = std::env::var("DATABASE_URL").map_err(|_| {
98        AppError::InvalidInput("DATABASE_URL environment variable is required".to_string())
99    })?;
100
101    let max_connections = std::env::var("DATABASE_MAX_CONNECTIONS")
102        .unwrap_or_else(|_| "10".to_string())
103        .parse::<u32>()
104        .map_err(|e| {
105            AppError::InvalidInput(format!("Invalid DATABASE_MAX_CONNECTIONS value: {e}"))
106        })?;
107
108    Ok(DatabaseConfig {
109        url,
110        max_connections,
111    })
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use serde::{Deserialize, Serialize};
118
119    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
120    struct TestStruct {
121        name: String,
122        value: i32,
123        optional: Option<f64>,
124    }
125
126    #[test]
127    fn test_serialize_to_json_simple_struct() {
128        let test_data = TestStruct {
129            name: "test".to_string(),
130            value: 42,
131            optional: Some(3.15),
132        };
133
134        let result = serialize_to_json(&test_data);
135        assert!(result.is_ok());
136
137        let json = result.expect("should serialize");
138        assert!(json.contains("\"name\":\"test\""));
139        assert!(json.contains("\"value\":42"));
140        assert!(json.contains("\"optional\":3.15"));
141    }
142
143    #[test]
144    fn test_serialize_to_json_with_none() {
145        let test_data = TestStruct {
146            name: "none_test".to_string(),
147            value: 0,
148            optional: None,
149        };
150
151        let result = serialize_to_json(&test_data);
152        assert!(result.is_ok());
153
154        let json = result.expect("should serialize");
155        assert!(json.contains("\"optional\":null"));
156    }
157
158    #[test]
159    fn test_deserialize_from_json_valid() {
160        let json = r#"{"name":"deserialized","value":100,"optional":2.5}"#;
161
162        let result: Result<TestStruct, _> = deserialize_from_json(json);
163        assert!(result.is_ok());
164
165        let data = result.expect("should deserialize");
166        assert_eq!(data.name, "deserialized");
167        assert_eq!(data.value, 100);
168        assert_eq!(data.optional, Some(2.5));
169    }
170
171    #[test]
172    fn test_deserialize_from_json_with_null() {
173        let json = r#"{"name":"null_test","value":50,"optional":null}"#;
174
175        let result: Result<TestStruct, _> = deserialize_from_json(json);
176        assert!(result.is_ok());
177
178        let data = result.expect("should deserialize");
179        assert_eq!(data.name, "null_test");
180        assert_eq!(data.value, 50);
181        assert!(data.optional.is_none());
182    }
183
184    #[test]
185    fn test_deserialize_from_json_invalid() {
186        let json = r#"{"invalid": "json for TestStruct"}"#;
187
188        let result: Result<TestStruct, _> = deserialize_from_json(json);
189        assert!(result.is_err());
190    }
191
192    #[test]
193    fn test_deserialize_from_json_malformed() {
194        let json = r#"{"name": "incomplete"#;
195
196        let result: Result<TestStruct, _> = deserialize_from_json(json);
197        assert!(result.is_err());
198    }
199
200    #[test]
201    fn test_serialize_deserialize_roundtrip() {
202        let original = TestStruct {
203            name: "roundtrip".to_string(),
204            value: 999,
205            optional: Some(1.234),
206        };
207
208        let json = serialize_to_json(&original).expect("should serialize");
209        let deserialized: TestStruct = deserialize_from_json(&json).expect("should deserialize");
210
211        assert_eq!(original, deserialized);
212    }
213
214    #[test]
215    fn test_serialize_vec() {
216        let vec = vec![
217            TestStruct {
218                name: "first".to_string(),
219                value: 1,
220                optional: None,
221            },
222            TestStruct {
223                name: "second".to_string(),
224                value: 2,
225                optional: Some(2.0),
226            },
227        ];
228
229        let result = serialize_to_json(&vec);
230        assert!(result.is_ok());
231
232        let json = result.expect("should serialize");
233        assert!(json.starts_with('['));
234        assert!(json.ends_with(']'));
235        assert!(json.contains("\"first\""));
236        assert!(json.contains("\"second\""));
237    }
238
239    #[test]
240    fn test_deserialize_vec() {
241        let json =
242            r#"[{"name":"a","value":1,"optional":null},{"name":"b","value":2,"optional":3.0}]"#;
243
244        let result: Result<Vec<TestStruct>, _> = deserialize_from_json(json);
245        assert!(result.is_ok());
246
247        let vec = result.expect("should deserialize");
248        assert_eq!(vec.len(), 2);
249        assert_eq!(vec[0].name, "a");
250        assert_eq!(vec[1].name, "b");
251    }
252
253    #[test]
254    fn test_serialize_empty_string() {
255        let test_data = TestStruct {
256            name: String::new(),
257            value: 0,
258            optional: None,
259        };
260
261        let result = serialize_to_json(&test_data);
262        assert!(result.is_ok());
263
264        let json = result.expect("should serialize");
265        assert!(json.contains("\"name\":\"\""));
266    }
267
268    #[test]
269    fn test_serialize_special_characters() {
270        let test_data = TestStruct {
271            name: "test\"with\\special\nchars".to_string(),
272            value: 0,
273            optional: None,
274        };
275
276        let result = serialize_to_json(&test_data);
277        assert!(result.is_ok());
278
279        let json = result.expect("should serialize");
280        // JSON should escape special characters
281        assert!(json.contains("\\\""));
282        assert!(json.contains("\\\\"));
283        assert!(json.contains("\\n"));
284    }
285
286    #[test]
287    fn test_database_config_creation() {
288        let config = DatabaseConfig {
289            url: "postgres://localhost/test".to_string(),
290            max_connections: 5,
291        };
292        assert_eq!(config.url, "postgres://localhost/test");
293        assert_eq!(config.max_connections, 5);
294    }
295}