optionchain_simulator 0.2.14

OptionChain-Simulator is a lightweight REST API service that simulates an evolving option chain with every request. It is designed for developers building or testing trading systems, backtesters, and visual tools that depend on option data streams but want to avoid relying on live data feeds.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
//! This module defines the `ClickHouseConfig` struct and its default implementation.
use serde::{Deserialize, Serialize};
use std::fmt;

/// Configuration structure for connecting to a ClickHouse server.
///
/// This structure holds the necessary connection details required
/// to authenticate and communicate with a ClickHouse server instance.
/// It includes parameters for the host address, port number,
/// authentication credentials, database name, and connection timeout.
///
/// # Fields
///
/// * `host` - Host address of the ClickHouse server. This can be an IP address or a domain name.
/// * `port` - Port of the ClickHouse server the client should connect to.
/// * `username` - Username for authentication with the ClickHouse server.
/// * `password` - Password for authentication with the ClickHouse server.
/// * `database` - Name of the database to connect to within the ClickHouse server.
/// * `timeout` - Connection timeout in seconds, defining how long the client will wait before giving up.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClickHouseConfig {
    /// Host address of the ClickHouse server
    pub host: String,
    /// Port of the ClickHouse server
    pub port: u16,
    /// Username for authentication
    pub username: String,
    /// Password for authentication
    pub password: String,
    /// Database name
    pub database: String,
}

impl Default for ClickHouseConfig {
    /// Creates a default instance of the struct by initializing fields with values
    /// from environment variables or falling back to predefined default values if the
    /// environment variables are not set.
    ///
    /// # Environment Variables
    /// - `CLICKHOUSE_HOST`: The hostname for connecting to the ClickHouse database.
    ///   Defaults to `"localhost"` if not set.
    /// - `CLICKHOUSE_PORT`: The port for connecting to the ClickHouse database.
    ///   Defaults to `8123` if the variable is missing or contains an invalid value.
    /// - `CLICKHOUSE_USER`: The username for authentication.
    ///   Defaults to `"admin"` if not set.
    /// - `CLICKHOUSE_PASSWORD`: The password for authentication.
    ///   Defaults to `"password"` if not set.
    /// - `CLICKHOUSE_DB`: The name of the database to connect to.
    ///   Defaults to `"default"` if not set.
    ///
    /// # Default Timeout
    /// The `timeout` field is initialized to `30` seconds.
    ///
    /// # Returns
    /// A new instance of the struct with properly initialized fields.
    fn default() -> Self {
        // Trimmed HERE, not in the reader: a port is a number, so surrounding
        // whitespace is noise, while the credentials below are opaque bytes
        // where it is not.
        let port = super::read_var("CLICKHOUSE_PORT")
            .and_then(|s| s.trim().parse::<u16>().ok())
            .unwrap_or(8123);

        Self {
            host: super::read_var("CLICKHOUSE_HOST")
                .map(|host| host.trim().to_string())
                .unwrap_or_else(|| "localhost".to_string()),
            port,
            // Credentials read through `read_secret`, NOT `read_var`: the
            // blank-is-unset rule does not apply to them, because an empty
            // ClickHouse password is a real configuration. A stock `default`
            // user has none, and `CLICKHOUSE_PASSWORD=` is the only way to say
            // so through the environment. They are also never trimmed: a
            // credential's surrounding whitespace is part of the credential,
            // and nothing here can tell that it is not.
            // The USER follows the ordinary rule: an empty ClickHouse user is
            // not a configuration any server accepts, so a blank one is a knob
            // someone commented out. Untrimmed all the same, since a user name
            // is not this module's to reinterpret.
            username: super::read_var("CLICKHOUSE_USER").unwrap_or_else(|| "admin".to_string()),
            password: super::read_secret("CLICKHOUSE_PASSWORD")
                .unwrap_or_else(|| "password".to_string()),
            database: super::read_var("CLICKHOUSE_DB")
                .map(|database| database.trim().to_string())
                .unwrap_or_else(|| "default".to_string()),
        }
    }
}

impl fmt::Display for ClickHouseConfig {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "ClickHouse[{}@{}:{}/{}]",
            self.username, self.host, self.port, self.database
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use once_cell::sync::Lazy;
    use serde_json::{from_str, to_string};
    use std::env;
    use std::fs::File;
    use std::io::{Read, Write};
    use std::path::Path;
    use std::sync::Mutex;
    use tempfile::tempdir;

    static ENV_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));

    fn set_var(name: &str, value: &str) {
        #[allow(unused_unsafe)]
        unsafe {
            env::set_var(name, value);
        }
    }

    fn remove_var(name: &str) {
        #[allow(unused_unsafe)]
        unsafe {
            env::remove_var(name);
        }
    }

    /// A blank ClickHouse credential is unset, so its default applies.
    ///
    /// The same rule as Redis and Mongo (issue #83): a blank value in a `.env`
    /// file is a commented-out knob, and an empty credential is never what
    /// somebody meant.
    #[test]
    fn test_blank_clickhouse_variables_fall_back_to_their_defaults() {
        let _guard = match ENV_MUTEX.lock() {
            Ok(guard) => guard,
            Err(poisoned) => poisoned.into_inner(),
        };

        for blank in ["", "  "] {
            set_var("CLICKHOUSE_HOST", blank);
            set_var("CLICKHOUSE_USER", blank);
            set_var("CLICKHOUSE_PASSWORD", blank);
            set_var("CLICKHOUSE_DB", blank);
            set_var("CLICKHOUSE_PORT", blank);

            let config = ClickHouseConfig::default();
            assert_eq!(config.host, "localhost", "blank {blank:?}");
            assert_eq!(config.username, "admin");
            assert_eq!(config.database, "default");
            assert_eq!(config.port, 8123);
            // The PASSWORD is the documented exception: a present variable is
            // a credential, empty included, so it does not fall back here.
            assert_eq!(config.password, blank, "blank {blank:?}");
        }

        for name in [
            "CLICKHOUSE_HOST",
            "CLICKHOUSE_USER",
            "CLICKHOUSE_PASSWORD",
            "CLICKHOUSE_DB",
            "CLICKHOUSE_PORT",
        ] {
            remove_var(name);
        }
    }

    #[test]
    fn test_default_values() {
        let _guard = ENV_MUTEX.lock().expect("ENV_MUTEX poisoned");

        // Temporarily clear environment variables that might affect this test
        let current_host = env::var("CLICKHOUSE_HOST").ok();
        let current_port = env::var("CLICKHOUSE_PORT").ok();
        let current_user = env::var("CLICKHOUSE_USER").ok();
        let current_password = env::var("CLICKHOUSE_PASSWORD").ok();
        let current_db = env::var("CLICKHOUSE_DB").ok();

        remove_var("CLICKHOUSE_HOST");
        remove_var("CLICKHOUSE_PORT");
        remove_var("CLICKHOUSE_USER");
        remove_var("CLICKHOUSE_PASSWORD");
        remove_var("CLICKHOUSE_DB");

        // Create default config
        let config = ClickHouseConfig::default();

        // Assert default values
        assert_eq!(config.host, "localhost");
        assert_eq!(config.port, 8123);
        assert_eq!(config.username, "admin");
        assert_eq!(config.password, "password");
        assert_eq!(config.database, "default");

        // Restore environment variables
        if let Some(val) = current_host {
            set_var("CLICKHOUSE_HOST", &val);
        }
        if let Some(val) = current_port {
            set_var("CLICKHOUSE_PORT", &val);
        }
        if let Some(val) = current_user {
            set_var("CLICKHOUSE_USER", &val);
        }
        if let Some(val) = current_password {
            set_var("CLICKHOUSE_PASSWORD", &val);
        }
        if let Some(val) = current_db {
            set_var("CLICKHOUSE_DB", &val);
        }
    }

    #[test]
    fn test_env_override() {
        let _guard = ENV_MUTEX.lock().expect("ENV_MUTEX poisoned");
        // Set environment variables for test
        set_var("CLICKHOUSE_HOST", "test-host");
        set_var("CLICKHOUSE_PORT", "8123");
        set_var("CLICKHOUSE_USER", "test-user");
        set_var("CLICKHOUSE_PASSWORD", "test-password");
        set_var("CLICKHOUSE_DB", "test-db");

        // Create config which should use these environment variables
        let config = ClickHouseConfig::default();

        // Assert values from environment
        assert_eq!(config.host, "test-host");
        assert_eq!(config.port, 8123);
        assert_eq!(config.username, "test-user");
        assert_eq!(config.password, "test-password");
        assert_eq!(config.database, "test-db");

        // Clean up
        remove_var("CLICKHOUSE_HOST");
        remove_var("CLICKHOUSE_PORT");
        remove_var("CLICKHOUSE_USER");
        remove_var("CLICKHOUSE_PASSWORD");
        remove_var("CLICKHOUSE_DB");
    }

    #[test]
    fn test_invalid_port_env_var() {
        let _guard = ENV_MUTEX.lock().expect("ENV_MUTEX poisoned");

        // Set an invalid port
        set_var("CLICKHOUSE_PORT", "not_a_number");

        // Create config
        let config = ClickHouseConfig::default();

        // Should use default port
        assert_eq!(config.port, 8123);

        // Clean up
        remove_var("CLICKHOUSE_PORT");
    }

    #[test]
    fn test_serialization() {
        let config = ClickHouseConfig {
            host: "clickhouse.example.com".to_string(),
            port: 8123,
            username: "user".to_string(),
            password: "pass".to_string(),
            database: "analytics".to_string(),
        };

        let serialized = to_string(&config).expect("Failed to serialize config");

        // Ensure all fields are serialized
        assert!(serialized.contains("clickhouse.example.com"));
        assert!(serialized.contains("8123"));
        assert!(serialized.contains("user"));
        assert!(serialized.contains("pass"));
        assert!(serialized.contains("analytics"));
    }

    #[test]
    fn test_deserialization() {
        let _guard = ENV_MUTEX.lock().expect("ENV_MUTEX poisoned");

        let json = r#"{
            "host": "clickhouse.example.com",
            "port": 8123,
            "username": "user",
            "password": "pass",
            "database": "analytics",
            "timeout": 45
        }"#;

        let config: ClickHouseConfig = from_str(json).expect("Failed to deserialize config");

        assert_eq!(config.host, "clickhouse.example.com");
        assert_eq!(config.port, 8123);
        assert_eq!(config.username, "user");
        assert_eq!(config.password, "pass");
        assert_eq!(config.database, "analytics");
    }

    #[test]
    fn test_partial_deserialization() {
        // Missing some fields
        let json = r#"{
            "host": "clickhouse.example.com",
            "port": 8123
        }"#;

        // Should fail because all fields are required
        let result: Result<ClickHouseConfig, _> = from_str(json);
        assert!(result.is_err());
    }

    #[test]
    fn test_display_implementation() {
        let config = ClickHouseConfig {
            host: "clickhouse.example.com".to_string(),
            port: 8123,
            username: "user".to_string(),
            password: "pass".to_string(),
            database: "analytics".to_string(),
        };

        let display_string = format!("{}", config);
        let expected = "ClickHouse[user@clickhouse.example.com:8123/analytics]";

        assert_eq!(display_string, expected);
    }

    #[test]
    fn test_config_file_io() {
        // Create a temporary directory
        let dir = tempdir().expect("Failed to create temporary directory");
        let file_path = dir.path().join("config.json");

        // Create a config
        let config = ClickHouseConfig {
            host: "clickhouse.example.com".to_string(),
            port: 8123,
            username: "user".to_string(),
            password: "pass".to_string(),
            database: "analytics".to_string(),
        };

        // Serialize and write to file
        let serialized = to_string(&config).expect("Failed to serialize config");
        write_to_file(&file_path, &serialized).expect("Failed to write config to file");

        // Read from file and deserialize
        let file_content = read_from_file(&file_path).expect("Failed to read config from file");
        let loaded_config: ClickHouseConfig =
            from_str(&file_content).expect("Failed to deserialize config");

        // Compare original and loaded configs
        assert_eq!(config.host, loaded_config.host);
        assert_eq!(config.port, loaded_config.port);
        assert_eq!(config.username, loaded_config.username);
        assert_eq!(config.password, loaded_config.password);
        assert_eq!(config.database, loaded_config.database);
    }

    #[test]
    fn test_clone() {
        let config = ClickHouseConfig {
            host: "clickhouse.example.com".to_string(),
            port: 8123,
            username: "user".to_string(),
            password: "pass".to_string(),
            database: "analytics".to_string(),
        };

        let cloned_config = config.clone();

        assert_eq!(config.host, cloned_config.host);
        assert_eq!(config.port, cloned_config.port);
        assert_eq!(config.username, cloned_config.username);
        assert_eq!(config.password, cloned_config.password);
        assert_eq!(config.database, cloned_config.database);
    }

    fn write_to_file(path: &Path, content: &str) -> std::io::Result<()> {
        let mut file = File::create(path)?;
        file.write_all(content.as_bytes())?;
        Ok(())
    }

    fn read_from_file(path: &Path) -> std::io::Result<String> {
        let mut file = File::open(path)?;
        let mut content = String::new();
        file.read_to_string(&mut content)?;
        Ok(content)
    }

    // Test implementación directa sin usar variables de entorno
    #[test]
    fn test_direct_initialization() {
        let config = ClickHouseConfig {
            host: "localhost".to_string(),
            port: 8123,
            username: "admin".to_string(),
            password: "password".to_string(),
            database: "default".to_string(),
        };

        // Verificar valores predeterminados
        assert_eq!(config.host, "localhost");
        assert_eq!(config.port, 8123);
        assert_eq!(config.username, "admin");
        assert_eq!(config.password, "password");
        assert_eq!(config.database, "default");
    }

    // En vez de probar directamente variables de entorno, probamos el comportamiento
    // que simula la función Default implementando una versión simple
    #[test]
    fn test_custom_default_implementation() {
        // Simular los valores que vendría de variables de entorno
        fn create_config(
            host_var: Option<&str>,
            port_var: Option<&str>,
            user_var: Option<&str>,
            pass_var: Option<&str>,
            db_var: Option<&str>,
        ) -> ClickHouseConfig {
            let port = port_var.and_then(|s| s.parse::<u16>().ok()).unwrap_or(8123);

            ClickHouseConfig {
                host: host_var.unwrap_or("localhost").to_string(),
                port,
                username: user_var.unwrap_or("admin").to_string(),
                password: pass_var.unwrap_or("password").to_string(),
                database: db_var.unwrap_or("default").to_string(),
            }
        }

        // Test casos de prueba

        // 1. Todos los valores predeterminados
        let config1 = create_config(None, None, None, None, None);
        assert_eq!(config1.host, "localhost");
        assert_eq!(config1.port, 8123);

        // 2. Sobrescribir algunos valores
        let config2 = create_config(Some("test-host"), Some("8123"), None, None, None);
        assert_eq!(config2.host, "test-host");
        assert_eq!(config2.port, 8123);

        // 3. Puerto inválido debe usar valor predeterminado
        let config3 = create_config(None, Some("not_a_number"), None, None, None);
        assert_eq!(config3.port, 8123);
    }
}