use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FileRole {
Service,
Controller,
Model,
Repository,
Utility,
Config,
Test,
Migration,
Middleware,
Documentation,
Unknown,
}
impl FileRole {
pub fn semantic_boost(self) -> f32 {
match self {
Self::Service | Self::Controller => 1.15,
Self::Middleware => 1.10,
Self::Repository => 1.05,
Self::Utility | Self::Unknown => 1.00,
Self::Config => 0.90,
Self::Model => 0.85,
Self::Migration => 0.80,
Self::Documentation => 0.75,
Self::Test => 0.70,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Service => "service",
Self::Controller => "controller",
Self::Model => "model",
Self::Repository => "repository",
Self::Utility => "utility",
Self::Config => "config",
Self::Test => "test",
Self::Migration => "migration",
Self::Middleware => "middleware",
Self::Documentation => "documentation",
Self::Unknown => "unknown",
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Self {
match s {
"service" => Self::Service,
"controller" => Self::Controller,
"model" => Self::Model,
"repository" => Self::Repository,
"utility" => Self::Utility,
"config" => Self::Config,
"test" => Self::Test,
"migration" => Self::Migration,
"middleware" => Self::Middleware,
"documentation" => Self::Documentation,
_ => Self::Unknown,
}
}
}
const PATH_PATTERNS: &[(&[&str], FileRole)] = &[
(&["services/", "service/", "/svc/"], FileRole::Service),
(
&[
"controllers/",
"controller/",
"handlers/",
"handler/",
"routes/",
"router/",
],
FileRole::Controller,
),
(
&[
"models/",
"model/",
"entities/",
"entity/",
"schemas/",
"schema/",
"dto/",
],
FileRole::Model,
),
(
&[
"repositories/",
"repository/",
"repos/",
"repo/",
"dal/",
"dao/",
],
FileRole::Repository,
),
(
&["utils/", "util/", "helpers/", "helper/", "lib/", "common/"],
FileRole::Utility,
),
(
&["config/", "configuration/", "settings/"],
FileRole::Config,
),
(
&[
"test/",
"tests/",
"__tests__/",
"spec/",
"_test.",
".test.",
".spec.",
],
FileRole::Test,
),
(
&["migrations/", "migration/", "migrate/", "db/migrate/"],
FileRole::Migration,
),
(
&[
"middleware/",
"interceptor/",
"interceptors/",
"guard/",
"guards/",
],
FileRole::Middleware,
),
(
&[
"docs/",
"doc/",
"documentation/",
"legal/",
".md",
"changelog",
"license",
"readme",
],
FileRole::Documentation,
),
];
pub fn classify_file(relative_path: &Path) -> FileRole {
let path_str = relative_path.to_string_lossy().to_lowercase();
for (patterns, role) in PATH_PATTERNS {
for pattern in *patterns {
if path_str.contains(pattern) {
return *role;
}
}
}
FileRole::Unknown
}
pub fn classify_with_imports(relative_path: &Path, imports: &[String]) -> FileRole {
let path_role = classify_file(relative_path);
if path_role != FileRole::Unknown {
return path_role;
}
let import_str = imports.join(" ").to_lowercase();
if import_str.contains("orm")
|| import_str.contains("prisma")
|| import_str.contains("typeorm")
|| import_str.contains("sqlalchemy")
|| import_str.contains("diesel")
{
return FileRole::Repository;
}
if import_str.contains("express")
|| import_str.contains("fastapi")
|| import_str.contains("actix")
|| import_str.contains("axum")
|| import_str.contains("gin")
{
return FileRole::Controller;
}
if import_str.contains("jest")
|| import_str.contains("pytest")
|| import_str.contains("mocha")
|| import_str.contains("#[cfg(test)]")
{
return FileRole::Test;
}
FileRole::Unknown
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn classify_service_path() {
assert_eq!(
classify_file(&PathBuf::from("src/services/auth.ts")),
FileRole::Service
);
}
#[test]
fn classify_test_path() {
assert_eq!(
classify_file(&PathBuf::from("test/auth.test.ts")),
FileRole::Test
);
}
#[test]
fn classify_unknown_path() {
assert_eq!(
classify_file(&PathBuf::from("src/index.ts")),
FileRole::Unknown
);
}
#[test]
fn classify_with_prisma_import() {
assert_eq!(
classify_with_imports(
&PathBuf::from("src/data.ts"),
&["import { PrismaClient }".to_string()]
),
FileRole::Repository
);
}
#[test]
fn semantic_boost_values() {
assert!(FileRole::Service.semantic_boost() > FileRole::Test.semantic_boost());
assert!((FileRole::Unknown.semantic_boost() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn roundtrip_str_conversion() {
let roles = [
FileRole::Service,
FileRole::Controller,
FileRole::Model,
FileRole::Repository,
FileRole::Utility,
FileRole::Config,
FileRole::Test,
FileRole::Migration,
FileRole::Middleware,
FileRole::Documentation,
FileRole::Unknown,
];
for role in roles {
assert_eq!(FileRole::from_str(role.as_str()), role);
}
}
#[test]
fn classify_documentation_paths() {
assert_eq!(
classify_file(&PathBuf::from("docs/architecture.md")),
FileRole::Documentation
);
assert_eq!(
classify_file(&PathBuf::from("README.md")),
FileRole::Documentation
);
assert_eq!(
classify_file(&PathBuf::from("assets/legal/terms.md")),
FileRole::Documentation
);
assert_eq!(
classify_file(&PathBuf::from("CHANGELOG.md")),
FileRole::Documentation
);
}
#[test]
fn documentation_boost_less_than_service() {
assert!(FileRole::Documentation.semantic_boost() < FileRole::Service.semantic_boost());
assert!(FileRole::Documentation.semantic_boost() < FileRole::Unknown.semantic_boost());
assert!(FileRole::Documentation.semantic_boost() > FileRole::Test.semantic_boost());
}
}