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