1use std::fmt;
2use tramli::{FlowDefinition, FlowEngine, FlowState};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum PluginKind {
7 Analysis,
8 Store,
9 Engine,
10 RuntimeAdapter,
11 Generation,
12 Documentation,
13}
14
15#[derive(Debug, Clone)]
17pub struct PluginDescriptor {
18 pub id: &'static str,
19 pub display_name: &'static str,
20 pub description: &'static str,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum FindingLocation {
26 Transition { from_state: String, to_state: String },
27 State { state: String },
28 Data { data_key: String },
29 Flow,
30}
31
32impl fmt::Display for FindingLocation {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 FindingLocation::Transition { from_state, to_state } => {
36 write!(f, "transition({} -> {})", from_state, to_state)
37 }
38 FindingLocation::State { state } => write!(f, "state({})", state),
39 FindingLocation::Data { data_key } => write!(f, "data({})", data_key),
40 FindingLocation::Flow => write!(f, "flow"),
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct Finding {
48 pub plugin_id: String,
49 pub severity: String,
50 pub message: String,
51 pub location: Option<FindingLocation>,
52}
53
54#[derive(Debug, Default)]
56pub struct PluginReport {
57 entries: Vec<Finding>,
58}
59
60impl PluginReport {
61 pub fn new() -> Self {
62 Self { entries: Vec::new() }
63 }
64
65 pub fn add(&mut self, plugin_id: &str, severity: &str, message: &str) {
66 self.entries.push(Finding {
67 plugin_id: plugin_id.to_string(),
68 severity: severity.to_string(),
69 message: message.to_string(),
70 location: None,
71 });
72 }
73
74 pub fn warn(&mut self, plugin_id: &str, message: &str) {
75 self.add(plugin_id, "WARN", message);
76 }
77
78 pub fn error(&mut self, plugin_id: &str, message: &str) {
79 self.add(plugin_id, "ERROR", message);
80 }
81
82 pub fn warn_at(&mut self, plugin_id: &str, message: &str, location: FindingLocation) {
83 self.entries.push(Finding {
84 plugin_id: plugin_id.to_string(),
85 severity: "WARN".to_string(),
86 message: message.to_string(),
87 location: Some(location),
88 });
89 }
90
91 pub fn error_at(&mut self, plugin_id: &str, message: &str, location: FindingLocation) {
92 self.entries.push(Finding {
93 plugin_id: plugin_id.to_string(),
94 severity: "ERROR".to_string(),
95 message: message.to_string(),
96 location: Some(location),
97 });
98 }
99
100 pub fn findings(&self) -> &[Finding] {
101 &self.entries
102 }
103
104 pub fn as_text(&self) -> String {
105 if self.entries.is_empty() {
106 return "No findings.".to_string();
107 }
108 self.entries
109 .iter()
110 .map(|e| {
111 let base = format!("[{}] {}: {}", e.severity, e.plugin_id, e.message);
112 match &e.location {
113 Some(loc) => format!("{} @ {}", base, loc),
114 None => base,
115 }
116 })
117 .collect::<Vec<_>>()
118 .join("\n")
119 }
120}
121
122impl fmt::Display for PluginReport {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 write!(f, "{}", self.as_text())
125 }
126}
127
128pub trait AnalysisPlugin<S: FlowState>: Send + Sync {
130 fn descriptor(&self) -> PluginDescriptor;
131 fn analyze(&self, definition: &FlowDefinition<S>, report: &mut PluginReport);
132}
133
134pub trait EnginePlugin<S: FlowState>: Send + Sync {
136 fn descriptor(&self) -> PluginDescriptor;
137 fn install(&self, engine: &mut FlowEngine<S>);
138}
139
140pub trait RuntimeAdapterPlugin<S: FlowState>: Send + Sync {
142 fn descriptor(&self) -> PluginDescriptor;
143 fn id(&self) -> &str { self.descriptor().id }
144}