1use diesel::prelude::*;
3use diesel::sqlite::SqliteConnection;
4use dotenv::dotenv;
5use std::env;
6use std::ffi::{OsStr, OsString};
7use std::fs;
8use std::path::{Path, PathBuf};
9use crate::{ErrorKind, Result, ResultExt, Error};
12use crate::schema::languages;
13use crate::schema::words;
15embed_migrations!("migrations/");
19
20pub type DatabaseSession = SqliteConnection;
21
22pub const DATABASE_ENV_VAR: &'static str = "DATABASE_URL";
23const DATABASE_STANDARD_RELATIVE_PATH: &'static str = ".cifra/cifra_database.sqlite";
26
27struct DATABASE_STANDARD_PATH;
32
33impl DATABASE_STANDARD_PATH{
34 fn get_database_standard_path()-> PathBuf{
35 let mut path: PathBuf = dirs::home_dir().expect("No home user found to place database file.");
36 path.push(PathBuf::from(DATABASE_STANDARD_RELATIVE_PATH));
37 path
38 }
39}
40
41impl From<DATABASE_STANDARD_PATH> for String {
42 fn from(_: DATABASE_STANDARD_PATH) -> Self {
43 let path = DATABASE_STANDARD_PATH::get_database_standard_path();
44 String::from(path.to_str().unwrap())
45 }
46}
47
48impl From<DATABASE_STANDARD_PATH> for PathBuf {
49 fn from(_: DATABASE_STANDARD_PATH) -> Self {
50 DATABASE_STANDARD_PATH::get_database_standard_path()
51 }
52}
53
54impl From<DATABASE_STANDARD_PATH> for OsString {
55 fn from(_: DATABASE_STANDARD_PATH) -> Self {
56 let path = DATABASE_STANDARD_PATH::get_database_standard_path();
57 path.into_os_string()
58 }
59}
60
61fn check_database_url_env_var_exists()-> Result<String>{
72 return env::var(DATABASE_ENV_VAR)
73 .chain_err (|| {
74 ErrorKind::DatabaseError(String::from("Error finding out if database env var existed previously."))
75 })
76}
77
78pub fn create_database()-> Result<Database> {
80 let database = Database::new()?;
81 embedded_migrations::run(&database.session)
82 .chain_err(|| ErrorKind::DatabaseError(String::from("Error running database migrations.")))?;
83 Ok(database)
84}
85
86fn create_folder_path(path: &Path) -> Result<()>{
88 if let Ok(()) = fs::create_dir_all(path) {
89 Ok(())
90 } else {
91 let path_string = path.as_os_str();
92 bail!(String::from(path_string.to_str().unwrap()))
93 }
94}
95
96pub struct Database {
97 pub session: DatabaseSession,
98 database_path: String
99}
100
101impl Database {
102
103 pub fn new() -> Result<Self> {
116 if let Ok(database_path) = check_database_url_env_var_exists() {
117 Ok(Database {
119 session: Self::open_session()?,
120 database_path
121 })
122 } else {
123 let database_path: OsString = DATABASE_STANDARD_PATH.into();
125 env::set_var(DATABASE_ENV_VAR, database_path.as_os_str());
126 let database_path = PathBuf::from(DATABASE_STANDARD_PATH);
127 let database_folder = database_path.parent()
128 .chain_err(|| ErrorKind::FolderError(String::from(database_path.as_os_str().to_str().unwrap())))?;
129 create_folder_path(database_folder);
130 create_database()
131 }
132 }
133
134 fn open_session() -> Result<DatabaseSession> {
139 dotenv().ok();
140 let database_url = env::var("DATABASE_URL")
141 .chain_err(|| ErrorKind::DatabaseError(String::from("DATABASE_URL must be set")))?;
142 SqliteConnection::establish(&database_url)
143 .chain_err(|| ErrorKind::DatabaseError(format!("Error connecting to DATABASE_URL: {}", database_url)))
144 }
145}
146
147#[derive(Queryable, Identifiable, Associations, Debug, PartialEq)]
149#[table_name="languages"]
150pub struct Language {
152 pub id: i32,
153 pub language: String,
154}
155
156#[derive(Insertable)]
157#[table_name="languages"]
158pub struct NewLanguage<'a> {
159 pub language: &'a str,
160}
161
162
163#[derive(Queryable, Identifiable, Associations, Debug, PartialEq)]
165#[table_name="words"]
166#[belongs_to(Language)]
167pub struct Word {
168 pub id: i32,
169 pub word: String,
170 pub word_pattern: String,
171 pub language_id: i32
172}
173
174#[derive(Insertable)]
175#[table_name="words"]
176pub struct NewWord<'a> {
177 pub word: &'a str,
178 pub word_pattern: String,
179 pub language_id: i32
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use std::ffi::OsStr;
186 use std::path::Path;
187 use test_common::fs::tmp::TestEnvironment;
188 use test_common::system::env::TemporalEnvironmentVariable;
189
190 #[test]
191 fn test_create_database() {
192 let test_folder = TestEnvironment::new();
193 let absolute_path_to_database = test_folder.path().join("cifra_database.sqlite");
194 let absolute_pathname_to_database = match absolute_path_to_database.to_str() {
195 Some(path)=> path,
196 None=> panic!("Path uses non valid characters.")
197 };
198 let database_path = Path::new(absolute_pathname_to_database);
200 assert!(!database_path.exists());
201 let test_env = TemporalEnvironmentVariable::new("DATABASE_URL", absolute_pathname_to_database);
202 create_database();
203 assert!(database_path.exists());
205 }
206
207 #[test]
208 fn test_create_database_path() {
209 let test_folder = TestEnvironment::new();
210 let absolute_path_to_database = test_folder.path().join(".cifra/parent1/parent2/cifra_database.sqlite");
211 let database_folder = absolute_path_to_database.parent().unwrap();
212 assert!(!database_folder.exists());
213 create_folder_path(database_folder);
214 assert!(database_folder.exists());
215 }
216
217 #[test]
218 fn test_database_standard_path() {
219 let mut expected_database_folder = dirs::home_dir().unwrap();
220 expected_database_folder.push(DATABASE_STANDARD_RELATIVE_PATH);
221 assert_eq!(expected_database_folder.into_os_string(), OsString::from(DATABASE_STANDARD_PATH));
222 }
223
224
225}