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
222impl<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}