unity_hub/unity/hub/
editors.rs

1use std::cmp::Ordering;
2use super::*;
3use crate::unity::hub::paths;
4use crate::unity::installation::{UnityInstallation, Installation, FromInstallation};
5use serde_json;
6use std::collections::HashMap;
7use std::fs;
8use std::fs::File;
9use std::io::Write;
10use std::iter::{IntoIterator, Iterator};
11use std::path::PathBuf;
12use log::{debug, trace};
13use serde::{Deserialize, Serialize};
14use unity_version::Version;
15
16#[derive(Debug)]
17pub struct Editors {
18    map: HashMap<Version, EditorInstallation>,
19}
20
21impl Editors {
22    pub fn load() -> Result<Editors> {
23        let path = paths::editors_config_path()
24            .ok_or_else(|| (UnityHubError::ConfigDirectoryNotFound))?;
25
26        let map: HashMap<Version, EditorInstallation> = if path.exists() {
27            debug!("load hub editors from file: {}", path.display());
28            File::open(path)
29                .map_err(|source| {
30                    UnityHubError::ReadConfigError{ config: "editors.json".to_string(), source: source.into() }
31                })
32                .and_then(|f| {
33                    serde_json::from_reader(f).map_err(|source| {
34                        UnityHubError::ReadConfigError { config: "editors.json".to_string(), source: source.into()}
35                    })
36                })?
37        } else {
38            debug!("hub editors file doesn't exist return empty map");
39            HashMap::new()
40        };
41        trace!("loaded editors map: {:?}", map);
42        Ok(Editors::create(map))
43    }
44
45    pub fn create(mut map: HashMap<Version, EditorInstallation>) -> Editors {
46        trace!("create Editors map");
47        map.retain(|version, installation| {
48            trace!(
49                "filter: version: {} - installaton: {:?}",
50                version,
51                installation
52            );
53            let check_installation = UnityInstallation::new(installation.location.to_path_buf());
54            if let Ok(check_installation) = check_installation {
55                trace!(
56                    "Found api installation at with version {} at location: {}",
57                    check_installation.version(),
58                    installation.location.display()
59                );
60                trace!(
61                    "Installation has correct version: {}",
62                    check_installation.version() == version
63                );
64                check_installation.version() == version
65            } else {
66                trace!(
67                    "No installtion found at location: {}",
68                    installation.location.display()
69                );
70                false
71            }
72        });
73        Editors { map }
74    }
75
76    pub fn add(&mut self, editor: &EditorInstallation) -> Option<EditorInstallation> {
77        self.map.insert(editor.version.clone(), editor.clone())
78    }
79
80    pub fn remove(&mut self, editor: &EditorInstallation) -> Option<EditorInstallation> {
81        self.map.remove(&editor.version)
82    }
83
84    pub fn remove_version(&mut self, version: &Version) -> Option<EditorInstallation> {
85        self.map.remove(&version)
86    }
87
88    pub fn flush(&self) -> Result<()> {
89        let config_path =
90            paths::config_path().ok_or_else(|| (UnityHubError::ConfigDirectoryNotFound))?;
91
92        let path = paths::editors_config_path()
93            .ok_or_else(|| UnityHubError::ConfigNotFound("editors.json".to_string()))?;
94
95        fs::create_dir_all(config_path).map_err(|source| UnityHubError::FailedToCreateConfigDirectory{source})?;
96        let mut file = File::create(path).map_err(|source| UnityHubError::FailedToCreateConfig{config: "editors.json".to_string(), source})?;
97
98        let j = serde_json::to_string(&self.map).map_err(|source| {
99            UnityHubError::WriteConfigError {config: "editors.json".to_string(), source: source.into()}
100        })?;
101        write!(file, "{}", &j).map_err(|source| {
102            UnityHubError::WriteConfigError {config: "editors.json".to_string(), source: source.into()}
103        })
104    }
105}
106
107impl IntoIterator for Editors {
108    type Item = EditorInstallation;
109    type IntoIter = ::std::vec::IntoIter<EditorInstallation>;
110
111    fn into_iter(self) -> Self::IntoIter {
112        self.map
113            .values()
114            .cloned()
115            .collect::<Vec<Self::Item>>()
116            .into_iter()
117    }
118}
119
120#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
121pub struct EditorInstallation {
122    version: Version,
123    #[serde(with = "editor_value_location")]
124    location: PathBuf,
125    manual: bool,
126}
127
128#[allow(dead_code)]
129trait EditorInstallationMarker {}
130
131impl EditorInstallationMarker for EditorInstallation {}
132
133impl Installation for EditorInstallation {
134    fn path(&self) -> &PathBuf {
135        &self.location()
136    }
137
138    fn version(&self) -> &Version {
139        &self.version
140    }
141}
142
143impl EditorInstallation {
144    pub fn new(version: Version, location: PathBuf) -> EditorInstallation {
145        trace!("create new editor installation: {} at {}", version, location.display());
146        EditorInstallation {
147            version,
148            location,
149            manual: true,
150        }
151    }
152
153    pub fn version(&self) -> &Version {
154        &self.version
155    }
156
157    pub fn location(&self) -> &PathBuf {
158        &self.location
159    }
160}
161
162pub mod editor_value_location {
163    use serde::de::Unexpected;
164    use serde::ser::SerializeSeq;
165    use serde::{self, Deserialize, Deserializer, Serializer};
166    use std::path::{Path, PathBuf};
167    use log::trace;
168
169    pub fn deserialize<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
170    where
171        D: Deserializer<'de>,
172    {
173        let paths: Vec<String> = Vec::deserialize(deserializer)?;
174        let path = paths
175            .first()
176            .ok_or_else(|| serde::de::Error::invalid_length(0, &"1"))?;
177        let location = Path::new(&path)
178            .parent()
179            .and_then(|location| {
180                if cfg!(target_os = "windows") || cfg!(target_os = "linux") {
181                    return location.parent();
182                }
183                Some(location)
184            })
185            .ok_or_else(|| {
186                serde::de::Error::invalid_value(
187                    Unexpected::Other("location with empty parent"),
188                    &"valid api location",
189                )
190            })?;
191        trace!("found Editor location: {}", location.display());
192        Ok(location.to_path_buf())
193    }
194
195    pub fn serialize<S>(location: &PathBuf, serializer: S) -> Result<S::Ok, S::Error>
196    where
197        S: Serializer,
198    {
199        let mut seq = serializer.serialize_seq(Some(1))?;
200
201        #[cfg(target_os = "windows")]
202        seq.serialize_element(&location.join("Editors\\Unity.exe"))?;
203        #[cfg(target_os = "linux")]
204        seq.serialize_element(&location.join("Editors/Unity"))?;
205        #[cfg(target_os = "macos")]
206        seq.serialize_element(&location.join("Unity.app"))?;
207        #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
208        seq.serialize_element(&location)?;
209
210        seq.end()
211    }
212}
213
214impl Ord for EditorInstallation {
215    fn cmp(&self, other: &EditorInstallation) -> Ordering {
216        self.version.cmp(&other.version)
217    }
218}
219
220impl PartialOrd for EditorInstallation {
221    fn partial_cmp(&self, other: &EditorInstallation) -> Option<Ordering> {
222        Some(self.cmp(other))
223    }
224}
225
226// const INSTALLATION_BINARY: &str = "Unity.app";
227//
228// impl From<UnityInstallation> for EditorInstallation {
229//     fn from(installation: UnityInstallation) -> Self {
230//         let location = installation.path().join(INSTALLATION_BINARY);
231//         let version = installation.into_version();
232//         EditorInstallation {
233//             version,
234//             location,
235//             manual: true,
236//         }
237//     }
238// }
239
240impl<I> FromInstallation<I> for EditorInstallation
241where I: Installation + Sized {
242    fn from_installation(value: I) -> Self {
243        EditorInstallation {
244            location: value.location().to_path_buf(),
245            version: value.version().clone(),
246            manual: true,
247        }
248    }
249}
250
251
252#[cfg(test)]
253mod tests {
254    use std::collections::HashMap;
255    use std::path::Path;
256    use unity_version::{ReleaseType, Version};
257    use crate::unity::hub::editors::EditorInstallation;
258
259    #[test]
260    fn parse_editors() {
261        #[cfg(target_os = "macos")]
262        let data = r#"{
263                        "2018.2.5f1": { "version": "2018.2.5f1", "location": ["/Applications/Unity-2018.2.5f1/Unity.app"], "manual": true },
264                        "2017.1.0f3": { "version": "2017.1.0f3", "location": ["/Applications/Unity-2017.1.0f3/Unity.app"], "manual": true }
265                  }"#;
266
267        #[cfg(target_os = "windows")]
268        let data = r#"{
269                      "2018.2.5f1": { "version": "2018.2.5f1", "location": ["C:\\Program Files\\Unity-2018.2.5f1\\Editor\\Unity.exe"], "manual": true },
270                      "2017.1.0f3": { "version": "2017.1.0f3", "location": ["C:\\Program Files\\Unity-2017.1.0f3\\Editor\\Unity.exe"], "manual": true }
271                }"#;
272
273        #[cfg(target_os = "linux")]
274        let data = r#"{
275                        "2018.2.5f1": { "version": "2018.2.5f1", "location": ["/homce/ci/.local/share/Unity-2018.2.5f1/Editor/Unity"], "manual": true },
276                        "2017.1.0f3": { "version": "2017.1.0f3", "location": ["/homce/ci/.local/share/Unity-2017.1.0f3/Editor/Unity"], "manual": true }
277                  }"#;
278
279        let editors: HashMap<Version, EditorInstallation> =
280            serde_json::from_str(data).unwrap();
281
282        let v = Version::new(2018, 2, 5, ReleaseType::Final, 1);
283
284        #[cfg(target_os = "macos")]
285        let p = Path::new("/Applications/Unity-2018.2.5f1");
286        #[cfg(target_os = "windows")]
287        let p = Path::new("C:\\Program Files\\Unity-2018.2.5f1");
288        #[cfg(target_os = "linux")]
289        let p = Path::new("/homce/ci/.local/share/Unity-2018.2.5f1");
290
291        assert_eq!(
292            &editors[&v],
293            &EditorInstallation {
294                version: v,
295                location: p.to_path_buf(),
296                manual: true
297            }
298        );
299    }
300
301    #[test]
302    fn write_editors() {
303        let v = Version::new(2018, 2, 5, ReleaseType::Final, 1);
304
305        #[cfg(target_os = "macos")]
306        let p = Path::new("/Applications/Unity-2018.2.5f1");
307        #[cfg(target_os = "windows")]
308        let p = Path::new("C:\\Program Files\\Unity-2018.2.5f1");
309        #[cfg(target_os = "linux")]
310        let p = Path::new("/homce/ci/.local/share/Unity-2018.2.5f1");
311
312        #[cfg(target_os = "macos")]
313        let expected_result = r#"{"2018.2.5f1":{"version":"2018.2.5f1","location":["/Applications/Unity-2018.2.5f1/Unity.app"],"manual":true}}"#;
314
315        #[cfg(target_os = "windows")]
316        let expected_result = r#"{"2018.2.5f1":{"version":"2018.2.5f1","location":["C:\\Program Files\\Unity-2018.2.5f1\\Editor\\Unity.exe"],"manual":true}}"#;
317
318        #[cfg(target_os = "linux")]
319        let expected_result = r#"{"2018.2.5f1":{"version":"2018.2.5f1","location":["/homce/ci/.local/share/Unity-2018.2.5f1/Editor/Unity"],"manual":true}}"#;
320
321        let i = EditorInstallation {
322            version: v.clone(),
323            location: p.to_path_buf(),
324            manual: true,
325        };
326
327        let expected_editors: HashMap<Version, EditorInstallation> =
328            serde_json::from_str(&expected_result).unwrap();
329
330        let mut editors: HashMap<Version, EditorInstallation> = HashMap::new();
331        editors.insert(v, i);
332        let json = serde_json::to_string(&editors).expect("convert editors map to json");
333        let written_editors: HashMap<Version, EditorInstallation> =
334            serde_json::from_str(&json).unwrap();
335
336        assert_eq!(
337            written_editors,
338            expected_editors
339        );
340    }
341}