Skip to main content

plot/lib/core/
job_init.rs

1use std::{
2	collections::{BTreeSet, HashMap},
3	fs,
4	path::{Path, PathBuf},
5};
6
7use crate::lib::{
8	core::{TOML_DEFAULT_MINIMAL, default_config_path, execution_dir},
9	logic::{file_backup, file_ensure, file_save_safe_if_changed},
10	schema::{RawTomlFileJobs, RawTomlJob},
11};
12
13/// Rezultat działania funkcji init
14#[derive(Debug, Clone)]
15pub struct AnchoredRuntime {
16	execution_dir: PathBuf, // Ścieżka, z której uruchomiono program (CWD)
17	config_path: PathBuf,
18	jobs_store: Vec<RawTomlJob>, // SSoT: Jedyne źródło prawdy o wczytanych zadaniach
19}
20
21impl AnchoredRuntime {
22	/// Konstruktor wewnętrzny (używany przez funkcje w tym module)
23	pub(crate) fn new(execution_dir: PathBuf, config_path: PathBuf, jobs_store: Vec<RawTomlJob>) -> Self {
24		Self { execution_dir, config_path, jobs_store }
25	}
26
27	/// Bezpieczny dostęp do ścieżki konfiguracji
28	pub fn path_xdo(&self) -> &Path { &self.config_path }
29
30	/// Bezpieczny dostęp do ścieżki cwd
31	pub fn path_cwd(&self) -> &Path { &self.execution_dir }
32
33	/// Zwraca ilość wczytanych zadań
34	pub fn count_jobs(&self) -> usize { self.jobs_store.len() }
35
36	/// Zwraca indeks w formie HashMapy (ID -> Description) do szybkiego wyszukiwania
37	pub fn get_jobs_index(&self) -> HashMap<String, String> {
38		self.jobs_store.iter().map(|job| (job.id.clone(), job.description.clone().unwrap_or_default())).collect()
39	}
40
41	/// Wykonuje podany callback dla każdego zadania w strukturze.
42	/// Pętla `for` jest całkowicie ukryta przed użytkownikiem (Inwersja Sterowania).
43	pub fn get_jobs<F>(&self, mut f: F)
44	where F: FnMut(&RawTomlJob) {
45		for job in &self.jobs_store {
46			f(job);
47		}
48	}
49
50	pub fn get_jobs_some<F>(&self, ids: &BTreeSet<String>, mut f: F)
51	where F: FnMut(&RawTomlJob) {
52		self.jobs_store.iter().filter(|job| ids.contains(&job.id)).for_each(|job| f(job));
53	}
54
55	/// Pobiera konkretne zadanie na podstawie ID.
56	/// Zwraca Option<&RawTomlJob>, co pozwala obsłużyć przypadek braku zadania.
57	pub fn get_job(&self, id: &str) -> Option<&RawTomlJob> { self.jobs_store.iter().find(|job| job.id == id) }
58}
59
60// Struktura pomocnicza używana WYŁĄCZNIE do bezpiecznego parsowania z "wycinaniem",
61// aby nie utracić reszty konfiguracji podczas usuwania duplikatów
62#[derive(Debug, serde::Deserialize, serde::Serialize)]
63struct InitTomlJobs {
64	job: Vec<InitTomlJob>,
65}
66
67#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
68struct InitTomlJob {
69	id: String,
70	// Przechowujemy całą resztę kluczy bez ich analizowania, żeby przy zapisie nic nie zginęło
71	#[serde(flatten)]
72	extra: HashMap<String, toml::Value>,
73}
74
75/// Funkcja inicjująca potok "od zera".
76/// Ignoruje ewentualne błędy i stan obecnego pliku - jeśli plik istnieje,
77/// ZAWSZE tworzy jego kopię zapasową (.bak) i nadpisuje go minimalną konfiguracją.
78pub fn start_blank(path_toml: Option<PathBuf>) -> AnchoredRuntime {
79	let config_path = path_toml.unwrap_or_else(default_config_path);
80
81	// Cała logika "skopiuj jeśli istnieje i nadpisz domyślnym"
82	// jest zamknięta w naszej bezpiecznej funkcji narzędziowej!
83	file_save_safe_if_changed(&config_path, TOML_DEFAULT_MINIMAL);
84
85	build_normalized_result(&config_path)
86}
87
88/// Główna funkcja orkiestrująca (Różdżkarz)
89pub fn start(path_toml: Option<PathBuf>) -> AnchoredRuntime {
90	let config_path = path_toml.unwrap_or_else(default_config_path);
91
92	if !config_path.exists() {
93		file_ensure(&config_path, TOML_DEFAULT_MINIMAL);
94		return build_normalized_result(&config_path);
95	}
96
97	let content = match fs::read_to_string(&config_path) {
98		Ok(c) => c,
99		Err(_) => {
100			handle_corrupted(&config_path);
101			return build_normalized_result(&config_path);
102		}
103	};
104
105	let mut parsed: InitTomlJobs = match toml::from_str(&content) {
106		Ok(p) => p,
107		Err(_) => {
108			handle_corrupted(&config_path);
109			return build_normalized_result(&config_path);
110		}
111	};
112
113	let mut id_counts = HashMap::new();
114	for job in &parsed.job {
115		*id_counts.entry(job.id.clone()).or_insert(0) += 1;
116	}
117
118	let has_any_unique_id = id_counts.values().any(|&count| count == 1);
119	let has_duplicates = id_counts.values().any(|&count| count > 1);
120
121	if !has_any_unique_id || parsed.job.is_empty() {
122		handle_corrupted(&config_path);
123		return build_normalized_result(&config_path);
124	}
125
126	if has_duplicates {
127		file_backup(&config_path);
128
129		parsed.job.retain(|job| id_counts[&job.id] == 1);
130
131		let fixed_toml = toml::to_string(&parsed).expect("Błąd serializacji naprawionego TOML");
132		fs::write(&config_path, fixed_toml).expect("Błąd zapisu naprawionego TOML");
133	}
134
135	build_normalized_result(&config_path)
136}
137
138fn handle_corrupted(path: &Path) {
139	file_backup(path);
140	file_ensure(path, TOML_DEFAULT_MINIMAL);
141}
142
143fn build_normalized_result(path: &Path) -> AnchoredRuntime {
144	let content = fs::read_to_string(path).expect("Krytyczny błąd odczytu znormalizowanego pliku");
145
146	// Ładujemy to już w nasze pełne, twarde modele domenowe (RawTomlFileJobs)
147	let parsed: RawTomlFileJobs = toml::from_str(&content).expect("Krytyczny błąd parsowania znormalizowanego pliku");
148
149	AnchoredRuntime::new(execution_dir(), path.to_path_buf(), parsed.job.0)
150}