1use std::path::Path;
2
3use rusqlite::Connection;
4use serde::Deserialize;
5use serde_json::Value;
6use thiserror::Error;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct CodexProvider {
11 name: String,
12 auth_json: String,
13 config_toml: String,
14}
15
16impl CodexProvider {
17 #[must_use]
29 pub fn new(name: String, auth_json: String, config_toml: String) -> Self {
30 Self {
31 name,
32 auth_json,
33 config_toml,
34 }
35 }
36
37 #[must_use]
43 pub fn name(&self) -> &str {
44 &self.name
45 }
46
47 #[must_use]
53 pub fn auth_json(&self) -> &str {
54 &self.auth_json
55 }
56
57 #[must_use]
63 pub fn config_toml(&self) -> &str {
64 &self.config_toml
65 }
66}
67
68#[derive(Debug, Error)]
70pub enum ProviderError {
71 #[error("configuration database error: {0}")]
73 Database(#[from] rusqlite::Error),
74
75 #[error("invalid settings JSON for provider '{provider_name}': {source}")]
77 SettingsJson {
78 provider_name: String,
80 source: serde_json::Error,
82 },
83
84 #[error("providers table must contain a settings_config column")]
86 UnsupportedSchema,
87}
88
89#[derive(Debug, Deserialize)]
90struct ProviderSettingsConfig {
91 auth: Value,
92 config: String,
93}
94
95pub fn load_codex_providers(database_path: &Path) -> Result<Vec<CodexProvider>, ProviderError> {
110 let connection = Connection::open(database_path)?;
111 load_codex_providers_from_connection(&connection)
112}
113
114pub fn load_codex_providers_from_connection(
129 connection: &Connection,
130) -> Result<Vec<CodexProvider>, ProviderError> {
131 let columns = provider_table_columns(connection)?;
132 if columns.iter().any(|column| column == "settings_config") {
133 return load_codex_providers_from_settings_config_column(connection);
134 }
135
136 Err(ProviderError::UnsupportedSchema)
137}
138
139fn provider_table_columns(connection: &Connection) -> Result<Vec<String>, ProviderError> {
140 let mut statement = connection.prepare("PRAGMA table_info(providers)")?;
141 let rows = statement.query_map([], |row| row.get::<_, String>(1))?;
142 let mut columns = Vec::new();
143 for row in rows {
144 columns.push(row?);
145 }
146 Ok(columns)
147}
148
149fn load_codex_providers_from_settings_config_column(
150 connection: &Connection,
151) -> Result<Vec<CodexProvider>, ProviderError> {
152 let mut statement = connection.prepare(
153 "SELECT name, settings_config FROM providers WHERE app_type = ?1 ORDER BY rowid ASC",
154 )?;
155 let rows = statement.query_map(["codex"], |row| {
156 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
157 })?;
158
159 let mut providers = Vec::new();
160 for row in rows {
161 let (name, settings_json) = row?;
162 let settings = parse_settings(&name, &settings_json)?;
163 let auth_json = serde_json::to_string_pretty(&settings.auth).map_err(|source| {
164 ProviderError::SettingsJson {
165 provider_name: name.clone(),
166 source,
167 }
168 })?;
169 providers.push(CodexProvider::new(name, auth_json, settings.config));
170 }
171
172 Ok(providers)
173}
174
175fn parse_settings(
176 provider_name: &str,
177 settings_json: &str,
178) -> Result<ProviderSettingsConfig, ProviderError> {
179 serde_json::from_str(settings_json).map_err(|source| ProviderError::SettingsJson {
180 provider_name: provider_name.to_owned(),
181 source,
182 })
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 fn connection_with_providers() -> Connection {
190 let connection = Connection::open_in_memory().expect("in-memory SQLite should open");
191 connection
192 .execute(
193 "CREATE TABLE providers (
194 name TEXT NOT NULL,
195 app_type TEXT NOT NULL,
196 settings_config TEXT NOT NULL
197 )",
198 [],
199 )
200 .expect("providers table should be created");
201 connection
202 }
203
204 #[test]
205 fn load_codex_providers_filters_and_maps_rows() {
206 let connection = connection_with_providers();
207 connection
208 .execute(
209 "INSERT INTO providers (name, app_type, settings_config) VALUES (?1, ?2, ?3)",
210 [
211 "primary",
212 "codex",
213 r#"{"auth":{"OPENAI_API_KEY":"test-key"},"config":"model = \"gpt-5.5\"\n"}"#,
214 ],
215 )
216 .expect("codex provider row should insert");
217 connection
218 .execute(
219 "INSERT INTO providers (name, app_type, settings_config) VALUES (?1, ?2, ?3)",
220 [
221 "other",
222 "claude",
223 r#"{"auth":{"OPENAI_API_KEY":"ignored"},"config":"ignored"}"#,
224 ],
225 )
226 .expect("non-codex provider row should insert");
227
228 let providers =
229 load_codex_providers_from_connection(&connection).expect("providers should load");
230
231 assert_eq!(
232 providers,
233 vec![CodexProvider::new(
234 "primary".to_owned(),
235 "{\n \"OPENAI_API_KEY\": \"test-key\"\n}".to_owned(),
236 "model = \"gpt-5.5\"\n".to_owned()
237 )]
238 );
239 }
240
241 #[test]
242 fn load_codex_providers_reports_invalid_settings_json() {
243 let connection = connection_with_providers();
244 connection
245 .execute(
246 "INSERT INTO providers (name, app_type, settings_config) VALUES (?1, ?2, ?3)",
247 ["broken", "codex", "{}"],
248 )
249 .expect("broken provider row should insert");
250
251 let error = load_codex_providers_from_connection(&connection)
252 .expect_err("invalid settings should fail");
253
254 assert!(matches!(
255 error,
256 ProviderError::SettingsJson { provider_name, .. } if provider_name == "broken"
257 ));
258 }
259}