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 if security.potential_issues.iter().any(|i| i.is_sensitive()) {
84 parts.push("⚠️ This query accesses potentially sensitive data.".to_string());
85 }
86
87 if query_info.max_depth > 3 {
89 parts.push(format!(
90 "Query has {} levels of nesting.",
91 query_info.max_depth
92 ));
93 }
94
95 let risk = security.assess_risk();
97 let risk_desc = match risk {
98 RiskLevel::Low => "Risk: LOW (read-only, no sensitive data)".to_string(),
99 RiskLevel::Medium => "Risk: MEDIUM (may access sensitive data)".to_string(),
100 RiskLevel::High => "Risk: HIGH (modifies multiple records)".to_string(),
101 RiskLevel::Critical => "Risk: CRITICAL (requires admin approval)".to_string(),
102 };
103 parts.push(risk_desc);
104
105 parts.join(" ")
106 }
107}
108
109#[allow(dead_code)]
111pub fn auto_approval_message(risk_level: RiskLevel) -> &'static str {
112 match risk_level {
113 RiskLevel::Low => "Auto-approved: low-risk read-only query",
114 RiskLevel::Medium => "Auto-approved: medium-risk query (configured to allow)",
115 RiskLevel::High => "Auto-approved: high-risk query (configured to allow)",
116 RiskLevel::Critical => "Auto-approved: critical-risk query (configured to allow)",
117 }
118}
119
120#[allow(dead_code)]
122pub fn mutations_not_allowed_message() -> &'static str {
123 "Mutations are not enabled for this server. Only read-only queries are allowed in Code Mode."
124}
125
126#[allow(dead_code)]
128pub fn code_mode_disabled_message() -> &'static str {
129 "Code Mode is not enabled for this server. Use the standard tools instead."
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use std::collections::HashSet;
136
137 fn sample_query_info() -> GraphQLQueryInfo {
138 GraphQLQueryInfo {
139 operation_type: GraphQLOperationType::Query,
140 operation_name: Some("GetUsers".to_string()),
141 root_fields: vec!["users".to_string()],
142 types_accessed: {
143 let mut set = HashSet::new();
144 set.insert("User".to_string());
145 set
146 },
147 fields_accessed: {
148 let mut set = HashSet::new();
149 set.insert("id".to_string());
150 set.insert("name".to_string());
151 set.insert("email".to_string());
152 set
153 },
154 has_variables: false,
155 variable_names: vec![],
156 max_depth: 2,
157 has_fragments: false,
158 fragment_names: vec![],
159 has_introspection: false,
160 }
161 }
162
163 fn sample_security() -> SecurityAnalysis {
164 let info = sample_query_info();
165 SecurityAnalysis {
166 is_read_only: true,
167 tables_accessed: info.types_accessed,
168 fields_accessed: info.fields_accessed,
169 has_aggregation: false,
170 has_subqueries: false,
171 estimated_complexity: crate::types::Complexity::Low,
172 potential_issues: vec![],
173 estimated_rows: None,
174 }
175 }
176
177 #[test]
178 fn test_basic_explanation() {
179 let generator = TemplateExplanationGenerator::new();
180 let info = sample_query_info();
181 let security = sample_security();
182
183 let explanation = generator.explain_graphql(&info, &security);
184
185 assert!(explanation.contains("read"));
186 assert!(explanation.contains("User"));
187 assert!(explanation.contains("Risk: LOW"));
188 }
189
190 #[test]
191 fn test_mutation_explanation() {
192 let generator = TemplateExplanationGenerator::new();
193 let mut info = sample_query_info();
194 info.operation_type = GraphQLOperationType::Mutation;
195
196 let mut security = sample_security();
197 security.is_read_only = false;
198
199 let explanation = generator.explain_graphql(&info, &security);
200
201 assert!(explanation.contains("modify"));
202 }
203}