dinghy_lib/
config.rs

1use itertools::Itertools;
2use serde::de;
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::result;
6use std::{collections, path};
7
8use crate::errors::*;
9
10#[derive(Clone, Debug)]
11pub struct TestData {
12    pub id: String,
13    pub base: path::PathBuf,
14    pub source: String,
15    pub target: String,
16    pub copy_git_ignored: bool,
17}
18
19#[derive(Serialize, Debug, Clone)]
20pub struct TestDataConfiguration {
21    pub copy_git_ignored: bool,
22    pub source: String,
23    pub target: Option<String>,
24}
25
26#[derive(Serialize, Deserialize, Debug, Clone)]
27pub struct DetailedTestDataConfiguration {
28    pub source: String,
29    pub copy_git_ignored: bool,
30    pub target: Option<String>,
31}
32
33impl<'de> de::Deserialize<'de> for TestDataConfiguration {
34    fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
35    where
36        D: de::Deserializer<'de>,
37    {
38        struct TestDataVisitor;
39
40        impl<'de> de::Visitor<'de> for TestDataVisitor {
41            type Value = TestDataConfiguration;
42
43            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
44                formatter.write_str(
45                    "a path like \"tests/my_test_data\" or a \
46                     detailed dependency like { source = \
47                     \"tests/my_test_data\", copy_git_ignored = true }",
48                )
49            }
50
51            fn visit_str<E>(self, s: &str) -> result::Result<Self::Value, E>
52            where
53                E: de::Error,
54            {
55                Ok(TestDataConfiguration {
56                    copy_git_ignored: false,
57                    source: s.to_owned(),
58                    target: None,
59                })
60            }
61
62            fn visit_map<V>(self, map: V) -> result::Result<Self::Value, V::Error>
63            where
64                V: de::MapAccess<'de>,
65            {
66                let mvd = de::value::MapAccessDeserializer::new(map);
67                let detailed = DetailedTestDataConfiguration::deserialize(mvd)?;
68                Ok(TestDataConfiguration {
69                    copy_git_ignored: detailed.copy_git_ignored,
70                    source: detailed.source,
71                    target: detailed.target,
72                })
73            }
74        }
75
76        deserializer.deserialize_any(TestDataVisitor)
77    }
78}
79
80#[derive(Clone, Debug, Default)]
81pub struct Configuration {
82    pub platforms: collections::BTreeMap<String, PlatformConfiguration>,
83    pub ssh_devices: collections::BTreeMap<String, SshDeviceConfiguration>,
84    pub script_devices: collections::BTreeMap<String, ScriptDeviceConfiguration>,
85    pub test_data: Vec<TestData>,
86    pub skip_source_copy: bool,
87}
88
89#[derive(Clone, Serialize, Deserialize, Debug, Default)]
90struct ConfigurationFileContent {
91    pub platforms: Option<collections::BTreeMap<String, PlatformConfiguration>>,
92    pub ssh_devices: Option<collections::BTreeMap<String, SshDeviceConfiguration>>,
93    pub script_devices: Option<collections::BTreeMap<String, ScriptDeviceConfiguration>>,
94    pub test_data: Option<collections::BTreeMap<String, TestDataConfiguration>>,
95    pub skip_source_copy: Option<bool>,
96}
97
98#[derive(Clone, Serialize, Deserialize, Debug, Default)]
99pub struct PlatformConfiguration {
100    pub deb_multiarch: Option<String>,
101    pub env: Option<collections::HashMap<String, String>>,
102    pub overlays: Option<collections::HashMap<String, OverlayConfiguration>>,
103    pub rustc_triple: Option<String>,
104    pub sysroot: Option<String>,
105    pub toolchain: Option<String>,
106}
107
108impl PlatformConfiguration {
109    pub fn empty() -> Self {
110        PlatformConfiguration {
111            deb_multiarch: None,
112            env: None,
113            overlays: None,
114            rustc_triple: None,
115            sysroot: None,
116            toolchain: None,
117        }
118    }
119
120    pub fn env(&self) -> Vec<(String, String)> {
121        self.env
122            .as_ref()
123            .map(|it| {
124                it.iter()
125                    .map(|(key, value)| (key.to_string(), value.to_string()))
126                    .collect_vec()
127            })
128            .unwrap_or(vec![])
129    }
130}
131
132#[derive(Clone, Serialize, Deserialize, Debug, Default)]
133pub struct OverlayConfiguration {
134    pub path: String,
135    pub scope: Option<String>,
136}
137
138#[derive(Clone, Serialize, Deserialize, Debug)]
139pub struct SshDeviceConfiguration {
140    pub hostname: String,
141    pub username: String,
142    pub port: Option<u16>,
143    pub path: Option<String>,
144    pub target: Option<String>,
145    pub toolchain: Option<String>,
146    pub platform: Option<String>,
147    #[serde(default)]
148    pub remote_shell_vars: collections::HashMap<String, String>,
149    pub install_adhoc_rsync_local_path: Option<String>,
150    pub use_legacy_scp_protocol_for_adhoc_rsync_copy: Option<bool>,
151}
152
153#[derive(Clone, Serialize, Deserialize, Debug)]
154pub struct ScriptDeviceConfiguration {
155    pub path: String,
156    pub platform: Option<String>,
157}
158
159impl Configuration {
160    pub fn merge(&mut self, file: &path::Path) -> Result<()> {
161        let other = read_config_file(&file)?;
162        if let Some(pfs) = other.platforms {
163            self.platforms.extend(pfs)
164        }
165        self.ssh_devices
166            .extend(other.ssh_devices.unwrap_or(collections::BTreeMap::new()));
167        self.script_devices
168            .extend(other.script_devices.unwrap_or(collections::BTreeMap::new()));
169        for (id, source) in other.test_data.unwrap_or(collections::BTreeMap::new()) {
170            // TODO Remove key
171            self.test_data.push(TestData {
172                id: id.to_string(),
173                base: file.to_path_buf(),
174                source: source.source.clone(),
175                target: source.target.unwrap_or(source.source.clone()),
176                copy_git_ignored: source.copy_git_ignored,
177            })
178        }
179        if let Some(skip_source_copy) = other.skip_source_copy {
180            self.skip_source_copy = skip_source_copy
181        }
182        Ok(())
183    }
184}
185
186fn read_config_file<P: AsRef<path::Path>>(file: P) -> Result<ConfigurationFileContent> {
187    let file = file.as_ref();
188    let data = std::fs::read_to_string(file).with_context(|| format!("Reading {file:?}"))?;
189    Ok(toml::from_str(&data)?)
190}
191
192pub fn dinghy_config<P: AsRef<path::Path>>(dir: P) -> Result<Configuration> {
193    let mut conf = Configuration::default();
194
195    let mut files_to_try = vec![];
196    let dir = dir.as_ref().to_path_buf();
197    let mut d = dir.as_path();
198    while d.parent().is_some() {
199        files_to_try.push(d.join("dinghy.toml"));
200        files_to_try.push(d.join(".dinghy.toml"));
201        files_to_try.push(d.join(".dinghy").join("dinghy.toml"));
202        files_to_try.push(d.join(".dinghy").join(".dinghy.toml"));
203        d = d.parent().unwrap();
204    }
205    files_to_try.push(d.join(".dinghy.toml"));
206    if let Some(home) = dirs::home_dir() {
207        if !dir.starts_with(&home) {
208            files_to_try.push(home.join("dinghy.toml"));
209            files_to_try.push(home.join(".dinghy.toml"));
210            files_to_try.push(home.join(".dinghy").join("dinghy.toml"));
211            files_to_try.push(home.join(".dinghy").join(".dinghy.toml"));
212        }
213    }
214    for file in files_to_try {
215        if path::Path::new(&file).exists() {
216            log::debug!("Loading configuration from {:?}", file);
217            conf.merge(&file)?;
218        } else {
219            log::trace!("No configuration found at {:?}", file);
220        }
221    }
222
223    log::debug!("Configuration: {:#?}", conf);
224
225    Ok(conf)
226}
227
228#[cfg(test)]
229mod tests {
230    #[test]
231    fn load_config_with_str_test_data() {
232        let config_file = ::std::env::current_exe()
233            .unwrap()
234            .parent()
235            .unwrap()
236            .join("../../../test-ws/test-app/.dinghy.toml");
237        super::read_config_file(config_file).unwrap();
238    }
239}