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