use crate::analysis::rust::macro_analyzer::{JobMacro, MacroAnalyzer, SummerMacro};
use crate::protocol::types::{LocationResponse, PositionResponse, RangeResponse};
use lsp_types::Url;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use walkdir::WalkDir;
pub struct JobScanner {
macro_analyzer: MacroAnalyzer,
}
impl JobScanner {
pub fn new() -> Self {
Self {
macro_analyzer: MacroAnalyzer::new(),
}
}
pub fn scan_jobs(&self, project_path: &Path) -> Result<Vec<JobInfoResponse>, ScanError> {
let mut jobs = Vec::new();
let src_path = project_path.join("src");
if !src_path.exists() {
return Err(ScanError::InvalidProject(
"src directory not found".to_string(),
));
}
for entry in WalkDir::new(&src_path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
{
let file_path = entry.path();
let content = match fs::read_to_string(file_path) {
Ok(content) => content,
Err(e) => {
tracing::warn!("Failed to read file {:?}: {}", file_path, e);
continue;
}
};
let file_url = match Url::from_file_path(file_path) {
Ok(url) => url,
Err(_) => {
tracing::warn!("Failed to convert path to URL: {:?}", file_path);
continue;
}
};
let rust_doc = match self.macro_analyzer.parse(file_url.clone(), content) {
Ok(doc) => doc,
Err(e) => {
tracing::warn!("Failed to parse file {:?}: {}", file_path, e);
continue;
}
};
let rust_doc = match self.macro_analyzer.extract_macros(rust_doc) {
Ok(doc) => doc,
Err(e) => {
tracing::warn!("Failed to extract macros from {:?}: {}", file_path, e);
continue;
}
};
for summer_macro in &rust_doc.macros {
if let SummerMacro::Job(job_macro) = summer_macro {
let (job_type, schedule) = match job_macro {
JobMacro::Cron { expression, .. } => (JobType::Cron, expression.clone()),
JobMacro::FixDelay { seconds, .. } => {
(JobType::FixDelay, format!("{} seconds", seconds))
}
JobMacro::FixRate { seconds, .. } => {
(JobType::FixRate, format!("{} seconds", seconds))
}
};
let range = match job_macro {
JobMacro::Cron { range, .. }
| JobMacro::FixDelay { range, .. }
| JobMacro::FixRate { range, .. } => range,
};
jobs.push(JobInfoResponse {
name: "job_function".to_string(), job_type,
schedule,
location: LocationResponse {
uri: file_url.to_string(),
range: RangeResponse {
start: PositionResponse {
line: range.start.line,
character: range.start.character,
},
end: PositionResponse {
line: range.end.line,
character: range.end.character,
},
},
},
});
}
}
}
Ok(jobs)
}
}
impl Default for JobScanner {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum JobType {
Cron,
FixDelay,
FixRate,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobInfoResponse {
pub name: String,
#[serde(rename = "jobType")]
pub job_type: JobType,
pub schedule: String,
pub location: LocationResponse,
}
#[derive(Debug, Deserialize)]
pub struct JobsRequest {
#[serde(rename = "appPath")]
pub app_path: String,
}
#[derive(Debug, Serialize)]
pub struct JobsResponse {
pub jobs: Vec<JobInfoResponse>,
}
#[derive(Debug, thiserror::Error)]
pub enum ScanError {
#[error("Failed to read file: {0}")]
FileRead(#[from] std::io::Error),
#[error("Invalid project structure: {0}")]
InvalidProject(String),
#[error("No jobs found")]
NoJobs,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_job_scanner_new() {
let _scanner = JobScanner::new();
}
#[test]
fn test_job_scanner_default() {
let _scanner = JobScanner::default();
}
}