Skip to main content

opencode_rs/http/
config.rs

1//! Config API for `OpenCode`.
2//!
3//! Endpoints for configuration management.
4
5use crate::error::Result;
6use crate::http::HttpClient;
7use crate::types::config::Config;
8use crate::types::config::ConfigProviders;
9use reqwest::Method;
10
11/// Config API client.
12#[derive(Clone)]
13pub struct ConfigApi {
14    http: HttpClient,
15}
16
17impl ConfigApi {
18    /// Create a new Config API client.
19    pub fn new(http: HttpClient) -> Self {
20        Self { http }
21    }
22
23    /// Get current configuration.
24    ///
25    /// # Errors
26    ///
27    /// Returns an error if the request fails.
28    pub async fn get(&self) -> Result<Config> {
29        self.http.request_json(Method::GET, "/config", None).await
30    }
31
32    /// Update configuration.
33    ///
34    /// # Errors
35    ///
36    /// Returns an error if the request fails.
37    pub async fn update(&self, req: &Config) -> Result<Config> {
38        let body = serde_json::to_value(req)?;
39        self.http
40            .request_json(Method::PATCH, "/config", Some(body))
41            .await
42    }
43
44    /// Get provider configuration.
45    ///
46    /// # Errors
47    ///
48    /// Returns an error if the request fails.
49    pub async fn providers(&self) -> Result<ConfigProviders> {
50        self.http
51            .request_json(Method::GET, "/config/providers", None)
52            .await
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::http::HttpConfig;
60    use std::time::Duration;
61    use wiremock::Mock;
62    use wiremock::MockServer;
63    use wiremock::ResponseTemplate;
64    use wiremock::matchers::method;
65    use wiremock::matchers::path;
66
67    #[tokio::test]
68    async fn test_get_config_success() {
69        let mock_server = MockServer::start().await;
70
71        Mock::given(method("GET"))
72            .and(path("/config"))
73            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
74                "provider": "anthropic",
75                "model": "claude-sonnet-4-20250514"
76            })))
77            .mount(&mock_server)
78            .await;
79
80        let http = HttpClient::new(HttpConfig {
81            base_url: mock_server.uri(),
82            directory: None,
83            workspace: None,
84            timeout: Duration::from_secs(30),
85        })
86        .unwrap();
87
88        let config = ConfigApi::new(http);
89        let result = config.get().await;
90        assert!(result.is_ok());
91    }
92
93    #[tokio::test]
94    async fn test_update_config_success() {
95        let mock_server = MockServer::start().await;
96
97        Mock::given(method("PATCH"))
98            .and(path("/config"))
99            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
100                "provider": "openai",
101                "model": "gpt-4"
102            })))
103            .mount(&mock_server)
104            .await;
105
106        let http = HttpClient::new(HttpConfig {
107            base_url: mock_server.uri(),
108            directory: None,
109            workspace: None,
110            timeout: Duration::from_secs(30),
111        })
112        .unwrap();
113
114        let config = ConfigApi::new(http);
115        let result = config
116            .update(&Config {
117                provider: Some(serde_json::json!("openai")),
118                model: Some(serde_json::json!("gpt-4")),
119                agent: None,
120                auto_compact: None,
121                mcp: None,
122                extra: serde_json::Value::Null,
123            })
124            .await;
125        assert!(result.is_ok());
126    }
127
128    #[tokio::test]
129    async fn test_providers_config_success() {
130        let mock_server = MockServer::start().await;
131
132        Mock::given(method("GET"))
133            .and(path("/config/providers"))
134            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
135                "providers": [
136                    {"id": "anthropic", "name": "Anthropic"},
137                    {"id": "openai", "name": "OpenAI"}
138                ]
139            })))
140            .mount(&mock_server)
141            .await;
142
143        let http = HttpClient::new(HttpConfig {
144            base_url: mock_server.uri(),
145            directory: None,
146            workspace: None,
147            timeout: Duration::from_secs(30),
148        })
149        .unwrap();
150
151        let config = ConfigApi::new(http);
152        let result = config.providers().await;
153        assert!(result.is_ok());
154    }
155
156    #[tokio::test]
157    async fn test_update_config_validation_error() {
158        let mock_server = MockServer::start().await;
159
160        Mock::given(method("PATCH"))
161            .and(path("/config"))
162            .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
163                "name": "ValidationError",
164                "message": "Invalid provider"
165            })))
166            .mount(&mock_server)
167            .await;
168
169        let http = HttpClient::new(HttpConfig {
170            base_url: mock_server.uri(),
171            directory: None,
172            workspace: None,
173            timeout: Duration::from_secs(30),
174        })
175        .unwrap();
176
177        let config = ConfigApi::new(http);
178        let result = config
179            .update(&Config {
180                provider: Some(serde_json::json!("invalid")),
181                model: None,
182                agent: None,
183                auto_compact: None,
184                mcp: None,
185                extra: serde_json::Value::Null,
186            })
187            .await;
188        assert!(result.is_err());
189        assert!(result.unwrap_err().is_validation_error());
190    }
191}