use std::path::Path;
#[derive(Debug, Clone)]
pub struct ValidationIssue {
pub kind: IssueKind,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IssueKind {
MissingFile,
MalformedContent,
SubstrateWiring,
}
pub trait Validator: Sync {
fn validate(&self, path: &Path) -> Vec<ValidationIssue>;
}
fn check_file(path: &Path, relative: &str, kind: IssueKind) -> Option<ValidationIssue> {
if path.join(relative).is_file() { return None; }
Some(ValidationIssue { kind, message: format!("missing required file: {relative}") })
}
fn check_text_contains(
path: &Path, relative: &str, needle: &str, kind: IssueKind,
) -> Option<ValidationIssue> {
let p = path.join(relative);
if !p.is_file() {
return Some(ValidationIssue { kind: IssueKind::MissingFile,
message: format!("missing required file: {relative}") });
}
let text = std::fs::read_to_string(&p).unwrap_or_default();
if text.contains(needle) { return None; }
Some(ValidationIssue { kind,
message: format!("{relative}: missing required marker {needle:?}") })
}
fn check_substrate_wiring(path: &Path) -> Vec<ValidationIssue> {
let mut issues = Vec::new();
if let Some(i) = check_file(path, ".github/workflows/auto-release.yml", IssueKind::MissingFile) { issues.push(i); }
if let Some(i) = check_file(path, ".pleme-io-release.toml", IssueKind::MissingFile) { issues.push(i); }
if !contains_substrate_release_marker(path) {
issues.push(ValidationIssue {
kind: IssueKind::SubstrateWiring,
message: String::from(
".github/workflows/auto-release.yml: missing substrate release-workflow delegation \
(expected `uses: pleme-io/substrate/.github/workflows/<lang>-(auto-)release.yml@main`)",
),
});
}
if let Some(i) = check_text_contains(path, ".pleme-io-release.toml",
"[bump]", IssueKind::SubstrateWiring) { issues.push(i); }
if let Some(i) = check_text_contains(path, ".pleme-io-release.toml",
"default-type = \"patch\"", IssueKind::SubstrateWiring) { issues.push(i); }
issues
}
fn contains_substrate_release_marker(path: &Path) -> bool {
let p = path.join(".github/workflows/auto-release.yml");
let Ok(text) = std::fs::read_to_string(&p) else { return false; };
text.lines().any(|l| {
let t = l.trim();
t.starts_with("uses: pleme-io/substrate/.github/workflows/")
&& (t.ends_with("-release.yml@main")
|| t.ends_with("-auto-release.yml@main")
|| t.ends_with("/auto-release.yml@main"))
})
}
fn check_persistent_spec(path: &Path, expected_name: &str) -> Option<ValidationIssue> {
if !expected_name.is_empty() {
let mut name = String::from(expected_name);
name.push_str(".caixa.lisp");
if path.join(&name).is_file() { return None; }
}
if let Ok(entries) = std::fs::read_dir(path) {
if entries.flatten().any(|e| e.file_name().to_string_lossy().ends_with(".caixa.lisp")) {
return None;
}
}
Some(ValidationIssue { kind: IssueKind::MissingFile,
message: "no .caixa.lisp source persisted in output directory".to_string() })
}
pub struct RustSingleCrateValidator;
impl Validator for RustSingleCrateValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "Cargo.toml", "[package]", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "Cargo.toml", "edition", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct RustWorkspaceValidator;
impl Validator for RustWorkspaceValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "Cargo.toml", "[workspace]", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct NpmValidator;
impl Validator for NpmValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "package.json", "\"name\"", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "package.json", "\"version\"", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct PythonValidator;
impl Validator for PythonValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "pyproject.toml", "[project]", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct HelmValidator;
impl Validator for HelmValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "Chart.yaml", "apiVersion:", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "Chart.yaml", "name:", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct RubyGemValidator;
impl Validator for RubyGemValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
let has_gemspec = std::fs::read_dir(path).ok()
.map(|entries| entries.flatten().any(|e|
e.file_name().to_str()
.map(|n| n.ends_with(".gemspec"))
.unwrap_or(false)))
.unwrap_or(false);
if !has_gemspec {
issues.push(ValidationIssue {
kind: IssueKind::MissingFile,
message: "no *.gemspec at repo root".into(),
});
}
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct OcamlDuneValidator;
impl Validator for OcamlDuneValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "dune-project", "(lang dune", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "dune-project", "(name ", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct SwiftSpmValidator;
impl Validator for SwiftSpmValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "Package.swift", "swift-tools-version:", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "Package.swift", "Package(", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct ElixirMixValidator;
impl Validator for ElixirMixValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "mix.exs", "defmodule", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "mix.exs", "use Mix.Project", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct JavaGradleKtsValidator;
impl Validator for JavaGradleKtsValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_file(path, "build.gradle.kts", IssueKind::MissingFile) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct CppCmakeValidator;
impl Validator for CppCmakeValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "CMakeLists.txt", "project(", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "CMakeLists.txt", "cmake_minimum_required", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct CppMesonValidator;
impl Validator for CppMesonValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "meson.build", "project(", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct CppConanValidator;
impl Validator for CppConanValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "conanfile.py", "ConanFile", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct HaskellCabalValidator;
impl Validator for HaskellCabalValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
let has_cabal = std::fs::read_dir(path).ok().map(|es| es.flatten().any(|e|
e.file_name().to_str().map(|n| n.ends_with(".cabal")).unwrap_or(false))).unwrap_or(false);
if !has_cabal {
issues.push(ValidationIssue { kind: IssueKind::MissingFile, message: "no *.cabal at repo root".into() });
}
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct NimNimbleValidator;
impl Validator for NimNimbleValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
let has_nimble = std::fs::read_dir(path).ok().map(|es| es.flatten().any(|e|
e.file_name().to_str().map(|n| n.ends_with(".nimble")).unwrap_or(false))).unwrap_or(false);
if !has_nimble {
issues.push(ValidationIssue { kind: IssueKind::MissingFile, message: "no *.nimble at repo root".into() });
}
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct LuaRockspecValidator;
impl Validator for LuaRockspecValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
let has_rs = std::fs::read_dir(path).ok().map(|es| es.flatten().any(|e|
e.file_name().to_str().map(|n| n.ends_with(".rockspec")).unwrap_or(false))).unwrap_or(false);
if !has_rs {
issues.push(ValidationIssue { kind: IssueKind::MissingFile, message: "no *.rockspec at repo root".into() });
}
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct RDescriptionValidator;
impl Validator for RDescriptionValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "DESCRIPTION", "Package:", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "DESCRIPTION", "Version:", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct PythonPipenvValidator;
impl Validator for PythonPipenvValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_file(path, "Pipfile", IssueKind::MissingFile) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct GithubActionValidator;
impl Validator for GithubActionValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if !path.join("action.yml").is_file() && !path.join("action.yaml").is_file() {
issues.push(ValidationIssue { kind: IssueKind::MissingFile, message: "no action.yml or action.yaml at repo root".into() });
}
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct JavaMavenValidator;
impl Validator for JavaMavenValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "pom.xml", "<groupId>", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "pom.xml", "<artifactId>", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct JsDenoValidator;
impl Validator for JsDenoValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "deno.json", "\"name\"", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct CppVcpkgValidator;
impl Validator for CppVcpkgValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "vcpkg.json", "\"name\"", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct PythonCondaValidator;
impl Validator for PythonCondaValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "meta.yaml", "package:", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "meta.yaml", "build:", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct AdaAlireValidator;
impl Validator for AdaAlireValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
let has_alire = std::fs::read_dir(path).ok()
.map(|entries| entries.flatten().any(|e|
e.file_name().to_str()
.map(|n| n.starts_with("alire-") && n.ends_with(".toml"))
.unwrap_or(false)))
.unwrap_or(false);
if !has_alire {
issues.push(ValidationIssue {
kind: IssueKind::MissingFile,
message: "no alire-*.toml at repo root".into(),
});
}
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct ZigValidator;
impl Validator for ZigValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_file(path, "build.zig", IssueKind::MissingFile) { issues.push(i); }
if let Some(i) = check_file(path, "build.zig.zon", IssueKind::MissingFile) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct FortranFpmValidator;
impl Validator for FortranFpmValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "fpm.toml", "name =", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "fpm.toml", "version =", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct GleamValidator;
impl Validator for GleamValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "gleam.toml", "name =", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "gleam.toml", "version =", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct RacketInfoValidator;
impl Validator for RacketInfoValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "info.rkt", "#lang info", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "info.rkt", "(define ", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct CrystalValidator;
impl Validator for CrystalValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "shard.yml", "name:", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "shard.yml", "version:", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct DartValidator;
impl Validator for DartValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "pubspec.yaml", "name:", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "pubspec.yaml", "version:", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct ComposerValidator;
impl Validator for ComposerValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "composer.json", "\"name\"", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "composer.json", "\"require\"", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct JuliaValidator;
impl Validator for JuliaValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "Project.toml", "name =", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "Project.toml", "version =", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct ScalaSbtValidator;
impl Validator for ScalaSbtValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "build.sbt", "name :=", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "build.sbt", "scalaVersion :=", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct ClojureDepsValidator;
impl Validator for ClojureDepsValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "deps.edn", ":paths", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "deps.edn", ":deps", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct DotnetCsprojValidator;
impl Validator for DotnetCsprojValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
let has_csproj = std::fs::read_dir(path).ok()
.map(|entries| entries.flatten().any(|e|
e.file_name().to_str()
.map(|n| n.ends_with(".csproj"))
.unwrap_or(false)))
.unwrap_or(false);
if !has_csproj {
issues.push(ValidationIssue {
kind: IssueKind::MissingFile,
message: "no *.csproj at repo root".into(),
});
}
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct GoValidator;
impl Validator for GoValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_text_contains(path, "go.mod", "module ", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_text_contains(path, "go.mod", "go ", IssueKind::MalformedContent) { issues.push(i); }
if let Some(i) = check_file(path, "main.go", IssueKind::MissingFile) { issues.push(i); }
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub struct SubstrateOnlyValidator;
impl Validator for SubstrateOnlyValidator {
fn validate(&self, path: &Path) -> Vec<ValidationIssue> {
let mut issues = check_substrate_wiring(path);
if let Some(i) = check_persistent_spec(path, "") { issues.push(i); }
issues
}
}
pub fn validator_for(ecosystem: &str) -> &'static dyn Validator {
match ecosystem {
"rust-single-crate" => &RustSingleCrateValidator,
"rust-workspace" => &RustWorkspaceValidator,
"npm" | "js-pnpm" => &NpmValidator,
"python" | "python-pdm" => &PythonValidator,
"helm" => &HelmValidator,
"go" => &GoValidator,
"ruby-gem" => &RubyGemValidator,
"ocaml-dune" => &OcamlDuneValidator,
"dotnet-csproj" => &DotnetCsprojValidator,
"swift-spm" => &SwiftSpmValidator,
"elixir-mix" => &ElixirMixValidator,
"scala-sbt" => &ScalaSbtValidator,
"clojure-deps" => &ClojureDepsValidator,
"crystal" => &CrystalValidator,
"dart" => &DartValidator,
"composer" => &ComposerValidator,
"julia" => &JuliaValidator,
"zig" => &ZigValidator,
"fortran-fpm" => &FortranFpmValidator,
"gleam" => &GleamValidator,
"racket-info" => &RacketInfoValidator,
"java-maven" => &JavaMavenValidator,
"js-deno" => &JsDenoValidator,
"cpp-vcpkg" => &CppVcpkgValidator,
"python-conda" => &PythonCondaValidator,
"ada-alire" => &AdaAlireValidator,
"java-gradle-kts" => &JavaGradleKtsValidator,
"cpp-cmake" => &CppCmakeValidator,
"cpp-meson" => &CppMesonValidator,
"cpp-conan" => &CppConanValidator,
"haskell-cabal" => &HaskellCabalValidator,
"nim-nimble" => &NimNimbleValidator,
"lua-rockspec" => &LuaRockspecValidator,
"r-description" => &RDescriptionValidator,
"python-pipenv" => &PythonPipenvValidator,
"github-action" => &GithubActionValidator,
_ => &SubstrateOnlyValidator,
}
}
pub fn validate_dir(path: &Path) -> Option<(String, Vec<ValidationIssue>)> {
let detected = crate::discover::detect(path)?;
let validator = validator_for(detected.ecosystem);
Some((detected.ecosystem.to_string(), validator.validate(path)))
}
#[cfg(test)]
mod tests {
use super::*;
fn mk(files: &[(&str, &str)]) -> tempdir::TempDir {
let tmp = tempdir::TempDir::new("validator").expect("tempdir");
for (n, b) in files {
if let Some(parent) = Path::new(n).parent() {
let _ = std::fs::create_dir_all(tmp.path().join(parent));
}
std::fs::write(tmp.path().join(n), b).expect("write");
}
tmp
}
fn well_formed_rust() -> Vec<(&'static str, &'static str)> {
vec![
("Cargo.toml", "[package]\nname = \"x\"\nedition = \"2024\"\n"),
(".github/workflows/auto-release.yml",
"jobs:\n release:\n uses: pleme-io/substrate/.github/workflows/auto-release.yml@main\n"),
(".pleme-io-release.toml", "[bump]\ndefault-type = \"patch\"\n"),
("x.caixa.lisp", "(defcaixa x)\n"),
]
}
#[test]
fn rust_single_crate_clean_passes() {
let dir = mk(&well_formed_rust());
let issues = RustSingleCrateValidator.validate(dir.path());
assert!(issues.is_empty(), "expected clean, got: {issues:?}");
}
#[test]
fn missing_autorelease_yml_fails_with_typed_kind() {
let mut files = well_formed_rust();
files.retain(|(n, _)| *n != ".github/workflows/auto-release.yml");
let dir = mk(&files);
let issues = RustSingleCrateValidator.validate(dir.path());
assert!(!issues.is_empty());
assert!(issues.iter().any(|i| i.kind == IssueKind::MissingFile),
"expected MissingFile issue; got: {issues:?}");
}
#[test]
fn substrate_wiring_marker_required_in_autorelease_yml() {
let mut files = well_formed_rust();
files.iter_mut().for_each(|f| {
if f.0 == ".github/workflows/auto-release.yml" {
f.1 = "name: bogus\n";
}
});
let dir = mk(&files);
let issues = RustSingleCrateValidator.validate(dir.path());
assert!(issues.iter().any(|i| i.kind == IssueKind::SubstrateWiring));
}
#[test]
fn npm_validator_checks_name_and_version_keys() {
let files = vec![
("package.json", "{\"name\":\"x\",\"version\":\"1.0\"}"),
(".github/workflows/auto-release.yml",
"jobs:\n release:\n uses: pleme-io/substrate/.github/workflows/auto-release.yml@main\n"),
(".pleme-io-release.toml", "[bump]\ndefault-type = \"patch\"\n"),
("x.caixa.lisp", "(defcaixa x)\n"),
];
let dir = mk(&files);
let issues = NpmValidator.validate(dir.path());
assert!(issues.is_empty(), "expected clean npm, got: {issues:?}");
}
#[test]
fn validate_dir_auto_detects_ecosystem() {
let dir = mk(&well_formed_rust());
let (eco, issues) = validate_dir(dir.path()).expect("must detect");
assert_eq!(eco, "rust-single-crate");
assert!(issues.is_empty());
}
#[test]
fn validate_dir_returns_none_for_undetectable() {
let dir = mk(&[("README.md", "no manifest")]);
assert!(validate_dir(dir.path()).is_none());
}
#[test]
fn unknown_ecosystem_falls_back_to_substrate_only_validator() {
let v = validator_for("some-future-ecosystem");
let files = vec![
(".github/workflows/auto-release.yml",
"jobs:\n release:\n uses: pleme-io/substrate/.github/workflows/auto-release.yml@main\n"),
(".pleme-io-release.toml", "[bump]\ndefault-type = \"patch\"\n"),
("x.caixa.lisp", "(defcaixa x)\n"),
];
let dir = mk(&files);
let issues = v.validate(dir.path());
assert!(issues.is_empty(), "substrate-only floor should pass; got: {issues:?}");
}
}