llm_config_api/
routes.rs

1//! REST API routes
2
3use 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/// API state shared across handlers
15#[derive(Clone)]
16pub struct ApiState {
17    pub manager: Arc<ConfigManager>,
18}
19
20/// Standard API error response
21#[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/// API error types
46#[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/// Query parameters for get config
61#[derive(Debug, Deserialize)]
62pub struct GetConfigQuery {
63    #[serde(default)]
64    env: Option<String>,
65    #[serde(default)]
66    #[allow(dead_code)] // Reserved for future use
67    with_overrides: bool,
68}
69
70/// Request body for set config
71#[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/// Response for config operations
86#[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
178/// GET /health - Health check endpoint
179pub 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
187/// GET /api/v1/configs/:namespace/:key - Get a configuration value
188pub 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
208/// POST /api/v1/configs/:namespace/:key - Set a configuration value
209pub 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        // Store as encrypted secret
221        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
236/// GET /api/v1/configs/:namespace - List configurations in a namespace
237pub 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
255/// DELETE /api/v1/configs/:namespace/:key - Delete a configuration
256pub 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
277/// GET /api/v1/configs/:namespace/:key/history - Get version history
278pub 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/// POST /api/v1/configs/:namespace/:key/rollback/:version - Rollback to a specific version
309#[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}