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}