1use axum::{
4 extract::{Path, Query, State},
5 http::StatusCode,
6 response::{IntoResponse, Response},
7 Json,
8};
9use llm_config_core::{ConfigEntry, ConfigManager, ConfigValue, Environment};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::sync::Arc;
13
14#[derive(Clone)]
16pub struct ApiState {
17 pub manager: Arc<ConfigManager>,
18}
19
20#[derive(Debug, Serialize)]
22pub struct ErrorResponse {
23 pub error: String,
24 pub message: String,
25}
26
27impl IntoResponse for ApiError {
28 fn into_response(self) -> Response {
29 let (status, error_message) = match self {
30 ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
31 ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
32 ApiError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
33 ApiError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg),
34 };
35
36 let body = Json(ErrorResponse {
37 error: status.canonical_reason().unwrap_or("Unknown").to_string(),
38 message: error_message,
39 });
40
41 (status, body).into_response()
42 }
43}
44
45#[derive(Debug)]
47pub enum ApiError {
48 NotFound(String),
49 BadRequest(String),
50 InternalError(String),
51 Unauthorized(String),
52}
53
54impl From<llm_config_core::ConfigError> for ApiError {
55 fn from(err: llm_config_core::ConfigError) -> Self {
56 ApiError::InternalError(err.to_string())
57 }
58}
59
60#[derive(Debug, Deserialize)]
62pub struct GetConfigQuery {
63 #[serde(default)]
64 env: Option<String>,
65 #[serde(default)]
66 #[allow(dead_code)] with_overrides: bool,
68}
69
70#[derive(Debug, Deserialize)]
72pub struct SetConfigRequest {
73 pub value: serde_json::Value,
74 pub env: String,
75 #[serde(default = "default_user")]
76 pub user: String,
77 #[serde(default)]
78 pub secret: bool,
79}
80
81fn default_user() -> String {
82 "api-user".to_string()
83}
84
85#[derive(Debug, Serialize)]
87pub struct ConfigResponse {
88 pub id: String,
89 pub namespace: String,
90 pub key: String,
91 pub value: serde_json::Value,
92 pub environment: String,
93 pub version: u64,
94 pub metadata: ConfigMetadataResponse,
95}
96
97#[derive(Debug, Serialize)]
98pub struct ConfigMetadataResponse {
99 pub created_at: String,
100 pub created_by: String,
101 pub updated_at: String,
102 pub updated_by: String,
103 pub tags: Vec<String>,
104 pub description: Option<String>,
105}
106
107impl From<ConfigEntry> for ConfigResponse {
108 fn from(entry: ConfigEntry) -> Self {
109 Self {
110 id: entry.id.to_string(),
111 namespace: entry.namespace,
112 key: entry.key,
113 value: config_value_to_json(&entry.value),
114 environment: entry.environment.to_string(),
115 version: entry.version,
116 metadata: ConfigMetadataResponse {
117 created_at: entry.metadata.created_at.to_rfc3339(),
118 created_by: entry.metadata.created_by,
119 updated_at: entry.metadata.updated_at.to_rfc3339(),
120 updated_by: entry.metadata.updated_by,
121 tags: entry.metadata.tags,
122 description: entry.metadata.description,
123 },
124 }
125 }
126}
127
128fn config_value_to_json(value: &ConfigValue) -> serde_json::Value {
129 match value {
130 ConfigValue::String(s) => serde_json::Value::String(s.clone()),
131 ConfigValue::Integer(i) => serde_json::Value::Number((*i).into()),
132 ConfigValue::Float(f) => serde_json::Value::Number(
133 serde_json::Number::from_f64(*f).unwrap_or(serde_json::Number::from(0)),
134 ),
135 ConfigValue::Boolean(b) => serde_json::Value::Bool(*b),
136 ConfigValue::Array(arr) => {
137 serde_json::Value::Array(arr.iter().map(config_value_to_json).collect())
138 }
139 ConfigValue::Object(map) => {
140 let obj: HashMap<String, serde_json::Value> = map
141 .iter()
142 .map(|(k, v)| (k.clone(), config_value_to_json(v)))
143 .collect();
144 serde_json::Value::Object(obj.into_iter().collect())
145 }
146 ConfigValue::Secret(_) => serde_json::Value::String("<encrypted>".to_string()),
147 }
148}
149
150fn json_to_config_value(value: &serde_json::Value) -> Result<ConfigValue, ApiError> {
151 Ok(match value {
152 serde_json::Value::String(s) => ConfigValue::String(s.clone()),
153 serde_json::Value::Number(n) => {
154 if let Some(i) = n.as_i64() {
155 ConfigValue::Integer(i)
156 } else if let Some(f) = n.as_f64() {
157 ConfigValue::Float(f)
158 } else {
159 return Err(ApiError::BadRequest("Invalid number format".to_string()));
160 }
161 }
162 serde_json::Value::Bool(b) => ConfigValue::Boolean(*b),
163 serde_json::Value::Array(arr) => {
164 let values: Result<Vec<_>, _> = arr.iter().map(json_to_config_value).collect();
165 ConfigValue::Array(values?)
166 }
167 serde_json::Value::Object(map) => {
168 let mut config_map = HashMap::new();
169 for (k, v) in map {
170 config_map.insert(k.clone(), json_to_config_value(v)?);
171 }
172 ConfigValue::Object(config_map)
173 }
174 serde_json::Value::Null => ConfigValue::String(String::new()),
175 })
176}
177
178pub async fn health_check() -> impl IntoResponse {
180 Json(serde_json::json!({
181 "status": "healthy",
182 "service": "llm-config-manager",
183 "version": env!("CARGO_PKG_VERSION")
184 }))
185}
186
187pub async fn get_config(
189 State(state): State<ApiState>,
190 Path((namespace, key)): Path<(String, String)>,
191 Query(params): Query<GetConfigQuery>,
192) -> Result<Json<ConfigResponse>, ApiError> {
193 let env: Environment = params
194 .env
195 .as_deref()
196 .unwrap_or("development")
197 .parse()
198 .map_err(|e| ApiError::BadRequest(e))?;
199
200 let entry = state
201 .manager
202 .get(&namespace, &key, env)?
203 .ok_or_else(|| ApiError::NotFound(format!("Configuration not found: {}:{}", namespace, key)))?;
204
205 Ok(Json(entry.into()))
206}
207
208pub async fn set_config(
210 State(state): State<ApiState>,
211 Path((namespace, key)): Path<(String, String)>,
212 Json(req): Json<SetConfigRequest>,
213) -> Result<Json<ConfigResponse>, ApiError> {
214 let env: Environment = req
215 .env
216 .parse()
217 .map_err(|e| ApiError::BadRequest(e))?;
218
219 let entry = if req.secret {
220 let value_str = req.value.as_str()
222 .ok_or_else(|| ApiError::BadRequest("Secret value must be a string".to_string()))?;
223 state
224 .manager
225 .set_secret(&namespace, &key, value_str.as_bytes(), env, &req.user)?
226 } else {
227 let config_value = json_to_config_value(&req.value)?;
228 state
229 .manager
230 .set(&namespace, &key, config_value, env, &req.user)?
231 };
232
233 Ok(Json(entry.into()))
234}
235
236pub async fn list_configs(
238 State(state): State<ApiState>,
239 Path(namespace): Path<String>,
240 Query(params): Query<GetConfigQuery>,
241) -> Result<Json<Vec<ConfigResponse>>, ApiError> {
242 let env: Environment = params
243 .env
244 .as_deref()
245 .unwrap_or("development")
246 .parse()
247 .map_err(|e| ApiError::BadRequest(e))?;
248
249 let entries = state.manager.list(&namespace, env)?;
250 let responses: Vec<ConfigResponse> = entries.into_iter().map(|e| e.into()).collect();
251
252 Ok(Json(responses))
253}
254
255pub async fn delete_config(
257 State(state): State<ApiState>,
258 Path((namespace, key)): Path<(String, String)>,
259 Query(params): Query<GetConfigQuery>,
260) -> Result<StatusCode, ApiError> {
261 let env: Environment = params
262 .env
263 .as_deref()
264 .unwrap_or("development")
265 .parse()
266 .map_err(|e| ApiError::BadRequest(e))?;
267
268 let deleted = state.manager.delete(&namespace, &key, env)?;
269
270 if deleted {
271 Ok(StatusCode::NO_CONTENT)
272 } else {
273 Err(ApiError::NotFound(format!("Configuration not found: {}:{}", namespace, key)))
274 }
275}
276
277pub async fn get_history(
279 State(state): State<ApiState>,
280 Path((namespace, key)): Path<(String, String)>,
281 Query(params): Query<GetConfigQuery>,
282) -> Result<Json<Vec<serde_json::Value>>, ApiError> {
283 let env: Environment = params
284 .env
285 .as_deref()
286 .unwrap_or("development")
287 .parse()
288 .map_err(|e| ApiError::BadRequest(e))?;
289
290 let history = state.manager.get_history(&namespace, &key, env)?;
291
292 let response: Vec<serde_json::Value> = history
293 .into_iter()
294 .map(|v| {
295 serde_json::json!({
296 "version": v.version,
297 "value": config_value_to_json(&v.value),
298 "created_at": v.created_at.to_rfc3339(),
299 "created_by": v.created_by,
300 "change_description": v.change_description,
301 })
302 })
303 .collect();
304
305 Ok(Json(response))
306}
307
308#[derive(Debug, Deserialize)]
310pub struct RollbackQuery {
311 env: Option<String>,
312}
313
314pub async fn rollback_config(
315 State(state): State<ApiState>,
316 Path((namespace, key, version)): Path<(String, String, u64)>,
317 Query(params): Query<RollbackQuery>,
318) -> Result<Json<ConfigResponse>, ApiError> {
319 let env: Environment = params
320 .env
321 .as_deref()
322 .unwrap_or("development")
323 .parse()
324 .map_err(|e| ApiError::BadRequest(e))?;
325
326 let entry = state
327 .manager
328 .rollback(&namespace, &key, env, version)?
329 .ok_or_else(|| ApiError::NotFound(format!("Version {} not found", version)))?;
330
331 Ok(Json(entry.into()))
332}