1pub mod ai_suggestions;
12pub mod anomaly;
13pub mod async_jobs;
14pub mod audit;
15pub mod auth;
16pub mod cache;
17pub mod collaborative;
18pub mod config;
19pub mod contract_test;
20pub mod edge_cache;
22pub mod field_selection;
23pub mod gateway;
24pub mod graphql;
25#[cfg(feature = "grpc")]
26pub mod grpc;
27pub mod live_queries;
28pub mod load_test;
29pub mod logging;
30mod metrics;
31pub mod multitenancy;
32pub mod oauth2_provider;
33pub mod observability;
34pub mod openapi;
35pub mod persisted_queries;
36pub mod presence;
37pub mod query_batch;
38pub mod query_cost;
39pub mod rate_limit;
40pub mod rebac;
41pub mod sampling;
42pub mod schema_stitching;
43pub mod security;
44pub mod slo;
45pub mod telemetry;
46pub mod versioning;
47pub mod websocket;
48
49pub mod cqrs;
51pub mod event_replay;
52pub mod event_schema;
53pub mod event_sourcing;
54pub mod event_streaming;
55
56pub mod changelog;
58pub mod mocking;
59pub mod playground;
60pub mod sdk_generator;
61pub mod sdk_notifications;
62pub mod test_utils;
63
64use axum::{
65 Extension, Json, Router,
66 extract::{Path, Query, State},
67 http::StatusCode,
68 middleware,
69 response::{
70 IntoResponse,
71 sse::{Event, KeepAlive, Sse},
72 },
73 routing::{get, post},
74};
75use futures::stream::{self, Stream};
76use legalis_core::Statute;
77use legalis_viz::DecisionTree;
78use serde::{Deserialize, Serialize};
79use std::convert::Infallible;
80use std::sync::Arc;
81use std::time::Duration;
82use thiserror::Error;
83use tokio::sync::RwLock;
84use tower_http::{compression::CompressionLayer, cors::CorsLayer};
85use tracing::info;
86
87#[derive(Debug, Error)]
89pub enum ApiError {
90 #[error("Not found: {0}")]
91 NotFound(String),
92
93 #[error("Invalid request: {0}")]
94 BadRequest(String),
95
96 #[error("Internal error: {0}")]
97 Internal(String),
98
99 #[error("Validation failed: {0}")]
100 ValidationFailed(String),
101
102 #[error(transparent)]
103 Auth(#[from] auth::AuthError),
104}
105
106impl IntoResponse for ApiError {
107 fn into_response(self) -> axum::response::Response {
108 let (status, message) = match self {
109 ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
110 ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
111 ApiError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
112 ApiError::ValidationFailed(msg) => (StatusCode::UNPROCESSABLE_ENTITY, msg),
113 ApiError::Auth(err) => return err.into_response(),
114 };
115
116 let body = Json(ErrorResponse { error: message });
117 (status, body).into_response()
118 }
119}
120
121#[derive(Serialize)]
123struct ErrorResponse {
124 error: String,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct ApiResponse<T> {
130 pub data: T,
131 pub meta: Option<ResponseMeta>,
132}
133
134impl<T> ApiResponse<T> {
135 pub fn new(data: T) -> Self {
136 Self { data, meta: None }
137 }
138
139 pub fn with_meta(mut self, meta: ResponseMeta) -> Self {
140 self.meta = Some(meta);
141 self
142 }
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize, Default)]
147pub struct ResponseMeta {
148 pub total: Option<usize>,
149 pub page: Option<usize>,
150 pub per_page: Option<usize>,
151 pub next_cursor: Option<String>,
152 pub prev_cursor: Option<String>,
153 pub has_more: Option<bool>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct VerificationJobResult {
159 pub passed: bool,
160 pub errors: Vec<String>,
161 pub warnings: Vec<String>,
162 pub statute_count: usize,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct SavedSimulation {
168 pub id: String,
169 pub name: String,
170 pub description: Option<String>,
171 pub statute_ids: Vec<String>,
172 pub population_size: usize,
173 pub deterministic_outcomes: usize,
174 pub discretionary_outcomes: usize,
175 pub void_outcomes: usize,
176 pub deterministic_rate: f64,
177 pub discretionary_rate: f64,
178 pub void_rate: f64,
179 pub created_at: String,
180 pub created_by: String,
181}
182
183pub struct AppState {
185 pub statutes: RwLock<Vec<Statute>>,
187 pub rebac: RwLock<rebac::ReBACEngine>,
189 pub verification_jobs: async_jobs::JobManager<VerificationJobResult>,
191 pub saved_simulations: RwLock<Vec<SavedSimulation>>,
193 pub cache: Arc<cache::CacheStore>,
195 pub ws_broadcaster: websocket::WsBroadcaster,
197 pub audit_log: Arc<audit::AuditLog>,
199 pub api_keys: RwLock<Vec<auth::ApiKey>>,
201 pub collaborative_editor: Arc<collaborative::CollaborativeEditor>,
203 pub presence_manager: Arc<presence::PresenceManager>,
205}
206
207impl AppState {
208 pub fn new() -> Self {
209 Self {
210 statutes: RwLock::new(Vec::new()),
211 rebac: RwLock::new(rebac::ReBACEngine::new()),
212 verification_jobs: async_jobs::JobManager::new(),
213 saved_simulations: RwLock::new(Vec::new()),
214 cache: Arc::new(cache::CacheStore::new()),
215 ws_broadcaster: websocket::WsBroadcaster::new(),
216 audit_log: Arc::new(audit::AuditLog::new()),
217 api_keys: RwLock::new(Vec::new()),
218 collaborative_editor: Arc::new(collaborative::CollaborativeEditor::new()),
219 presence_manager: Arc::new(presence::PresenceManager::new(30)),
220 }
221 }
222}
223
224impl Default for AppState {
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230#[derive(Serialize)]
232pub struct StatuteListResponse {
233 pub statutes: Vec<StatuteSummary>,
234}
235
236#[derive(Serialize)]
238pub struct StatuteSummary {
239 pub id: String,
240 pub title: String,
241 pub has_discretion: bool,
242 pub precondition_count: usize,
243}
244
245#[derive(Deserialize)]
247pub struct StatutePermissionRequest {
248 pub user_id: String,
250 pub permission: String,
252}
253
254#[derive(Serialize)]
256pub struct StatutePermissionsResponse {
257 pub statute_id: String,
258 pub permissions: Vec<StatutePermissionEntry>,
259}
260
261#[derive(Serialize)]
263pub struct StatutePermissionEntry {
264 pub user_id: String,
265 pub permission: String,
266}
267
268impl From<&Statute> for StatuteSummary {
269 fn from(s: &Statute) -> Self {
270 Self {
271 id: s.id.clone(),
272 title: s.title.clone(),
273 has_discretion: s.discretion_logic.is_some(),
274 precondition_count: s.preconditions.len(),
275 }
276 }
277}
278
279#[derive(Deserialize)]
281pub struct CreateStatuteRequest {
282 pub statute: Statute,
283}
284
285#[derive(Deserialize)]
287pub struct VerifyRequest {
288 pub statute_ids: Vec<String>,
289}
290
291#[derive(Serialize)]
293pub struct VerifyResponse {
294 pub passed: bool,
295 pub errors: Vec<String>,
296 pub warnings: Vec<String>,
297}
298
299#[derive(Serialize)]
301pub struct AsyncVerifyStartResponse {
302 pub job_id: String,
303 pub status: String,
304 pub poll_url: String,
305}
306
307#[derive(Serialize)]
309pub struct JobStatusResponse<T> {
310 pub id: String,
311 pub status: String,
312 pub progress: f32,
313 pub result: Option<T>,
314 pub error: Option<String>,
315 pub created_at: String,
316 pub updated_at: String,
317}
318
319#[derive(Serialize)]
321pub struct DetailedVerifyResponse {
322 pub passed: bool,
323 pub total_errors: usize,
324 pub total_warnings: usize,
325 pub total_suggestions: usize,
326 pub errors: Vec<String>,
327 pub warnings: Vec<String>,
328 pub suggestions: Vec<String>,
329 pub statute_count: usize,
330 pub verified_at: String,
331}
332
333#[derive(Deserialize)]
335pub struct BatchVerifyRequest {
336 pub jobs: Vec<VerifyJob>,
338}
339
340#[derive(Deserialize)]
342pub struct VerifyJob {
343 pub job_id: Option<String>,
345 pub statute_ids: Vec<String>,
347}
348
349#[derive(Serialize)]
351pub struct BatchVerifyResponse {
352 pub results: Vec<BatchVerifyResult>,
354 pub total_jobs: usize,
356 pub passed_jobs: usize,
358 pub failed_jobs: usize,
360}
361
362#[derive(Serialize)]
364pub struct BatchVerifyResult {
365 pub job_id: Option<String>,
367 pub passed: bool,
369 pub errors: Vec<String>,
371 pub warnings: Vec<String>,
373 pub statute_count: usize,
375}
376
377#[derive(Serialize)]
379pub struct ComplexityResponse {
380 pub statute_id: String,
381 pub complexity_score: f64,
382 pub precondition_count: usize,
383 pub nesting_depth: usize,
384 pub has_discretion: bool,
385}
386
387#[derive(Deserialize)]
389pub struct StatuteSearchQuery {
390 pub title: Option<String>,
392 pub has_discretion: Option<bool>,
394 pub min_preconditions: Option<usize>,
396 pub max_preconditions: Option<usize>,
398 pub limit: Option<usize>,
400 pub offset: Option<usize>,
402 pub cursor: Option<String>,
404 pub fields: Option<String>,
406}
407
408#[derive(Deserialize)]
410pub struct StatuteComparisonRequest {
411 pub statute_id_a: String,
412 pub statute_id_b: String,
413}
414
415#[derive(Deserialize)]
417pub struct StatuteComparisonMatrixRequest {
418 pub statute_ids: Vec<String>,
420}
421
422#[derive(Serialize)]
424pub struct StatuteComparisonMatrixResponse {
425 pub statutes: Vec<StatuteSummary>,
427 pub similarity_matrix: Vec<Vec<f64>>,
429 pub comparisons: Vec<ComparisonMatrixEntry>,
431}
432
433#[derive(Serialize)]
435pub struct ComparisonMatrixEntry {
436 pub statute_a_id: String,
437 pub statute_b_id: String,
438 pub similarity_score: f64,
439 pub precondition_diff: i32,
440 pub discretion_differs: bool,
441}
442
443#[derive(Deserialize)]
445pub struct CreateApiKeyRequest {
446 pub name: String,
448 pub role: auth::Role,
450 pub scopes: Option<Vec<String>>,
452 pub expires_in_days: Option<i64>,
454}
455
456#[derive(Serialize)]
458pub struct ApiKeyResponse {
459 pub id: String,
460 pub key: Option<String>, pub name: String,
462 pub role: String,
463 pub scopes: Vec<String>,
464 pub created_at: String,
465 pub expires_at: Option<String>,
466 pub active: bool,
467 pub last_used_at: Option<String>,
468}
469
470#[derive(Serialize)]
472pub struct ApiKeyListResponse {
473 pub keys: Vec<ApiKeyResponse>,
474}
475
476#[derive(Serialize)]
478pub struct ApiKeyRotationResponse {
479 pub old_key_id: String,
480 pub new_key: ApiKeyResponse,
481}
482
483#[derive(Serialize)]
485pub struct StatuteComparisonResponse {
486 pub statute_a: StatuteSummary,
487 pub statute_b: StatuteSummary,
488 pub differences: ComparisonDifferences,
489 pub similarity_score: f64,
490}
491
492#[derive(Serialize)]
494pub struct ComparisonDifferences {
495 pub precondition_count_diff: i32,
496 pub nesting_depth_diff: i32,
497 pub both_have_discretion: bool,
498 pub discretion_differs: bool,
499}
500
501#[derive(Deserialize)]
503pub struct BatchCreateStatutesRequest {
504 pub statutes: Vec<Statute>,
505}
506
507#[derive(Serialize)]
509pub struct BatchCreateStatutesResponse {
510 pub created: usize,
511 pub failed: usize,
512 pub errors: Vec<String>,
513}
514
515#[derive(Deserialize)]
517pub struct BatchDeleteStatutesRequest {
518 pub statute_ids: Vec<String>,
519}
520
521#[derive(Serialize)]
523pub struct BatchDeleteStatutesResponse {
524 pub deleted: usize,
525 pub not_found: Vec<String>,
526}
527
528#[derive(Deserialize)]
530pub struct CreateVersionRequest {
531 pub title: Option<String>,
533 pub preconditions: Option<Vec<legalis_core::Condition>>,
534 pub effect: Option<legalis_core::Effect>,
535 pub discretion_logic: Option<String>,
536}
537
538#[derive(Serialize)]
540pub struct StatuteVersionListResponse {
541 pub base_id: String,
542 pub versions: Vec<StatuteVersionInfo>,
543 pub total_versions: usize,
544}
545
546#[derive(Serialize)]
548pub struct StatuteVersionInfo {
549 pub id: String,
550 pub version: u32,
551 pub title: String,
552 pub created_at: Option<String>,
553}
554
555#[derive(Deserialize)]
557pub struct ConflictDetectionRequest {
558 pub statute_ids: Vec<String>,
559}
560
561#[derive(Serialize)]
563pub struct ConflictDetectionResponse {
564 pub conflicts: Vec<ConflictInfo>,
565 pub conflict_count: usize,
566}
567
568#[derive(Serialize)]
570pub struct ConflictInfo {
571 pub statute_a_id: String,
572 pub statute_b_id: String,
573 pub conflict_type: String,
574 pub description: String,
575}
576
577#[derive(Deserialize)]
579pub struct SimulationRequest {
580 pub statute_ids: Vec<String>,
581 pub population_size: usize,
582 pub entity_params: std::collections::HashMap<String, String>,
583}
584
585#[derive(Serialize, Deserialize)]
587pub struct SimulationResponse {
588 pub simulation_id: String,
589 pub total_entities: usize,
590 pub deterministic_outcomes: usize,
591 pub discretionary_outcomes: usize,
592 pub void_outcomes: usize,
593 pub deterministic_rate: f64,
594 pub discretionary_rate: f64,
595 pub void_rate: f64,
596 pub completed_at: String,
597}
598
599#[derive(Deserialize)]
601pub struct SimulationComparisonRequest {
602 pub statute_ids_a: Vec<String>,
603 pub statute_ids_b: Vec<String>,
604 pub population_size: usize,
605}
606
607#[derive(Serialize)]
609pub struct SimulationComparisonResponse {
610 pub scenario_a: SimulationScenarioResult,
611 pub scenario_b: SimulationScenarioResult,
612 pub differences: SimulationDifferences,
613}
614
615#[derive(Deserialize)]
617pub struct SaveSimulationRequest {
618 pub name: String,
619 pub description: Option<String>,
620 pub simulation_result: SimulationResponse,
621}
622
623#[derive(Deserialize)]
625pub struct ComplianceCheckRequest {
626 pub statute_ids: Vec<String>,
627 pub entity_attributes: std::collections::HashMap<String, String>,
628}
629
630#[derive(Serialize)]
632pub struct ComplianceCheckResponse {
633 pub compliant: bool,
634 pub requires_discretion: bool,
635 pub not_applicable: bool,
636 pub applicable_statutes: Vec<String>,
637 pub checked_statute_count: usize,
638}
639
640#[derive(Deserialize)]
642pub struct WhatIfRequest {
643 pub statute_ids: Vec<String>,
644 pub baseline_attributes: std::collections::HashMap<String, String>,
645 pub modified_attributes: std::collections::HashMap<String, String>,
646}
647
648#[derive(Serialize)]
650pub struct WhatIfResponse {
651 pub baseline_compliant: bool,
652 pub modified_compliant: bool,
653 pub impact: String,
654 pub baseline_requires_discretion: bool,
655 pub modified_requires_discretion: bool,
656 pub changed_attribute_count: usize,
657}
658
659#[derive(Deserialize)]
661pub struct ListSavedSimulationsQuery {
662 pub limit: Option<usize>,
663 pub offset: Option<usize>,
664}
665
666#[derive(Debug, Clone, Copy, Deserialize)]
668#[serde(rename_all = "lowercase")]
669#[derive(Default)]
670pub enum VizFormat {
671 Dot,
672 Ascii,
673 Mermaid,
674 PlantUml,
675 #[default]
676 Svg,
677 Html,
678}
679
680#[derive(Deserialize)]
682pub struct VizQuery {
683 #[serde(default)]
685 pub format: VizFormat,
686 pub theme: Option<String>,
688}
689
690#[derive(Serialize)]
692pub struct VisualizationResponse {
693 pub statute_id: String,
694 pub format: String,
695 pub content: String,
696 pub node_count: usize,
697 pub discretionary_count: usize,
698}
699
700#[derive(Serialize)]
702pub struct SimulationScenarioResult {
703 pub name: String,
704 pub deterministic_rate: f64,
705 pub discretionary_rate: f64,
706 pub void_rate: f64,
707}
708
709#[derive(Serialize)]
711pub struct SimulationDifferences {
712 pub deterministic_diff: f64,
713 pub discretionary_diff: f64,
714 pub void_diff: f64,
715 pub significant_change: bool,
716}
717
718async fn get_statute_permissions(
720 user: auth::AuthUser,
721 State(state): State<Arc<AppState>>,
722 Path(statute_id): Path<String>,
723) -> Result<impl IntoResponse, ApiError> {
724 user.require_permission(auth::Permission::ReadStatutes)?;
725
726 let statutes = state.statutes.read().await;
728 if !statutes.iter().any(|s| s.id == statute_id) {
729 return Err(ApiError::NotFound(format!(
730 "Statute not found: {}",
731 statute_id
732 )));
733 }
734 drop(statutes);
735
736 let permissions_list = vec![StatutePermissionEntry {
741 user_id: "system".to_string(),
742 permission: "owner".to_string(),
743 }];
744
745 Ok(Json(ApiResponse::new(StatutePermissionsResponse {
746 statute_id,
747 permissions: permissions_list,
748 })))
749}
750
751async fn grant_statute_permission(
753 user: auth::AuthUser,
754 State(state): State<Arc<AppState>>,
755 Path(statute_id): Path<String>,
756 Json(req): Json<StatutePermissionRequest>,
757) -> Result<impl IntoResponse, ApiError> {
758 user.require_permission(auth::Permission::ManageUsers)?;
759
760 let statutes = state.statutes.read().await;
762 if !statutes.iter().any(|s| s.id == statute_id) {
763 return Err(ApiError::NotFound(format!(
764 "Statute not found: {}",
765 statute_id
766 )));
767 }
768 drop(statutes);
769
770 let target_user_id = uuid::Uuid::parse_str(&req.user_id)
772 .map_err(|_| ApiError::BadRequest("Invalid user ID format".to_string()))?;
773
774 use std::hash::{Hash, Hasher};
776 let mut hasher = std::collections::hash_map::DefaultHasher::new();
777 statute_id.hash(&mut hasher);
778 let hash_value = hasher.finish();
779
780 let resource_uuid = uuid::Uuid::from_u128(hash_value as u128);
782
783 let relation = match req.permission.as_str() {
785 "owner" => rebac::Relation::Owner,
786 "editor" => rebac::Relation::Editor,
787 "viewer" => rebac::Relation::Viewer,
788 _ => {
789 return Err(ApiError::BadRequest(format!(
790 "Invalid permission type: {}. Must be one of: owner, editor, viewer",
791 req.permission
792 )));
793 }
794 };
795
796 let mut rebac = state.rebac.write().await;
797
798 let tuple = rebac::RelationTuple::new(
800 target_user_id,
801 relation,
802 rebac::ResourceType::Statute,
803 resource_uuid,
804 );
805 rebac.add_tuple(tuple);
806
807 metrics::PERMISSION_OPERATIONS
809 .with_label_values(&["grant"])
810 .inc();
811
812 state
814 .audit_log
815 .log_success(
816 audit::AuditEventType::PermissionGranted,
817 user.id.to_string(),
818 user.username.clone(),
819 "grant_statute_permission".to_string(),
820 Some(statute_id.clone()),
821 Some("statute".to_string()),
822 serde_json::json!({
823 "statute_id": statute_id,
824 "granted_to": req.user_id,
825 "permission": req.permission
826 }),
827 )
828 .await;
829
830 Ok((
831 StatusCode::OK,
832 Json(ApiResponse::new(serde_json::json!({
833 "message": "Permission granted successfully",
834 "statute_id": statute_id,
835 "user_id": req.user_id,
836 "permission": req.permission
837 }))),
838 ))
839}
840
841async fn revoke_statute_permission(
843 user: auth::AuthUser,
844 State(state): State<Arc<AppState>>,
845 Path(statute_id): Path<String>,
846 Json(req): Json<StatutePermissionRequest>,
847) -> Result<impl IntoResponse, ApiError> {
848 user.require_permission(auth::Permission::ManageUsers)?;
849
850 let statutes = state.statutes.read().await;
852 if !statutes.iter().any(|s| s.id == statute_id) {
853 return Err(ApiError::NotFound(format!(
854 "Statute not found: {}",
855 statute_id
856 )));
857 }
858 drop(statutes);
859
860 let target_user_id = uuid::Uuid::parse_str(&req.user_id)
862 .map_err(|_| ApiError::BadRequest("Invalid user ID format".to_string()))?;
863
864 use std::hash::{Hash, Hasher};
866 let mut hasher = std::collections::hash_map::DefaultHasher::new();
867 statute_id.hash(&mut hasher);
868 let hash_value = hasher.finish();
869
870 let resource_uuid = uuid::Uuid::from_u128(hash_value as u128);
872
873 let relation = match req.permission.as_str() {
875 "owner" => rebac::Relation::Owner,
876 "editor" => rebac::Relation::Editor,
877 "viewer" => rebac::Relation::Viewer,
878 _ => {
879 return Err(ApiError::BadRequest(format!(
880 "Invalid permission type: {}. Must be one of: owner, editor, viewer",
881 req.permission
882 )));
883 }
884 };
885
886 let mut rebac = state.rebac.write().await;
887
888 let tuple = rebac::RelationTuple::new(
890 target_user_id,
891 relation,
892 rebac::ResourceType::Statute,
893 resource_uuid,
894 );
895 rebac.remove_tuple(&tuple);
896
897 metrics::PERMISSION_OPERATIONS
899 .with_label_values(&["revoke"])
900 .inc();
901
902 state
904 .audit_log
905 .log_success(
906 audit::AuditEventType::PermissionRevoked,
907 user.id.to_string(),
908 user.username.clone(),
909 "revoke_statute_permission".to_string(),
910 Some(statute_id.clone()),
911 Some("statute".to_string()),
912 serde_json::json!({
913 "statute_id": statute_id,
914 "revoked_from": req.user_id,
915 "permission": req.permission
916 }),
917 )
918 .await;
919
920 Ok((
921 StatusCode::OK,
922 Json(ApiResponse::new(serde_json::json!({
923 "message": "Permission revoked successfully",
924 "statute_id": statute_id,
925 "user_id": req.user_id,
926 "permission": req.permission
927 }))),
928 ))
929}
930
931async fn create_api_key(
933 user: auth::AuthUser,
934 State(state): State<Arc<AppState>>,
935 Json(req): Json<CreateApiKeyRequest>,
936) -> Result<impl IntoResponse, ApiError> {
937 user.require_permission(auth::Permission::ManageApiKeys)?;
938
939 let scopes = if let Some(scope_strs) = req.scopes {
941 let mut parsed_scopes = std::collections::HashSet::new();
942 for scope_str in scope_strs {
943 let permission = match scope_str.as_str() {
944 "read_statutes" => auth::Permission::ReadStatutes,
945 "create_statutes" => auth::Permission::CreateStatutes,
946 "update_statutes" => auth::Permission::UpdateStatutes,
947 "delete_statutes" => auth::Permission::DeleteStatutes,
948 "verify_statutes" => auth::Permission::VerifyStatutes,
949 "run_simulations" => auth::Permission::RunSimulations,
950 "view_analytics" => auth::Permission::ViewAnalytics,
951 "manage_users" => auth::Permission::ManageUsers,
952 "manage_api_keys" => auth::Permission::ManageApiKeys,
953 "admin" => auth::Permission::Admin,
954 _ => {
955 return Err(ApiError::BadRequest(format!(
956 "Invalid permission: {}",
957 scope_str
958 )));
959 }
960 };
961 parsed_scopes.insert(permission);
962 }
963 parsed_scopes
964 } else {
965 req.role.permissions()
966 };
967
968 let api_key = if let Some(expires_in_days) = req.expires_in_days {
970 auth::ApiKey::with_expiration(req.name, user.id, req.role, expires_in_days)
971 } else {
972 auth::ApiKey::with_scopes(req.name, user.id, req.role, scopes)
973 };
974
975 let key_id = api_key.id.to_string();
976 let key_value = api_key.key.clone();
977
978 let mut api_keys = state.api_keys.write().await;
980 api_keys.push(api_key.clone());
981 drop(api_keys);
982
983 state
985 .audit_log
986 .log_success(
987 audit::AuditEventType::ApiKeyCreated,
988 user.id.to_string(),
989 user.username.clone(),
990 "create_api_key".to_string(),
991 Some(key_id.clone()),
992 Some("api_key".to_string()),
993 serde_json::json!({
994 "key_id": key_id,
995 "name": api_key.name,
996 "role": format!("{:?}", api_key.role)
997 }),
998 )
999 .await;
1000
1001 let response = ApiKeyResponse {
1002 id: key_id,
1003 key: Some(key_value), name: api_key.name,
1005 role: format!("{:?}", api_key.role),
1006 scopes: api_key.scopes.iter().map(|s| format!("{:?}", s)).collect(),
1007 created_at: chrono::DateTime::from_timestamp(api_key.created_at, 0)
1008 .unwrap_or_default()
1009 .to_rfc3339(),
1010 expires_at: api_key.expires_at.map(|ts| {
1011 chrono::DateTime::from_timestamp(ts, 0)
1012 .unwrap_or_default()
1013 .to_rfc3339()
1014 }),
1015 active: api_key.active,
1016 last_used_at: None,
1017 };
1018
1019 Ok((StatusCode::CREATED, Json(ApiResponse::new(response))))
1020}
1021
1022async fn list_api_keys(
1024 user: auth::AuthUser,
1025 State(state): State<Arc<AppState>>,
1026) -> Result<impl IntoResponse, ApiError> {
1027 user.require_permission(auth::Permission::ManageApiKeys)?;
1028
1029 let api_keys = state.api_keys.read().await;
1030
1031 let keys: Vec<ApiKeyResponse> = api_keys
1032 .iter()
1033 .filter(|key| key.owner_id == user.id || user.has_permission(auth::Permission::Admin))
1034 .map(|key| ApiKeyResponse {
1035 id: key.id.to_string(),
1036 key: None, name: key.name.clone(),
1038 role: format!("{:?}", key.role),
1039 scopes: key.scopes.iter().map(|s| format!("{:?}", s)).collect(),
1040 created_at: chrono::DateTime::from_timestamp(key.created_at, 0)
1041 .unwrap_or_default()
1042 .to_rfc3339(),
1043 expires_at: key.expires_at.map(|ts| {
1044 chrono::DateTime::from_timestamp(ts, 0)
1045 .unwrap_or_default()
1046 .to_rfc3339()
1047 }),
1048 active: key.active,
1049 last_used_at: key.last_used_at.map(|ts| {
1050 chrono::DateTime::from_timestamp(ts, 0)
1051 .unwrap_or_default()
1052 .to_rfc3339()
1053 }),
1054 })
1055 .collect();
1056
1057 Ok(Json(ApiResponse::new(ApiKeyListResponse { keys })))
1058}
1059
1060#[allow(dead_code)]
1062async fn get_api_key(
1063 user: auth::AuthUser,
1064 State(state): State<Arc<AppState>>,
1065 Path(id): Path<String>,
1066) -> Result<impl IntoResponse, ApiError> {
1067 user.require_permission(auth::Permission::ManageApiKeys)?;
1068
1069 let key_id = uuid::Uuid::parse_str(&id)
1070 .map_err(|_| ApiError::BadRequest("Invalid key ID format".to_string()))?;
1071
1072 let api_keys = state.api_keys.read().await;
1073
1074 let key = api_keys
1075 .iter()
1076 .find(|k| {
1077 k.id == key_id
1078 && (k.owner_id == user.id || user.has_permission(auth::Permission::Admin))
1079 })
1080 .ok_or_else(|| ApiError::NotFound("API key not found".to_string()))?;
1081
1082 let response = ApiKeyResponse {
1083 id: key.id.to_string(),
1084 key: None, name: key.name.clone(),
1086 role: format!("{:?}", key.role),
1087 scopes: key.scopes.iter().map(|s| format!("{:?}", s)).collect(),
1088 created_at: chrono::DateTime::from_timestamp(key.created_at, 0)
1089 .unwrap_or_default()
1090 .to_rfc3339(),
1091 expires_at: key.expires_at.map(|ts| {
1092 chrono::DateTime::from_timestamp(ts, 0)
1093 .unwrap_or_default()
1094 .to_rfc3339()
1095 }),
1096 active: key.active,
1097 last_used_at: key.last_used_at.map(|ts| {
1098 chrono::DateTime::from_timestamp(ts, 0)
1099 .unwrap_or_default()
1100 .to_rfc3339()
1101 }),
1102 };
1103
1104 Ok(Json(ApiResponse::new(response)))
1105}
1106
1107async fn revoke_api_key(
1109 user: auth::AuthUser,
1110 State(state): State<Arc<AppState>>,
1111 Path(id): Path<String>,
1112) -> Result<impl IntoResponse, ApiError> {
1113 user.require_permission(auth::Permission::ManageApiKeys)?;
1114
1115 let key_id = uuid::Uuid::parse_str(&id)
1116 .map_err(|_| ApiError::BadRequest("Invalid key ID format".to_string()))?;
1117
1118 let mut api_keys = state.api_keys.write().await;
1119
1120 let key_index = api_keys
1121 .iter()
1122 .position(|k| {
1123 k.id == key_id
1124 && (k.owner_id == user.id || user.has_permission(auth::Permission::Admin))
1125 })
1126 .ok_or_else(|| ApiError::NotFound("API key not found".to_string()))?;
1127
1128 let key = api_keys.remove(key_index);
1129
1130 drop(api_keys);
1131
1132 state
1134 .audit_log
1135 .log_success(
1136 audit::AuditEventType::ApiKeyRevoked,
1137 user.id.to_string(),
1138 user.username.clone(),
1139 "revoke_api_key".to_string(),
1140 Some(key.id.to_string()),
1141 Some("api_key".to_string()),
1142 serde_json::json!({
1143 "key_id": key.id.to_string(),
1144 "name": key.name
1145 }),
1146 )
1147 .await;
1148
1149 Ok((
1150 StatusCode::OK,
1151 Json(ApiResponse::new(serde_json::json!({
1152 "message": "API key revoked successfully",
1153 "key_id": key.id.to_string()
1154 }))),
1155 ))
1156}
1157
1158async fn rotate_api_key(
1160 user: auth::AuthUser,
1161 State(state): State<Arc<AppState>>,
1162 Path(id): Path<String>,
1163) -> Result<impl IntoResponse, ApiError> {
1164 user.require_permission(auth::Permission::ManageApiKeys)?;
1165
1166 let key_id = uuid::Uuid::parse_str(&id)
1167 .map_err(|_| ApiError::BadRequest("Invalid key ID format".to_string()))?;
1168
1169 let mut api_keys = state.api_keys.write().await;
1170
1171 let old_key = api_keys
1172 .iter_mut()
1173 .find(|k| {
1174 k.id == key_id
1175 && (k.owner_id == user.id || user.has_permission(auth::Permission::Admin))
1176 })
1177 .ok_or_else(|| ApiError::NotFound("API key not found".to_string()))?;
1178
1179 let new_key = old_key.rotate();
1181 let new_key_value = new_key.key.clone();
1182
1183 old_key.active = false;
1185
1186 api_keys.push(new_key.clone());
1188 drop(api_keys);
1189
1190 state
1192 .audit_log
1193 .log_success(
1194 audit::AuditEventType::ApiKeyRotated,
1195 user.id.to_string(),
1196 user.username.clone(),
1197 "rotate_api_key".to_string(),
1198 Some(new_key.id.to_string()),
1199 Some("api_key".to_string()),
1200 serde_json::json!({
1201 "old_key_id": key_id.to_string(),
1202 "new_key_id": new_key.id.to_string()
1203 }),
1204 )
1205 .await;
1206
1207 let response = ApiKeyRotationResponse {
1208 old_key_id: key_id.to_string(),
1209 new_key: ApiKeyResponse {
1210 id: new_key.id.to_string(),
1211 key: Some(new_key_value), name: new_key.name,
1213 role: format!("{:?}", new_key.role),
1214 scopes: new_key.scopes.iter().map(|s| format!("{:?}", s)).collect(),
1215 created_at: chrono::DateTime::from_timestamp(new_key.created_at, 0)
1216 .unwrap_or_default()
1217 .to_rfc3339(),
1218 expires_at: new_key.expires_at.map(|ts| {
1219 chrono::DateTime::from_timestamp(ts, 0)
1220 .unwrap_or_default()
1221 .to_rfc3339()
1222 }),
1223 active: new_key.active,
1224 last_used_at: None,
1225 },
1226 };
1227
1228 Ok((StatusCode::OK, Json(ApiResponse::new(response))))
1229}
1230
1231async fn query_audit_logs(
1233 user: auth::AuthUser,
1234 State(state): State<Arc<AppState>>,
1235 Query(filter): Query<audit::AuditQueryFilter>,
1236) -> Result<impl IntoResponse, ApiError> {
1237 user.require_permission(auth::Permission::Admin)?;
1239
1240 let entries = state.audit_log.query(filter.clone()).await;
1241 let total = state.audit_log.count_filtered(filter).await;
1242
1243 let meta = ResponseMeta {
1244 total: Some(total),
1245 ..Default::default()
1246 };
1247
1248 Ok(Json(ApiResponse::new(entries).with_meta(meta)))
1249}
1250
1251async fn audit_stats(
1253 user: auth::AuthUser,
1254 State(state): State<Arc<AppState>>,
1255) -> Result<impl IntoResponse, ApiError> {
1256 user.require_permission(auth::Permission::Admin)?;
1258
1259 let total_count = state.audit_log.count().await;
1260
1261 let statute_created = state
1263 .audit_log
1264 .count_filtered(audit::AuditQueryFilter {
1265 event_type: Some(audit::AuditEventType::StatuteCreated),
1266 ..Default::default()
1267 })
1268 .await;
1269
1270 let statute_deleted = state
1271 .audit_log
1272 .count_filtered(audit::AuditQueryFilter {
1273 event_type: Some(audit::AuditEventType::StatuteDeleted),
1274 ..Default::default()
1275 })
1276 .await;
1277
1278 let simulations_saved = state
1279 .audit_log
1280 .count_filtered(audit::AuditQueryFilter {
1281 event_type: Some(audit::AuditEventType::SimulationSaved),
1282 ..Default::default()
1283 })
1284 .await;
1285
1286 let stats = serde_json::json!({
1287 "total_audit_entries": total_count,
1288 "by_event_type": {
1289 "statute_created": statute_created,
1290 "statute_deleted": statute_deleted,
1291 "simulations_saved": simulations_saved
1292 }
1293 });
1294
1295 Ok(Json(ApiResponse::new(stats)))
1296}
1297
1298async fn graphql_handler(
1300 schema: axum::extract::Extension<graphql::LegalisSchema>,
1301 req: async_graphql_axum::GraphQLRequest,
1302) -> async_graphql_axum::GraphQLResponse {
1303 schema.execute(req.into_inner()).await.into()
1304}
1305
1306async fn graphql_playground() -> impl IntoResponse {
1308 axum::response::Html(async_graphql::http::playground_source(
1309 async_graphql::http::GraphQLPlaygroundConfig::new("/graphql"),
1310 ))
1311}
1312
1313pub fn create_router(state: Arc<AppState>) -> Router {
1315 metrics::init();
1317
1318 let graphql_state = graphql::GraphQLState::with_broadcaster(state.ws_broadcaster.clone());
1320 let graphql_schema = graphql::create_schema(graphql_state);
1321
1322 Router::new()
1323 .route("/health", get(health_check))
1324 .route("/health/ready", get(readiness_check))
1325 .route("/metrics", get(metrics_endpoint))
1326 .route("/api/v1/statutes", get(list_statutes).post(create_statute))
1327 .route("/api/v1/statutes/search", get(search_statutes))
1328 .route("/api/v1/statutes/suggest", post(suggest_statutes))
1329 .route("/api/v1/statutes/batch", post(batch_create_statutes))
1330 .route("/api/v1/statutes/batch/delete", post(batch_delete_statutes))
1331 .route("/api/v1/statutes/compare", post(compare_statutes))
1332 .route(
1333 "/api/v1/statutes/compare/matrix",
1334 post(compare_statutes_matrix),
1335 )
1336 .route(
1337 "/api/v1/statutes/{id}",
1338 get(get_statute).delete(delete_statute),
1339 )
1340 .route("/api/v1/statutes/{id}/complexity", get(analyze_complexity))
1341 .route("/api/v1/statutes/{id}/versions", get(get_statute_versions))
1342 .route(
1343 "/api/v1/statutes/{id}/versions/new",
1344 post(create_statute_version),
1345 )
1346 .route("/api/v1/verify", post(verify_statutes))
1347 .route("/api/v1/verify/detailed", post(verify_statutes_detailed))
1348 .route("/api/v1/verify/conflicts", post(detect_conflicts))
1349 .route("/api/v1/verify/batch", post(verify_batch))
1350 .route("/api/v1/verify/bulk/stream", post(verify_bulk_stream))
1351 .route("/api/v1/verify/async", post(verify_statutes_async))
1352 .route(
1353 "/api/v1/verify/async/{job_id}",
1354 get(get_verification_job_status),
1355 )
1356 .route("/api/v1/simulate", post(run_simulation))
1357 .route("/api/v1/simulate/stream", post(stream_simulation))
1358 .route("/api/v1/simulate/compare", post(compare_simulations))
1359 .route("/api/v1/simulate/compliance", post(check_compliance))
1360 .route("/api/v1/simulate/whatif", post(whatif_analysis))
1361 .route(
1362 "/api/v1/simulate/saved",
1363 get(list_saved_simulations).post(save_simulation),
1364 )
1365 .route(
1366 "/api/v1/simulate/saved/{id}",
1367 get(get_saved_simulation).delete(delete_saved_simulation),
1368 )
1369 .route("/api/v1/visualize/{id}", get(visualize_statute))
1370 .route("/api-docs/openapi.json", get(openapi_spec))
1371 .route("/api-docs", get(swagger_ui))
1372 .route("/graphql", post(graphql_handler))
1373 .route("/graphql/playground", get(graphql_playground))
1374 .route("/ws", get(websocket::ws_handler))
1375 .route("/api/v1/audit", get(query_audit_logs))
1376 .route("/api/v1/audit/stats", get(audit_stats))
1377 .route(
1378 "/api/v1/statutes/{id}/permissions",
1379 get(get_statute_permissions)
1380 .post(grant_statute_permission)
1381 .delete(revoke_statute_permission),
1382 )
1383 .route("/api/v1/api-keys", get(list_api_keys).post(create_api_key))
1384 .route(
1385 "/api/v1/api-keys/{id}",
1386 get(get_api_key).delete(revoke_api_key),
1387 )
1388 .route("/api/v1/api-keys/{id}/rotate", post(rotate_api_key))
1389 .layer(Extension(graphql_schema))
1390 .layer(middleware::from_fn(logging::log_request))
1391 .layer(CompressionLayer::new())
1392 .layer(CorsLayer::permissive())
1393 .with_state(state)
1394}
1395
1396async fn openapi_spec() -> impl IntoResponse {
1398 Json(openapi::generate_spec())
1399}
1400
1401async fn swagger_ui() -> impl IntoResponse {
1403 axum::response::Html(openapi::generate_swagger_ui_html())
1404}
1405
1406async fn health_check() -> impl IntoResponse {
1408 Json(serde_json::json!({
1409 "status": "healthy",
1410 "service": "legalis-api",
1411 "version": env!("CARGO_PKG_VERSION"),
1412 "timestamp": chrono::Utc::now().to_rfc3339()
1413 }))
1414}
1415
1416async fn readiness_check(
1418 State(state): State<Arc<AppState>>,
1419) -> Result<impl IntoResponse, ApiError> {
1420 let statutes_available = state.statutes.try_read().is_ok();
1422 let rebac_available = state.rebac.try_read().is_ok();
1423
1424 let is_ready = statutes_available && rebac_available;
1425
1426 let response = serde_json::json!({
1427 "status": if is_ready { "ready" } else { "not_ready" },
1428 "service": "legalis-api",
1429 "version": env!("CARGO_PKG_VERSION"),
1430 "timestamp": chrono::Utc::now().to_rfc3339(),
1431 "checks": {
1432 "statutes_store": if statutes_available { "ok" } else { "unavailable" },
1433 "rebac_engine": if rebac_available { "ok" } else { "unavailable" }
1434 }
1435 });
1436
1437 if is_ready {
1438 Ok(Json(response))
1439 } else {
1440 Err(ApiError::Internal("Service not ready".to_string()))
1441 }
1442}
1443
1444async fn metrics_endpoint() -> Result<String, ApiError> {
1446 metrics::encode().map_err(|e| ApiError::Internal(format!("Failed to encode metrics: {}", e)))
1447}
1448
1449async fn list_statutes(
1451 user: auth::AuthUser,
1452 State(state): State<Arc<AppState>>,
1453) -> Result<impl IntoResponse, ApiError> {
1454 user.require_permission(auth::Permission::ReadStatutes)?;
1455
1456 let statutes = state.statutes.read().await;
1457 let summaries: Vec<StatuteSummary> = statutes.iter().map(StatuteSummary::from).collect();
1458
1459 Ok(Json(ApiResponse::new(StatuteListResponse {
1460 statutes: summaries,
1461 })))
1462}
1463
1464async fn search_statutes(
1466 user: auth::AuthUser,
1467 State(state): State<Arc<AppState>>,
1468 Query(query): Query<StatuteSearchQuery>,
1469) -> Result<impl IntoResponse, ApiError> {
1470 user.require_permission(auth::Permission::ReadStatutes)?;
1471
1472 let _field_query = field_selection::FieldsQuery {
1474 fields: query.fields.clone(),
1475 };
1476
1477 let statutes = state.statutes.read().await;
1478
1479 let mut filtered: Vec<&Statute> = statutes.iter().collect();
1480
1481 if let Some(ref title_query) = query.title {
1483 let title_lower = title_query.to_lowercase();
1484 filtered.retain(|s| s.title.to_lowercase().contains(&title_lower));
1485 }
1486
1487 if let Some(has_discretion) = query.has_discretion {
1489 filtered.retain(|s| s.discretion_logic.is_some() == has_discretion);
1490 }
1491
1492 if let Some(min) = query.min_preconditions {
1494 filtered.retain(|s| s.preconditions.len() >= min);
1495 }
1496
1497 if let Some(max) = query.max_preconditions {
1499 filtered.retain(|s| s.preconditions.len() <= max);
1500 }
1501
1502 let total = filtered.len();
1503
1504 let (paginated, meta) = if let Some(cursor) = query.cursor {
1506 let limit = query.limit.unwrap_or(100).min(1000);
1508
1509 let cursor_decoded = base64_decode(&cursor)
1511 .map_err(|_| ApiError::BadRequest("Invalid cursor".to_string()))?;
1512
1513 let cursor_parts: Vec<&str> = cursor_decoded.split(':').collect();
1514 if cursor_parts.len() != 2 {
1515 return Err(ApiError::BadRequest("Invalid cursor format".to_string()));
1516 }
1517
1518 let cursor_id = cursor_parts[0];
1519 let cursor_version: u32 = cursor_parts[1]
1520 .parse()
1521 .map_err(|_| ApiError::BadRequest("Invalid cursor version".to_string()))?;
1522
1523 let cursor_pos = filtered
1525 .iter()
1526 .position(|s| s.id == cursor_id && s.version == cursor_version);
1527
1528 let start_pos = cursor_pos.map(|p| p + 1).unwrap_or(0);
1529
1530 let results: Vec<StatuteSummary> = filtered
1531 .iter()
1532 .skip(start_pos)
1533 .take(limit + 1) .map(|s| StatuteSummary::from(*s))
1535 .collect();
1536
1537 let has_more = results.len() > limit;
1538 let mut final_results = results;
1539 if has_more {
1540 final_results.pop(); }
1542
1543 let next_cursor = if has_more && !final_results.is_empty() {
1545 let last = &final_results[final_results.len() - 1];
1546 Some(base64_encode(&format!("{}:{}", last.id, 1))) } else {
1548 None
1549 };
1550
1551 let meta = ResponseMeta {
1552 total: Some(total),
1553 next_cursor,
1554 has_more: Some(has_more),
1555 ..Default::default()
1556 };
1557
1558 (final_results, meta)
1559 } else {
1560 let offset = query.offset.unwrap_or(0);
1562 let limit = query.limit.unwrap_or(100).min(1000);
1563
1564 let paginated = filtered
1565 .into_iter()
1566 .skip(offset)
1567 .take(limit)
1568 .map(StatuteSummary::from)
1569 .collect();
1570
1571 let meta = ResponseMeta {
1572 total: Some(total),
1573 page: Some(offset / limit),
1574 per_page: Some(limit),
1575 ..Default::default()
1576 };
1577
1578 (paginated, meta)
1579 };
1580
1581 Ok(Json(
1582 ApiResponse::new(StatuteListResponse {
1583 statutes: paginated,
1584 })
1585 .with_meta(meta),
1586 ))
1587}
1588
1589async fn suggest_statutes(
1591 user: auth::AuthUser,
1592 State(state): State<Arc<AppState>>,
1593 Json(request): Json<ai_suggestions::SuggestionRequest>,
1594) -> Result<impl IntoResponse, ApiError> {
1595 user.require_permission(auth::Permission::ReadStatutes)?;
1596
1597 let statutes = state.statutes.read().await;
1599 let statute_vec: Vec<_> = statutes.iter().cloned().collect();
1600
1601 let engine = ai_suggestions::SuggestionEngine::new();
1603
1604 let response = engine
1606 .suggest(request, &statute_vec)
1607 .await
1608 .map_err(|e| ApiError::Internal(format!("Suggestion failed: {}", e)))?;
1609
1610 Ok(Json(ApiResponse::new(response)))
1611}
1612
1613fn base64_encode(s: &str) -> String {
1615 use base64::{Engine as _, engine::general_purpose};
1616 general_purpose::STANDARD.encode(s)
1617}
1618
1619fn base64_decode(s: &str) -> Result<String, base64::DecodeError> {
1621 use base64::{Engine as _, engine::general_purpose};
1622 let bytes = general_purpose::STANDARD.decode(s)?;
1623 Ok(String::from_utf8_lossy(&bytes).to_string())
1624}
1625
1626async fn get_statute(
1628 user: auth::AuthUser,
1629 State(state): State<Arc<AppState>>,
1630 Path(id): Path<String>,
1631) -> Result<impl IntoResponse, ApiError> {
1632 user.require_permission(auth::Permission::ReadStatutes)?;
1633
1634 let statutes = state.statutes.read().await;
1635 let statute = statutes
1636 .iter()
1637 .find(|s| s.id == id)
1638 .ok_or_else(|| ApiError::NotFound(format!("Statute not found: {}", id)))?;
1639
1640 Ok(Json(ApiResponse::new(statute.clone())))
1641}
1642
1643async fn create_statute(
1645 user: auth::AuthUser,
1646 State(state): State<Arc<AppState>>,
1647 Json(req): Json<CreateStatuteRequest>,
1648) -> Result<impl IntoResponse, ApiError> {
1649 user.require_permission(auth::Permission::CreateStatutes)?;
1650
1651 let mut statutes = state.statutes.write().await;
1652
1653 if statutes.iter().any(|s| s.id == req.statute.id) {
1655 return Err(ApiError::BadRequest(format!(
1656 "Statute with ID '{}' already exists",
1657 req.statute.id
1658 )));
1659 }
1660
1661 info!(
1662 "Creating statute: {} by user {}",
1663 req.statute.id, user.username
1664 );
1665
1666 let statute_id = req.statute.id.clone();
1667 let statute_title = req.statute.title.clone();
1668 statutes.push(req.statute.clone());
1669
1670 metrics::STATUTE_OPERATIONS
1672 .with_label_values(&["create"])
1673 .inc();
1674 metrics::STATUTES_TOTAL.inc();
1675
1676 state
1678 .audit_log
1679 .log_success(
1680 audit::AuditEventType::StatuteCreated,
1681 user.id.to_string(),
1682 user.username.clone(),
1683 "create_statute".to_string(),
1684 Some(statute_id.clone()),
1685 Some("statute".to_string()),
1686 serde_json::json!({
1687 "statute_id": statute_id,
1688 "title": statute_title
1689 }),
1690 )
1691 .await;
1692
1693 state
1695 .ws_broadcaster
1696 .broadcast(websocket::WsNotification::StatuteCreated {
1697 statute_id: statute_id.clone(),
1698 title: statute_title,
1699 created_by: user.username.clone(),
1700 });
1701
1702 Ok((StatusCode::CREATED, Json(ApiResponse::new(req.statute))))
1703}
1704
1705async fn compare_statutes_matrix(
1707 user: auth::AuthUser,
1708 State(state): State<Arc<AppState>>,
1709 Json(req): Json<StatuteComparisonMatrixRequest>,
1710) -> Result<impl IntoResponse, ApiError> {
1711 user.require_permission(auth::Permission::ReadStatutes)?;
1712
1713 if req.statute_ids.len() < 2 {
1714 return Err(ApiError::BadRequest(
1715 "At least 2 statutes required for comparison matrix".to_string(),
1716 ));
1717 }
1718
1719 if req.statute_ids.len() > 20 {
1720 return Err(ApiError::BadRequest(
1721 "Maximum 20 statutes allowed for comparison matrix".to_string(),
1722 ));
1723 }
1724
1725 let statutes = state.statutes.read().await;
1726
1727 let mut statute_list = Vec::new();
1729 for id in &req.statute_ids {
1730 if let Some(statute) = statutes.iter().find(|s| &s.id == id) {
1731 statute_list.push(statute.clone());
1732 } else {
1733 return Err(ApiError::NotFound(format!("Statute not found: {}", id)));
1734 }
1735 }
1736
1737 let count = statute_list.len();
1738
1739 let mut similarity_matrix = vec![vec![0.0; count]; count];
1741 let mut comparisons = Vec::new();
1742
1743 for i in 0..count {
1744 for j in i..count {
1745 if i == j {
1746 similarity_matrix[i][j] = 100.0;
1748 } else {
1749 let stat_a = &statute_list[i];
1751 let stat_b = &statute_list[j];
1752
1753 let precond_count_a = stat_a.preconditions.len() as i32;
1754 let precond_count_b = stat_b.preconditions.len() as i32;
1755 let precondition_diff = precond_count_b - precond_count_a;
1756
1757 let depth_a = calculate_nesting_depth(&stat_a.preconditions) as i32;
1758 let depth_b = calculate_nesting_depth(&stat_b.preconditions) as i32;
1759 let depth_diff = depth_b - depth_a;
1760
1761 let discretion_a = stat_a.discretion_logic.is_some();
1762 let discretion_b = stat_b.discretion_logic.is_some();
1763 let discretion_differs = discretion_a != discretion_b;
1764
1765 let mut similarity = 100.0;
1767 similarity -= (precondition_diff.abs() as f64) * 5.0;
1768 similarity -= (depth_diff.abs() as f64) * 10.0;
1769 if discretion_differs {
1770 similarity -= 20.0;
1771 }
1772 similarity = similarity.clamp(0.0, 100.0);
1773
1774 similarity_matrix[i][j] = similarity;
1776 similarity_matrix[j][i] = similarity;
1777
1778 comparisons.push(ComparisonMatrixEntry {
1779 statute_a_id: stat_a.id.clone(),
1780 statute_b_id: stat_b.id.clone(),
1781 similarity_score: similarity,
1782 precondition_diff,
1783 discretion_differs,
1784 });
1785 }
1786 }
1787 }
1788
1789 let summaries: Vec<StatuteSummary> = statute_list.iter().map(StatuteSummary::from).collect();
1790
1791 Ok(Json(ApiResponse::new(StatuteComparisonMatrixResponse {
1792 statutes: summaries,
1793 similarity_matrix,
1794 comparisons,
1795 })))
1796}
1797
1798async fn compare_statutes(
1800 user: auth::AuthUser,
1801 State(state): State<Arc<AppState>>,
1802 Json(req): Json<StatuteComparisonRequest>,
1803) -> Result<impl IntoResponse, ApiError> {
1804 user.require_permission(auth::Permission::ReadStatutes)?;
1805
1806 let statutes = state.statutes.read().await;
1807
1808 let statute_a = statutes
1809 .iter()
1810 .find(|s| s.id == req.statute_id_a)
1811 .ok_or_else(|| ApiError::NotFound(format!("Statute not found: {}", req.statute_id_a)))?;
1812
1813 let statute_b = statutes
1814 .iter()
1815 .find(|s| s.id == req.statute_id_b)
1816 .ok_or_else(|| ApiError::NotFound(format!("Statute not found: {}", req.statute_id_b)))?;
1817
1818 let summary_a = StatuteSummary::from(statute_a);
1819 let summary_b = StatuteSummary::from(statute_b);
1820
1821 let precondition_count_a = statute_a.preconditions.len() as i32;
1822 let precondition_count_b = statute_b.preconditions.len() as i32;
1823
1824 let nesting_depth_a = calculate_nesting_depth(&statute_a.preconditions) as i32;
1825 let nesting_depth_b = calculate_nesting_depth(&statute_b.preconditions) as i32;
1826
1827 let has_discretion_a = statute_a.discretion_logic.is_some();
1828 let has_discretion_b = statute_b.discretion_logic.is_some();
1829
1830 let differences = ComparisonDifferences {
1831 precondition_count_diff: precondition_count_b - precondition_count_a,
1832 nesting_depth_diff: nesting_depth_b - nesting_depth_a,
1833 both_have_discretion: has_discretion_a && has_discretion_b,
1834 discretion_differs: has_discretion_a != has_discretion_b,
1835 };
1836
1837 let mut similarity_score = 100.0;
1839
1840 let precond_diff = (precondition_count_b - precondition_count_a).abs() as f64;
1842 similarity_score -= precond_diff * 5.0;
1843
1844 let depth_diff = (nesting_depth_b - nesting_depth_a).abs() as f64;
1846 similarity_score -= depth_diff * 10.0;
1847
1848 if differences.discretion_differs {
1850 similarity_score -= 20.0;
1851 }
1852
1853 similarity_score = similarity_score.clamp(0.0, 100.0);
1854
1855 Ok(Json(ApiResponse::new(StatuteComparisonResponse {
1856 statute_a: summary_a,
1857 statute_b: summary_b,
1858 differences,
1859 similarity_score,
1860 })))
1861}
1862
1863async fn batch_create_statutes(
1865 user: auth::AuthUser,
1866 State(state): State<Arc<AppState>>,
1867 Json(req): Json<BatchCreateStatutesRequest>,
1868) -> Result<impl IntoResponse, ApiError> {
1869 user.require_permission(auth::Permission::CreateStatutes)?;
1870
1871 if req.statutes.is_empty() {
1872 return Err(ApiError::BadRequest("No statutes provided".to_string()));
1873 }
1874
1875 let mut statutes = state.statutes.write().await;
1876 let mut created = 0;
1877 let mut failed = 0;
1878 let mut errors = Vec::new();
1879 let total_requested = req.statutes.len();
1880
1881 for statute in req.statutes {
1882 if statutes.iter().any(|s| s.id == statute.id) {
1884 errors.push(format!("Statute with ID '{}' already exists", statute.id));
1885 failed += 1;
1886 continue;
1887 }
1888
1889 info!(
1890 "Creating statute: {} by user {} (batch)",
1891 statute.id, user.username
1892 );
1893 statutes.push(statute);
1894 created += 1;
1895 }
1896
1897 state
1899 .audit_log
1900 .log_success(
1901 audit::AuditEventType::BatchStatutesCreated,
1902 user.id.to_string(),
1903 user.username.clone(),
1904 "batch_create_statutes".to_string(),
1905 None,
1906 Some("statute".to_string()),
1907 serde_json::json!({
1908 "created": created,
1909 "failed": failed,
1910 "total": total_requested
1911 }),
1912 )
1913 .await;
1914
1915 Ok((
1916 if created > 0 {
1917 StatusCode::CREATED
1918 } else {
1919 StatusCode::BAD_REQUEST
1920 },
1921 Json(ApiResponse::new(BatchCreateStatutesResponse {
1922 created,
1923 failed,
1924 errors,
1925 })),
1926 ))
1927}
1928
1929async fn batch_delete_statutes(
1931 user: auth::AuthUser,
1932 State(state): State<Arc<AppState>>,
1933 Json(req): Json<BatchDeleteStatutesRequest>,
1934) -> Result<impl IntoResponse, ApiError> {
1935 user.require_permission(auth::Permission::DeleteStatutes)?;
1936
1937 if req.statute_ids.is_empty() {
1938 return Err(ApiError::BadRequest("No statute IDs provided".to_string()));
1939 }
1940
1941 let mut statutes = state.statutes.write().await;
1942 let mut deleted = 0;
1943 let mut not_found = Vec::new();
1944 let total_requested = req.statute_ids.len();
1945
1946 for id in req.statute_ids {
1947 let initial_len = statutes.len();
1948 statutes.retain(|s| s.id != id);
1949
1950 if statutes.len() < initial_len {
1951 info!("Deleted statute: {} by user {} (batch)", id, user.username);
1952 deleted += 1;
1953 } else {
1954 not_found.push(id);
1955 }
1956 }
1957
1958 state
1960 .audit_log
1961 .log_success(
1962 audit::AuditEventType::BatchStatutesDeleted,
1963 user.id.to_string(),
1964 user.username.clone(),
1965 "batch_delete_statutes".to_string(),
1966 None,
1967 Some("statute".to_string()),
1968 serde_json::json!({
1969 "deleted": deleted,
1970 "not_found": not_found.len(),
1971 "total": total_requested
1972 }),
1973 )
1974 .await;
1975
1976 Ok(Json(ApiResponse::new(BatchDeleteStatutesResponse {
1977 deleted,
1978 not_found,
1979 })))
1980}
1981
1982async fn delete_statute(
1984 user: auth::AuthUser,
1985 State(state): State<Arc<AppState>>,
1986 Path(id): Path<String>,
1987) -> Result<impl IntoResponse, ApiError> {
1988 user.require_permission(auth::Permission::DeleteStatutes)?;
1989
1990 let mut statutes = state.statutes.write().await;
1991 let initial_len = statutes.len();
1992 statutes.retain(|s| s.id != id);
1993
1994 if statutes.len() == initial_len {
1995 return Err(ApiError::NotFound(format!("Statute not found: {}", id)));
1996 }
1997
1998 info!("Deleted statute: {} by user {}", id, user.username);
1999
2000 metrics::STATUTE_OPERATIONS
2002 .with_label_values(&["delete"])
2003 .inc();
2004 metrics::STATUTES_TOTAL.dec();
2005
2006 state
2008 .audit_log
2009 .log_success(
2010 audit::AuditEventType::StatuteDeleted,
2011 user.id.to_string(),
2012 user.username.clone(),
2013 "delete_statute".to_string(),
2014 Some(id.clone()),
2015 Some("statute".to_string()),
2016 serde_json::json!({
2017 "statute_id": id
2018 }),
2019 )
2020 .await;
2021
2022 state
2024 .ws_broadcaster
2025 .broadcast(websocket::WsNotification::StatuteDeleted {
2026 statute_id: id.clone(),
2027 deleted_by: user.username.clone(),
2028 });
2029
2030 Ok(StatusCode::NO_CONTENT)
2031}
2032
2033async fn verify_statutes(
2035 user: auth::AuthUser,
2036 State(state): State<Arc<AppState>>,
2037 Json(req): Json<VerifyRequest>,
2038) -> Result<impl IntoResponse, ApiError> {
2039 user.require_permission(auth::Permission::VerifyStatutes)?;
2040
2041 let statutes = state.statutes.read().await;
2042
2043 let to_verify: Vec<&Statute> = if req.statute_ids.is_empty() {
2044 statutes.iter().collect()
2045 } else {
2046 statutes
2047 .iter()
2048 .filter(|s| req.statute_ids.contains(&s.id))
2049 .collect()
2050 };
2051
2052 if to_verify.is_empty() {
2053 return Err(ApiError::BadRequest("No statutes to verify".to_string()));
2054 }
2055
2056 let verifier = legalis_verifier::StatuteVerifier::new();
2057 let to_verify_owned: Vec<Statute> = to_verify.into_iter().cloned().collect();
2058 let result = verifier.verify(&to_verify_owned);
2059
2060 metrics::VERIFICATIONS_TOTAL.inc();
2062 metrics::VERIFICATION_RESULTS
2063 .with_label_values(&[if result.passed { "passed" } else { "failed" }])
2064 .inc();
2065
2066 Ok(Json(ApiResponse::new(VerifyResponse {
2067 passed: result.passed,
2068 errors: result.errors.iter().map(|e| e.to_string()).collect(),
2069 warnings: result.warnings.clone(),
2070 })))
2071}
2072
2073async fn verify_statutes_detailed(
2075 user: auth::AuthUser,
2076 State(state): State<Arc<AppState>>,
2077 Json(req): Json<VerifyRequest>,
2078) -> Result<impl IntoResponse, ApiError> {
2079 user.require_permission(auth::Permission::VerifyStatutes)?;
2080
2081 let statutes = state.statutes.read().await;
2082
2083 let to_verify: Vec<&Statute> = if req.statute_ids.is_empty() {
2084 statutes.iter().collect()
2085 } else {
2086 statutes
2087 .iter()
2088 .filter(|s| req.statute_ids.contains(&s.id))
2089 .collect()
2090 };
2091
2092 if to_verify.is_empty() {
2093 return Err(ApiError::BadRequest("No statutes to verify".to_string()));
2094 }
2095
2096 let verifier = legalis_verifier::StatuteVerifier::new();
2097 let to_verify_owned: Vec<Statute> = to_verify.into_iter().cloned().collect();
2098 let result = verifier.verify(&to_verify_owned);
2099
2100 let errors: Vec<String> = result.errors.iter().map(|e| e.to_string()).collect();
2101 let warnings = result.warnings.clone();
2102 let suggestions = result.suggestions.clone();
2103
2104 Ok(Json(ApiResponse::new(DetailedVerifyResponse {
2105 passed: result.passed,
2106 total_errors: errors.len(),
2107 total_warnings: warnings.len(),
2108 total_suggestions: suggestions.len(),
2109 errors,
2110 warnings,
2111 suggestions,
2112 statute_count: to_verify_owned.len(),
2113 verified_at: chrono::Utc::now().to_rfc3339(),
2114 })))
2115}
2116
2117async fn detect_conflicts(
2119 user: auth::AuthUser,
2120 State(state): State<Arc<AppState>>,
2121 Json(req): Json<ConflictDetectionRequest>,
2122) -> Result<impl IntoResponse, ApiError> {
2123 user.require_permission(auth::Permission::VerifyStatutes)?;
2124
2125 let statutes = state.statutes.read().await;
2126
2127 let to_check: Vec<&Statute> = if req.statute_ids.is_empty() {
2128 statutes.iter().collect()
2129 } else {
2130 statutes
2131 .iter()
2132 .filter(|s| req.statute_ids.contains(&s.id))
2133 .collect()
2134 };
2135
2136 if to_check.len() < 2 {
2137 return Err(ApiError::BadRequest(
2138 "At least 2 statutes required for conflict detection".to_string(),
2139 ));
2140 }
2141
2142 let verifier = legalis_verifier::StatuteVerifier::new();
2143 let to_check_owned: Vec<Statute> = to_check.into_iter().cloned().collect();
2144 let result = verifier.verify(&to_check_owned);
2145
2146 let mut conflicts = Vec::new();
2147
2148 for error in result.errors.iter() {
2150 let error_str = error.to_string();
2151 if error_str.contains("conflict") || error_str.contains("contradiction") {
2152 conflicts.push(ConflictInfo {
2155 statute_a_id: "statute-a".to_string(), statute_b_id: "statute-b".to_string(), conflict_type: "logical-contradiction".to_string(),
2158 description: error_str,
2159 });
2160 }
2161 }
2162
2163 Ok(Json(ApiResponse::new(ConflictDetectionResponse {
2164 conflict_count: conflicts.len(),
2165 conflicts,
2166 })))
2167}
2168
2169async fn verify_statutes_async(
2172 user: auth::AuthUser,
2173 State(state): State<Arc<AppState>>,
2174 Json(req): Json<VerifyRequest>,
2175) -> Result<impl IntoResponse, ApiError> {
2176 user.require_permission(auth::Permission::VerifyStatutes)?;
2177
2178 let job_id = state.verification_jobs.create_job().await;
2180
2181 let state_clone = Arc::clone(&state);
2183 let statute_ids = req.statute_ids.clone();
2184 let job_id_clone = job_id.clone();
2185
2186 tokio::spawn(async move {
2188 let job_id = job_id_clone;
2189 state_clone
2191 .verification_jobs
2192 .update_job(&job_id, |job| {
2193 job.set_running();
2194 })
2195 .await;
2196
2197 let statutes = state_clone.statutes.read().await;
2199
2200 let to_verify: Vec<&Statute> = if statute_ids.is_empty() {
2201 statutes.iter().collect()
2202 } else {
2203 statutes
2204 .iter()
2205 .filter(|s| statute_ids.contains(&s.id))
2206 .collect()
2207 };
2208
2209 if to_verify.is_empty() {
2210 state_clone
2211 .verification_jobs
2212 .update_job(&job_id, |job| {
2213 job.fail("No statutes to verify".to_string());
2214 })
2215 .await;
2216 return;
2217 }
2218
2219 state_clone
2221 .verification_jobs
2222 .update_job(&job_id, |job| {
2223 job.set_progress(30.0);
2224 })
2225 .await;
2226
2227 let verifier = legalis_verifier::StatuteVerifier::new();
2229 let to_verify_owned: Vec<Statute> = to_verify.into_iter().cloned().collect();
2230 let statute_count = to_verify_owned.len();
2231
2232 state_clone
2233 .verification_jobs
2234 .update_job(&job_id, |job| {
2235 job.set_progress(60.0);
2236 })
2237 .await;
2238
2239 let result = verifier.verify(&to_verify_owned);
2240
2241 state_clone
2242 .verification_jobs
2243 .update_job(&job_id, |job| {
2244 job.set_progress(90.0);
2245 })
2246 .await;
2247
2248 let job_result = VerificationJobResult {
2250 passed: result.passed,
2251 errors: result.errors.iter().map(|e| e.to_string()).collect(),
2252 warnings: result.warnings,
2253 statute_count,
2254 };
2255
2256 let passed = job_result.passed;
2257 let errors_count = job_result.errors.len();
2258 let warnings_count = job_result.warnings.len();
2259
2260 state_clone
2261 .verification_jobs
2262 .update_job(&job_id, |job| {
2263 job.complete(job_result);
2264 })
2265 .await;
2266
2267 state_clone
2269 .ws_broadcaster
2270 .broadcast(websocket::WsNotification::VerificationCompleted {
2271 job_id: job_id.clone(),
2272 passed,
2273 errors_count,
2274 warnings_count,
2275 });
2276 });
2277
2278 let poll_url = format!("/api/v1/verify/async/{}", job_id);
2279
2280 Ok((
2281 StatusCode::ACCEPTED,
2282 Json(ApiResponse::new(AsyncVerifyStartResponse {
2283 job_id,
2284 status: "pending".to_string(),
2285 poll_url,
2286 })),
2287 ))
2288}
2289
2290async fn get_verification_job_status(
2292 user: auth::AuthUser,
2293 State(state): State<Arc<AppState>>,
2294 Path(job_id): Path<String>,
2295) -> Result<impl IntoResponse, ApiError> {
2296 user.require_permission(auth::Permission::VerifyStatutes)?;
2297
2298 let job = state
2299 .verification_jobs
2300 .get_job(&job_id)
2301 .await
2302 .ok_or_else(|| ApiError::NotFound(format!("Job not found: {}", job_id)))?;
2303
2304 let status_str = match job.status {
2305 async_jobs::JobStatus::Pending => "pending",
2306 async_jobs::JobStatus::Running => "running",
2307 async_jobs::JobStatus::Completed => "completed",
2308 async_jobs::JobStatus::Failed => "failed",
2309 }
2310 .to_string();
2311
2312 Ok(Json(ApiResponse::new(JobStatusResponse {
2313 id: job.id,
2314 status: status_str,
2315 progress: job.progress,
2316 result: job.result,
2317 error: job.error,
2318 created_at: job.created_at.to_rfc3339(),
2319 updated_at: job.updated_at.to_rfc3339(),
2320 })))
2321}
2322
2323async fn verify_bulk_stream(
2326 user: auth::AuthUser,
2327 State(state): State<Arc<AppState>>,
2328 Json(req): Json<BatchVerifyRequest>,
2329) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, ApiError> {
2330 user.require_permission(auth::Permission::VerifyStatutes)?;
2331
2332 if req.jobs.is_empty() {
2333 return Err(ApiError::BadRequest(
2334 "No verification jobs provided".to_string(),
2335 ));
2336 }
2337
2338 let statutes = state.statutes.read().await.clone();
2340
2341 let stream = stream::unfold(
2343 (req.jobs, statutes, 0usize),
2344 |(mut jobs, statutes, processed)| async move {
2345 if processed == 0 {
2346 let event = Event::default()
2348 .event("start")
2349 .json_data(serde_json::json!({
2350 "total_jobs": jobs.len(),
2351 "status": "started"
2352 }))
2353 .ok()?;
2354 return Some((Ok::<_, Infallible>(event), (jobs, statutes, processed)));
2355 }
2356
2357 if jobs.is_empty() {
2358 let event = Event::default()
2360 .event("complete")
2361 .json_data(serde_json::json!({
2362 "status": "completed",
2363 "total_processed": processed
2364 }))
2365 .ok()?;
2366 return Some((Ok::<_, Infallible>(event), (jobs, statutes, processed)));
2367 }
2368
2369 let job = jobs.remove(0);
2371 let verifier = legalis_verifier::StatuteVerifier::new();
2372
2373 let to_verify: Vec<&Statute> = if job.statute_ids.is_empty() {
2374 statutes.iter().collect()
2375 } else {
2376 statutes
2377 .iter()
2378 .filter(|s| job.statute_ids.contains(&s.id))
2379 .collect()
2380 };
2381
2382 let to_verify_owned: Vec<Statute> = to_verify.into_iter().cloned().collect();
2383 let statute_count = to_verify_owned.len();
2384
2385 let result = if statute_count == 0 {
2386 BatchVerifyResult {
2387 job_id: job.job_id.clone(),
2388 passed: false,
2389 errors: vec!["No statutes found for verification".to_string()],
2390 warnings: vec![],
2391 statute_count: 0,
2392 }
2393 } else {
2394 let verify_result = verifier.verify(&to_verify_owned);
2395 BatchVerifyResult {
2396 job_id: job.job_id,
2397 passed: verify_result.passed,
2398 errors: verify_result.errors.iter().map(|e| e.to_string()).collect(),
2399 warnings: verify_result.warnings.clone(),
2400 statute_count,
2401 }
2402 };
2403
2404 let processed_count = processed + 1;
2405 let event = Event::default()
2406 .event("result")
2407 .json_data(serde_json::json!({
2408 "job_index": processed_count,
2409 "total_jobs": processed_count + jobs.len(),
2410 "result": result,
2411 "progress": (processed_count as f64 / (processed_count + jobs.len()) as f64) * 100.0
2412 }))
2413 .ok()?;
2414
2415 Some((
2416 Ok::<_, Infallible>(event),
2417 (jobs, statutes, processed_count),
2418 ))
2419 },
2420 );
2421
2422 Ok(Sse::new(stream).keep_alive(KeepAlive::default()))
2423}
2424
2425async fn verify_batch(
2428 user: auth::AuthUser,
2429 State(state): State<Arc<AppState>>,
2430 Json(req): Json<BatchVerifyRequest>,
2431) -> Result<impl IntoResponse, ApiError> {
2432 user.require_permission(auth::Permission::VerifyStatutes)?;
2433
2434 if req.jobs.is_empty() {
2435 return Err(ApiError::BadRequest(
2436 "No verification jobs provided".to_string(),
2437 ));
2438 }
2439
2440 let statutes = state.statutes.read().await;
2441 let verifier = legalis_verifier::StatuteVerifier::new();
2442
2443 let mut results = Vec::new();
2445 let total_jobs = req.jobs.len();
2446 for job in req.jobs {
2447 let to_verify: Vec<&Statute> = if job.statute_ids.is_empty() {
2448 statutes.iter().collect()
2449 } else {
2450 statutes
2451 .iter()
2452 .filter(|s| job.statute_ids.contains(&s.id))
2453 .collect()
2454 };
2455
2456 let to_verify_owned: Vec<Statute> = to_verify.into_iter().cloned().collect();
2457 let statute_count = to_verify_owned.len();
2458
2459 if statute_count == 0 {
2461 results.push(BatchVerifyResult {
2462 job_id: job.job_id.clone(),
2463 passed: false,
2464 errors: vec!["No statutes found for verification".to_string()],
2465 warnings: vec![],
2466 statute_count: 0,
2467 });
2468 continue;
2469 }
2470
2471 let result = verifier.verify(&to_verify_owned);
2472
2473 results.push(BatchVerifyResult {
2474 job_id: job.job_id,
2475 passed: result.passed,
2476 errors: result.errors.iter().map(|e| e.to_string()).collect(),
2477 warnings: result.warnings.clone(),
2478 statute_count,
2479 });
2480 }
2481
2482 let passed_jobs = results.iter().filter(|r| r.passed).count();
2483 let failed_jobs = results.len() - passed_jobs;
2484
2485 Ok(Json(ApiResponse::new(BatchVerifyResponse {
2486 results,
2487 total_jobs,
2488 passed_jobs,
2489 failed_jobs,
2490 })))
2491}
2492
2493async fn analyze_complexity(
2495 user: auth::AuthUser,
2496 State(state): State<Arc<AppState>>,
2497 Path(id): Path<String>,
2498) -> Result<impl IntoResponse, ApiError> {
2499 user.require_permission(auth::Permission::ReadStatutes)?;
2500
2501 let statutes = state.statutes.read().await;
2502 let statute = statutes
2503 .iter()
2504 .find(|s| s.id == id)
2505 .ok_or_else(|| ApiError::NotFound(format!("Statute not found: {}", id)))?;
2506
2507 let precondition_count = statute.preconditions.len();
2509 let nesting_depth = calculate_nesting_depth(&statute.preconditions);
2510 let has_discretion = statute.discretion_logic.is_some();
2511
2512 let complexity_score = (precondition_count as f64 * 1.5)
2514 + (nesting_depth as f64 * 2.0)
2515 + if has_discretion { 5.0 } else { 0.0 };
2516
2517 Ok(Json(ApiResponse::new(ComplexityResponse {
2518 statute_id: id,
2519 complexity_score,
2520 precondition_count,
2521 nesting_depth,
2522 has_discretion,
2523 })))
2524}
2525
2526async fn get_statute_versions(
2529 user: auth::AuthUser,
2530 State(state): State<Arc<AppState>>,
2531 Path(base_id): Path<String>,
2532) -> Result<impl IntoResponse, ApiError> {
2533 user.require_permission(auth::Permission::ReadStatutes)?;
2534
2535 let statutes = state.statutes.read().await;
2536
2537 let versions: Vec<StatuteVersionInfo> = statutes
2540 .iter()
2541 .filter(|s| s.id == base_id || s.id.starts_with(&format!("{}-v", base_id)))
2542 .map(|s| StatuteVersionInfo {
2543 id: s.id.clone(),
2544 version: s.version,
2545 title: s.title.clone(),
2546 created_at: None, })
2548 .collect();
2549
2550 if versions.is_empty() {
2551 return Err(ApiError::NotFound(format!(
2552 "No statutes found with base ID: {}",
2553 base_id
2554 )));
2555 }
2556
2557 let total_versions = versions.len();
2558
2559 Ok(Json(ApiResponse::new(StatuteVersionListResponse {
2560 base_id,
2561 versions,
2562 total_versions,
2563 })))
2564}
2565
2566async fn create_statute_version(
2568 user: auth::AuthUser,
2569 State(state): State<Arc<AppState>>,
2570 Path(id): Path<String>,
2571 Json(req): Json<CreateVersionRequest>,
2572) -> Result<impl IntoResponse, ApiError> {
2573 user.require_permission(auth::Permission::CreateStatutes)?;
2574
2575 let mut statutes = state.statutes.write().await;
2576
2577 let original = statutes
2579 .iter()
2580 .find(|s| s.id == id)
2581 .ok_or_else(|| ApiError::NotFound(format!("Statute not found: {}", id)))?
2582 .clone();
2583
2584 let base_id = if original.id.contains("-v") {
2586 original.id.split("-v").next().unwrap_or(&original.id)
2587 } else {
2588 &original.id
2589 };
2590
2591 let max_version = statutes
2592 .iter()
2593 .filter(|s| s.id == base_id || s.id.starts_with(&format!("{}-v", base_id)))
2594 .map(|s| s.version)
2595 .max()
2596 .unwrap_or(original.version);
2597
2598 let new_version = max_version + 1;
2599 let new_id = format!("{}-v{}", base_id, new_version);
2600
2601 if statutes.iter().any(|s| s.id == new_id) {
2603 return Err(ApiError::BadRequest(format!(
2604 "Statute version already exists: {}",
2605 new_id
2606 )));
2607 }
2608
2609 let mut new_statute = original.clone();
2611 new_statute.id = new_id.clone();
2612 new_statute.version = new_version;
2613
2614 if let Some(title) = req.title {
2615 new_statute.title = title;
2616 }
2617
2618 if let Some(preconditions) = req.preconditions {
2619 new_statute.preconditions = preconditions;
2620 }
2621
2622 if let Some(effect) = req.effect {
2623 new_statute.effect = effect;
2624 }
2625
2626 if let Some(discretion) = req.discretion_logic {
2627 new_statute.discretion_logic = Some(discretion);
2628 }
2629
2630 info!(
2631 "Creating statute version: {} (v{}) by user {}",
2632 new_id, new_version, user.username
2633 );
2634 statutes.push(new_statute.clone());
2635
2636 state
2638 .audit_log
2639 .log_success(
2640 audit::AuditEventType::StatuteVersionCreated,
2641 user.id.to_string(),
2642 user.username.clone(),
2643 "create_statute_version".to_string(),
2644 Some(new_id.clone()),
2645 Some("statute".to_string()),
2646 serde_json::json!({
2647 "statute_id": new_id,
2648 "version": new_version,
2649 "base_id": base_id
2650 }),
2651 )
2652 .await;
2653
2654 Ok((StatusCode::CREATED, Json(ApiResponse::new(new_statute))))
2655}
2656
2657fn calculate_nesting_depth(conditions: &[legalis_core::Condition]) -> usize {
2659 use legalis_core::Condition;
2660
2661 fn depth_of_condition(cond: &Condition) -> usize {
2662 match cond {
2663 Condition::And(left, right) | Condition::Or(left, right) => {
2664 1 + depth_of_condition(left).max(depth_of_condition(right))
2665 }
2666 Condition::Not(inner) => 1 + depth_of_condition(inner),
2667 _ => 0,
2668 }
2669 }
2670
2671 conditions.iter().map(depth_of_condition).max().unwrap_or(0)
2672}
2673
2674async fn run_simulation(
2676 user: auth::AuthUser,
2677 State(state): State<Arc<AppState>>,
2678 Json(req): Json<SimulationRequest>,
2679) -> Result<impl IntoResponse, ApiError> {
2680 user.require_permission(auth::Permission::VerifyStatutes)?;
2681
2682 if req.population_size == 0 {
2683 return Err(ApiError::BadRequest(
2684 "Population size must be greater than 0".to_string(),
2685 ));
2686 }
2687
2688 if req.population_size > 10000 {
2689 return Err(ApiError::BadRequest(
2690 "Population size cannot exceed 10000".to_string(),
2691 ));
2692 }
2693
2694 let statutes = state.statutes.read().await;
2695
2696 let to_simulate: Vec<Statute> = if req.statute_ids.is_empty() {
2697 statutes.clone()
2698 } else {
2699 statutes
2700 .iter()
2701 .filter(|s| req.statute_ids.contains(&s.id))
2702 .cloned()
2703 .collect()
2704 };
2705
2706 if to_simulate.is_empty() {
2707 return Err(ApiError::BadRequest("No statutes to simulate".to_string()));
2708 }
2709
2710 use legalis_core::{LegalEntity, TypedEntity};
2712 let mut population: Vec<Box<dyn LegalEntity>> = Vec::new();
2713 for i in 0..req.population_size {
2714 let mut entity = TypedEntity::new();
2715
2716 entity.set_u32("age", 18 + (i % 50) as u32);
2718
2719 entity.set_u64("income", 20000 + ((i * 1000) % 80000) as u64);
2721
2722 for (key, value) in &req.entity_params {
2724 entity.set_string(key, value);
2725 }
2726
2727 population.push(Box::new(entity));
2728 }
2729
2730 use legalis_sim::SimEngine;
2732 let engine = SimEngine::new(to_simulate.clone(), population);
2733 let sim_metrics = engine.run_simulation().await;
2734
2735 metrics::SIMULATIONS_TOTAL.inc();
2737 metrics::SIMULATION_OUTCOMES
2738 .with_label_values(&["deterministic"])
2739 .inc_by(sim_metrics.deterministic_count as u64);
2740 metrics::SIMULATION_OUTCOMES
2741 .with_label_values(&["discretionary"])
2742 .inc_by(sim_metrics.discretion_count as u64);
2743 metrics::SIMULATION_OUTCOMES
2744 .with_label_values(&["void"])
2745 .inc_by(sim_metrics.void_count as u64);
2746
2747 let total = sim_metrics.total_applications as f64;
2748 let deterministic_rate = if total > 0.0 {
2749 (sim_metrics.deterministic_count as f64 / total) * 100.0
2750 } else {
2751 0.0
2752 };
2753 let discretionary_rate = if total > 0.0 {
2754 (sim_metrics.discretion_count as f64 / total) * 100.0
2755 } else {
2756 0.0
2757 };
2758 let void_rate = if total > 0.0 {
2759 (sim_metrics.void_count as f64 / total) * 100.0
2760 } else {
2761 0.0
2762 };
2763
2764 Ok(Json(ApiResponse::new(SimulationResponse {
2765 simulation_id: uuid::Uuid::new_v4().to_string(),
2766 total_entities: req.population_size,
2767 deterministic_outcomes: sim_metrics.deterministic_count,
2768 discretionary_outcomes: sim_metrics.discretion_count,
2769 void_outcomes: sim_metrics.void_count,
2770 deterministic_rate,
2771 discretionary_rate,
2772 void_rate,
2773 completed_at: chrono::Utc::now().to_rfc3339(),
2774 })))
2775}
2776
2777async fn stream_simulation(
2779 user: auth::AuthUser,
2780 State(state): State<Arc<AppState>>,
2781 Json(req): Json<SimulationRequest>,
2782) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, ApiError> {
2783 user.require_permission(auth::Permission::VerifyStatutes)?;
2784
2785 if req.population_size == 0 {
2786 return Err(ApiError::BadRequest(
2787 "Population size must be greater than 0".to_string(),
2788 ));
2789 }
2790
2791 if req.population_size > 10000 {
2792 return Err(ApiError::BadRequest(
2793 "Population size cannot exceed 10000".to_string(),
2794 ));
2795 }
2796
2797 let statutes = state.statutes.read().await;
2798
2799 let to_simulate: Vec<Statute> = if req.statute_ids.is_empty() {
2800 statutes.clone()
2801 } else {
2802 statutes
2803 .iter()
2804 .filter(|s| req.statute_ids.contains(&s.id))
2805 .cloned()
2806 .collect()
2807 };
2808
2809 if to_simulate.is_empty() {
2810 return Err(ApiError::BadRequest("No statutes to simulate".to_string()));
2811 }
2812
2813 drop(statutes); use legalis_core::{LegalEntity, TypedEntity};
2817 let mut population: Vec<Box<dyn LegalEntity>> = Vec::new();
2818 for i in 0..req.population_size {
2819 let mut entity = TypedEntity::new();
2820 entity.set_u32("age", 18 + (i % 50) as u32);
2821 entity.set_u64("income", 20000 + ((i * 1000) % 80000) as u64);
2822
2823 for (key, value) in &req.entity_params {
2824 entity.set_string(key, value);
2825 }
2826
2827 population.push(Box::new(entity));
2828 }
2829
2830 let simulation_id = uuid::Uuid::new_v4().to_string();
2831 let total_entities = req.population_size;
2832
2833 let stream = stream::unfold(
2835 (
2836 to_simulate,
2837 population,
2838 0usize,
2839 simulation_id.clone(),
2840 total_entities,
2841 ),
2842 |(statutes, population, progress, sim_id, total_entities)| async move {
2843 if progress == 0 {
2844 let event = Event::default()
2846 .event("start")
2847 .json_data(serde_json::json!({
2848 "simulation_id": sim_id,
2849 "total_entities": population.len(),
2850 "status": "started"
2851 }))
2852 .ok()?;
2853 return Some((
2854 Ok::<_, Infallible>(event),
2855 (statutes, population, 10, sim_id, total_entities),
2856 ));
2857 }
2858
2859 if progress < 100 {
2860 tokio::time::sleep(Duration::from_millis(100)).await;
2862 let event = Event::default()
2863 .event("progress")
2864 .json_data(serde_json::json!({
2865 "simulation_id": sim_id,
2866 "progress": progress,
2867 "status": "running"
2868 }))
2869 .ok()?;
2870 return Some((
2871 Ok::<_, Infallible>(event),
2872 (statutes, population, progress + 10, sim_id, total_entities),
2873 ));
2874 }
2875
2876 if progress == 100 {
2877 use legalis_sim::SimEngine;
2879 let engine = SimEngine::new(statutes.clone(), population);
2880 let metrics = engine.run_simulation().await;
2881
2882 let total = metrics.total_applications as f64;
2883 let deterministic_rate = if total > 0.0 {
2884 (metrics.deterministic_count as f64 / total) * 100.0
2885 } else {
2886 0.0
2887 };
2888 let discretionary_rate = if total > 0.0 {
2889 (metrics.discretion_count as f64 / total) * 100.0
2890 } else {
2891 0.0
2892 };
2893 let void_rate = if total > 0.0 {
2894 (metrics.void_count as f64 / total) * 100.0
2895 } else {
2896 0.0
2897 };
2898
2899 let event = Event::default()
2901 .event("complete")
2902 .json_data(serde_json::json!({
2903 "simulation_id": sim_id,
2904 "status": "completed",
2905 "total_entities": total_entities,
2906 "deterministic_outcomes": metrics.deterministic_count,
2907 "discretionary_outcomes": metrics.discretion_count,
2908 "void_outcomes": metrics.void_count,
2909 "deterministic_rate": deterministic_rate,
2910 "discretionary_rate": discretionary_rate,
2911 "void_rate": void_rate,
2912 "completed_at": chrono::Utc::now().to_rfc3339()
2913 }))
2914 .ok()?;
2915 return Some((
2916 Ok::<_, Infallible>(event),
2917 (statutes, vec![], 101, sim_id, total_entities),
2918 ));
2919 }
2920
2921 None
2922 },
2923 );
2924
2925 Ok(Sse::new(stream).keep_alive(KeepAlive::default()))
2926}
2927
2928async fn compare_simulations(
2930 user: auth::AuthUser,
2931 State(state): State<Arc<AppState>>,
2932 Json(req): Json<SimulationComparisonRequest>,
2933) -> Result<impl IntoResponse, ApiError> {
2934 user.require_permission(auth::Permission::VerifyStatutes)?;
2935
2936 if req.population_size == 0 || req.population_size > 10000 {
2937 return Err(ApiError::BadRequest(
2938 "Population size must be between 1 and 10000".to_string(),
2939 ));
2940 }
2941
2942 let statutes = state.statutes.read().await;
2943
2944 let statutes_a: Vec<Statute> = statutes
2945 .iter()
2946 .filter(|s| req.statute_ids_a.contains(&s.id))
2947 .cloned()
2948 .collect();
2949
2950 let statutes_b: Vec<Statute> = statutes
2951 .iter()
2952 .filter(|s| req.statute_ids_b.contains(&s.id))
2953 .cloned()
2954 .collect();
2955
2956 if statutes_a.is_empty() || statutes_b.is_empty() {
2957 return Err(ApiError::BadRequest(
2958 "Both scenarios must have at least one statute".to_string(),
2959 ));
2960 }
2961
2962 fn create_population(size: usize) -> Vec<Box<dyn legalis_core::LegalEntity>> {
2964 use legalis_core::TypedEntity;
2965 let mut population: Vec<Box<dyn legalis_core::LegalEntity>> = Vec::new();
2966 for i in 0..size {
2967 let mut entity = TypedEntity::new();
2968 entity.set_u32("age", 18 + (i % 50) as u32);
2969 entity.set_u64("income", 20000 + ((i * 1000) % 80000) as u64);
2970 population.push(Box::new(entity));
2971 }
2972 population
2973 }
2974
2975 use legalis_sim::SimEngine;
2977 let population_a = create_population(req.population_size);
2978 let engine_a = SimEngine::new(statutes_a, population_a);
2979 let metrics_a = engine_a.run_simulation().await;
2980
2981 let population_b = create_population(req.population_size);
2983 let engine_b = SimEngine::new(statutes_b, population_b);
2984 let metrics_b = engine_b.run_simulation().await;
2985
2986 let total = req.population_size as f64;
2987
2988 let det_rate_a = (metrics_a.deterministic_count as f64 / total) * 100.0;
2989 let disc_rate_a = (metrics_a.discretion_count as f64 / total) * 100.0;
2990 let void_rate_a = (metrics_a.void_count as f64 / total) * 100.0;
2991
2992 let det_rate_b = (metrics_b.deterministic_count as f64 / total) * 100.0;
2993 let disc_rate_b = (metrics_b.discretion_count as f64 / total) * 100.0;
2994 let void_rate_b = (metrics_b.void_count as f64 / total) * 100.0;
2995
2996 let det_diff = det_rate_b - det_rate_a;
2997 let disc_diff = disc_rate_b - disc_rate_a;
2998 let void_diff = void_rate_b - void_rate_a;
2999
3000 let significant_change =
3002 det_diff.abs() > 10.0 || disc_diff.abs() > 10.0 || void_diff.abs() > 10.0;
3003
3004 Ok(Json(ApiResponse::new(SimulationComparisonResponse {
3005 scenario_a: SimulationScenarioResult {
3006 name: "Scenario A".to_string(),
3007 deterministic_rate: det_rate_a,
3008 discretionary_rate: disc_rate_a,
3009 void_rate: void_rate_a,
3010 },
3011 scenario_b: SimulationScenarioResult {
3012 name: "Scenario B".to_string(),
3013 deterministic_rate: det_rate_b,
3014 discretionary_rate: disc_rate_b,
3015 void_rate: void_rate_b,
3016 },
3017 differences: SimulationDifferences {
3018 deterministic_diff: det_diff,
3019 discretionary_diff: disc_diff,
3020 void_diff,
3021 significant_change,
3022 },
3023 })))
3024}
3025
3026async fn check_compliance(
3028 user: auth::AuthUser,
3029 State(state): State<Arc<AppState>>,
3030 Json(req): Json<ComplianceCheckRequest>,
3031) -> Result<impl IntoResponse, ApiError> {
3032 user.require_permission(auth::Permission::VerifyStatutes)?;
3033
3034 let statutes = state.statutes.read().await;
3035
3036 let to_check: Vec<Statute> = if req.statute_ids.is_empty() {
3037 statutes.clone()
3038 } else {
3039 statutes
3040 .iter()
3041 .filter(|s| req.statute_ids.contains(&s.id))
3042 .cloned()
3043 .collect()
3044 };
3045
3046 if to_check.is_empty() {
3047 return Err(ApiError::BadRequest("No statutes to check".to_string()));
3048 }
3049
3050 drop(statutes);
3051
3052 use legalis_core::TypedEntity;
3054 let mut entity = TypedEntity::new();
3055 for (key, value) in &req.entity_attributes {
3056 if let Ok(num) = value.parse::<u32>() {
3058 entity.set_u32(key, num);
3059 } else if let Ok(num) = value.parse::<u64>() {
3060 entity.set_u64(key, num);
3061 } else {
3062 entity.set_string(key, value);
3063 }
3064 }
3065
3066 use legalis_sim::SimEngine;
3068 let population: Vec<Box<dyn legalis_core::LegalEntity>> = vec![Box::new(entity)];
3069 let engine = SimEngine::new(to_check.clone(), population);
3070 let metrics = engine.run_simulation().await;
3071
3072 let compliant = metrics.deterministic_count > 0;
3073 let requires_discretion = metrics.discretion_count > 0;
3074 let not_applicable = metrics.void_count > 0;
3075
3076 let applicable_statutes: Vec<String> = to_check.iter().map(|s| s.id.clone()).collect();
3078
3079 Ok(Json(ApiResponse::new(ComplianceCheckResponse {
3080 compliant,
3081 requires_discretion,
3082 not_applicable,
3083 applicable_statutes,
3084 checked_statute_count: to_check.len(),
3085 })))
3086}
3087
3088async fn whatif_analysis(
3090 user: auth::AuthUser,
3091 State(state): State<Arc<AppState>>,
3092 Json(req): Json<WhatIfRequest>,
3093) -> Result<impl IntoResponse, ApiError> {
3094 user.require_permission(auth::Permission::VerifyStatutes)?;
3095
3096 let statutes = state.statutes.read().await;
3097
3098 let to_analyze: Vec<Statute> = if req.statute_ids.is_empty() {
3099 statutes.clone()
3100 } else {
3101 statutes
3102 .iter()
3103 .filter(|s| req.statute_ids.contains(&s.id))
3104 .cloned()
3105 .collect()
3106 };
3107
3108 if to_analyze.is_empty() {
3109 return Err(ApiError::BadRequest(
3110 "No statutes for what-if analysis".to_string(),
3111 ));
3112 }
3113
3114 drop(statutes);
3115
3116 fn create_entity(
3118 attributes: &std::collections::HashMap<String, String>,
3119 ) -> legalis_core::TypedEntity {
3120 use legalis_core::TypedEntity;
3121 let mut entity = TypedEntity::new();
3122 for (key, value) in attributes {
3123 if let Ok(num) = value.parse::<u32>() {
3124 entity.set_u32(key, num);
3125 } else if let Ok(num) = value.parse::<u64>() {
3126 entity.set_u64(key, num);
3127 } else {
3128 entity.set_string(key, value);
3129 }
3130 }
3131 entity
3132 }
3133
3134 let baseline_entity = create_entity(&req.baseline_attributes);
3136 let baseline_pop: Vec<Box<dyn legalis_core::LegalEntity>> = vec![Box::new(baseline_entity)];
3137
3138 use legalis_sim::SimEngine;
3139 let baseline_engine = SimEngine::new(to_analyze.clone(), baseline_pop);
3140 let baseline_metrics = baseline_engine.run_simulation().await;
3141
3142 let modified_entity = create_entity(&req.modified_attributes);
3144 let modified_pop: Vec<Box<dyn legalis_core::LegalEntity>> = vec![Box::new(modified_entity)];
3145
3146 let modified_engine = SimEngine::new(to_analyze.clone(), modified_pop);
3147 let modified_metrics = modified_engine.run_simulation().await;
3148
3149 let baseline_compliant = baseline_metrics.deterministic_count > 0;
3150 let modified_compliant = modified_metrics.deterministic_count > 0;
3151
3152 let impact = if baseline_compliant && !modified_compliant {
3153 "negative".to_string()
3154 } else if !baseline_compliant && modified_compliant {
3155 "positive".to_string()
3156 } else {
3157 "none".to_string()
3158 };
3159
3160 Ok(Json(ApiResponse::new(WhatIfResponse {
3161 baseline_compliant,
3162 modified_compliant,
3163 impact,
3164 baseline_requires_discretion: baseline_metrics.discretion_count > 0,
3165 modified_requires_discretion: modified_metrics.discretion_count > 0,
3166 changed_attribute_count: req.modified_attributes.len(),
3167 })))
3168}
3169
3170async fn save_simulation(
3172 user: auth::AuthUser,
3173 State(state): State<Arc<AppState>>,
3174 Json(req): Json<SaveSimulationRequest>,
3175) -> Result<impl IntoResponse, ApiError> {
3176 user.require_permission(auth::Permission::CreateStatutes)?;
3177
3178 let saved = SavedSimulation {
3179 id: uuid::Uuid::new_v4().to_string(),
3180 name: req.name,
3181 description: req.description,
3182 statute_ids: vec![], population_size: req.simulation_result.total_entities,
3184 deterministic_outcomes: req.simulation_result.deterministic_outcomes,
3185 discretionary_outcomes: req.simulation_result.discretionary_outcomes,
3186 void_outcomes: req.simulation_result.void_outcomes,
3187 deterministic_rate: req.simulation_result.deterministic_rate,
3188 discretionary_rate: req.simulation_result.discretionary_rate,
3189 void_rate: req.simulation_result.void_rate,
3190 created_at: chrono::Utc::now().to_rfc3339(),
3191 created_by: user.username.clone(),
3192 };
3193
3194 let mut simulations = state.saved_simulations.write().await;
3195 simulations.push(saved.clone());
3196
3197 info!("Saved simulation: {} by user {}", saved.id, user.username);
3198
3199 state
3201 .audit_log
3202 .log_success(
3203 audit::AuditEventType::SimulationSaved,
3204 user.id.to_string(),
3205 user.username.clone(),
3206 "save_simulation".to_string(),
3207 Some(saved.id.clone()),
3208 Some("simulation".to_string()),
3209 serde_json::json!({
3210 "simulation_id": saved.id,
3211 "name": saved.name
3212 }),
3213 )
3214 .await;
3215
3216 Ok((StatusCode::CREATED, Json(ApiResponse::new(saved))))
3217}
3218
3219async fn list_saved_simulations(
3221 user: auth::AuthUser,
3222 State(state): State<Arc<AppState>>,
3223 Query(query): Query<ListSavedSimulationsQuery>,
3224) -> Result<impl IntoResponse, ApiError> {
3225 user.require_permission(auth::Permission::ReadStatutes)?;
3226
3227 let simulations = state.saved_simulations.read().await;
3228 let total = simulations.len();
3229
3230 let offset = query.offset.unwrap_or(0);
3231 let limit = query.limit.unwrap_or(100).min(1000);
3232
3233 let paginated: Vec<SavedSimulation> = simulations
3234 .iter()
3235 .skip(offset)
3236 .take(limit)
3237 .cloned()
3238 .collect();
3239
3240 let meta = ResponseMeta {
3241 total: Some(total),
3242 page: Some(offset / limit),
3243 per_page: Some(limit),
3244 next_cursor: None,
3245 prev_cursor: None,
3246 has_more: None,
3247 };
3248
3249 Ok(Json(ApiResponse::new(paginated).with_meta(meta)))
3250}
3251
3252async fn get_saved_simulation(
3254 user: auth::AuthUser,
3255 State(state): State<Arc<AppState>>,
3256 Path(id): Path<String>,
3257) -> Result<impl IntoResponse, ApiError> {
3258 user.require_permission(auth::Permission::ReadStatutes)?;
3259
3260 let simulations = state.saved_simulations.read().await;
3261 let simulation = simulations
3262 .iter()
3263 .find(|s| s.id == id)
3264 .ok_or_else(|| ApiError::NotFound(format!("Saved simulation not found: {}", id)))?;
3265
3266 Ok(Json(ApiResponse::new(simulation.clone())))
3267}
3268
3269async fn delete_saved_simulation(
3271 user: auth::AuthUser,
3272 State(state): State<Arc<AppState>>,
3273 Path(id): Path<String>,
3274) -> Result<impl IntoResponse, ApiError> {
3275 user.require_permission(auth::Permission::DeleteStatutes)?;
3276
3277 let mut simulations = state.saved_simulations.write().await;
3278 let initial_len = simulations.len();
3279 simulations.retain(|s| s.id != id);
3280
3281 if simulations.len() == initial_len {
3282 return Err(ApiError::NotFound(format!(
3283 "Saved simulation not found: {}",
3284 id
3285 )));
3286 }
3287
3288 info!("Deleted saved simulation: {} by user {}", id, user.username);
3289
3290 state
3292 .audit_log
3293 .log_success(
3294 audit::AuditEventType::SimulationDeleted,
3295 user.id.to_string(),
3296 user.username.clone(),
3297 "delete_saved_simulation".to_string(),
3298 Some(id.clone()),
3299 Some("simulation".to_string()),
3300 serde_json::json!({
3301 "simulation_id": id
3302 }),
3303 )
3304 .await;
3305
3306 Ok(StatusCode::NO_CONTENT)
3307}
3308
3309async fn visualize_statute(
3311 user: auth::AuthUser,
3312 State(state): State<Arc<AppState>>,
3313 Path(id): Path<String>,
3314 Query(query): Query<VizQuery>,
3315) -> Result<impl IntoResponse, ApiError> {
3316 user.require_permission(auth::Permission::ReadStatutes)?;
3317
3318 let statutes = state.statutes.read().await;
3319 let statute = statutes
3320 .iter()
3321 .find(|s| s.id == id)
3322 .ok_or_else(|| ApiError::NotFound(format!("Statute not found: {}", id)))?;
3323
3324 let tree = DecisionTree::from_statute(statute)
3326 .map_err(|e| ApiError::Internal(format!("Visualization error: {}", e)))?;
3327
3328 let theme = match query.theme.as_deref() {
3330 Some("dark") => legalis_viz::Theme::dark(),
3331 Some("high_contrast") => legalis_viz::Theme::high_contrast(),
3332 Some("colorblind_friendly") => legalis_viz::Theme::colorblind_friendly(),
3333 _ => legalis_viz::Theme::light(),
3334 };
3335
3336 let (content, format_str) = match query.format {
3338 VizFormat::Dot => (tree.to_dot(), "dot"),
3339 VizFormat::Ascii => (tree.to_ascii(), "ascii"),
3340 VizFormat::Mermaid => (tree.to_mermaid(), "mermaid"),
3341 VizFormat::PlantUml => (tree.to_plantuml(), "plantuml"),
3342 VizFormat::Svg => (tree.to_svg_with_theme(&theme), "svg"),
3343 VizFormat::Html => (tree.to_html_with_theme(&theme), "html"),
3344 };
3345
3346 Ok(Json(ApiResponse::new(VisualizationResponse {
3347 statute_id: id,
3348 format: format_str.to_string(),
3349 content,
3350 node_count: tree.node_count(),
3351 discretionary_count: tree.discretionary_count(),
3352 })))
3353}
3354
3355#[derive(Debug, Clone)]
3357pub struct ServerConfig {
3358 pub host: String,
3360 pub port: u16,
3362}
3363
3364impl Default for ServerConfig {
3365 fn default() -> Self {
3366 Self {
3367 host: "127.0.0.1".to_string(),
3368 port: 3000,
3369 }
3370 }
3371}
3372
3373impl ServerConfig {
3374 pub fn bind_addr(&self) -> String {
3376 format!("{}:{}", self.host, self.port)
3377 }
3378}
3379
3380#[cfg(test)]
3381mod tests {
3382 use super::*;
3383 use axum::body::Body;
3384 use axum::http::Request;
3385 #[allow(unused_imports)]
3386 use legalis_core::{Effect, EffectType};
3387 use tower::ServiceExt;
3388
3389 fn create_test_router() -> Router {
3390 let state = Arc::new(AppState::new());
3391 create_router(state)
3392 }
3393
3394 #[tokio::test]
3395 async fn test_health_check() {
3396 let app = create_test_router();
3397 let response = app
3398 .oneshot(
3399 Request::builder()
3400 .uri("/health")
3401 .body(Body::empty())
3402 .unwrap(),
3403 )
3404 .await
3405 .unwrap();
3406
3407 assert_eq!(response.status(), StatusCode::OK);
3408 }
3409
3410 #[tokio::test]
3411 async fn test_list_statutes_empty() {
3412 let app = create_test_router();
3413 let response = app
3414 .oneshot(
3415 Request::builder()
3416 .uri("/api/v1/statutes")
3417 .header("Authorization", "ApiKey lgl_12345678901234567890")
3418 .body(Body::empty())
3419 .unwrap(),
3420 )
3421 .await
3422 .unwrap();
3423
3424 assert_eq!(response.status(), StatusCode::OK);
3425 }
3426
3427 #[tokio::test]
3428 async fn test_list_statutes_unauthorized() {
3429 let app = create_test_router();
3430 let response = app
3431 .oneshot(
3432 Request::builder()
3433 .uri("/api/v1/statutes")
3434 .body(Body::empty())
3435 .unwrap(),
3436 )
3437 .await
3438 .unwrap();
3439
3440 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
3441 }
3442
3443 #[tokio::test]
3444 async fn test_statute_search() {
3445 let state = Arc::new(AppState::new());
3446
3447 {
3449 let mut statutes = state.statutes.write().await;
3450 statutes.push(
3451 Statute::new(
3452 "search-test-1",
3453 "Searchable Statute",
3454 Effect::new(EffectType::Grant, "Test grant"),
3455 )
3456 .with_jurisdiction("TEST"),
3457 );
3458 }
3459
3460 let app = create_router(state);
3461
3462 let response = app
3463 .oneshot(
3464 Request::builder()
3465 .uri("/api/v1/statutes/search?title=Searchable")
3466 .header("Authorization", "ApiKey lgl_12345678901234567890")
3467 .body(Body::empty())
3468 .unwrap(),
3469 )
3470 .await
3471 .unwrap();
3472
3473 assert_eq!(response.status(), StatusCode::OK);
3474
3475 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
3476 .await
3477 .unwrap();
3478 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
3479 assert!(!json["data"]["statutes"].as_array().unwrap().is_empty());
3480 }
3481
3482 #[tokio::test]
3483 async fn test_graphql_integration() {
3484 let state = graphql::GraphQLState::new();
3486 let schema = graphql::create_schema(state);
3487
3488 use auth::{AuthMethod, AuthUser, Role};
3490 let admin_user = AuthUser::new(
3491 uuid::Uuid::new_v4(),
3492 "admin".to_string(),
3493 Role::Admin,
3494 AuthMethod::Jwt,
3495 );
3496
3497 let mutation = r#"
3498 mutation {
3499 createStatute(input: {
3500 id: "graphql-test-1"
3501 title: "GraphQL Test Statute"
3502 effectDescription: "Test benefit"
3503 effectType: "Grant"
3504 jurisdiction: "TEST"
3505 }) {
3506 id
3507 title
3508 }
3509 }
3510 "#;
3511
3512 let request = async_graphql::Request::new(mutation).data(admin_user);
3513 let result = schema.execute(request).await;
3514 assert!(result.errors.is_empty());
3515
3516 let query = r#"
3517 {
3518 statutes {
3519 id
3520 title
3521 }
3522 }
3523 "#;
3524
3525 let result = schema.execute(query).await;
3526 assert!(result.errors.is_empty());
3527 }
3528
3529 #[tokio::test]
3530 async fn test_readiness_check() {
3531 let app = create_test_router();
3532 let response = app
3533 .oneshot(
3534 Request::builder()
3535 .uri("/health/ready")
3536 .body(Body::empty())
3537 .unwrap(),
3538 )
3539 .await
3540 .unwrap();
3541
3542 assert_eq!(response.status(), StatusCode::OK);
3543 }
3544
3545 #[tokio::test]
3546 async fn test_metrics_endpoint() {
3547 let app = create_test_router();
3548 let response = app
3549 .oneshot(
3550 Request::builder()
3551 .uri("/metrics")
3552 .body(Body::empty())
3553 .unwrap(),
3554 )
3555 .await
3556 .unwrap();
3557
3558 assert_eq!(response.status(), StatusCode::OK);
3559 }
3560}