1use std::collections::BTreeMap;
4
5use crate::{
6 RepoManifest, TemplateFile,
7 diagnostic::{Diagnostic, RepoctlError},
8 domain::{
9 EdgeKind, GraphEdge, ProcessCommand, ProcessOutput, ProjectManifest, RepoGraph,
10 RepoRelativePath, RepoRoot, RepoSnapshot, ResolvedTemplateSource, TemplateManifest,
11 TemplateSource, Toolchain, WorkspaceLanguage, WorkspaceSpec,
12 },
13 manifest::ManifestSource,
14};
15
16pub trait RepoLocator: Send + Sync {
18 fn locate(&self, start: Option<&std::path::Path>) -> Result<RepoRoot, RepoctlError>;
20}
21
22#[derive(Clone, Debug)]
24pub struct FixedRepoLocator {
25 pub root: RepoRoot,
27}
28
29impl RepoLocator for FixedRepoLocator {
30 fn locate(&self, _start: Option<&std::path::Path>) -> Result<RepoRoot, RepoctlError> {
31 Ok(self.root.clone())
32 }
33}
34
35pub trait RepoFileSystem: Send + Sync {
37 fn read_file(&self, root: &RepoRoot, path: &RepoRelativePath) -> Result<Vec<u8>, RepoctlError>;
39
40 fn walk(
42 &self,
43 root: &RepoRoot,
44 request: &WalkRequest,
45 ) -> Result<Vec<RepoRelativePath>, RepoctlError>;
46}
47
48#[derive(Clone, Debug, Default)]
50pub struct InMemoryRepoFileSystem {
51 files: BTreeMap<RepoRelativePath, Vec<u8>>,
52}
53
54impl InMemoryRepoFileSystem {
55 pub fn new() -> Self {
57 Self::default()
58 }
59
60 pub fn insert(&mut self, path: RepoRelativePath, bytes: impl Into<Vec<u8>>) -> Option<Vec<u8>> {
62 self.files.insert(path, bytes.into())
63 }
64}
65
66impl RepoFileSystem for InMemoryRepoFileSystem {
67 fn read_file(
68 &self,
69 _root: &RepoRoot,
70 path: &RepoRelativePath,
71 ) -> Result<Vec<u8>, RepoctlError> {
72 self.files.get(path).cloned().ok_or_else(|| {
73 RepoctlError::diagnostic(
74 Diagnostic::error("repo.fs.not_found", format!("file `{path}` was not found"))
75 .with_path(path.as_str()),
76 )
77 })
78 }
79
80 fn walk(
81 &self,
82 _root: &RepoRoot,
83 request: &WalkRequest,
84 ) -> Result<Vec<RepoRelativePath>, RepoctlError> {
85 let mut files = self
86 .files
87 .keys()
88 .filter(|path| request.roots.iter().any(|root| path.starts_with(root)))
89 .take(request.max_files.saturating_add(1))
90 .cloned()
91 .collect::<Vec<_>>();
92 if files.len() > request.max_files {
93 return Err(RepoctlError::diagnostic(Diagnostic::error(
94 "repo.walk.too_many_files",
95 format!("repository walk exceeded {} files", request.max_files),
96 )));
97 }
98 files.sort();
99 Ok(files)
100 }
101}
102
103pub trait ManifestParser: Send + Sync {
105 fn parse_repo(&self, source: ManifestSource) -> Result<RepoManifest, RepoctlError>;
107
108 fn parse_project(&self, source: ManifestSource) -> Result<ProjectManifest, RepoctlError>;
110
111 fn parse_template(&self, source: ManifestSource) -> Result<TemplateManifest, RepoctlError>;
113}
114
115#[derive(Clone, Debug)]
117pub struct WalkRequest {
118 pub roots: Vec<RepoRelativePath>,
120 pub max_files: usize,
122}
123
124impl Default for WalkRequest {
125 fn default() -> Self {
126 Self {
127 roots: vec![RepoRelativePath::root()],
128 max_files: 20_000,
129 }
130 }
131}
132
133#[derive(Clone, Debug)]
135pub struct GraphBuildInput {
136 pub root: RepoRoot,
138 pub repo_manifest: RepoManifest,
140 pub projects: Vec<ProjectManifest>,
142}
143
144pub trait GraphBuilder: Send + Sync {
146 fn build(&self, input: GraphBuildInput) -> Result<RepoGraph, RepoctlError>;
148}
149
150#[derive(Clone, Debug, Default)]
152pub struct StaticGraphBuilder {
153 pub graph: RepoGraph,
155}
156
157impl GraphBuilder for StaticGraphBuilder {
158 fn build(&self, _input: GraphBuildInput) -> Result<RepoGraph, RepoctlError> {
159 Ok(self.graph.clone())
160 }
161}
162
163#[derive(Clone, Debug)]
165pub struct WorkspaceInspectionInput<'a> {
166 pub root: &'a RepoRoot,
168 pub projects: &'a [ProjectManifest],
170 pub project: &'a ProjectManifest,
172 pub workspace: &'a WorkspaceSpec,
174}
175
176#[derive(Clone, Debug, Eq, PartialEq)]
178pub struct DiscoveredEdge {
179 pub from_project: String,
181 pub from_workspace: String,
183 pub to_project: String,
185 pub kind: EdgeKind,
187 pub evidence: Option<String>,
189}
190
191pub trait WorkspaceInspector: Send + Sync {
193 fn language(&self) -> WorkspaceLanguage;
195
196 fn inspect(
198 &self,
199 input: &WorkspaceInspectionInput<'_>,
200 ) -> Result<Vec<DiscoveredEdge>, RepoctlError>;
201}
202
203#[derive(Clone, Debug)]
205pub struct PolicyContext<'a> {
206 pub snapshot: &'a RepoSnapshot,
208 pub changed_files: &'a [RepoRelativePath],
210}
211
212pub trait PolicyRule: Send + Sync {
214 fn name(&self) -> &'static str;
216
217 fn evaluate(&self, context: &PolicyContext<'_>) -> Result<Vec<Diagnostic>, RepoctlError>;
219}
220
221pub trait ProcessRunner: Send + Sync {
223 fn run(&self, command: &ProcessCommand) -> Result<ProcessOutput, RepoctlError>;
225}
226
227#[derive(Clone, Debug)]
229pub struct ToolchainEnvironmentInput<'a> {
230 pub workspace: &'a WorkspaceSpec,
232}
233
234pub trait ToolchainAdapter: Send + Sync {
236 fn toolchain(&self) -> Toolchain;
238
239 fn environment(
241 &self,
242 input: &ToolchainEnvironmentInput<'_>,
243 ) -> Result<BTreeMap<String, String>, RepoctlError>;
244}
245
246pub trait TemplateSourceResolver: Send + Sync {
248 fn resolve(
250 &self,
251 root: &RepoRoot,
252 source: &TemplateSource,
253 ) -> Result<ResolvedTemplateSource, RepoctlError>;
254}
255
256#[derive(Clone, Debug)]
258pub struct RenderRequest {
259 pub template: TemplateManifest,
261 pub file: TemplateFile,
263 pub context: serde_json::Value,
265}
266
267#[derive(Clone, Debug, Eq, PartialEq)]
269pub struct RenderedTemplate {
270 pub bytes: Vec<u8>,
272}
273
274pub trait TemplateEngine: Send + Sync {
276 fn render(&self, request: &RenderRequest) -> Result<RenderedTemplate, RepoctlError>;
278}
279
280#[derive(Clone, Debug, Default)]
282pub struct FakeProcessRunner {
283 pub output: ProcessOutput,
285}
286
287impl ProcessRunner for FakeProcessRunner {
288 fn run(&self, _command: &ProcessCommand) -> Result<ProcessOutput, RepoctlError> {
289 Ok(self.output.clone())
290 }
291}
292
293#[derive(Clone, Debug, Default)]
295pub struct FakeTemplateEngine;
296
297impl TemplateEngine for FakeTemplateEngine {
298 fn render(&self, request: &RenderRequest) -> Result<RenderedTemplate, RepoctlError> {
299 Ok(RenderedTemplate {
300 bytes: request.file.target.as_bytes().to_vec(),
301 })
302 }
303}
304
305pub fn discovered_to_graph_edge(edge: &DiscoveredEdge) -> GraphEdge {
307 GraphEdge {
308 from: format!("project:{}", edge.from_project),
309 to: format!("project:{}", edge.to_project),
310 kind: edge.kind.clone(),
311 evidence: edge.evidence.clone(),
312 }
313}