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.clone().into_boxed_str(),
55 raw: PathBuf::from(raw),
56 });
57 }
58
59 Ok(Self {
60 inner: Arc::new(ProjectPathsInner {
61 project_root,
62 source_path,
63 source: source.into_boxed_slice(),
64 entries,
65 by_name,
66 }),
67 })
68 }
69
70 pub fn project_root(&self) -> &Path {
71 &self.inner.project_root
72 }
73
74 pub fn source_path(&self) -> &Path {
75 &self.inner.source_path
76 }
77
78 pub fn source_json(&self) -> &[u8] {
79 &self.inner.source
80 }
81
82 pub fn len(&self) -> usize {
83 self.inner.entries.len()
84 }
85
86 pub fn is_empty(&self) -> bool {
87 self.inner.entries.is_empty()
88 }
89
90 pub fn contains(&self, key: &str) -> bool {
91 self.inner.by_name.contains_key(key)
92 }
93
94 pub fn path(&self, key: &str) -> Option<&Path> {
95 let &position = self.inner.by_name.get(key)?;
96 Some(&self.inner.entries[position].raw)
97 }
98
99 pub fn require_path(&self, key: &str) -> Result<&Path, ConfigurationError> {
100 self.path(key)
101 .ok_or_else(|| ConfigurationError::UnknownProjectPath {
102 key: key.to_owned(),
103 })
104 }
105
106 pub fn resolve_path(&self, key: &str) -> Result<PathBuf, ConfigurationError> {
107 let path = self.require_path(key)?;
108 if path.is_absolute() {
109 Ok(path.to_path_buf())
110 } else {
111 Ok(self.project_root().join(path))
112 }
113 }
114
115 pub fn keys(&self) -> impl ExactSizeIterator<Item = &str> {
116 self.inner.entries.iter().map(|entry| entry.name.as_ref())
117 }
118
119 pub fn iter(&self) -> impl ExactSizeIterator<Item = (&str, &Path)> {
120 self.inner
121 .entries
122 .iter()
123 .map(|entry| (entry.name.as_ref(), entry.raw.as_path()))
124 }
125
126 pub fn to_json_value(&self) -> Value {
128 Value::Object(
129 self.inner
130 .entries
131 .iter()
132 .map(|entry| {
133 (
134 entry.name.to_string(),
135 Value::String(entry.source.to_string()),
136 )
137 })
138 .collect::<Map<_, _>>(),
139 )
140 }
141}
142
143impl fmt::Debug for ProjectPaths {
144 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
145 formatter
146 .debug_struct("ProjectPaths")
147 .field("project_root", &self.project_root())
148 .field("source_path", &self.source_path())
149 .field("entries", &self.len())
150 .finish_non_exhaustive()
151 }
152}
153
154struct ProjectPathsInner {
155 project_root: PathBuf,
156 source_path: PathBuf,
157 source: Box<[u8]>,
158 entries: Vec<PathEntry>,
159 by_name: HashMap<Box<str>, usize>,
160}
161
162struct PathEntry {
163 name: Box<str>,
164 source: Box<str>,
165 raw: PathBuf,
166}