scientific_workflow/configuration/
project_config.rs1use std::fmt;
28use std::fs::{self, File, OpenOptions};
29use std::io::{self, Write};
30use std::iter::FusedIterator;
31use std::path::{Path, PathBuf};
32
33use serde::Serialize;
34use serde::de::DeserializeOwned;
35use serde_json::Value;
36
37use super::error::ConfigurationError;
38use super::parameter_key_tuple::ParameterKeyTuple;
39use super::parameters::{ParameterSpace, TaskParameters, TaskParametersIter};
40use super::paths::ProjectPaths;
41
42const CONFIGURATION_DIRECTORY: &str = "config";
43const FIXED_FILE: &str = "fixed.json";
44const SWEEP_FILE: &str = "sweep.json";
45const PATHS_FILE: &str = "paths.json";
46
47#[derive(Clone)]
54pub struct ProjectConfig {
55 project_root: PathBuf,
56 parameters: ParameterSpace,
57 paths: ProjectPaths,
58}
59
60impl ProjectConfig {
61 pub fn load(project_root: impl Into<PathBuf>) -> Result<Self, ConfigurationError> {
72 let project_root = project_root.into();
73 let configuration_directory = project_root.join(CONFIGURATION_DIRECTORY);
74 let parameters = ParameterSpace::load(&configuration_directory)?;
75 let paths = ProjectPaths::load(&project_root)?;
76 Ok(Self {
77 project_root,
78 parameters,
79 paths,
80 })
81 }
82
83 pub fn project_root(&self) -> &Path {
85 &self.project_root
86 }
87
88 pub fn configuration_directory(&self) -> &Path {
90 self.parameters.configuration_directory()
91 }
92
93 pub fn parameters(&self) -> &ParameterSpace {
95 &self.parameters
96 }
97
98 pub fn paths(&self) -> &ProjectPaths {
100 &self.paths
101 }
102
103 pub fn task_count(&self) -> u64 {
105 self.parameters.task_count()
106 }
107
108 pub fn task_config(&self, ordinal: u64) -> Result<TaskConfig, ConfigurationError> {
113 Ok(TaskConfig {
114 parameters: self.parameters.task(ordinal)?,
115 paths: self.paths.clone(),
116 })
117 }
118
119 pub fn task_configs(&self) -> TaskConfigIter {
126 TaskConfigIter {
127 parameters: self.parameters.tasks(),
128 paths: self.paths.clone(),
129 }
130 }
131
132 pub fn task_configs_matching<V>(
140 &self,
141 key: impl Into<String>,
142 value: V,
143 ) -> Result<MatchingTaskConfigIter, ConfigurationError>
144 where
145 V: Serialize,
146 {
147 let key = key.into();
148 if !self
149 .parameters
150 .sweep_keys()
151 .any(|candidate| candidate == key)
152 {
153 return Err(ConfigurationError::UnknownSweepParameter { key });
154 }
155 let value = serde_json::to_value(value).map_err(|source| {
156 ConfigurationError::EncodeTaskSelection {
157 key: key.clone(),
158 source,
159 }
160 })?;
161 Ok(MatchingTaskConfigIter {
162 tasks: self.task_configs(),
163 key: key.into_boxed_str(),
164 value,
165 })
166 }
167
168 pub fn unique_task_config_matching<V>(
175 &self,
176 key: impl Into<String>,
177 value: V,
178 ) -> Result<TaskConfig, ConfigurationError>
179 where
180 V: Serialize,
181 {
182 let key = key.into();
183 let mut matches = self.task_configs_matching(key.clone(), value)?;
184 let task = matches
185 .next()
186 .ok_or_else(|| ConfigurationError::NoMatchingTaskConfiguration { key: key.clone() })?;
187 if matches.next().is_some() {
188 return Err(ConfigurationError::AmbiguousTaskConfiguration { key });
189 }
190 Ok(task)
191 }
192
193 pub fn into_parts(self) -> (ParameterSpace, ProjectPaths) {
198 (self.parameters, self.paths)
199 }
200
201 pub fn write_source_config(
215 &self,
216 destination_project_root: impl AsRef<Path>,
217 ) -> Result<(), ConfigurationError> {
218 let destination_project_root = destination_project_root.as_ref();
219 create_destination_root(destination_project_root)?;
220 let destination = destination_project_root.join(CONFIGURATION_DIRECTORY);
221 create_configuration_directory(&destination)?;
222
223 write_source_file(
224 &destination.join(FIXED_FILE),
225 self.parameters.fixed_source_json(),
226 )?;
227 write_source_file(
228 &destination.join(SWEEP_FILE),
229 self.parameters.sweep_source_json(),
230 )?;
231 write_source_file(&destination.join(PATHS_FILE), self.paths.source_json())?;
232 sync_directory(&destination)?;
233 sync_directory(destination_project_root)
234 }
235}
236
237#[derive(Clone)]
245pub struct TaskConfig {
246 parameters: TaskParameters,
247 paths: ProjectPaths,
248}
249
250impl TaskConfig {
251 pub fn task_ordinal(&self) -> u64 {
253 self.parameters.task_ordinal()
254 }
255
256 pub fn parameters(&self) -> &TaskParameters {
258 &self.parameters
259 }
260
261 pub fn paths(&self) -> &ProjectPaths {
263 &self.paths
264 }
265
266 pub fn value(&self, key: &str) -> Option<&Value> {
268 self.parameters.value(key)
269 }
270
271 pub fn require_value(&self, key: &str) -> Result<&Value, ConfigurationError> {
273 self.parameters.require_value(key)
274 }
275
276 pub fn decode_value<T>(&self, key: &str) -> Result<T, ConfigurationError>
278 where
279 T: DeserializeOwned,
280 {
281 self.parameters.decode_value(key)
282 }
283
284 pub fn decode_values<Values, Keys>(&self, keys: Keys) -> Result<Values, ConfigurationError>
286 where
287 Keys: ParameterKeyTuple<Values>,
288 {
289 self.parameters.decode_values(keys)
290 }
291
292 pub fn resolve_path(&self, key: &str) -> Result<PathBuf, ConfigurationError> {
294 self.paths.resolve_path(key)
295 }
296}
297
298impl fmt::Debug for TaskConfig {
299 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
301 formatter
302 .debug_struct("TaskConfig")
303 .field("task_ordinal", &self.task_ordinal())
304 .field("parameters", &self.parameters.len())
305 .field("paths", &self.paths.len())
306 .finish_non_exhaustive()
307 }
308}
309
310#[derive(Clone)]
312pub struct TaskConfigIter {
313 parameters: TaskParametersIter,
314 paths: ProjectPaths,
315}
316
317impl Iterator for TaskConfigIter {
318 type Item = TaskConfig;
319
320 fn next(&mut self) -> Option<Self::Item> {
321 self.parameters.next().map(|parameters| TaskConfig {
322 parameters,
323 paths: self.paths.clone(),
324 })
325 }
326
327 fn size_hint(&self) -> (usize, Option<usize>) {
328 self.parameters.size_hint()
329 }
330}
331
332impl FusedIterator for TaskConfigIter {}
333
334impl fmt::Debug for TaskConfigIter {
335 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
337 formatter
338 .debug_struct("TaskConfigIter")
339 .field("parameters", &self.parameters)
340 .field("paths", &self.paths.len())
341 .finish_non_exhaustive()
342 }
343}
344
345pub struct MatchingTaskConfigIter {
347 tasks: TaskConfigIter,
348 key: Box<str>,
349 value: Value,
350}
351
352impl Iterator for MatchingTaskConfigIter {
353 type Item = TaskConfig;
354
355 fn next(&mut self) -> Option<Self::Item> {
356 self.tasks
357 .find(|task| task.value(&self.key) == Some(&self.value))
358 }
359
360 fn size_hint(&self) -> (usize, Option<usize>) {
361 (0, self.tasks.size_hint().1)
362 }
363}
364
365impl FusedIterator for MatchingTaskConfigIter {}
366
367impl fmt::Debug for MatchingTaskConfigIter {
368 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
370 formatter
371 .debug_struct("MatchingTaskConfigIter")
372 .field("key", &self.key)
373 .field("tasks", &self.tasks)
374 .finish_non_exhaustive()
375 }
376}
377
378impl fmt::Debug for ProjectConfig {
379 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
381 formatter
382 .debug_struct("ProjectConfig")
383 .field("project_root", &self.project_root())
384 .field("parameters", &self.parameters.parameter_count())
385 .field("tasks", &self.parameters.task_count())
386 .field("paths", &self.paths.len())
387 .finish_non_exhaustive()
388 }
389}
390
391fn create_destination_root(path: &Path) -> Result<(), ConfigurationError> {
394 match fs::create_dir_all(path) {
395 Ok(()) => Ok(()),
396 Err(source) => Err(write_error(path.to_path_buf(), source)),
397 }
398}
399
400fn create_configuration_directory(path: &Path) -> Result<(), ConfigurationError> {
403 fs::create_dir(path).map_err(|source| write_error(path.to_path_buf(), source))
404}
405
406fn write_source_file(path: &Path, source_bytes: &[u8]) -> Result<(), ConfigurationError> {
408 let mut output = OpenOptions::new()
409 .write(true)
410 .create_new(true)
411 .open(path)
412 .map_err(|source| write_error(path.to_path_buf(), source))?;
413 output
414 .write_all(source_bytes)
415 .map_err(|source| write_error(path.to_path_buf(), source))?;
416 output
417 .sync_all()
418 .map_err(|source| write_error(path.to_path_buf(), source))
419}
420
421fn sync_directory(path: &Path) -> Result<(), ConfigurationError> {
423 File::open(path)
424 .and_then(|directory| directory.sync_all())
425 .map_err(|source| write_error(path.to_path_buf(), source))
426}
427
428fn write_error(path: PathBuf, source: io::Error) -> ConfigurationError {
430 ConfigurationError::WriteConfigurationFile { path, source }
431}