pecto_core/
context_export.rs1use crate::model::ProjectSpec;
2
3pub fn to_context(spec: &ProjectSpec) -> String {
6 let mut out = String::new();
7
8 out.push_str(&format!(
10 "Project: {} ({} files, {} capabilities)\n",
11 spec.name,
12 spec.files_analyzed,
13 spec.capabilities.len()
14 ));
15
16 if !spec.domains.is_empty() {
18 let domain_names: Vec<&str> = spec.domains.iter().map(|d| d.name.as_str()).collect();
19 out.push_str(&format!("Domains: {}\n", domain_names.join(", ")));
20 }
21
22 if !spec.dependencies.is_empty() {
24 out.push_str(&format!(
25 "Dependencies: {} edges\n",
26 spec.dependencies.len()
27 ));
28 }
29
30 out.push('\n');
31
32 if !spec.domains.is_empty() {
34 for domain in &spec.domains {
35 out.push_str(&format!("## {}\n", domain.name));
36
37 for cap_name in &domain.capabilities {
38 if let Some(cap) = spec.capabilities.iter().find(|c| &c.name == cap_name) {
39 format_capability(&mut out, cap);
40 }
41 }
42
43 if !domain.external_dependencies.is_empty() {
44 out.push_str(&format!(
45 " Depends on: {}\n",
46 domain.external_dependencies.join(", ")
47 ));
48 }
49 out.push('\n');
50 }
51
52 let domain_caps: std::collections::HashSet<&str> = spec
54 .domains
55 .iter()
56 .flat_map(|d| d.capabilities.iter().map(|c| c.as_str()))
57 .collect();
58
59 let orphans: Vec<_> = spec
60 .capabilities
61 .iter()
62 .filter(|c| !domain_caps.contains(c.name.as_str()))
63 .collect();
64
65 if !orphans.is_empty() {
66 out.push_str("## Other\n");
67 for cap in orphans {
68 format_capability(&mut out, cap);
69 }
70 out.push('\n');
71 }
72 } else {
73 for cap in &spec.capabilities {
74 format_capability(&mut out, cap);
75 }
76 }
77
78 if !spec.dependencies.is_empty() {
80 out.push_str("## Dependencies\n");
81 for dep in &spec.dependencies {
82 out.push_str(&format!(" {} → {} ({:?})\n", dep.from, dep.to, dep.kind));
83 }
84 }
85
86 out
87}
88
89fn format_capability(out: &mut String, cap: &crate::model::Capability) {
90 if !cap.endpoints.is_empty() {
91 for ep in &cap.endpoints {
92 out.push_str(&format!(" {:?} {}", ep.method, ep.path));
93 if let Some(sec) = &ep.security
94 && sec.authentication.is_some()
95 {
96 out.push_str(" [auth]");
97 }
98 out.push('\n');
99
100 if !ep.validation.is_empty() {
102 let fields: Vec<&str> = ep.validation.iter().map(|v| v.field.as_str()).collect();
103 out.push_str(&format!(" validates: {}\n", fields.join(", ")));
104 }
105
106 for b in &ep.behaviors {
108 if b.name != "success" {
109 out.push_str(&format!(" {} → {}\n", b.name, b.returns.status));
110 }
111 }
112 }
113 }
114
115 if !cap.entities.is_empty() {
116 for entity in &cap.entities {
117 let field_names: Vec<&str> = entity.fields.iter().map(|f| f.name.as_str()).collect();
118 out.push_str(&format!(
119 " Entity {} (table: {}) [{}]\n",
120 entity.name,
121 entity.table,
122 field_names.join(", ")
123 ));
124 }
125 }
126
127 if !cap.operations.is_empty() {
128 for op in &cap.operations {
129 out.push_str(&format!(" {}", op.name));
130 if let Some(tx) = &op.transaction {
131 out.push_str(&format!(" [tx:{}]", tx));
132 }
133 let effects: Vec<String> = op
134 .behaviors
135 .iter()
136 .flat_map(|b| &b.side_effects)
137 .map(format_side_effect)
138 .collect();
139 if !effects.is_empty() {
140 out.push_str(&format!(" → {}", effects.join(", ")));
141 }
142 out.push('\n');
143 }
144 }
145
146 if !cap.scheduled_tasks.is_empty() {
147 for task in &cap.scheduled_tasks {
148 out.push_str(&format!(" Scheduled: {} ({})\n", task.name, task.schedule));
149 }
150 }
151}
152
153fn format_side_effect(effect: &crate::model::SideEffect) -> String {
154 match effect {
155 crate::model::SideEffect::DbInsert { table } => format!("insert:{}", table),
156 crate::model::SideEffect::DbUpdate { description } => format!("update:{}", description),
157 crate::model::SideEffect::Event { name } => format!("event:{}", name),
158 crate::model::SideEffect::ServiceCall { target } => format!("call:{}", target),
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::model::*;
166
167 #[test]
168 fn test_context_export_basic() {
169 let mut spec = ProjectSpec::new("my-app");
170 spec.files_analyzed = 50;
171
172 let mut cap = Capability::new("user-controller", "UserController.java");
173 cap.endpoints.push(Endpoint {
174 method: HttpMethod::Get,
175 path: "/api/users".to_string(),
176 input: None,
177 validation: Vec::new(),
178 behaviors: vec![Behavior {
179 name: "success".to_string(),
180 condition: None,
181 returns: ResponseSpec {
182 status: 200,
183 body: None,
184 },
185 side_effects: Vec::new(),
186 }],
187 security: None,
188 });
189 spec.capabilities.push(cap);
190
191 let ctx = to_context(&spec);
192 assert!(ctx.contains("my-app"));
193 assert!(ctx.contains("50 files"));
194 assert!(
196 ctx.contains("/api/users"),
197 "Should contain endpoint path. Got: {}",
198 ctx
199 );
200 }
201
202 #[test]
203 fn test_context_export_with_domains() {
204 let mut spec = ProjectSpec::new("test");
205 spec.files_analyzed = 10;
206
207 spec.capabilities
208 .push(Capability::new("user-service", "UserService.java"));
209 spec.domains.push(Domain {
210 name: "user".to_string(),
211 capabilities: vec!["user-service".to_string()],
212 external_dependencies: vec![],
213 });
214
215 let ctx = to_context(&spec);
216 assert!(ctx.contains("Domains: user"));
217 assert!(ctx.contains("## user"));
218 }
219}