1use std::{collections::BTreeMap, sync::Arc};
4
5use repoctl_core::{
6 DependencySurface, DependencyTarget, EdgeKind, GraphBuildInput, GraphBuilder, GraphEdge,
7 GraphNode, GraphNodeKind, ProjectKind, RepoGraph, RepoctlError, WorkspaceInspectionInput,
8 WorkspaceInspector,
9};
10
11use crate::RustWorkspaceInspector;
12
13#[derive(Clone)]
15pub struct DefaultGraphBuilder {
16 inspectors: Vec<Arc<dyn WorkspaceInspector>>,
17}
18
19impl std::fmt::Debug for DefaultGraphBuilder {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 f.debug_struct("DefaultGraphBuilder")
22 .field("inspectors", &self.inspectors.len())
23 .finish()
24 }
25}
26
27impl Default for DefaultGraphBuilder {
28 fn default() -> Self {
29 Self {
30 inspectors: vec![Arc::new(RustWorkspaceInspector)],
31 }
32 }
33}
34
35impl DefaultGraphBuilder {
36 pub fn new(inspectors: Vec<Arc<dyn WorkspaceInspector>>) -> Self {
38 Self { inspectors }
39 }
40}
41
42impl GraphBuilder for DefaultGraphBuilder {
43 fn build(&self, input: GraphBuildInput) -> Result<RepoGraph, RepoctlError> {
44 let mut graph = RepoGraph::default();
45 let by_name = input
46 .projects
47 .iter()
48 .map(|project| (project.name.clone(), project))
49 .collect::<BTreeMap<_, _>>();
50 for project in &input.projects {
51 graph.add_node(GraphNode {
52 id: project.node_id(),
53 kind: GraphNodeKind::Project,
54 label: project.name.to_string(),
55 project: Some(project.name.clone()),
56 workspace: None,
57 });
58 for workspace in &project.workspaces {
59 let workspace_node = format!("workspace:{}:{}", project.name, workspace.name);
60 graph.add_node(GraphNode {
61 id: workspace_node.clone(),
62 kind: GraphNodeKind::Workspace,
63 label: workspace.name.to_string(),
64 project: Some(project.name.clone()),
65 workspace: Some(workspace.name.clone()),
66 });
67 graph.add_edge(GraphEdge {
68 from: project.node_id(),
69 to: workspace_node,
70 kind: EdgeKind::ContainsWorkspace,
71 evidence: Some(workspace.manifest.to_string()),
72 });
73 }
74 add_proto_edges(&mut graph, project);
75 add_iac_edges(&mut graph, project);
76 }
77 for project in &input.projects {
78 for dependency in &project.depends_on {
79 match &dependency.target {
80 DependencyTarget::Project(target_name) => {
81 if let Some(target) = by_name.get(target_name) {
82 graph.add_edge(GraphEdge {
83 from: project.node_id(),
84 to: target.node_id(),
85 kind: classify_declared_dependency(
86 &project.kind,
87 &target.kind,
88 &dependency.surface,
89 ),
90 evidence: Some(dependency.id.clone()),
91 });
92 }
93 }
94 DependencyTarget::ProtoPackage(package) => {
95 let node_id = format!("proto:{package}");
96 graph.add_node(GraphNode {
97 id: node_id.clone(),
98 kind: GraphNodeKind::ProtoPackage,
99 label: package.to_string(),
100 project: None,
101 workspace: None,
102 });
103 graph.add_edge(GraphEdge {
104 from: project.node_id(),
105 to: node_id,
106 kind: EdgeKind::ConsumesProto,
107 evidence: Some(dependency.id.clone()),
108 });
109 }
110 }
111 }
112 }
113 for project in &input.projects {
114 for workspace in &project.workspaces {
115 for inspector in &self.inspectors {
116 if inspector.language() != workspace.language {
117 continue;
118 }
119 let inspection = WorkspaceInspectionInput {
120 root: &input.root,
121 projects: &input.projects,
122 project,
123 workspace,
124 };
125 for discovered in inspector.inspect(&inspection)? {
126 graph.add_edge(repoctl_core::discovered_to_graph_edge(&discovered));
127 }
128 }
129 }
130 }
131 graph.nodes.sort_by(|left, right| left.id.cmp(&right.id));
132 graph.edges.sort_by(|left, right| {
133 (&left.from, &left.to, edge_rank(&left.kind), &left.evidence).cmp(&(
134 &right.from,
135 &right.to,
136 edge_rank(&right.kind),
137 &right.evidence,
138 ))
139 });
140 Ok(graph)
141 }
142}
143
144fn add_proto_edges(graph: &mut RepoGraph, project: &repoctl_core::ProjectManifest) {
145 for pattern in &project.protos.owns {
146 let node_id = format!("proto:{}", pattern.as_str());
147 graph.add_node(GraphNode {
148 id: node_id.clone(),
149 kind: GraphNodeKind::ProtoPackage,
150 label: pattern.to_string(),
151 project: None,
152 workspace: None,
153 });
154 graph.add_edge(GraphEdge {
155 from: project.node_id(),
156 to: node_id,
157 kind: EdgeKind::OwnsProto,
158 evidence: Some(pattern.to_string()),
159 });
160 }
161 for pattern in &project.protos.consumes {
162 let node_id = format!("proto:{}", pattern.as_str());
163 graph.add_node(GraphNode {
164 id: node_id.clone(),
165 kind: GraphNodeKind::ProtoPackage,
166 label: pattern.to_string(),
167 project: None,
168 workspace: None,
169 });
170 graph.add_edge(GraphEdge {
171 from: project.node_id(),
172 to: node_id,
173 kind: EdgeKind::ConsumesProto,
174 evidence: Some(pattern.to_string()),
175 });
176 }
177}
178
179fn add_iac_edges(graph: &mut RepoGraph, project: &repoctl_core::ProjectManifest) {
180 let Some(iac) = &project.iac else {
181 return;
182 };
183 if iac.stacks.is_empty() {
184 add_iac_target(graph, project, "default", &iac.root);
185 return;
186 }
187 for stack in &iac.stacks {
188 add_iac_target(graph, project, stack, &iac.root);
189 }
190}
191
192fn add_iac_target(
193 graph: &mut RepoGraph,
194 project: &repoctl_core::ProjectManifest,
195 stack: &str,
196 root: &repoctl_core::ProjectRelativePath,
197) {
198 let node_id = format!("iac:{}:{stack}", project.name);
199 graph.add_node(GraphNode {
200 id: node_id.clone(),
201 kind: GraphNodeKind::IacTarget,
202 label: stack.to_string(),
203 project: Some(project.name.clone()),
204 workspace: None,
205 });
206 graph.add_edge(GraphEdge {
207 from: project.node_id(),
208 to: node_id,
209 kind: EdgeKind::OwnsIac,
210 evidence: Some(root.to_string()),
211 });
212}
213
214fn classify_declared_dependency(
215 source: &ProjectKind,
216 target: &ProjectKind,
217 surface: &DependencySurface,
218) -> EdgeKind {
219 match (source, target, surface) {
220 (ProjectKind::App, ProjectKind::Framework, DependencySurface::FrameworkInternal) => {
221 EdgeKind::UsesFrameworkInternal
222 }
223 (ProjectKind::App, ProjectKind::Framework, _) => EdgeKind::UsesFrameworkFacade,
224 (
225 ProjectKind::App,
226 ProjectKind::FoundationService,
227 DependencySurface::FoundationPublicClient,
228 ) => EdgeKind::UsesFoundationClient,
229 (ProjectKind::App, ProjectKind::FoundationService, _) => EdgeKind::UsesFoundationInternal,
230 (
231 _,
232 ProjectKind::CoreInfra | ProjectKind::CoreInfraComponent,
233 DependencySurface::CoreInfraInternalModule,
234 ) => EdgeKind::UsesCoreInfraInternalModule,
235 (_, ProjectKind::CoreInfra | ProjectKind::CoreInfraComponent, _) => {
236 EdgeKind::UsesCoreInfraModule
237 }
238 _ => EdgeKind::DependsOnProject,
239 }
240}
241
242fn edge_rank(kind: &EdgeKind) -> u8 {
243 match kind {
244 EdgeKind::DependsOnProject => 0,
245 EdgeKind::ContainsWorkspace => 1,
246 EdgeKind::ConsumesProto => 2,
247 EdgeKind::OwnsProto => 3,
248 EdgeKind::UsesFrameworkFacade => 4,
249 EdgeKind::UsesFrameworkInternal => 5,
250 EdgeKind::UsesFoundationClient => 6,
251 EdgeKind::UsesFoundationInternal => 7,
252 EdgeKind::UsesCoreInfraModule => 8,
253 EdgeKind::UsesCoreInfraInternalModule => 9,
254 EdgeKind::OwnsIac => 10,
255 EdgeKind::RunsTask => 11,
256 }
257}