unity_hub/unity/hub/
editors.rs

1use std::cmp::Ordering;
2use super::*;
3use crate::unity;
4use crate::unity::hub::paths;
5use crate::unity::installation::{UnityInstallation, Installation, FromInstallation};
6use serde_json;
7use std::collections::HashMap;
8use std::fs;
9use std::fs::File;
10use std::io::Write;
11use std::iter::{IntoIterator, Iterator};
12use std::path::PathBuf;
13use log::{debug, trace};
14use serde::{Deserialize, Serialize};
15use unity_version::Version;
16
17#[derive(Debug)]
18pub struct Editors {
19    map: HashMap<Version, EditorInstallation>,
20}
21
22impl Editors {
23    pub fn load() -> Result<Editors> {
24        let path = paths::editors_config_path()
25            .ok_or_else(|| (UnityHubError::ConfigDirectoryNotFound))?;
26
27        let map: HashMap<Version, EditorInstallation> = if path.exists() {
28            debug!("load hub editors from file: {}", path.display());
29            File::open(path)
30                .map_err(|source| {
31                    UnityHubError::ReadConfigError{ config: "editors.json".to_string(), source: source.into() }
32                })
33                .and_then(|f| {
34                    serde_json::from_reader(f).map_err(|source| {
35                        UnityHubError::ReadConfigError { config: "editors.json".to_string(), source: source.into()}
36                    })
37                })?
38        } else {
39            debug!("hub editors file doesn't exist return empty map");
40            HashMap::new()
41        };
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
128trait EditorInstallationMarker {}
129
130impl EditorInstallationMarker for EditorInstallation {}
131
132impl Installation for EditorInstallation {
133    fn path(&self) -> &PathBuf {
134        &self.location()
135    }
136
137    fn version(&self) -> &Version {
138        &self.version
139    }
140}
141
142impl EditorInstallation {
143    pub fn new(version: Version, location: PathBuf) -> EditorInstallation {
144        EditorInstallation {
145            version,
146            location,
147            manual: true,
148        }
149    }
150
151    pub fn version(&self) -> &Version {
152        &self.version
153    }
154
155    pub fn location(&self) -> &PathBuf {
156        &self.location
157    }
158}
159
160pub mod editor_value_location {
161    use serde::de::Unexpected;
162    use serde::ser::SerializeSeq;
163    use serde::{self, Deserialize, Deserializer, Serializer};
164    use std::path::{Path, PathBuf};
165
166    pub fn deserialize<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
167    where
168        D: Deserializer<'de>,
169    {
170        let paths: Vec<String> = Vec::deserialize(deserializer)?;
171        let path = paths
172            .first()
173            .ok_or_else(|| serde::de::Error::invalid_length(0, &"1"))?;
174        let location = Path::new(&path)
175            .parent()
176            .and_then(|location| {
177                if cfg!(target_os = "windows") || cfg!(target_os = "linux") {
178                    return location.parent();
179                }
180                Some(location)
181            })
182            .ok_or_else(|| {
183                serde::de::Error::invalid_value(
184                    Unexpected::Other("location with empty parent"),
185                    &"valid api location",
186                )
187            })?;
188        Ok(location.to_path_buf())
189    }
190
191    pub fn serialize<S>(location: &PathBuf, serializer: S) -> Result<S::Ok, S::Error>
192    where
193        S: Serializer,
194    {
195        let mut seq = serializer.serialize_seq(Some(1))?;
196
197        #[cfg(target_os = "windows")]
198        seq.serialize_element(&location.join("Editors\\Unity.exe"))?;
199        #[cfg(target_os = "linux")]
200        seq.serialize_element(&location.join("Editors/Unity"))?;
201        #[cfg(target_os = "macos")]
202        seq.serialize_element(&location.join("Unity.app"))?;
203        #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
204        seq.serialize_element(&location)?;
205
206        seq.end()
207    }
208}
209
210impl Ord for EditorInstallation {
211    fn cmp(&self, other: &EditorInstallation) -> Ordering {
212        self.version.cmp(&other.version)
213    }
214}
215
216impl PartialOrd for EditorInstallation {
217    fn partial_cmp(&self, other: &EditorInstallation) -> Option<Ordering> {
218        Some(self.cmp(other))
219    }
220}
221
222// const INSTALLATION_BINARY: &str = "Unity.app";
223//
224// impl From<UnityInstallation> for EditorInstallation {
225//     fn from(installation: UnityInstallation) -> Self {
226//         let location = installation.path().join(INSTALLATION_BINARY);
227//         let version = installation.into_version();
228//         EditorInstallation {
229//             version,
230//             location,
231//             manual: true,
232//         }
233//     }
234// }
235
236impl<I> FromInstallation<I> for EditorInstallation
237where I: Installation + Sized {
238    fn from_installation(value: I) -> Self {
239        EditorInstallation {
240            location: value.location().to_path_buf(),
241            version: value.version().clone(),
242            manual: true,
243        }
244    }
245}
246
247
248#[cfg(test)]
249mod tests {
250    use std::collections::HashMap;
251    use std::path::Path;
252    use unity_version::{ReleaseType, Version};
253    use crate::unity::hub::editors::EditorInstallation;
254
255    #[test]
256    fn parse_editors() {
257        #[cfg(target_os = "macos")]
258        let data = r#"{
259                        "2018.2.5f1": { "version": "2018.2.5f1", "location": ["/Applications/Unity-2018.2.5f1/Unity.app"], "manual": true },
260                        "2017.1.0f3": { "version": "2017.1.0f3", "location": ["/Applications/Unity-2017.1.0f3/Unity.app"], "manual": true }
261                  }"#;
262
263        #[cfg(target_os = "windows")]
264        let data = r#"{
265                      "2018.2.5f1": { "version": "2018.2.5f1", "location": ["C:\\Program Files\\Unity-2018.2.5f1\\Editor\\Unity.exe"], "manual": true },
266                      "2017.1.0f3": { "version": "2017.1.0f3", "location": ["C:\\Program Files\\Unity-2017.1.0f3\\Editor\\Unity.exe"], "manual": true }
267                }"#;
268
269        #[cfg(target_os = "linux")]
270        let data = r#"{
271                        "2018.2.5f1": { "version": "2018.2.5f1", "location": ["/homce/ci/.local/share/Unity-2018.2.5f1/Editor/Unity"], "manual": true },
272                        "2017.1.0f3": { "version": "2017.1.0f3", "location": ["/homce/ci/.local/share/Unity-2017.1.0f3/Editor/Unity"], "manual": true }
273                  }"#;
274
275        let editors: HashMap<Version, EditorInstallation> =
276            serde_json::from_str(data).unwrap();
277
278        let v = Version::new(2018, 2, 5, ReleaseType::Final, 1);
279
280        #[cfg(target_os = "macos")]
281        let p = Path::new("/Applications/Unity-2018.2.5f1");
282        #[cfg(target_os = "windows")]
283        let p = Path::new("C:\\Program Files\\Unity-2018.2.5f1");
284        #[cfg(target_os = "linux")]
285        let p = Path::new("/homce/ci/.local/share/Unity-2018.2.5f1");
286
287        assert_eq!(
288            &editors[&v],
289            &EditorInstallation {
290                version: v,
291                location: p.to_path_buf(),
292                manual: true
293            }
294        );
295    }
296
297    #[test]
298    fn write_editors() {
299        let v = Version::new(2018, 2, 5, ReleaseType::Final, 1);
300
301        #[cfg(target_os = "macos")]
302        let p = Path::new("/Applications/Unity-2018.2.5f1");
303        #[cfg(target_os = "windows")]
304        let p = Path::new("C:\\Program Files\\Unity-2018.2.5f1");
305        #[cfg(target_os = "linux")]
306        let p = Path::new("/homce/ci/.local/share/Unity-2018.2.5f1");
307
308        #[cfg(target_os = "macos")]
309        let expected_result = r#"{"2018.2.5f1":{"version":"2018.2.5f1","location":["/Applications/Unity-2018.2.5f1/Unity.app"],"manual":true}}"#;
310
311        #[cfg(target_os = "windows")]
312        let expected_result = r#"{"2018.2.5f1":{"version":"2018.2.5f1","location":["C:\\Program Files\\Unity-2018.2.5f1\\Editor\\Unity.exe"],"manual":true}}"#;
313
314        #[cfg(target_os = "linux")]
315        let expected_result = r#"{"2018.2.5f1":{"version":"2018.2.5f1","location":["/homce/ci/.local/share/Unity-2018.2.5f1/Editor/Unity"],"manual":true}}"#;
316
317        let i = EditorInstallation {
318            version: v.clone(),
319            location: p.to_path_buf(),
320            manual: true,
321        };
322
323        let expected_editors: HashMap<Version, EditorInstallation> =
324            serde_json::from_str(&expected_result).unwrap();
325
326        let mut editors: HashMap<Version, EditorInstallation> = HashMap::new();
327        editors.insert(v, i);
328        let json = serde_json::to_string(&editors).expect("convert editors map to json");
329        let written_editors: HashMap<Version, EditorInstallation> =
330            serde_json::from_str(&json).unwrap();
331
332        assert_eq!(
333            written_editors,
334            expected_editors
335        );
336    }
337}