scientific_workflow/configuration/
paths.rs1use std::collections::HashMap;
4use std::fmt;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use serde_json::{Map, Value};
9
10use super::error::ConfigurationError;
11use super::source::{
12 StrictValue, invalid, parse_strict_json, read_source, require_object, validate_name,
13};
14
15const CONFIGURATION_DIRECTORY: &str = "config";
16const PATHS_FILE: &str = "paths.json";
17
18#[derive(Clone)]
23pub struct ProjectPaths {
24 inner: Arc<ProjectPathsInner>,
25}
26
27impl ProjectPaths {
28 pub fn load(project_root: impl Into<PathBuf>) -> Result<Self, ConfigurationError> {
30 let project_root = project_root.into();
31 let source_path = project_root.join(CONFIGURATION_DIRECTORY).join(PATHS_FILE);
32 let source = read_source(&source_path)?;
33 let document = parse_strict_json(&source_path, &source)?;
34 let fields = require_object(&source_path, document, "paths.json root must be an object")?;
35 let mut entries = Vec::with_capacity(fields.len());
36 let mut by_name = HashMap::with_capacity(fields.len());
37 for (position, (name, value)) in fields.into_iter().enumerate() {
38 validate_name(&source_path, &name, "project path")?;
39 let StrictValue::String(raw) = value else {
40 return invalid(
41 &source_path,
42 format!("project path `{name}` must be a JSON string"),
43 );
44 };
45 if raw.trim().is_empty() {
46 return invalid(
47 &source_path,
48 format!("project path `{name}` must not be empty or whitespace-only"),
49 );
50 }
51 by_name.insert(name.clone().into_boxed_str(), position);
52 entries.push(PathEntry {
53 name: name.into_boxed_str(),
54 source: raw.into_boxed_str(),
55 });
56 }
57
58 Ok(Self {
59 inner: Arc::new(ProjectPathsInner {
60 project_root,
61 source_path,
62 source: source.into_boxed_slice(),
63 entries,
64 by_name,
65 }),
66 })
67 }
68
69 pub fn project_root(&self) -> &Path {
71 &self.inner.project_root
72 }
73
74 pub fn source_path(&self) -> &Path {
76 &self.inner.source_path
77 }
78
79 pub fn source_json(&self) -> &[u8] {
81 &self.inner.source
82 }
83
84 pub fn len(&self) -> usize {
86 self.inner.entries.len()
87 }
88
89 pub fn is_empty(&self) -> bool {
91 self.inner.entries.is_empty()
92 }
93
94 pub fn contains(&self, key: &str) -> bool {
96 self.inner.by_name.contains_key(key)
97 }
98
99 pub fn path(&self, key: &str) -> Option<&Path> {
101 let &position = self.inner.by_name.get(key)?;
102 Some(Path::new(self.inner.entries[position].source.as_ref()))
103 }
104
105 pub fn require_path(&self, key: &str) -> Result<&Path, ConfigurationError> {
107 self.path(key)
108 .ok_or_else(|| ConfigurationError::UnknownProjectPath {
109 key: key.to_owned(),
110 })
111 }
112
113 pub fn resolve_path(&self, key: &str) -> Result<PathBuf, ConfigurationError> {
115 let path = self.require_path(key)?;
116 if path.is_absolute() {
117 Ok(path.to_path_buf())
118 } else {
119 Ok(self.project_root().join(path))
120 }
121 }
122
123 pub fn keys(&self) -> impl ExactSizeIterator<Item = &str> {
125 self.inner.entries.iter().map(|entry| entry.name.as_ref())
126 }
127
128 pub fn iter(&self) -> impl ExactSizeIterator<Item = (&str, &Path)> {
130 self.inner
131 .entries
132 .iter()
133 .map(|entry| (entry.name.as_ref(), Path::new(entry.source.as_ref())))
134 }
135
136 pub fn to_json_value(&self) -> Value {
138 Value::Object(
139 self.inner
140 .entries
141 .iter()
142 .map(|entry| {
143 (
144 entry.name.to_string(),
145 Value::String(entry.source.to_string()),
146 )
147 })
148 .collect::<Map<_, _>>(),
149 )
150 }
151}
152
153impl fmt::Debug for ProjectPaths {
154 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
155 formatter
156 .debug_struct("ProjectPaths")
157 .field("project_root", &self.project_root())
158 .field("source_path", &self.source_path())
159 .field("entries", &self.len())
160 .finish_non_exhaustive()
161 }
162}
163
164struct ProjectPathsInner {
165 project_root: PathBuf,
166 source_path: PathBuf,
167 source: Box<[u8]>,
168 entries: Vec<PathEntry>,
169 by_name: HashMap<Box<str>, usize>,
170}
171
172struct PathEntry {
173 name: Box<str>,
174 source: Box<str>,
175}