smaug_lib/
config.rs

1use derive_more::Display;
2use derive_more::Error;
3use linked_hash_map::LinkedHashMap;
4use log::*;
5use relative_path::RelativePathBuf;
6use semver::VersionReq;
7use serde::de;
8use serde::de::Deserializer;
9use serde::de::MapAccess;
10use serde::de::Visitor;
11use serde::Deserialize;
12use serde::Serialize;
13use std::fmt;
14use std::path::Path;
15use std::path::PathBuf;
16
17#[derive(Debug, Deserialize, Serialize)]
18pub struct Config {
19    pub package: Option<Package>,
20    pub project: Option<Project>,
21    pub dragonruby: DragonRuby,
22    pub itch: Option<Itch>,
23    #[serde(default)]
24    pub dependencies: LinkedHashMap<String, DependencyOptions>,
25}
26
27#[derive(Clone, Debug, Deserialize, Serialize)]
28pub struct Package {
29    pub name: String,
30    pub description: Option<String>,
31    pub homepage: Option<String>,
32    pub documentation: Option<String>,
33    pub repository: Option<String>,
34    pub readme: Option<String>,
35    pub version: String,
36    #[serde(default)]
37    pub keywords: Vec<String>,
38    #[serde(default)]
39    pub authors: Vec<String>,
40    #[serde(default)]
41    pub installs: LinkedHashMap<RelativePathBuf, RelativePathBuf>,
42    #[serde(default)]
43    pub requires: Vec<RelativePathBuf>,
44}
45
46#[derive(Clone, Debug, Deserialize, Serialize)]
47pub struct Project {
48    pub name: String,
49    pub title: String,
50    pub version: String,
51    pub authors: Vec<String>,
52    pub icon: String,
53    #[serde(default)]
54    pub compile_ruby: bool,
55}
56
57#[derive(Debug, Deserialize, Serialize)]
58pub struct DragonRuby {
59    pub version: String,
60    pub edition: String,
61}
62
63#[derive(Debug, Deserialize, Serialize)]
64pub struct Itch {
65    pub url: String,
66    pub username: String,
67}
68
69#[derive(Debug, Serialize)]
70pub enum DependencyOptions {
71    Dir {
72        dir: PathBuf,
73    },
74    File {
75        file: PathBuf,
76    },
77    Git {
78        branch: Option<String>,
79        repo: String,
80        rev: Option<String>,
81        tag: Option<String>,
82    },
83    Registry {
84        version: String,
85    },
86    Url {
87        url: String,
88    },
89}
90
91#[derive(Debug, Display, Error)]
92pub enum Error {
93    #[display(fmt = "Could not find Smaug.toml at {}", "path.display()")]
94    FileNotFound { path: PathBuf },
95    #[display(
96        fmt = "Could not parse Smaug.toml at {}: {}",
97        "path.display()",
98        "parent"
99    )]
100    ParseError {
101        path: PathBuf,
102        parent: toml::de::Error,
103    },
104}
105
106pub fn load<P: AsRef<Path>>(path: &P) -> Result<Config, Error> {
107    let canonical = std::fs::canonicalize(path.as_ref());
108    if canonical.is_err() {
109        return Err(Error::FileNotFound {
110            path: path.as_ref().to_path_buf(),
111        });
112    }
113
114    let path = canonical.unwrap();
115    if !path.is_file() {
116        return Err(Error::FileNotFound { path });
117    }
118
119    std::env::set_current_dir(&path.parent().unwrap()).unwrap();
120    let contents = std::fs::read_to_string(path.clone()).expect("Could not read Smaug.toml");
121    from_str(&contents, &path)
122}
123
124pub fn from_str<S: AsRef<str>>(contents: &S, path: &Path) -> Result<Config, Error> {
125    match toml::from_str(contents.as_ref()) {
126        Ok(config) => Ok(config),
127        Err(err) => Err(Error::ParseError {
128            path: path.to_path_buf(),
129            parent: err,
130        }),
131    }
132}
133
134impl<'de> Deserialize<'de> for DependencyOptions {
135    fn deserialize<D>(deserializer: D) -> Result<DependencyOptions, D::Error>
136    where
137        D: Deserializer<'de>,
138    {
139        struct DependencyOptionsVisitor;
140
141        impl<'de> Visitor<'de> for DependencyOptionsVisitor {
142            type Value = DependencyOptions;
143
144            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
145                formatter.write_str("struct DependencyOptions")
146            }
147
148            fn visit_str<E>(self, value: &str) -> Result<DependencyOptions, E>
149            where
150                E: de::Error,
151            {
152                let path = if let Ok(expanded) = shellexpand::full(&value) {
153                    let expanded = expanded.clone();
154                    let expanded_string = expanded.to_string();
155                    debug!("Expanded Path: {}", expanded_string);
156                    let pb = std::env::current_dir();
157                    debug!("{:?}", pb);
158                    PathBuf::from(expanded_string)
159                } else {
160                    PathBuf::from(value)
161                };
162
163                if VersionReq::parse(value).is_ok() {
164                    Ok(DependencyOptions::Registry {
165                        version: value.to_string(),
166                    })
167                } else if let Some("git") = path.extension().and_then(|str| str.to_str()) {
168                    Ok(DependencyOptions::Git {
169                        repo: value.to_string(),
170                        branch: None,
171                        rev: None,
172                        tag: None,
173                    })
174                } else if path.is_dir() {
175                    let canonical =
176                        std::fs::canonicalize(path.clone()).expect("Could not find path.");
177                    Ok(DependencyOptions::Dir { dir: canonical })
178                } else if path.is_file() {
179                    Ok(DependencyOptions::File {
180                        file: path.to_path_buf(),
181                    })
182                } else if let Ok(_url) = url::Url::parse(value) {
183                    Ok(DependencyOptions::Url {
184                        url: value.to_string(),
185                    })
186                } else {
187                    Err(de::Error::invalid_value(
188                        de::Unexpected::Map,
189                        &"version or options",
190                    ))
191                }
192            }
193
194            fn visit_map<M>(self, mut map: M) -> Result<DependencyOptions, M::Error>
195            where
196                M: MapAccess<'de>,
197            {
198                let mut repo: Option<String> = None;
199                let mut branch: Option<String> = None;
200                let mut tag: Option<String> = None;
201                let mut rev: Option<String> = None;
202                let mut dir: Option<String> = None;
203                let mut file: Option<String> = None;
204                let mut version: Option<String> = None;
205                let mut url: Option<String> = None;
206
207                while let Some(key) = map.next_key()? {
208                    match key {
209                        "branch" => branch = Some(map.next_value()?),
210                        "repo" => repo = Some(map.next_value()?),
211                        "tag" => tag = Some(map.next_value()?),
212                        "rev" => rev = Some(map.next_value()?),
213                        "dir" => dir = Some(map.next_value()?),
214                        "file" => file = Some(map.next_value()?),
215                        "version" => version = Some(map.next_value()?),
216                        "url" => url = Some(map.next_value()?),
217                        _ => unreachable!(),
218                    }
219                }
220
221                if let Some(repo) = repo {
222                    Ok(DependencyOptions::Git {
223                        repo,
224                        branch,
225                        tag,
226                        rev,
227                    })
228                } else if let Some(dir) = dir {
229                    Ok(DependencyOptions::Dir {
230                        dir: Path::new(&dir).to_path_buf(),
231                    })
232                } else if let Some(file) = file {
233                    Ok(DependencyOptions::File {
234                        file: Path::new(&file).to_path_buf(),
235                    })
236                } else if let Some(version) = version {
237                    Ok(DependencyOptions::Registry { version })
238                } else if let Some(url) = url {
239                    Ok(DependencyOptions::Url { url })
240                } else {
241                    Err(de::Error::invalid_value(
242                        de::Unexpected::Map,
243                        &"version or options",
244                    ))
245                }
246            }
247        }
248
249        deserializer.deserialize_any(DependencyOptionsVisitor)
250    }
251}