pmcp_code_mode/
explanation.rs1use crate::graphql::{GraphQLOperationType, GraphQLQueryInfo};
6use crate::types::{RiskLevel, SecurityAnalysis};
7
8pub trait ExplanationGenerator: Send + Sync {
10 fn explain_graphql(&self, query_info: &GraphQLQueryInfo, security: &SecurityAnalysis)
12 -> String;
13}
14
15pub struct TemplateExplanationGenerator;
17
18impl Default for TemplateExplanationGenerator {
19 fn default() -> Self {
20 Self
21 }
22}
23
24impl TemplateExplanationGenerator {
25 pub fn new() -> Self {
26 Self
27 }
28}
29
30impl ExplanationGenerator for TemplateExplanationGenerator {
31 fn explain_graphql(
32 &self,
33 query_info: &GraphQLQueryInfo,
34 security: &SecurityAnalysis,
35 ) -> String {
36 let mut parts = Vec::new();
37
38 let op_desc = match query_info.operation_type {
40 GraphQLOperationType::Query => "This query will read",
41 GraphQLOperationType::Mutation => "This mutation will modify",
42 GraphQLOperationType::Subscription => "This subscription will watch",
43 };
44
45 let types: Vec<&str> = security
47 .tables_accessed
48 .iter()
49 .map(|s| s.as_str())
50 .collect();
51 let types_desc = if types.is_empty() {
52 "data".to_string()
53 } else if types.len() == 1 {
54 format!("{} data", types[0])
55 } else {
56 let last = types.last().unwrap();
57 let rest = &types[..types.len() - 1];
58 format!("{} and {} data", rest.join(", "), last)
59 };
60
61 parts.push(format!("{} {}.", op_desc, types_desc));
62
63 let fields: Vec<&str> = security
65 .fields_accessed
66 .iter()
67 .map(|s| s.as_str())
68 .collect();
69 if !fields.is_empty() {
70 let field_count = fields.len();
71 if field_count <= 5 {
72 parts.push(format!("Fields: {}.", fields.join(", ")));
73 } else {
74 parts.push(format!(
75 "Accessing {} fields including: {}.",
76 field_count,
77 fields[..5].join(", ")
78 ));
79 }
80 }
81
82 let sensitive_issues: Vec<_> = security
84 .potential_issues
85 .iter()
86 .filter(|i| i.is_sensitive())
87 .collect();
88
89 if !sensitive_issues.is_empty() {
90 parts.push("⚠️ This query accesses potentially sensitive data.".to_string());
91 }
92
93 if query_info.max_depth > 3 {
95 parts.push(format!(
96 "Query has {} levels of nesting.",
97 query_info.max_depth
98 ));
99 }
100
101 let risk = security.assess_risk();
103 let risk_desc = match risk {
104 RiskLevel::Low => "Risk: LOW (read-only, no sensitive data)".to_string(),
105 RiskLevel::Medium => "Risk: MEDIUM (may access sensitive data)".to_string(),
106 RiskLevel::High => "Risk: HIGH (modifies multiple records)".to_string(),
107 RiskLevel::Critical => "Risk: CRITICAL (requires admin approval)".to_string(),
108 };
109 parts.push(risk_desc);
110
111 parts.join(" ")
112 }
113}
114
115#[allow(dead_code)]
117pub fn auto_approval_message(risk_level: RiskLevel) -> &'static str {
118 match risk_level {
119 RiskLevel::Low => "Auto-approved: low-risk read-only query",
120 RiskLevel::Medium => "Auto-approved: medium-risk query (configured to allow)",
121 RiskLevel::High => "Auto-approved: high-risk query (configured to allow)",
122 RiskLevel::Critical => "Auto-approved: critical-risk query (configured to allow)",
123 }
124}
125
126#[allow(dead_code)]
128pub fn mutations_not_allowed_message() -> &'static str {
129 "Mutations are not enabled for this server. Only read-only queries are allowed in Code Mode."
130}
131
132#[allow(dead_code)]
134pub fn code_mode_disabled_message() -> &'static str {
135 "Code Mode is not enabled for this server. Use the standard tools instead."
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use std::collections::HashSet;
142
143 fn sample_query_info() -> GraphQLQueryInfo {
144 GraphQLQueryInfo {
145 operation_type: GraphQLOperationType::Query,
146 operation_name: Some("GetUsers".to_string()),
147 root_fields: vec!["users".to_string()],
148 types_accessed: {
149 let mut set = HashSet::new();
150 set.insert("User".to_string());
151 set
152 },
153 fields_accessed: {
154 let mut set = HashSet::new();
155 set.insert("id".to_string());
156 set.insert("name".to_string());
157 set.insert("email".to_string());
158 set
159 },
160 has_variables: false,
161 variable_names: vec![],
162 max_depth: 2,
163 has_fragments: false,
164 fragment_names: vec![],
165 has_introspection: false,
166 }
167 }
168
169 fn sample_security() -> SecurityAnalysis {
170 let info = sample_query_info();
171 SecurityAnalysis {
172 is_read_only: true,
173 tables_accessed: info.types_accessed,
174 fields_accessed: info.fields_accessed,
175 has_aggregation: false,
176 has_subqueries: false,
177 estimated_complexity: crate::types::Complexity::Low,
178 potential_issues: vec![],
179 estimated_rows: None,
180 }
181 }
182
183 #[test]
184 fn test_basic_explanation() {
185 let generator = TemplateExplanationGenerator::new();
186 let info = sample_query_info();
187 let security = sample_security();
188
189 let explanation = generator.explain_graphql(&info, &security);
190
191 assert!(explanation.contains("read"));
192 assert!(explanation.contains("User"));
193 assert!(explanation.contains("Risk: LOW"));
194 }
195
196 #[test]
197 fn test_mutation_explanation() {
198 let generator = TemplateExplanationGenerator::new();
199 let mut info = sample_query_info();
200 info.operation_type = GraphQLOperationType::Mutation;
201
202 let mut security = sample_security();
203 security.is_read_only = false;
204
205 let explanation = generator.explain_graphql(&info, &security);
206
207 assert!(explanation.contains("modify"));
208 }
209}