backhopper_core/config/
path_translation.rs1use std::cmp::Reverse;
8use std::collections::BTreeSet;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12use serde::{Deserialize, Serialize};
13
14use crate::config::PathTranslationRaw;
15use crate::errors::ConfigError;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum TranslationDirection {
20 SourceToTarget,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct PathTranslation {
25 pub name: String,
26 pub direction: TranslationDirection,
27 pub source_prefix: String,
28 pub target_prefix: String,
29 pub origin: TranslationOrigin,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(tag = "kind", rename_all = "snake_case")]
34pub enum TranslationOrigin {
35 ConfigStanza,
36 ExternalFile { path: PathBuf },
37}
38
39#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
40pub struct PathTranslations {
41 pub entries: Vec<PathTranslation>,
42}
43
44impl PathTranslations {
45 pub fn from_config_stanzas(raws: Vec<PathTranslationRaw>) -> Result<Self, ConfigError> {
46 let entries = raws
47 .into_iter()
48 .map(|r| parse_one(r, TranslationOrigin::ConfigStanza))
49 .collect::<Result<Vec<_>, _>>()?;
50 let mut out = Self { entries };
51 out.validate_uniqueness_and_overlap()?;
52 out.sort_by_specificity();
53 Ok(out)
54 }
55
56 pub fn load_external(path: &Path) -> Result<Vec<PathTranslation>, ConfigError> {
57 if !path.exists() {
58 return Err(ConfigError::PathTranslationFileMissing(path.to_path_buf()));
59 }
60 let text = fs::read_to_string(path)?;
61 let raw: ExternalFile = toml::from_str(&text)?;
62 let origin = TranslationOrigin::ExternalFile {
63 path: path.to_path_buf(),
64 };
65 raw.path_translations
66 .into_iter()
67 .map(|r| parse_one(r, origin.clone()))
68 .collect()
69 }
70
71 pub fn merge_external(&mut self, external: Vec<PathTranslation>) -> Result<(), ConfigError> {
72 self.entries.extend(external);
73 self.validate_uniqueness_and_overlap()?;
74 self.sort_by_specificity();
75 Ok(())
76 }
77
78 fn sort_by_specificity(&mut self) {
80 self.entries.sort_by_key(|e| Reverse(e.source_prefix.len()));
81 }
82
83 fn validate_uniqueness_and_overlap(&self) -> Result<(), ConfigError> {
84 let mut seen_names: BTreeSet<&str> = BTreeSet::new();
85 let mut seen_sources: BTreeSet<&str> = BTreeSet::new();
86 for entry in &self.entries {
87 if !seen_names.insert(&entry.name) {
88 return Err(ConfigError::PathTranslationDuplicateName(
89 entry.name.clone(),
90 ));
91 }
92 if !seen_sources.insert(&entry.source_prefix) {
93 return Err(ConfigError::PathTranslationDuplicateSource(
94 entry.source_prefix.clone(),
95 ));
96 }
97 }
98 for (i, a) in self.entries.iter().enumerate() {
99 for b in self.entries.iter().skip(i + 1) {
100 if a.source_prefix == b.source_prefix {
101 continue;
102 }
103 if a.source_prefix.starts_with(&b.source_prefix)
104 || b.source_prefix.starts_with(&a.source_prefix)
105 {
106 return Err(ConfigError::PathTranslationPrefixOverlap {
107 a: a.source_prefix.clone(),
108 b: b.source_prefix.clone(),
109 });
110 }
111 }
112 }
113 Ok(())
114 }
115
116 pub fn translate(&self, path: &str) -> Option<(PathBuf, &PathTranslation)> {
119 for entry in &self.entries {
120 if let Some(suffix) = path.strip_prefix(&entry.source_prefix) {
121 let rewritten = format!("{}{}", entry.target_prefix, suffix);
122 return Some((PathBuf::from(rewritten), entry));
123 }
124 }
125 None
126 }
127}
128
129#[derive(Debug, Deserialize)]
130#[serde(deny_unknown_fields)]
131struct ExternalFile {
132 #[serde(default, rename = "path_translation")]
133 path_translations: Vec<PathTranslationRaw>,
134}
135
136fn parse_one(
137 raw: PathTranslationRaw,
138 origin: TranslationOrigin,
139) -> Result<PathTranslation, ConfigError> {
140 if raw.name.is_empty() {
141 return Err(ConfigError::PathTranslationEmptyName);
142 }
143 if raw.source_prefix.is_empty() {
144 return Err(ConfigError::PathTranslationEmptyPrefix {
145 name: raw.name.clone(),
146 field: "source_prefix",
147 });
148 }
149 if raw.target_prefix.is_empty() {
150 return Err(ConfigError::PathTranslationEmptyPrefix {
151 name: raw.name.clone(),
152 field: "target_prefix",
153 });
154 }
155 if !raw.source_prefix.ends_with('/') {
156 return Err(ConfigError::PathTranslationPrefixNoTrailingSlash {
157 name: raw.name.clone(),
158 field: "source_prefix",
159 value: raw.source_prefix.clone(),
160 });
161 }
162 if !raw.target_prefix.ends_with('/') {
163 return Err(ConfigError::PathTranslationPrefixNoTrailingSlash {
164 name: raw.name.clone(),
165 field: "target_prefix",
166 value: raw.target_prefix.clone(),
167 });
168 }
169 let direction = match raw.direction.as_str() {
170 "source_to_target" => TranslationDirection::SourceToTarget,
171 other => {
172 return Err(ConfigError::PathTranslationUnknownDirection {
173 name: raw.name.clone(),
174 direction: other.to_owned(),
175 });
176 }
177 };
178 Ok(PathTranslation {
179 name: raw.name,
180 direction,
181 source_prefix: raw.source_prefix,
182 target_prefix: raw.target_prefix,
183 origin,
184 })
185}