1use std::collections::HashMap;
2
3use gobby_core::degradation::ServiceState;
4
5use crate::config::Context;
6
7#[cfg(test)]
8use super::RELATES_TO_CODE;
9use super::loading::load_report_snapshot;
10use super::render::{RenderMarkdownInput, render_markdown};
11use super::summary::{
12 normalize_bridge_edges, suggested_questions, summarize_bridge_edges, summarize_graph,
13 summarize_hotspots, target_frequencies,
14};
15use super::time::now_iso8601;
16use super::types::{
17 BridgeEdgeInput, ProjectGraphReport, ProjectGraphReportError, ProjectGraphReportOptions,
18 ReportDegradation, ReportGraphSnapshot,
19};
20
21pub fn generate_report(ctx: &Context) -> Result<ProjectGraphReport, ProjectGraphReportError> {
22 generate_report_with_options(ctx, ProjectGraphReportOptions::default())
23}
24
25pub fn generate_report_with_options(
26 ctx: &Context,
27 options: ProjectGraphReportOptions,
28) -> Result<ProjectGraphReport, ProjectGraphReportError> {
29 let Some(config) = ctx.falkordb.as_ref() else {
30 return Err(ProjectGraphReportError::GraphServiceNotConfigured);
31 };
32
33 let connection_config = config.connection_config();
34 let options = options.normalized();
35 let result = gobby_core::falkor::with_graph(
36 Some(&connection_config),
37 &config.graph_name,
38 ReportGraphSnapshot::default(),
39 |client| load_report_snapshot(client, &ctx.project_id, options.top_n),
40 );
41
42 match result {
43 Ok((snapshot, ServiceState::Available)) => Ok(generate_report_from_snapshot_with_options(
44 &ctx.project_id,
45 now_iso8601(),
46 snapshot,
47 options,
48 )),
49 Ok((_, ServiceState::NotConfigured)) => {
50 Err(ProjectGraphReportError::GraphServiceNotConfigured)
51 }
52 Ok((_, ServiceState::Unreachable { message })) => {
53 Err(ProjectGraphReportError::GraphServiceUnreachable { message })
54 }
55 Err(error) => Err(ProjectGraphReportError::GraphQueryFailed {
56 message: format!("{error:#}"),
57 }),
58 }
59}
60
61pub fn empty_report(project_id: impl Into<String>) -> ProjectGraphReport {
62 generate_report_from_snapshot(project_id, now_iso8601(), ReportGraphSnapshot::default())
63}
64
65pub(super) fn generate_report_from_snapshot(
66 project_id: impl Into<String>,
67 generated_at: impl Into<String>,
68 snapshot: ReportGraphSnapshot,
69) -> ProjectGraphReport {
70 generate_report_from_snapshot_with_options(
71 project_id,
72 generated_at,
73 snapshot,
74 ProjectGraphReportOptions::default().normalized(),
75 )
76}
77
78fn generate_report_from_snapshot_with_options(
79 project_id: impl Into<String>,
80 generated_at: impl Into<String>,
81 snapshot: ReportGraphSnapshot,
82 options: ProjectGraphReportOptions,
83) -> ProjectGraphReport {
84 let project_id = project_id.into();
85 let generated_at = generated_at.into();
86 let node_by_id = snapshot
87 .nodes
88 .iter()
89 .map(|node| (node.id.as_str(), node))
90 .collect::<HashMap<_, _>>();
91
92 let summary = snapshot
93 .summary
94 .clone()
95 .unwrap_or_else(|| summarize_graph(&snapshot.nodes, &snapshot.code_edges));
96 let hotspots = snapshot.hotspots.clone().unwrap_or_else(|| {
97 summarize_hotspots(&snapshot.nodes, &snapshot.code_edges, options.top_n)
98 });
99 let unresolved_targets = snapshot.unresolved_targets.clone().unwrap_or_else(|| {
100 target_frequencies(
101 &snapshot.code_edges,
102 &node_by_id,
103 "unresolved",
104 options.top_n,
105 )
106 });
107 let external_targets = snapshot.external_targets.clone().unwrap_or_else(|| {
108 target_frequencies(&snapshot.code_edges, &node_by_id, "external", options.top_n)
109 });
110
111 let (bridge_edges, mut degradation_details): (_, Vec<ReportDegradation>) =
112 match snapshot.bridge_edges {
113 BridgeEdgeInput::Available(edges) => (normalize_bridge_edges(edges), vec![]),
114 #[cfg(test)]
115 BridgeEdgeInput::Unavailable(reason) => (
116 vec![],
117 vec![ReportDegradation {
118 input: RELATES_TO_CODE.to_string(),
119 required: false,
120 detail: reason,
121 }],
122 ),
123 };
124 let bridge_summary = summarize_bridge_edges(&bridge_edges);
125 degradation_details.sort_by(|left, right| left.input.cmp(&right.input));
126
127 let suggested_investigation_questions = suggested_questions(
128 &hotspots,
129 &unresolved_targets,
130 &external_targets,
131 bridge_summary.as_ref(),
132 °radation_details,
133 );
134 let markdown = render_markdown(RenderMarkdownInput {
135 project_id: &project_id,
136 generated_at: &generated_at,
137 summary: &summary,
138 hotspots: &hotspots,
139 unresolved_targets: &unresolved_targets,
140 external_targets: &external_targets,
141 bridge_summary: bridge_summary.as_ref(),
142 degradation_details: °radation_details,
143 top_n: options.top_n,
144 });
145
146 ProjectGraphReport {
147 project_id,
148 generated_at,
149 summary,
150 hotspots,
151 unresolved_targets,
152 external_targets,
153 bridge_summary,
154 bridge_edges,
155 degradation_details,
156 suggested_investigation_questions,
157 markdown,
158 }
159}