mockforge_http/handlers/
consumer_contracts.rs

1//! Consumer contracts handlers
2//!
3//! This module provides HTTP handlers for managing consumer contracts and usage tracking.
4
5use axum::{
6    extract::{Path, Query, State},
7    http::StatusCode,
8    response::Json,
9};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::sync::Arc;
13
14use mockforge_core::consumer_contracts::{
15    Consumer, ConsumerBreakingChangeDetector, ConsumerIdentifier, ConsumerRegistry, ConsumerType,
16    ConsumerUsage, ConsumerViolation, UsageRecorder,
17};
18
19/// State for consumer contracts handlers
20#[derive(Clone)]
21pub struct ConsumerContractsState {
22    /// Consumer registry
23    pub registry: Arc<ConsumerRegistry>,
24    /// Usage recorder
25    pub usage_recorder: Arc<UsageRecorder>,
26    /// Breaking change detector
27    pub detector: Arc<ConsumerBreakingChangeDetector>,
28}
29
30/// Request to register a consumer
31#[derive(Debug, Deserialize, Serialize)]
32pub struct RegisterConsumerRequest {
33    /// Consumer name
34    pub name: String,
35    /// Consumer type
36    pub consumer_type: String,
37    /// Identifier value
38    pub identifier: String,
39    /// Workspace ID (optional)
40    pub workspace_id: Option<String>,
41    /// Additional metadata
42    pub metadata: Option<HashMap<String, serde_json::Value>>,
43}
44
45/// Response for consumer registration
46#[derive(Debug, Serialize)]
47pub struct ConsumerResponse {
48    /// Consumer ID
49    pub id: String,
50    /// Consumer name
51    pub name: String,
52    /// Consumer type
53    pub consumer_type: String,
54    /// Identifier
55    pub identifier: String,
56    /// Workspace ID
57    pub workspace_id: Option<String>,
58    /// Created at
59    pub created_at: i64,
60}
61
62/// Request to query consumers
63#[derive(Debug, Deserialize)]
64pub struct ListConsumersRequest {
65    /// Filter by workspace ID
66    pub workspace_id: Option<String>,
67    /// Filter by consumer type
68    pub consumer_type: Option<String>,
69    /// Limit results
70    pub limit: Option<usize>,
71    /// Offset for pagination
72    pub offset: Option<usize>,
73}
74
75/// Response for listing consumers
76#[derive(Debug, Serialize)]
77pub struct ListConsumersResponse {
78    /// List of consumers
79    pub consumers: Vec<ConsumerResponse>,
80    /// Total count
81    pub total: usize,
82}
83
84/// Response for consumer usage
85#[derive(Debug, Serialize)]
86pub struct ConsumerUsageResponse {
87    /// Consumer ID
88    pub consumer_id: String,
89    /// Usage data
90    pub usage: Vec<ConsumerUsage>,
91}
92
93/// Response for consumer violations
94#[derive(Debug, Serialize)]
95pub struct ConsumerViolationsResponse {
96    /// Consumer ID
97    pub consumer_id: String,
98    /// Violations
99    pub violations: Vec<ConsumerViolation>,
100}
101
102/// Register a consumer
103///
104/// POST /api/v1/consumers
105pub async fn register_consumer(
106    State(state): State<ConsumerContractsState>,
107    Json(request): Json<RegisterConsumerRequest>,
108) -> Result<Json<ConsumerResponse>, StatusCode> {
109    let consumer_type = match request.consumer_type.as_str() {
110        "workspace" => ConsumerType::Workspace,
111        "custom" => ConsumerType::Custom,
112        "api_key" => ConsumerType::ApiKey,
113        "auth_token" => ConsumerType::AuthToken,
114        _ => return Err(StatusCode::BAD_REQUEST),
115    };
116
117    let identifier = match consumer_type {
118        ConsumerType::Workspace => ConsumerIdentifier::workspace(request.identifier),
119        ConsumerType::Custom => ConsumerIdentifier::custom(request.identifier),
120        ConsumerType::ApiKey => ConsumerIdentifier::api_key(request.identifier),
121        ConsumerType::AuthToken => ConsumerIdentifier::auth_token(request.identifier),
122    };
123
124    let consumer = state
125        .registry
126        .get_or_create(identifier, request.name.clone(), request.workspace_id.clone())
127        .await;
128
129    Ok(Json(ConsumerResponse {
130        id: consumer.id,
131        name: consumer.name,
132        consumer_type: format!("{:?}", consumer.identifier.consumer_type),
133        identifier: consumer.identifier.value,
134        workspace_id: consumer.workspace_id,
135        created_at: consumer.created_at,
136    }))
137}
138
139/// List consumers
140///
141/// GET /api/v1/consumers
142pub async fn list_consumers(
143    State(state): State<ConsumerContractsState>,
144    Query(params): Query<HashMap<String, String>>,
145) -> Result<Json<ListConsumersResponse>, StatusCode> {
146    let mut consumers = state.registry.list_all().await;
147
148    // Apply filters
149    if let Some(workspace_id) = params.get("workspace_id") {
150        consumers.retain(|c| c.workspace_id.as_ref().map(|w| w == workspace_id).unwrap_or(false));
151    }
152
153    if let Some(consumer_type_str) = params.get("consumer_type") {
154        let consumer_type = match consumer_type_str.as_str() {
155            "workspace" => ConsumerType::Workspace,
156            "custom" => ConsumerType::Custom,
157            "api_key" => ConsumerType::ApiKey,
158            "auth_token" => ConsumerType::AuthToken,
159            _ => return Err(StatusCode::BAD_REQUEST),
160        };
161        consumers.retain(|c| c.identifier.consumer_type == consumer_type);
162    }
163
164    let total = consumers.len();
165
166    // Apply pagination
167    let offset = params.get("offset").and_then(|s| s.parse().ok()).unwrap_or(0);
168    let limit = params.get("limit").and_then(|s| s.parse().ok()).unwrap_or(100);
169
170    consumers = consumers.into_iter().skip(offset).take(limit).collect();
171
172    let consumer_responses: Vec<ConsumerResponse> = consumers
173        .into_iter()
174        .map(|c| ConsumerResponse {
175            id: c.id,
176            name: c.name,
177            consumer_type: format!("{:?}", c.identifier.consumer_type),
178            identifier: c.identifier.value,
179            workspace_id: c.workspace_id,
180            created_at: c.created_at,
181        })
182        .collect();
183
184    Ok(Json(ListConsumersResponse {
185        consumers: consumer_responses,
186        total,
187    }))
188}
189
190/// Get a specific consumer
191///
192/// GET /api/v1/consumers/{id}
193pub async fn get_consumer(
194    State(state): State<ConsumerContractsState>,
195    Path(id): Path<String>,
196) -> Result<Json<ConsumerResponse>, StatusCode> {
197    let consumer = state.registry.get_by_id(&id).await.ok_or(StatusCode::NOT_FOUND)?;
198
199    Ok(Json(ConsumerResponse {
200        id: consumer.id,
201        name: consumer.name,
202        consumer_type: format!("{:?}", consumer.identifier.consumer_type),
203        identifier: consumer.identifier.value,
204        workspace_id: consumer.workspace_id,
205        created_at: consumer.created_at,
206    }))
207}
208
209/// Get consumer usage
210///
211/// GET /api/v1/consumers/{id}/usage
212pub async fn get_consumer_usage(
213    State(state): State<ConsumerContractsState>,
214    Path(id): Path<String>,
215) -> Result<Json<ConsumerUsageResponse>, StatusCode> {
216    // Verify consumer exists
217    state.registry.get_by_id(&id).await.ok_or(StatusCode::NOT_FOUND)?;
218
219    let usage = state.usage_recorder.get_usage(&id).await;
220
221    Ok(Json(ConsumerUsageResponse {
222        consumer_id: id,
223        usage,
224    }))
225}
226
227/// Get consumer violations
228///
229/// GET /api/v1/consumers/{id}/violations
230pub async fn get_consumer_violations(
231    State(_state): State<ConsumerContractsState>,
232    Path(_id): Path<String>,
233) -> Result<Json<ConsumerViolationsResponse>, StatusCode> {
234    // In a full implementation, this would query violations from storage
235    // For now, return empty list
236    Ok(Json(ConsumerViolationsResponse {
237        consumer_id: _id,
238        violations: vec![],
239    }))
240}
241
242/// Create consumer contracts router
243pub fn consumer_contracts_router(state: ConsumerContractsState) -> axum::Router {
244    use axum::routing::{get, post};
245
246    axum::Router::new()
247        .route("/api/v1/consumers", post(register_consumer))
248        .route("/api/v1/consumers", get(list_consumers))
249        .route("/api/v1/consumers/{id}", get(get_consumer))
250        .route("/api/v1/consumers/{id}/usage", get(get_consumer_usage))
251        .route("/api/v1/consumers/{id}/violations", get(get_consumer_violations))
252        .with_state(state)
253}