Skip to main content

repoctl_core/
ports.rs

1//! Infrastructure and capability-service ports.
2
3use 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
16/// Locates a repository root from a starting path.
17pub trait RepoLocator: Send + Sync {
18    /// Finds the repository root.
19    fn locate(&self, start: Option<&std::path::Path>) -> Result<RepoRoot, RepoctlError>;
20}
21
22/// Fixed repo locator useful for tests.
23#[derive(Clone, Debug)]
24pub struct FixedRepoLocator {
25    /// Root returned for every locate request.
26    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
35/// Filesystem operations used by repoctl services.
36pub trait RepoFileSystem: Send + Sync {
37    /// Reads a repo-relative file.
38    fn read_file(&self, root: &RepoRoot, path: &RepoRelativePath) -> Result<Vec<u8>, RepoctlError>;
39
40    /// Walks repo-relative files according to the request.
41    fn walk(
42        &self,
43        root: &RepoRoot,
44        request: &WalkRequest,
45    ) -> Result<Vec<RepoRelativePath>, RepoctlError>;
46}
47
48/// In-memory repo filesystem useful for service tests.
49#[derive(Clone, Debug, Default)]
50pub struct InMemoryRepoFileSystem {
51    files: BTreeMap<RepoRelativePath, Vec<u8>>,
52}
53
54impl InMemoryRepoFileSystem {
55    /// Creates an empty in-memory filesystem.
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// Inserts a repo-relative file.
61    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
103/// Manifest parser boundary.
104pub trait ManifestParser: Send + Sync {
105    /// Parses a repo manifest.
106    fn parse_repo(&self, source: ManifestSource) -> Result<RepoManifest, RepoctlError>;
107
108    /// Parses a project manifest.
109    fn parse_project(&self, source: ManifestSource) -> Result<ProjectManifest, RepoctlError>;
110
111    /// Parses a template manifest.
112    fn parse_template(&self, source: ManifestSource) -> Result<TemplateManifest, RepoctlError>;
113}
114
115/// Request for a bounded repository walk.
116#[derive(Clone, Debug)]
117pub struct WalkRequest {
118    /// Root prefixes to walk.
119    pub roots: Vec<RepoRelativePath>,
120    /// Maximum files to return.
121    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/// Input required to build a repository graph.
134#[derive(Clone, Debug)]
135pub struct GraphBuildInput {
136    /// Repository root.
137    pub root: RepoRoot,
138    /// Repo-level manifest.
139    pub repo_manifest: RepoManifest,
140    /// Project manifests.
141    pub projects: Vec<ProjectManifest>,
142}
143
144/// Builds a repository graph from validated manifests and adapter-discovered edges.
145pub trait GraphBuilder: Send + Sync {
146    /// Builds the graph.
147    fn build(&self, input: GraphBuildInput) -> Result<RepoGraph, RepoctlError>;
148}
149
150/// Static graph builder useful for tests.
151#[derive(Clone, Debug, Default)]
152pub struct StaticGraphBuilder {
153    /// Graph returned for every build request.
154    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/// Input passed to workspace inspectors.
164#[derive(Clone, Debug)]
165pub struct WorkspaceInspectionInput<'a> {
166    /// Repository root.
167    pub root: &'a RepoRoot,
168    /// All discovered projects.
169    pub projects: &'a [ProjectManifest],
170    /// Project owning the workspace.
171    pub project: &'a ProjectManifest,
172    /// Workspace to inspect.
173    pub workspace: &'a WorkspaceSpec,
174}
175
176/// Edge discovered by a language or toolchain adapter.
177#[derive(Clone, Debug, Eq, PartialEq)]
178pub struct DiscoveredEdge {
179    /// Source project.
180    pub from_project: String,
181    /// Source workspace.
182    pub from_workspace: String,
183    /// Target project.
184    pub to_project: String,
185    /// Edge kind.
186    pub kind: EdgeKind,
187    /// Evidence path or package name.
188    pub evidence: Option<String>,
189}
190
191/// Inspects one workspace type for adapter-discovered dependencies.
192pub trait WorkspaceInspector: Send + Sync {
193    /// Language handled by the inspector.
194    fn language(&self) -> WorkspaceLanguage;
195
196    /// Discovers graph edges from the workspace.
197    fn inspect(
198        &self,
199        input: &WorkspaceInspectionInput<'_>,
200    ) -> Result<Vec<DiscoveredEdge>, RepoctlError>;
201}
202
203/// Context passed to policy rules.
204#[derive(Clone, Debug)]
205pub struct PolicyContext<'a> {
206    /// Repository snapshot.
207    pub snapshot: &'a RepoSnapshot,
208    /// Changed files used by path-based rules.
209    pub changed_files: &'a [RepoRelativePath],
210}
211
212/// Boundary or hygiene policy rule.
213pub trait PolicyRule: Send + Sync {
214    /// Stable rule name.
215    fn name(&self) -> &'static str;
216
217    /// Evaluates the rule.
218    fn evaluate(&self, context: &PolicyContext<'_>) -> Result<Vec<Diagnostic>, RepoctlError>;
219}
220
221/// Runs a process in argv form.
222pub trait ProcessRunner: Send + Sync {
223    /// Runs a command and captures output.
224    fn run(&self, command: &ProcessCommand) -> Result<ProcessOutput, RepoctlError>;
225}
226
227/// Input passed to language-specific toolchain adapters.
228#[derive(Clone, Debug)]
229pub struct ToolchainEnvironmentInput<'a> {
230    /// Workspace requiring task environment setup.
231    pub workspace: &'a WorkspaceSpec,
232}
233
234/// Supplies task environment variables for one workspace toolchain.
235pub trait ToolchainAdapter: Send + Sync {
236    /// Toolchain handled by the adapter.
237    fn toolchain(&self) -> Toolchain;
238
239    /// Returns environment variables for the workspace.
240    fn environment(
241        &self,
242        input: &ToolchainEnvironmentInput<'_>,
243    ) -> Result<BTreeMap<String, String>, RepoctlError>;
244}
245
246/// Resolves built-in and repo-local template sources.
247pub trait TemplateSourceResolver: Send + Sync {
248    /// Resolves a template source into a manifest and source root.
249    fn resolve(
250        &self,
251        root: &RepoRoot,
252        source: &TemplateSource,
253    ) -> Result<ResolvedTemplateSource, RepoctlError>;
254}
255
256/// Template render request.
257#[derive(Clone, Debug)]
258pub struct RenderRequest {
259    /// Template manifest.
260    pub template: TemplateManifest,
261    /// Template file mapping.
262    pub file: TemplateFile,
263    /// JSON render context.
264    pub context: serde_json::Value,
265}
266
267/// Rendered template content.
268#[derive(Clone, Debug, Eq, PartialEq)]
269pub struct RenderedTemplate {
270    /// Rendered bytes.
271    pub bytes: Vec<u8>,
272}
273
274/// Template engine boundary.
275pub trait TemplateEngine: Send + Sync {
276    /// Renders one template file.
277    fn render(&self, request: &RenderRequest) -> Result<RenderedTemplate, RepoctlError>;
278}
279
280/// In-memory process runner useful for tests.
281#[derive(Clone, Debug, Default)]
282pub struct FakeProcessRunner {
283    /// Output returned for every command.
284    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/// Deterministic fake template engine useful for tests.
294#[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
305/// Converts a discovered edge into a graph edge.
306pub 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}