Skip to main content

mockforge_http/management/
health.rs

1use axum::{
2    extract::State,
3    http::StatusCode,
4    response::{IntoResponse, Json},
5};
6use serde::Deserialize;
7
8use super::{ManagementState, ServerConfig, ServerStats};
9
10/// Request to validate configuration
11#[derive(Debug, Deserialize)]
12pub struct ValidateConfigRequest {
13    /// Configuration to validate (as JSON)
14    pub config: serde_json::Value,
15    /// Format of the configuration ("json" or "yaml")
16    #[serde(default = "default_format")]
17    pub format: String,
18}
19
20fn default_format() -> String {
21    "json".to_string()
22}
23
24/// Validate configuration without applying it
25pub(crate) async fn validate_config(
26    Json(request): Json<ValidateConfigRequest>,
27) -> impl IntoResponse {
28    use mockforge_core::config::ServerConfig;
29
30    let config_result: Result<ServerConfig, String> = match request.format.as_str() {
31        "yaml" | "yml" => {
32            let yaml_str = match serde_json::to_string(&request.config) {
33                Ok(s) => s,
34                Err(e) => {
35                    return (
36                        StatusCode::BAD_REQUEST,
37                        Json(serde_json::json!({
38                            "valid": false,
39                            "error": format!("Failed to convert to string: {}", e),
40                            "message": "Configuration validation failed"
41                        })),
42                    )
43                        .into_response();
44                }
45            };
46            serde_yaml::from_str(&yaml_str).map_err(|e| format!("YAML parse error: {}", e))
47        }
48        _ => serde_json::from_value(request.config).map_err(|e| format!("JSON parse error: {}", e)),
49    };
50
51    match config_result {
52        Ok(_) => Json(serde_json::json!({
53            "valid": true,
54            "message": "Configuration is valid"
55        }))
56        .into_response(),
57        Err(e) => (
58            StatusCode::BAD_REQUEST,
59            Json(serde_json::json!({
60                "valid": false,
61                "error": format!("Invalid configuration: {}", e),
62                "message": "Configuration validation failed"
63            })),
64        )
65            .into_response(),
66    }
67}
68
69/// Request for bulk configuration update
70#[derive(Debug, Deserialize)]
71pub struct BulkConfigUpdateRequest {
72    /// Partial configuration updates (only specified fields will be updated)
73    pub updates: serde_json::Value,
74}
75
76/// Bulk update configuration
77///
78/// This endpoint allows updating multiple configuration options at once.
79/// Only the specified fields in the updates object will be modified.
80///
81/// Configuration updates are applied to the server configuration if available
82/// in ManagementState. Changes take effect immediately for supported settings.
83pub(crate) async fn bulk_update_config(
84    State(state): State<ManagementState>,
85    Json(request): Json<BulkConfigUpdateRequest>,
86) -> impl IntoResponse {
87    // Validate the updates structure
88    if !request.updates.is_object() {
89        return (
90            StatusCode::BAD_REQUEST,
91            Json(serde_json::json!({
92                "error": "Invalid request",
93                "message": "Updates must be a JSON object"
94            })),
95        )
96            .into_response();
97    }
98
99    // Try to validate as partial ServerConfig
100    use mockforge_core::config::ServerConfig;
101
102    // Create a minimal valid config and try to merge updates
103    let base_config = ServerConfig::default();
104    let base_json = match serde_json::to_value(&base_config) {
105        Ok(v) => v,
106        Err(e) => {
107            return (
108                StatusCode::INTERNAL_SERVER_ERROR,
109                Json(serde_json::json!({
110                    "error": "Internal error",
111                    "message": format!("Failed to serialize base config: {}", e)
112                })),
113            )
114                .into_response();
115        }
116    };
117
118    // Merge updates into base config (simplified merge)
119    let mut merged = base_json.clone();
120    if let (Some(merged_obj), Some(updates_obj)) =
121        (merged.as_object_mut(), request.updates.as_object())
122    {
123        for (key, value) in updates_obj {
124            merged_obj.insert(key.clone(), value.clone());
125        }
126    }
127
128    // Validate the merged config
129    match serde_json::from_value::<ServerConfig>(merged) {
130        Ok(validated_config) => {
131            // Apply config if server_config is available in ManagementState
132            if let Some(ref config_lock) = state.server_config {
133                let mut config = config_lock.write().await;
134                *config = validated_config;
135                Json(serde_json::json!({
136                    "success": true,
137                    "message": "Bulk configuration update applied successfully",
138                    "updates_received": request.updates,
139                    "validated": true,
140                    "applied": true
141                }))
142                .into_response()
143            } else {
144                Json(serde_json::json!({
145                    "success": true,
146                    "message": "Bulk configuration update validated but not applied (no server config in state). Use .with_server_config() when building ManagementState.",
147                    "updates_received": request.updates,
148                    "validated": true,
149                    "applied": false
150                }))
151                .into_response()
152            }
153        }
154        Err(e) => (
155            StatusCode::BAD_REQUEST,
156            Json(serde_json::json!({
157                "error": "Invalid configuration",
158                "message": format!("Configuration validation failed: {}", e),
159                "validated": false
160            })),
161        )
162            .into_response(),
163    }
164}
165
166/// Get server statistics
167pub(crate) async fn get_stats(State(state): State<ManagementState>) -> Json<ServerStats> {
168    let mocks = state.mocks.read().await;
169    let request_count = *state.request_counter.read().await;
170
171    Json(ServerStats {
172        uptime_seconds: state.start_time.elapsed().as_secs(),
173        total_requests: request_count,
174        active_mocks: mocks.len(),
175        enabled_mocks: mocks.iter().filter(|m| m.enabled).count(),
176        registered_routes: mocks.len(), // This could be enhanced with actual route registry info
177    })
178}
179
180/// Get server configuration
181pub(crate) async fn get_config(State(state): State<ManagementState>) -> Json<super::ServerConfig> {
182    Json(super::ServerConfig {
183        version: env!("CARGO_PKG_VERSION").to_string(),
184        port: state.port,
185        has_openapi_spec: state.spec.is_some(),
186        spec_path: state.spec_path.clone(),
187    })
188}
189
190/// Serve the loaded OpenAPI spec as JSON
191pub(crate) async fn get_openapi_spec(
192    State(state): State<ManagementState>,
193) -> Result<Json<serde_json::Value>, StatusCode> {
194    match &state.spec {
195        Some(spec) => match &spec.raw_document {
196            Some(doc) => Ok(Json(doc.clone())),
197            None => {
198                // Fall back to serializing the parsed spec
199                match serde_json::to_value(&spec.spec) {
200                    Ok(val) => Ok(Json(val)),
201                    Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
202                }
203            }
204        },
205        None => Err(StatusCode::NOT_FOUND),
206    }
207}
208
209/// Health check endpoint
210pub(crate) async fn health_check() -> Json<serde_json::Value> {
211    Json(serde_json::json!({
212        "status": "healthy",
213        "service": "mockforge-management",
214        "timestamp": chrono::Utc::now().to_rfc3339()
215    }))
216}
217
218/// Report which features and protocols are available in this build.
219///
220/// The UI queries this on startup to auto-stub any missing features
221/// instead of maintaining hardcoded prefix lists.
222pub(crate) async fn get_capabilities() -> Json<serde_json::Value> {
223    let mut features = vec!["core", "http", "management", "mocks", "proxy", "ai"];
224
225    #[cfg(feature = "smtp")]
226    features.push("smtp");
227    #[cfg(feature = "mqtt")]
228    features.push("mqtt");
229    #[cfg(feature = "kafka")]
230    features.push("kafka");
231    #[cfg(feature = "conformance")]
232    features.push("conformance");
233    #[cfg(feature = "behavioral-cloning")]
234    features.push("behavioral-cloning");
235
236    // Always-available subsystems (registered unconditionally in the router)
237    features.extend_from_slice(&[
238        "chaos",
239        "network-profiles",
240        "state-machines",
241        "migration",
242        "snapshot-diff",
243        "mockai",
244    ]);
245
246    Json(serde_json::json!({
247        "features": features,
248        "version": env!("CARGO_PKG_VERSION"),
249    }))
250}