mockforge_http/handlers/
consistency.rs

1//! Consistency engine API handlers
2//!
3//! This module provides HTTP handlers for managing unified state across protocols.
4
5use axum::{
6    extract::{Path, Query, State},
7    http::StatusCode,
8    response::Json,
9};
10// ChaosScenario is now serde_json::Value to avoid circular dependency
11use mockforge_core::consistency::{
12    enrich_order_response, enrich_response_via_graph, enrich_user_response,
13    get_user_orders_via_graph, ConsistencyEngine, EntityState, UnifiedState,
14};
15use mockforge_core::reality::RealityLevel;
16use mockforge_data::{LifecycleState, PersonaLifecycle, PersonaProfile};
17use serde::{Deserialize, Serialize};
18use serde_json::Value as JsonValue;
19use serde_json::Value;
20use std::collections::HashMap;
21use std::sync::Arc;
22use tracing::{error, info};
23
24/// State for consistency handlers
25#[derive(Clone)]
26pub struct ConsistencyState {
27    /// Consistency engine
28    pub engine: Arc<ConsistencyEngine>,
29}
30
31/// Request to set active persona
32#[derive(Debug, Deserialize)]
33pub struct SetPersonaRequest {
34    /// Persona profile
35    pub persona: PersonaProfile,
36}
37
38/// Request to set active scenario
39#[derive(Debug, Deserialize)]
40pub struct SetScenarioRequest {
41    /// Scenario ID
42    pub scenario_id: String,
43}
44
45/// Request to set reality level
46#[derive(Debug, Deserialize)]
47pub struct SetRealityLevelRequest {
48    /// Reality level (1-5)
49    pub level: u8,
50}
51
52/// Request to set reality ratio
53#[derive(Debug, Deserialize)]
54pub struct SetRealityRatioRequest {
55    /// Reality ratio (0.0-1.0)
56    pub ratio: f64,
57}
58
59/// Request to register an entity
60#[derive(Debug, Deserialize)]
61pub struct RegisterEntityRequest {
62    /// Entity type
63    pub entity_type: String,
64    /// Entity ID
65    pub entity_id: String,
66    /// Entity data (JSON)
67    pub data: Value,
68    /// Optional persona ID
69    pub persona_id: Option<String>,
70}
71
72/// Request to activate chaos rule
73#[derive(Debug, Deserialize)]
74pub struct ActivateChaosRuleRequest {
75    /// Chaos scenario
76    pub rule: JsonValue, // ChaosScenario as JSON value
77}
78
79/// Request to deactivate chaos rule
80#[derive(Debug, Deserialize)]
81pub struct DeactivateChaosRuleRequest {
82    /// Rule name
83    pub rule_name: String,
84}
85
86/// Request to set persona lifecycle state
87#[derive(Debug, Deserialize)]
88pub struct SetPersonaLifecycleRequest {
89    /// Persona ID
90    pub persona_id: String,
91    /// Initial lifecycle state
92    pub initial_state: String,
93}
94
95/// Query parameters for workspace operations
96#[derive(Debug, Deserialize)]
97pub struct WorkspaceQuery {
98    /// Workspace ID (defaults to "default" if not provided)
99    #[serde(default = "default_workspace")]
100    pub workspace: String,
101}
102
103fn default_workspace() -> String {
104    "default".to_string()
105}
106
107/// Get unified state for a workspace
108///
109/// GET /api/v1/consistency/state?workspace={workspace_id}
110pub async fn get_state(
111    State(state): State<ConsistencyState>,
112    Query(params): Query<WorkspaceQuery>,
113) -> Result<Json<UnifiedState>, StatusCode> {
114    let unified_state = state.engine.get_state(&params.workspace).await.ok_or_else(|| {
115        error!("State not found for workspace: {}", params.workspace);
116        StatusCode::NOT_FOUND
117    })?;
118
119    Ok(Json(unified_state))
120}
121
122/// Set active persona for a workspace
123///
124/// POST /api/v1/consistency/persona?workspace={workspace_id}
125pub async fn set_persona(
126    State(state): State<ConsistencyState>,
127    Query(params): Query<WorkspaceQuery>,
128    Json(request): Json<SetPersonaRequest>,
129) -> Result<Json<Value>, StatusCode> {
130    state
131        .engine
132        .set_active_persona(&params.workspace, request.persona)
133        .await
134        .map_err(|e| {
135            error!("Failed to set persona: {}", e);
136            StatusCode::INTERNAL_SERVER_ERROR
137        })?;
138
139    info!("Set persona for workspace: {}", params.workspace);
140    Ok(Json(serde_json::json!({
141        "success": true,
142        "workspace": params.workspace,
143    })))
144}
145
146/// Set active scenario for a workspace
147///
148/// POST /api/v1/consistency/scenario?workspace={workspace_id}
149pub async fn set_scenario(
150    State(state): State<ConsistencyState>,
151    Query(params): Query<WorkspaceQuery>,
152    Json(request): Json<SetScenarioRequest>,
153) -> Result<Json<Value>, StatusCode> {
154    state
155        .engine
156        .set_active_scenario(&params.workspace, request.scenario_id)
157        .await
158        .map_err(|e| {
159            error!("Failed to set scenario: {}", e);
160            StatusCode::INTERNAL_SERVER_ERROR
161        })?;
162
163    info!("Set scenario for workspace: {}", params.workspace);
164    Ok(Json(serde_json::json!({
165        "success": true,
166        "workspace": params.workspace,
167    })))
168}
169
170/// Set reality level for a workspace
171///
172/// POST /api/v1/consistency/reality-level?workspace={workspace_id}
173pub async fn set_reality_level(
174    State(state): State<ConsistencyState>,
175    Query(params): Query<WorkspaceQuery>,
176    Json(request): Json<SetRealityLevelRequest>,
177) -> Result<Json<Value>, StatusCode> {
178    let level = RealityLevel::from_value(request.level).ok_or_else(|| {
179        error!("Invalid reality level: {}", request.level);
180        StatusCode::BAD_REQUEST
181    })?;
182
183    state.engine.set_reality_level(&params.workspace, level).await.map_err(|e| {
184        error!("Failed to set reality level: {}", e);
185        StatusCode::INTERNAL_SERVER_ERROR
186    })?;
187
188    info!("Set reality level for workspace: {}", params.workspace);
189    Ok(Json(serde_json::json!({
190        "success": true,
191        "workspace": params.workspace,
192        "level": request.level,
193    })))
194}
195
196/// Set reality continuum ratio for a workspace
197///
198/// POST /api/v1/consistency/reality-ratio?workspace={workspace_id}
199pub async fn set_reality_ratio(
200    State(state): State<ConsistencyState>,
201    Query(params): Query<WorkspaceQuery>,
202    Json(request): Json<SetRealityRatioRequest>,
203) -> Result<Json<Value>, StatusCode> {
204    if !(0.0..=1.0).contains(&request.ratio) {
205        return Err(StatusCode::BAD_REQUEST);
206    }
207
208    state
209        .engine
210        .set_reality_ratio(&params.workspace, request.ratio)
211        .await
212        .map_err(|e| {
213            error!("Failed to set reality ratio: {}", e);
214            StatusCode::INTERNAL_SERVER_ERROR
215        })?;
216
217    info!("Set reality ratio for workspace: {}", params.workspace);
218    Ok(Json(serde_json::json!({
219        "success": true,
220        "workspace": params.workspace,
221        "ratio": request.ratio,
222    })))
223}
224
225/// Register or update an entity
226///
227/// POST /api/v1/consistency/entities?workspace={workspace_id}
228pub async fn register_entity(
229    State(state): State<ConsistencyState>,
230    Query(params): Query<WorkspaceQuery>,
231    Json(request): Json<RegisterEntityRequest>,
232) -> Result<Json<Value>, StatusCode> {
233    let mut entity = EntityState::new(request.entity_type, request.entity_id, request.data);
234    if let Some(persona_id) = request.persona_id {
235        entity.persona_id = Some(persona_id);
236    }
237
238    state
239        .engine
240        .register_entity(&params.workspace, entity.clone())
241        .await
242        .map_err(|e| {
243            error!("Failed to register entity: {}", e);
244            StatusCode::INTERNAL_SERVER_ERROR
245        })?;
246
247    info!(
248        "Registered entity {}:{} for workspace: {}",
249        entity.entity_type, entity.entity_id, params.workspace
250    );
251    Ok(Json(serde_json::json!({
252        "success": true,
253        "workspace": params.workspace,
254        "entity": entity,
255    })))
256}
257
258/// Get entity by type and ID
259///
260/// GET /api/v1/consistency/entities/{entity_type}/{entity_id}?workspace={workspace_id}
261pub async fn get_entity(
262    State(state): State<ConsistencyState>,
263    Path((entity_type, entity_id)): Path<(String, String)>,
264    Query(params): Query<WorkspaceQuery>,
265) -> Result<Json<EntityState>, StatusCode> {
266    let entity = state
267        .engine
268        .get_entity(&params.workspace, &entity_type, &entity_id)
269        .await
270        .ok_or_else(|| {
271            error!(
272                "Entity not found: {}:{} in workspace: {}",
273                entity_type, entity_id, params.workspace
274            );
275            StatusCode::NOT_FOUND
276        })?;
277
278    Ok(Json(entity))
279}
280
281/// List all entities for a workspace
282///
283/// GET /api/v1/consistency/entities?workspace={workspace_id}
284pub async fn list_entities(
285    State(state): State<ConsistencyState>,
286    Query(params): Query<WorkspaceQuery>,
287) -> Result<Json<Value>, StatusCode> {
288    let unified_state = state.engine.get_state(&params.workspace).await.ok_or_else(|| {
289        error!("State not found for workspace: {}", params.workspace);
290        StatusCode::NOT_FOUND
291    })?;
292
293    let entities: Vec<&EntityState> = unified_state.entity_state.values().collect();
294    Ok(Json(serde_json::json!({
295        "workspace": params.workspace,
296        "entities": entities,
297        "count": entities.len(),
298    })))
299}
300
301/// Activate a chaos rule
302///
303/// POST /api/v1/consistency/chaos/activate?workspace={workspace_id}
304pub async fn activate_chaos_rule(
305    State(state): State<ConsistencyState>,
306    Query(params): Query<WorkspaceQuery>,
307    Json(request): Json<ActivateChaosRuleRequest>,
308) -> Result<Json<Value>, StatusCode> {
309    state
310        .engine
311        .activate_chaos_rule(&params.workspace, request.rule)
312        .await
313        .map_err(|e| {
314            error!("Failed to activate chaos rule: {}", e);
315            StatusCode::INTERNAL_SERVER_ERROR
316        })?;
317
318    info!("Activated chaos rule for workspace: {}", params.workspace);
319    Ok(Json(serde_json::json!({
320        "success": true,
321        "workspace": params.workspace,
322    })))
323}
324
325/// Deactivate a chaos rule
326///
327/// POST /api/v1/consistency/chaos/deactivate?workspace={workspace_id}
328pub async fn deactivate_chaos_rule(
329    State(state): State<ConsistencyState>,
330    Query(params): Query<WorkspaceQuery>,
331    Json(request): Json<DeactivateChaosRuleRequest>,
332) -> Result<Json<Value>, StatusCode> {
333    state
334        .engine
335        .deactivate_chaos_rule(&params.workspace, &request.rule_name)
336        .await
337        .map_err(|e| {
338            error!("Failed to deactivate chaos rule: {}", e);
339            StatusCode::INTERNAL_SERVER_ERROR
340        })?;
341
342    info!("Deactivated chaos rule for workspace: {}", params.workspace);
343    Ok(Json(serde_json::json!({
344        "success": true,
345        "workspace": params.workspace,
346        "rule_name": request.rule_name,
347    })))
348}
349
350/// Set persona lifecycle state
351///
352/// POST /api/v1/consistency/persona/lifecycle?workspace={workspace_id}
353pub async fn set_persona_lifecycle(
354    State(state): State<ConsistencyState>,
355    Query(params): Query<WorkspaceQuery>,
356    Json(request): Json<SetPersonaLifecycleRequest>,
357) -> Result<Json<Value>, StatusCode> {
358    // Parse lifecycle state
359    let lifecycle_state = match request.initial_state.to_lowercase().as_str() {
360        "new" | "new_signup" => LifecycleState::NewSignup,
361        "active" => LifecycleState::Active,
362        "power_user" | "poweruser" => LifecycleState::PowerUser,
363        "churn_risk" | "churnrisk" => LifecycleState::ChurnRisk,
364        "churned" => LifecycleState::Churned,
365        "upgrade_pending" | "upgradepending" => LifecycleState::UpgradePending,
366        "payment_failed" | "paymentfailed" => LifecycleState::PaymentFailed,
367        _ => {
368            error!("Invalid lifecycle state: {}", request.initial_state);
369            return Err(StatusCode::BAD_REQUEST);
370        }
371    };
372
373    // Get unified state to access active persona
374    let unified_state = state.engine.get_state(&params.workspace).await.ok_or_else(|| {
375        error!("State not found for workspace: {}", params.workspace);
376        StatusCode::NOT_FOUND
377    })?;
378
379    // Update persona lifecycle if active persona matches
380    if let Some(ref persona) = unified_state.active_persona {
381        if persona.id == request.persona_id {
382            let mut persona_mut = persona.clone();
383            let lifecycle = PersonaLifecycle::new(request.persona_id.clone(), lifecycle_state);
384            persona_mut.set_lifecycle(lifecycle);
385
386            // Apply lifecycle effects to persona traits
387            if let Some(ref lifecycle) = persona_mut.lifecycle {
388                let effects = lifecycle.apply_lifecycle_effects();
389                for (key, value) in effects {
390                    persona_mut.set_trait(key, value);
391                }
392            }
393
394            // Update the persona in the engine
395            state
396                .engine
397                .set_active_persona(&params.workspace, persona_mut)
398                .await
399                .map_err(|e| {
400                    error!("Failed to set persona lifecycle: {}", e);
401                    StatusCode::INTERNAL_SERVER_ERROR
402                })?;
403
404            info!(
405                "Set lifecycle state {} for persona {} in workspace: {}",
406                request.initial_state, request.persona_id, params.workspace
407            );
408
409            return Ok(Json(serde_json::json!({
410                "success": true,
411                "workspace": params.workspace,
412                "persona_id": request.persona_id,
413                "lifecycle_state": request.initial_state,
414            })));
415        }
416    }
417
418    error!(
419        "Persona {} not found or not active in workspace: {}",
420        request.persona_id, params.workspace
421    );
422    Err(StatusCode::NOT_FOUND)
423}
424
425/// Get user by ID with persona graph enrichment
426///
427/// GET /api/v1/consistency/users/{id}?workspace={workspace_id}
428/// This endpoint uses the persona graph to enrich the user response with related entities.
429pub async fn get_user_with_graph(
430    State(state): State<ConsistencyState>,
431    Path(user_id): Path<String>,
432    Query(params): Query<WorkspaceQuery>,
433) -> Result<Json<Value>, StatusCode> {
434    // Get user entity
435    let mut user_entity = state
436        .engine
437        .get_entity(&params.workspace, "user", &user_id)
438        .await
439        .ok_or_else(|| {
440            error!("User not found: {} in workspace: {}", user_id, params.workspace);
441            StatusCode::NOT_FOUND
442        })?;
443
444    // Enrich response with persona graph data
445    let mut response = user_entity.data.clone();
446    enrich_user_response(&state.engine, &params.workspace, &user_id, &mut response).await;
447
448    Ok(Json(response))
449}
450
451/// Get orders for a user using persona graph
452///
453/// GET /api/v1/consistency/users/{id}/orders?workspace={workspace_id}
454/// This endpoint uses the persona graph to find all orders related to the user.
455pub async fn get_user_orders_with_graph(
456    State(state): State<ConsistencyState>,
457    Path(user_id): Path<String>,
458    Query(params): Query<WorkspaceQuery>,
459) -> Result<Json<Value>, StatusCode> {
460    // Verify user exists
461    state
462        .engine
463        .get_entity(&params.workspace, "user", &user_id)
464        .await
465        .ok_or_else(|| {
466            error!("User not found: {} in workspace: {}", user_id, params.workspace);
467            StatusCode::NOT_FOUND
468        })?;
469
470    // Get orders via persona graph
471    let orders = get_user_orders_via_graph(&state.engine, &params.workspace, &user_id).await;
472
473    // Convert to JSON response
474    let orders_json: Vec<Value> = orders.iter().map(|e| e.data.clone()).collect();
475
476    Ok(Json(serde_json::json!({
477        "user_id": user_id,
478        "orders": orders_json,
479        "count": orders_json.len(),
480    })))
481}
482
483/// Get order by ID with persona graph enrichment
484///
485/// GET /api/v1/consistency/orders/{id}?workspace={workspace_id}
486/// This endpoint uses the persona graph to enrich the order response with related entities.
487pub async fn get_order_with_graph(
488    State(state): State<ConsistencyState>,
489    Path(order_id): Path<String>,
490    Query(params): Query<WorkspaceQuery>,
491) -> Result<Json<Value>, StatusCode> {
492    // Get order entity
493    let mut order_entity = state
494        .engine
495        .get_entity(&params.workspace, "order", &order_id)
496        .await
497        .ok_or_else(|| {
498            error!("Order not found: {} in workspace: {}", order_id, params.workspace);
499            StatusCode::NOT_FOUND
500        })?;
501
502    // Enrich response with persona graph data
503    let mut response = order_entity.data.clone();
504    enrich_order_response(&state.engine, &params.workspace, &order_id, &mut response).await;
505
506    Ok(Json(response))
507}
508
509/// Update persona lifecycle states based on current virtual time
510///
511/// POST /api/v1/consistency/persona/update-lifecycles?workspace={workspace_id}
512/// This endpoint checks all active personas and updates their lifecycle states
513/// based on elapsed time since state entry, using virtual time if time travel is enabled.
514pub async fn update_persona_lifecycles(
515    State(state): State<ConsistencyState>,
516    Query(params): Query<WorkspaceQuery>,
517) -> Result<Json<Value>, StatusCode> {
518    use mockforge_core::time_travel::now as get_virtual_time;
519
520    // Get unified state
521    let mut unified_state = state.engine.get_state(&params.workspace).await.ok_or_else(|| {
522        error!("State not found for workspace: {}", params.workspace);
523        StatusCode::NOT_FOUND
524    })?;
525
526    // Get current time (virtual if time travel is enabled, real otherwise)
527    let current_time = get_virtual_time();
528
529    // Update lifecycle state for active persona if present
530    let mut updated = false;
531    if let Some(ref mut persona) = unified_state.active_persona {
532        let old_state = persona
533            .lifecycle
534            .as_ref()
535            .map(|l| l.current_state)
536            .unwrap_or(mockforge_data::LifecycleState::Active);
537
538        // Update lifecycle state based on elapsed time
539        persona.update_lifecycle_state(current_time);
540
541        let new_state = persona
542            .lifecycle
543            .as_ref()
544            .map(|l| l.current_state)
545            .unwrap_or(mockforge_data::LifecycleState::Active);
546
547        if old_state != new_state {
548            updated = true;
549            info!(
550                "Persona {} lifecycle state updated: {:?} -> {:?}",
551                persona.id, old_state, new_state
552            );
553
554            // Update the persona in the engine
555            state
556                .engine
557                .set_active_persona(&params.workspace, persona.clone())
558                .await
559                .map_err(|e| {
560                    error!("Failed to update persona lifecycle: {}", e);
561                    StatusCode::INTERNAL_SERVER_ERROR
562                })?;
563        }
564    }
565
566    Ok(Json(serde_json::json!({
567        "success": true,
568        "workspace": params.workspace,
569        "updated": updated,
570        "current_time": current_time.to_rfc3339(),
571    })))
572}
573
574/// Create consistency router
575pub fn consistency_router(state: ConsistencyState) -> axum::Router {
576    use axum::routing::{get, post};
577
578    axum::Router::new()
579        // State management
580        .route("/api/v1/consistency/state", get(get_state))
581        // Persona management
582        .route("/api/v1/consistency/persona", post(set_persona))
583        .route("/api/v1/consistency/persona/lifecycle", post(set_persona_lifecycle))
584        .route("/api/v1/consistency/persona/update-lifecycles", post(update_persona_lifecycles))
585        // Scenario management
586        .route("/api/v1/consistency/scenario", post(set_scenario))
587        // Reality level management
588        .route("/api/v1/consistency/reality-level", post(set_reality_level))
589        // Reality ratio management
590        .route("/api/v1/consistency/reality-ratio", post(set_reality_ratio))
591        // Entity management
592        .route("/api/v1/consistency/entities", get(list_entities).post(register_entity))
593        .route(
594            "/api/v1/consistency/entities/{entity_type}/{entity_id}",
595            get(get_entity),
596        )
597        // Persona graph-enabled endpoints
598        .route("/api/v1/consistency/users/{id}", get(get_user_with_graph))
599        .route("/api/v1/consistency/users/{id}/orders", get(get_user_orders_with_graph))
600        .route("/api/v1/consistency/orders/{id}", get(get_order_with_graph))
601        // Chaos rule management
602        .route("/api/v1/consistency/chaos/activate", post(activate_chaos_rule))
603        .route("/api/v1/consistency/chaos/deactivate", post(deactivate_chaos_rule))
604        .with_state(state)
605}