opencode_cloud_core/config/
schema.rs

1//! Configuration schema for opencode-cloud
2//!
3//! Defines the structure and defaults for the config.json file.
4
5use serde::{Deserialize, Serialize};
6
7/// Main configuration structure for opencode-cloud
8///
9/// Serialized to/from `~/.config/opencode-cloud/config.json`
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11#[serde(deny_unknown_fields)]
12pub struct Config {
13    /// Config file version for migrations
14    pub version: u32,
15
16    /// Port for the opencode web UI (default: 3000)
17    #[serde(default = "default_opencode_web_port")]
18    pub opencode_web_port: u16,
19
20    /// Bind address (default: "localhost")
21    /// Use "localhost" for local-only access (secure default)
22    /// Use "0.0.0.0" for network access (requires explicit opt-in)
23    #[serde(default = "default_bind")]
24    pub bind: String,
25
26    /// Auto-restart service on crash (default: true)
27    #[serde(default = "default_auto_restart")]
28    pub auto_restart: bool,
29
30    /// Boot mode for service registration (default: "user")
31    /// "user" - Service starts on user login (no root required)
32    /// "system" - Service starts on boot (requires root)
33    #[serde(default = "default_boot_mode")]
34    pub boot_mode: String,
35
36    /// Number of restart attempts on crash (default: 3)
37    #[serde(default = "default_restart_retries")]
38    pub restart_retries: u32,
39
40    /// Seconds between restart attempts (default: 5)
41    #[serde(default = "default_restart_delay")]
42    pub restart_delay: u32,
43}
44
45fn default_opencode_web_port() -> u16 {
46    3000
47}
48
49fn default_bind() -> String {
50    "localhost".to_string()
51}
52
53fn default_auto_restart() -> bool {
54    true
55}
56
57fn default_boot_mode() -> String {
58    "user".to_string()
59}
60
61fn default_restart_retries() -> u32 {
62    3
63}
64
65fn default_restart_delay() -> u32 {
66    5
67}
68
69impl Default for Config {
70    fn default() -> Self {
71        Self {
72            version: 1,
73            opencode_web_port: default_opencode_web_port(),
74            bind: default_bind(),
75            auto_restart: default_auto_restart(),
76            boot_mode: default_boot_mode(),
77            restart_retries: default_restart_retries(),
78            restart_delay: default_restart_delay(),
79        }
80    }
81}
82
83impl Config {
84    /// Create a new Config with default values
85    pub fn new() -> Self {
86        Self::default()
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_default_config() {
96        let config = Config::default();
97        assert_eq!(config.version, 1);
98        assert_eq!(config.opencode_web_port, 3000);
99        assert_eq!(config.bind, "localhost");
100        assert!(config.auto_restart);
101        assert_eq!(config.boot_mode, "user");
102        assert_eq!(config.restart_retries, 3);
103        assert_eq!(config.restart_delay, 5);
104    }
105
106    #[test]
107    fn test_serialize_deserialize_roundtrip() {
108        let config = Config::default();
109        let json = serde_json::to_string(&config).unwrap();
110        let parsed: Config = serde_json::from_str(&json).unwrap();
111        assert_eq!(config, parsed);
112    }
113
114    #[test]
115    fn test_deserialize_with_missing_optional_fields() {
116        let json = r#"{"version": 1}"#;
117        let config: Config = serde_json::from_str(json).unwrap();
118        assert_eq!(config.version, 1);
119        assert_eq!(config.opencode_web_port, 3000);
120        assert_eq!(config.bind, "localhost");
121        assert!(config.auto_restart);
122        assert_eq!(config.boot_mode, "user");
123        assert_eq!(config.restart_retries, 3);
124        assert_eq!(config.restart_delay, 5);
125    }
126
127    #[test]
128    fn test_serialize_deserialize_roundtrip_with_service_fields() {
129        let config = Config {
130            version: 1,
131            opencode_web_port: 9000,
132            bind: "0.0.0.0".to_string(),
133            auto_restart: false,
134            boot_mode: "system".to_string(),
135            restart_retries: 5,
136            restart_delay: 10,
137        };
138        let json = serde_json::to_string(&config).unwrap();
139        let parsed: Config = serde_json::from_str(&json).unwrap();
140        assert_eq!(config, parsed);
141        assert_eq!(parsed.boot_mode, "system");
142        assert_eq!(parsed.restart_retries, 5);
143        assert_eq!(parsed.restart_delay, 10);
144    }
145
146    #[test]
147    fn test_reject_unknown_fields() {
148        let json = r#"{"version": 1, "unknown_field": "value"}"#;
149        let result: Result<Config, _> = serde_json::from_str(json);
150        assert!(result.is_err());
151    }
152}