soma/
lib.rs

1use std::cell::{RefCell, RefMut};
2use std::fmt;
3use std::fs::File;
4use std::io::Read;
5use std::ops::Deref;
6use std::path::Path;
7
8use bollard::Docker;
9use clap::crate_version;
10use hyper::client::connect::Connect;
11use lazy_static::lazy_static;
12use regex::Regex;
13use serde::de::{self, Deserializer, Unexpected, Visitor};
14use serde::ser::Serializer;
15use serde::{Deserialize, Serialize};
16
17use crate::data_dir::DataDirectory;
18use crate::prelude::*;
19use crate::repository::RepositoryManager;
20
21pub mod data_dir;
22pub mod docker;
23pub mod error;
24pub mod ops;
25pub mod prelude;
26pub mod problem;
27pub mod repository;
28pub mod template;
29
30pub const VERSION: &str = crate_version!();
31
32pub trait Printer {
33    type Handle;
34
35    fn get_current_handle(&mut self) -> Self::Handle;
36    fn write_line_at(&mut self, handle: &Self::Handle, message: &str);
37    fn write_line(&mut self, message: &str);
38}
39
40pub struct Environment<'a, C: 'static, P: Printer + 'static> {
41    username: NameString,
42    repo_manager: RepositoryManager<'a>,
43    docker: Docker<C>,
44    printer: RefCell<P>,
45}
46
47impl<'a, C, P> Environment<'a, C, P>
48where
49    C: Connect,
50    P: Printer,
51{
52    pub fn new(
53        username: String,
54        data_dir: &'a mut DataDirectory,
55        docker: Docker<C>,
56        printer: P,
57    ) -> SomaResult<Environment<'a, C, P>> {
58        let repo_manager = data_dir.register::<RepositoryManager>()?;
59        let username = NameString::try_from(username)?;
60
61        Ok(Environment {
62            username,
63            repo_manager,
64            docker,
65            printer: RefCell::new(printer),
66        })
67    }
68
69    pub fn username(&self) -> &NameString {
70        &self.username
71    }
72
73    pub fn printer(&self) -> RefMut<P> {
74        self.printer.borrow_mut()
75    }
76
77    pub fn repo_manager(&self) -> &RepositoryManager<'a> {
78        &self.repo_manager
79    }
80
81    pub fn repo_manager_mut(&mut self) -> &mut RepositoryManager<'a> {
82        &mut self.repo_manager
83    }
84}
85
86#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
87pub struct NameString {
88    inner: String,
89}
90
91lazy_static! {
92    static ref NAME_REGEX: Regex = Regex::new(r"^[a-z0-9]+((?:_|__|[-]*)[a-z0-9]+)*$").unwrap();
93}
94
95impl NameString {
96    // TODO: Use TryFrom trait when Rust stabilizes it.
97    pub fn try_from(s: impl AsRef<str>) -> SomaResult<NameString> {
98        let s = s.as_ref();
99        if NAME_REGEX.is_match(s) {
100            Ok(NameString {
101                inner: s.to_owned(),
102            })
103        } else {
104            Err(SomaError::InvalidName)?
105        }
106    }
107}
108
109impl PartialEq<String> for NameString {
110    fn eq(&self, other: &String) -> bool {
111        &self.inner == other
112    }
113}
114
115impl PartialEq<str> for NameString {
116    fn eq(&self, other: &str) -> bool {
117        self.inner == other
118    }
119}
120
121impl PartialEq<NameString> for String {
122    fn eq(&self, other: &NameString) -> bool {
123        self == &other.inner
124    }
125}
126
127impl PartialEq<NameString> for str {
128    fn eq(&self, other: &NameString) -> bool {
129        self == other.inner
130    }
131}
132
133impl fmt::Display for NameString {
134    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
135        write!(f, "{}", self.inner)
136    }
137}
138
139impl Deref for NameString {
140    type Target = String;
141
142    fn deref(&self) -> &String {
143        &self.inner
144    }
145}
146
147impl AsRef<str> for NameString {
148    fn as_ref(&self) -> &str {
149        &self.inner
150    }
151}
152
153impl AsRef<Path> for NameString {
154    fn as_ref(&self) -> &Path {
155        Path::new(&self.inner)
156    }
157}
158
159impl Serialize for NameString {
160    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
161    where
162        S: Serializer,
163    {
164        serializer.serialize_str(self.as_ref())
165    }
166}
167
168impl<'de> Deserialize<'de> for NameString {
169    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
170    where
171        D: Deserializer<'de>,
172    {
173        deserializer.deserialize_str(NameStringVisitor)
174    }
175}
176
177struct NameStringVisitor;
178
179impl<'de> Visitor<'de> for NameStringVisitor {
180    type Value = NameString;
181
182    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
183        write!(formatter, "a string satisfying docker name component rules")
184    }
185
186    fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
187    where
188        E: de::Error,
189    {
190        let name = NameString::try_from(s);
191        match name {
192            Ok(name) => Ok(name),
193            Err(_) => Err(de::Error::invalid_value(Unexpected::Str(s), &self)),
194        }
195    }
196}
197
198fn read_file_contents(path: impl AsRef<Path>) -> SomaResult<Vec<u8>> {
199    let mut file = File::open(path)?;
200    let mut contents = Vec::new();
201    file.read_to_end(&mut contents)?;
202    Ok(contents)
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use serde_test::{assert_de_tokens_error, assert_tokens, Token};
209
210    #[test]
211    fn test_name_eq() {
212        assert_eq!("asdf", &NameString::try_from("asdf").unwrap());
213        assert_eq!(&NameString::try_from("asdf").unwrap(), "asdf");
214        assert_eq!(String::from("asdf"), NameString::try_from("asdf").unwrap());
215        assert_eq!(NameString::try_from("asdf").unwrap(), String::from("asdf"));
216        assert_ne!("qwer", &NameString::try_from("asdf").unwrap());
217        assert_ne!(&NameString::try_from("qwer").unwrap(), "asdf");
218        assert_ne!(String::from("qwer"), NameString::try_from("asdf").unwrap());
219        assert_ne!(NameString::try_from("qwer").unwrap(), String::from("asdf"));
220    }
221
222    #[test]
223    fn test_name_serde() {
224        assert_tokens(
225            &NameString::try_from("asdf0").unwrap(),
226            &[Token::Str("asdf0")],
227        );
228        assert_tokens(
229            &NameString::try_from("asdf0_qwer").unwrap(),
230            &[Token::Str("asdf0_qwer")],
231        );
232        assert_tokens(
233            &NameString::try_from("asdf0__qwer").unwrap(),
234            &[Token::Str("asdf0__qwer")],
235        );
236        assert_tokens(
237            &NameString::try_from("asdf0-qwer").unwrap(),
238            &[Token::Str("asdf0-qwer")],
239        );
240        assert_tokens(
241            &NameString::try_from("asdf0--qwer").unwrap(),
242            &[Token::Str("asdf0--qwer")],
243        );
244        assert_tokens(
245            &NameString::try_from("asdf0---qwer").unwrap(),
246            &[Token::Str("asdf0---qwer")],
247        );
248    }
249
250    #[test]
251    fn test_name_de_error() {
252        assert_de_tokens_error::<NameString>(
253            &[Token::Str("")],
254            "invalid value: string \"\", expected a string satisfying docker name component rules",
255        );
256        assert_de_tokens_error::<NameString>(&[
257                Token::Str("ASDF")
258            ], "invalid value: string \"ASDF\", expected a string satisfying docker name component rules"
259        );
260        assert_de_tokens_error::<NameString>(&[
261                Token::Str("AS@DF")
262            ], "invalid value: string \"AS@DF\", expected a string satisfying docker name component rules"
263        );
264        assert_de_tokens_error::<NameString>(&[
265                Token::Str("asdf0.qwer")
266            ], "invalid value: string \"asdf0.qwer\", expected a string satisfying docker name component rules"
267        );
268        assert_de_tokens_error::<NameString>(&[
269                Token::Str("asdf..qwer")
270            ], "invalid value: string \"asdf..qwer\", expected a string satisfying docker name component rules"
271        );
272        assert_de_tokens_error::<NameString>(&[
273                Token::Str("asdf___qwer")
274            ], "invalid value: string \"asdf___qwer\", expected a string satisfying docker name component rules"
275        );
276        assert_de_tokens_error::<NameString>(&[
277                Token::Str("asdf.")
278            ], "invalid value: string \"asdf.\", expected a string satisfying docker name component rules"
279        );
280        assert_de_tokens_error::<NameString>(&[
281                Token::Str("asdf_")
282            ], "invalid value: string \"asdf_\", expected a string satisfying docker name component rules"
283        );
284        assert_de_tokens_error::<NameString>(&[
285                Token::Str("asdf-")
286            ], "invalid value: string \"asdf-\", expected a string satisfying docker name component rules"
287        );
288        assert_de_tokens_error::<NameString>(&[
289                Token::Str(".asdf")
290            ], "invalid value: string \".asdf\", expected a string satisfying docker name component rules"
291        );
292        assert_de_tokens_error::<NameString>(&[
293                Token::Str("_asdf")
294            ], "invalid value: string \"_asdf\", expected a string satisfying docker name component rules"
295        );
296        assert_de_tokens_error::<NameString>(&[
297                Token::Str("-asdf")
298            ], "invalid value: string \"-asdf\", expected a string satisfying docker name component rules"
299        );
300    }
301}