use std::collections::BTreeMap;
use crate::{
RepoManifest, TemplateFile,
diagnostic::{Diagnostic, RepoctlError},
domain::{
EdgeKind, GraphEdge, ProcessCommand, ProcessOutput, ProjectManifest, RepoGraph,
RepoRelativePath, RepoRoot, RepoSnapshot, ResolvedTemplateSource, TemplateManifest,
TemplateSource, Toolchain, WorkspaceLanguage, WorkspaceSpec,
},
manifest::ManifestSource,
};
pub trait RepoLocator: Send + Sync {
fn locate(&self, start: Option<&std::path::Path>) -> Result<RepoRoot, RepoctlError>;
}
#[derive(Clone, Debug)]
pub struct FixedRepoLocator {
pub root: RepoRoot,
}
impl RepoLocator for FixedRepoLocator {
fn locate(&self, _start: Option<&std::path::Path>) -> Result<RepoRoot, RepoctlError> {
Ok(self.root.clone())
}
}
pub trait RepoFileSystem: Send + Sync {
fn read_file(&self, root: &RepoRoot, path: &RepoRelativePath) -> Result<Vec<u8>, RepoctlError>;
fn walk(
&self,
root: &RepoRoot,
request: &WalkRequest,
) -> Result<Vec<RepoRelativePath>, RepoctlError>;
}
#[derive(Clone, Debug, Default)]
pub struct InMemoryRepoFileSystem {
files: BTreeMap<RepoRelativePath, Vec<u8>>,
}
impl InMemoryRepoFileSystem {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, path: RepoRelativePath, bytes: impl Into<Vec<u8>>) -> Option<Vec<u8>> {
self.files.insert(path, bytes.into())
}
}
impl RepoFileSystem for InMemoryRepoFileSystem {
fn read_file(
&self,
_root: &RepoRoot,
path: &RepoRelativePath,
) -> Result<Vec<u8>, RepoctlError> {
self.files.get(path).cloned().ok_or_else(|| {
RepoctlError::diagnostic(
Diagnostic::error("repo.fs.not_found", format!("file `{path}` was not found"))
.with_path(path.as_str()),
)
})
}
fn walk(
&self,
_root: &RepoRoot,
request: &WalkRequest,
) -> Result<Vec<RepoRelativePath>, RepoctlError> {
let mut files = self
.files
.keys()
.filter(|path| request.roots.iter().any(|root| path.starts_with(root)))
.take(request.max_files.saturating_add(1))
.cloned()
.collect::<Vec<_>>();
if files.len() > request.max_files {
return Err(RepoctlError::diagnostic(Diagnostic::error(
"repo.walk.too_many_files",
format!("repository walk exceeded {} files", request.max_files),
)));
}
files.sort();
Ok(files)
}
}
pub trait ManifestParser: Send + Sync {
fn parse_repo(&self, source: ManifestSource) -> Result<RepoManifest, RepoctlError>;
fn parse_project(&self, source: ManifestSource) -> Result<ProjectManifest, RepoctlError>;
fn parse_template(&self, source: ManifestSource) -> Result<TemplateManifest, RepoctlError>;
}
#[derive(Clone, Debug)]
pub struct WalkRequest {
pub roots: Vec<RepoRelativePath>,
pub max_files: usize,
}
impl Default for WalkRequest {
fn default() -> Self {
Self {
roots: vec![RepoRelativePath::root()],
max_files: 20_000,
}
}
}
#[derive(Clone, Debug)]
pub struct GraphBuildInput {
pub root: RepoRoot,
pub repo_manifest: RepoManifest,
pub projects: Vec<ProjectManifest>,
}
pub trait GraphBuilder: Send + Sync {
fn build(&self, input: GraphBuildInput) -> Result<RepoGraph, RepoctlError>;
}
#[derive(Clone, Debug, Default)]
pub struct StaticGraphBuilder {
pub graph: RepoGraph,
}
impl GraphBuilder for StaticGraphBuilder {
fn build(&self, _input: GraphBuildInput) -> Result<RepoGraph, RepoctlError> {
Ok(self.graph.clone())
}
}
#[derive(Clone, Debug)]
pub struct WorkspaceInspectionInput<'a> {
pub root: &'a RepoRoot,
pub projects: &'a [ProjectManifest],
pub project: &'a ProjectManifest,
pub workspace: &'a WorkspaceSpec,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DiscoveredEdge {
pub from_project: String,
pub from_workspace: String,
pub to_project: String,
pub kind: EdgeKind,
pub evidence: Option<String>,
}
pub trait WorkspaceInspector: Send + Sync {
fn language(&self) -> WorkspaceLanguage;
fn inspect(
&self,
input: &WorkspaceInspectionInput<'_>,
) -> Result<Vec<DiscoveredEdge>, RepoctlError>;
}
#[derive(Clone, Debug)]
pub struct PolicyContext<'a> {
pub snapshot: &'a RepoSnapshot,
pub changed_files: &'a [RepoRelativePath],
}
pub trait PolicyRule: Send + Sync {
fn name(&self) -> &'static str;
fn evaluate(&self, context: &PolicyContext<'_>) -> Result<Vec<Diagnostic>, RepoctlError>;
}
pub trait ProcessRunner: Send + Sync {
fn run(&self, command: &ProcessCommand) -> Result<ProcessOutput, RepoctlError>;
}
#[derive(Clone, Debug)]
pub struct ToolchainEnvironmentInput<'a> {
pub workspace: &'a WorkspaceSpec,
}
pub trait ToolchainAdapter: Send + Sync {
fn toolchain(&self) -> Toolchain;
fn environment(
&self,
input: &ToolchainEnvironmentInput<'_>,
) -> Result<BTreeMap<String, String>, RepoctlError>;
}
pub trait TemplateSourceResolver: Send + Sync {
fn resolve(
&self,
root: &RepoRoot,
source: &TemplateSource,
) -> Result<ResolvedTemplateSource, RepoctlError>;
}
#[derive(Clone, Debug)]
pub struct RenderRequest {
pub template: TemplateManifest,
pub file: TemplateFile,
pub context: serde_json::Value,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RenderedTemplate {
pub bytes: Vec<u8>,
}
pub trait TemplateEngine: Send + Sync {
fn render(&self, request: &RenderRequest) -> Result<RenderedTemplate, RepoctlError>;
}
#[derive(Clone, Debug, Default)]
pub struct FakeProcessRunner {
pub output: ProcessOutput,
}
impl ProcessRunner for FakeProcessRunner {
fn run(&self, _command: &ProcessCommand) -> Result<ProcessOutput, RepoctlError> {
Ok(self.output.clone())
}
}
#[derive(Clone, Debug, Default)]
pub struct FakeTemplateEngine;
impl TemplateEngine for FakeTemplateEngine {
fn render(&self, request: &RenderRequest) -> Result<RenderedTemplate, RepoctlError> {
Ok(RenderedTemplate {
bytes: request.file.target.as_bytes().to_vec(),
})
}
}
pub fn discovered_to_graph_edge(edge: &DiscoveredEdge) -> GraphEdge {
GraphEdge {
from: format!("project:{}", edge.from_project),
to: format!("project:{}", edge.to_project),
kind: edge.kind.clone(),
evidence: edge.evidence.clone(),
}
}