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 trace!("loaded editors map: {:?}", map);
43 Ok(Editors::create(map))
44 }
45
46 pub fn create(mut map: HashMap<Version, EditorInstallation>) -> Editors {
47 trace!("create Editors map");
48 map.retain(|version, installation| {
49 trace!(
50 "filter: version: {} - installaton: {:?}",
51 version,
52 installation
53 );
54 let check_installation = UnityInstallation::new(installation.location.to_path_buf());
55 if let Ok(check_installation) = check_installation {
56 trace!(
57 "Found api installation at with version {} at location: {}",
58 check_installation.version(),
59 installation.location.display()
60 );
61 trace!(
62 "Installation has correct version: {}",
63 check_installation.version() == version
64 );
65 check_installation.version() == version
66 } else {
67 trace!(
68 "No installtion found at location: {}",
69 installation.location.display()
70 );
71 false
72 }
73 });
74 Editors { map }
75 }
76
77 pub fn add(&mut self, editor: &EditorInstallation) -> Option<EditorInstallation> {
78 self.map.insert(editor.version.clone(), editor.clone())
79 }
80
81 pub fn remove(&mut self, editor: &EditorInstallation) -> Option<EditorInstallation> {
82 self.map.remove(&editor.version)
83 }
84
85 pub fn remove_version(&mut self, version: &Version) -> Option<EditorInstallation> {
86 self.map.remove(&version)
87 }
88
89 pub fn flush(&self) -> Result<()> {
90 let config_path =
91 paths::config_path().ok_or_else(|| (UnityHubError::ConfigDirectoryNotFound))?;
92
93 let path = paths::editors_config_path()
94 .ok_or_else(|| UnityHubError::ConfigNotFound("editors.json".to_string()))?;
95
96 fs::create_dir_all(config_path).map_err(|source| UnityHubError::FailedToCreateConfigDirectory{source})?;
97 let mut file = File::create(path).map_err(|source| UnityHubError::FailedToCreateConfig{config: "editors.json".to_string(), source})?;
98
99 let j = serde_json::to_string(&self.map).map_err(|source| {
100 UnityHubError::WriteConfigError {config: "editors.json".to_string(), source: source.into()}
101 })?;
102 write!(file, "{}", &j).map_err(|source| {
103 UnityHubError::WriteConfigError {config: "editors.json".to_string(), source: source.into()}
104 })
105 }
106}
107
108impl IntoIterator for Editors {
109 type Item = EditorInstallation;
110 type IntoIter = ::std::vec::IntoIter<EditorInstallation>;
111
112 fn into_iter(self) -> Self::IntoIter {
113 self.map
114 .values()
115 .cloned()
116 .collect::<Vec<Self::Item>>()
117 .into_iter()
118 }
119}
120
121#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
122pub struct EditorInstallation {
123 version: Version,
124 #[serde(with = "editor_value_location")]
125 location: PathBuf,
126 manual: bool,
127}
128
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
226impl<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}