diff --git a/Cargo.toml b/Cargo.toml index b8ac001..997fa4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,5 @@ -[package] -name = "naphtha" -version = "0.0.0" -authors = ["Lewin Probst "] -edition = "2018" -license = "MIT OR Apache-2.0" -description = "Work in progress" -homepage = "https://github.com/emirror-de/naphtha" -documentation = "https://github.com/emirror-de/naphtha" -repository = "https://github.com/emirror-de/naphtha" -readme = "README.md" -keywords = [] -categories = [] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] +[workspace] +members = [ + "naphtha", + "naphtha-proc-macro", +] diff --git a/naphtha-proc-macro/Cargo.toml b/naphtha-proc-macro/Cargo.toml new file mode 100644 index 0000000..6324e90 --- /dev/null +++ b/naphtha-proc-macro/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "naphtha-proc-macro" +version = "0.1.0" +authors = ["Lewin Probst "] +edition = "2018" +license = "MIT OR Apache-2.0" +description = "Supporting macro crate for naphtha" +homepage = "https://github.com/emirror-de/naphtha" +documentation = "https://github.com/emirror-de/naphtha" +repository = "https://github.com/emirror-de/naphtha" +readme = "README.md" +keywords = ["database", "models", "connection", "migration"] +categories = ["database"] + +[lib] +proc-macro = true + +[features] +default = [] +sqlite = [] +barrel-sqlite = [] + +[dependencies] +syn = { version = "^1.0.74", features = ["parsing"] } +proc-macro2 = "^1.0.28" +quote = "^1.0.9" diff --git a/naphtha-proc-macro/README.md b/naphtha-proc-macro/README.md new file mode 100644 index 0000000..939b5ef --- /dev/null +++ b/naphtha-proc-macro/README.md @@ -0,0 +1,4 @@ +# naphtha-proc-macro + +This crate is a support crate that contains the necessary macros for naphtha to +compile. diff --git a/naphtha-proc-macro/src/barrel_impl/mod.rs b/naphtha-proc-macro/src/barrel_impl/mod.rs new file mode 100644 index 0000000..b858794 --- /dev/null +++ b/naphtha-proc-macro/src/barrel_impl/mod.rs @@ -0,0 +1,2 @@ +#[cfg(any(feature = "barrel-sqlite", feature = "barrel-full"))] +pub(crate) mod sqlite; diff --git a/naphtha-proc-macro/src/barrel_impl/sqlite.rs b/naphtha-proc-macro/src/barrel_impl/sqlite.rs new file mode 100644 index 0000000..fcfea70 --- /dev/null +++ b/naphtha-proc-macro/src/barrel_impl/sqlite.rs @@ -0,0 +1,58 @@ +use quote::quote; + +pub(crate) fn impl_sqlite() -> ::proc_macro2::TokenStream { + quote! { + impl ::naphtha::barrel::DatabaseSqlMigrationExecutor<::diesel::SqliteConnection, usize> for Person + where + Self: ::naphtha::barrel::DatabaseSqlMigration, + { + fn execute_migration_up(conn: &::naphtha::DatabaseConnection<::diesel::SqliteConnection>) -> Result { + use { + ::log::error, + ::naphtha::{barrel::Migration, DatabaseConnection}, + crate::diesel::RunQueryDsl, + }; + let mut m = Migration::new(); + Self::migration_up(&mut m); + let m = m.make::<::naphtha::barrel::backend::Sqlite>(); + + let c = match conn.lock() { + Ok(c) => c, + Err(msg) => { + error!("Could not aquire lock on DatabaseSqlMigrationExecutor::execute_migration_up: {}", msg.to_string()); + return Err(msg.to_string()); + } + }; + + match ::diesel::sql_query(m).execute(&*c) { + Ok(u) => Ok(u), + Err(msg) => Err(msg.to_string()), + } + } + + fn execute_migration_down(conn: &::naphtha::DatabaseConnection<::diesel::SqliteConnection>) -> Result { + use { + ::log::error, + ::naphtha::{barrel::Migration, DatabaseConnection}, + crate::diesel::RunQueryDsl, + }; + let mut m = Migration::new(); + Self::migration_down(&mut m); + let m = m.make::<::naphtha::barrel::backend::Sqlite>(); + + let c = match conn.lock() { + Ok(c) => c, + Err(msg) => { + error!("Could not aquire lock on DatabaseSqlMigrationExecutor::execute_migration_down for model: {}", msg.to_string()); + return Err(msg.to_string()); + } + }; + + match ::diesel::sql_query(m).execute(&*c) { + Ok(u) => Ok(u), + Err(msg) => Err(msg.to_string()), + } + } + } + } +} diff --git a/naphtha-proc-macro/src/database_impl/mod.rs b/naphtha-proc-macro/src/database_impl/mod.rs new file mode 100644 index 0000000..f43b3c6 --- /dev/null +++ b/naphtha-proc-macro/src/database_impl/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "sqlite")] +pub(crate) mod sqlite; diff --git a/naphtha-proc-macro/src/database_impl/sqlite.rs b/naphtha-proc-macro/src/database_impl/sqlite.rs new file mode 100644 index 0000000..213637a --- /dev/null +++ b/naphtha-proc-macro/src/database_impl/sqlite.rs @@ -0,0 +1,163 @@ +use { + quote::quote, + syn::{Data::Struct, DeriveInput}, +}; + +pub(crate) fn impl_sqlite( + ast: &DeriveInput, + attr: &::proc_macro2::TokenStream, +) -> ::proc_macro2::TokenStream { + let database_modifier = impl_database_modifier(ast, attr); + quote! { + #database_modifier + } +} + +fn impl_database_modifier( + ast: &DeriveInput, + table_name: &::proc_macro2::TokenStream, +) -> ::proc_macro2::TokenStream { + let name = &ast.ident; + let table_name = crate::helper::extract_table_name(table_name); + //let table_name: syn::UsePath = syn::parse_quote! {#name::table_name()}; + + let insert_properties = generate_insert_properties(ast); + if !crate::helper::has_id(ast) { + panic!("No `id` member found in model `{}`. Currently only models having an `id` column of type `i32` are supported.", name); + } + + /* + let model_has_id_member = crate::helper::has_id(ast); + if (#model_has_id_member) { + #table_name.select(#table_name.primary_key()) + .order(#table_name.primary_key().desc()) + .first(&*c) + } else { + #table_name.select(#table_name.primary_key()) + .filter(#table_name.primary_key().eq(self.primary_key())) + .first(&*c) + } + */ + + quote! { + use { + ::diesel::{backend::Backend, prelude::*}, + ::log::error, + ::naphtha::{DatabaseModelModifier, DatabaseConnection}, + }; + impl DatabaseModelModifier for #name + where + Self: ::naphtha::DatabaseUpdateHandler, + { + fn insert(&mut self, conn: &DatabaseConnection) -> bool { + use { + ::naphtha::DatabaseModel, + schema::{#table_name, #table_name::dsl::*}, + }; + // preventing duplicate insertion if default primary key gets + // changed on database insertion. + if self.primary_key() != Self::default_primary_key() { + return false; + } + let c = match conn.lock() { + Ok(c) => c, + Err(msg) => { + error!("Could not aquire lock on DatabaseModifier::insert for model:\n{:#?}", self); + return false; + } + }; + let res_id = match c.transaction::<_, ::diesel::result::Error, _>(|| { + diesel::insert_into(#table_name) + .values((#insert_properties)) + .execute(&*c)?; + #table_name.select(#table_name.primary_key()) + .order(#table_name.primary_key().desc()) + .first(&*c) + }) { + Ok(v) => v, + Err(msg) => { + error!("Failed inserting entity:\n{:#?}", self); + return false; + } + }; + self.set_primary_key(&res_id); + true + } + + fn update(&mut self, conn: &DatabaseConnection) -> bool { + let c = match conn.lock() { + Ok(c) => c, + Err(msg) => { + error!("Could not aquire lock on DatabaseModifier::update for model:\n{:#?}", self); + return false; + } + }; + self.before_update(); + let update_result = match self.save_changes::(&*c) { + Ok(_) => true, + Err(msg) => { + error!("Failed updating entity:\n{:#?}", self); + return false; + }, + }; + self.after_update(); + update_result + } + + fn remove(self, conn: &DatabaseConnection) -> bool { + use { + ::log::info, + ::naphtha::DatabaseModel, + schema::{#table_name, #table_name::dsl::*}, + }; + let c = match conn.lock() { + Ok(c) => c, + Err(msg) => { + error!("Could not aquire lock on DatabaseModifier::remove for model:\n{:#?}", self); + return false; + } + }; + let num_deleted = ::diesel::delete( + #table_name.filter( + #table_name.primary_key().eq(self.primary_key()) + ) + ); + match num_deleted.execute(&*c) { + Ok(_) => { + #[cfg(debug_assertions)] + info!("Removed entity with primary key {} from database!", self.primary_key()); + true + }, + Err(msg) => { + error!("Could not aquire lock on DatabaseModifier::remove for model:\n{:#?}", self); + false + } + } + } + } + } +} + +fn generate_insert_properties(ast: &DeriveInput) -> ::proc_macro2::TokenStream { + let data = match &ast.data { + Struct(data) => data, + _ => panic!("Other data formats than \"struct\" is not supported yet!"), + }; + let mut collected_properties = quote! {}; + for field in data.fields.iter() { + if field.ident.is_none() { + continue; + } + let fieldname = field.ident.as_ref().unwrap(); + if &fieldname.to_string()[..] == "id" { + // field id is currently used as primary key and therfore generated + // by the database, so it must not be set during insertion. + continue; + } + collected_properties = quote! { + #collected_properties + #fieldname.eq(&self.#fieldname), + }; + } + collected_properties +} diff --git a/naphtha-proc-macro/src/helper.rs b/naphtha-proc-macro/src/helper.rs new file mode 100644 index 0000000..1921e66 --- /dev/null +++ b/naphtha-proc-macro/src/helper.rs @@ -0,0 +1,56 @@ +use syn::{Data::Struct, DeriveInput, Ident}; + +pub fn extract_table_name(attr: &::proc_macro2::TokenStream) -> Ident { + for t in attr.clone().into_iter() { + match t { + ::proc_macro2::TokenTree::Group(g) => { + let mut is_next = false; + for t in g.stream().into_iter() { + match t { + ::proc_macro2::TokenTree::Ident(i) => { + let ident = ::proc_macro2::Ident::new( + "table_name", + ::proc_macro2::Span::call_site(), + ); + is_next = ident == i; + } + ::proc_macro2::TokenTree::Literal(l) => { + if is_next { + let l = l.to_string(); + return Ident::new( + &l[1..l.len() - 1], + ::proc_macro2::Span::call_site(), + ); + } + } + _ => continue, + } + } + } + _ => continue, + } + } + + panic!("Attribute table_name has not been found in {}", attr); +} + +pub fn has_id(ast: &DeriveInput) -> bool { + let data = match &ast.data { + Struct(data) => data, + _ => { + return false; + } + }; + for field in data.fields.iter() { + if field.ident.is_none() { + continue; + } + let fieldname = field.ident.as_ref().unwrap(); + match &fieldname.to_string()[..] { + "id" => return true, + _ => continue, + }; + } + + false +} diff --git a/naphtha-proc-macro/src/lib.rs b/naphtha-proc-macro/src/lib.rs new file mode 100644 index 0000000..12c1578 --- /dev/null +++ b/naphtha-proc-macro/src/lib.rs @@ -0,0 +1,47 @@ +extern crate proc_macro; +extern crate quote; + +use { + quote::quote, + syn::{parse, DeriveInput}, +}; + +#[cfg(any(feature = "barrel-full", feature = "barrel-sqlite",))] +mod barrel_impl; +mod database_impl; +#[allow(dead_code)] +mod helper; + +#[proc_macro_attribute] +pub fn model( + attr: ::proc_macro::TokenStream, + item: ::proc_macro::TokenStream, +) -> ::proc_macro::TokenStream { + let ast: DeriveInput = parse(item).expect( + "proc_macro_attribute model: Could not parse TokenStream input!", + ); + let attr = format!("#[{}]", attr); + let attr: ::proc_macro2::TokenStream = attr.parse().unwrap(); + + #[cfg(not(feature = "sqlite"))] + let impl_sqlite = quote! {}; + #[cfg(feature = "sqlite")] + let impl_sqlite = database_impl::sqlite::impl_sqlite(&ast, &attr); + + #[cfg(not(any(feature = "barrel-full", feature = "barrel-sqlite")))] + let impl_barrel_sqlite = quote! {}; + #[cfg(any(feature = "barrel-full", feature = "barrel-sqlite"))] + let impl_barrel_sqlite = barrel_impl::sqlite::impl_sqlite(); + + let output = quote! { + use schema::*; + #[derive(Debug, Queryable, Identifiable, AsChangeset, Associations)] + #attr + #ast + + #impl_sqlite + #impl_barrel_sqlite + }; + + ::proc_macro::TokenStream::from(output) +} diff --git a/naphtha/Cargo.toml b/naphtha/Cargo.toml new file mode 100644 index 0000000..de3cfcb --- /dev/null +++ b/naphtha/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "naphtha" +version = "0.1.0" +authors = ["Lewin Probst "] +edition = "2018" +license = "MIT OR Apache-2.0" +description = "Work in progress" +homepage = "https://github.com/emirror-de/naphtha" +documentation = "https://github.com/emirror-de/naphtha" +repository = "https://github.com/emirror-de/naphtha" +readme = "README.md" +keywords = ["database", "interaction", "simplify", "models", "connection"] +categories = ["database"] + +[features] +default = ["full"] +full = ["sqlite", "barrel-full"] +sqlite = ["naphtha-proc-macro/sqlite", "diesel/sqlite"] +barrel-full = ["barrel-sqlite"] +barrel-sqlite = ["barrel-dep/sqlite3", "naphtha-proc-macro/barrel-sqlite"] + +[dependencies] +barrel-dep = { version = "^0.6.5", package = "barrel", optional = true } +chrono = { version = "^0.4.19" } +diesel = { version = "^1.4.7", features = ["chrono"] } +naphtha-proc-macro = { path = "../naphtha-proc-macro", version = "^0.1.0" } +log = "^0.4.14" diff --git a/naphtha/README.md b/naphtha/README.md new file mode 100644 index 0000000..dbda5b7 --- /dev/null +++ b/naphtha/README.md @@ -0,0 +1,3 @@ +# naphtha + +This crate is currently in progress. Visit the [documentation](https://docs.rs/naphtha) page for more information. diff --git a/naphtha/examples/person.rs b/naphtha/examples/person.rs new file mode 100644 index 0000000..4ed60e5 --- /dev/null +++ b/naphtha/examples/person.rs @@ -0,0 +1,112 @@ +#[macro_use] +extern crate diesel; + +use chrono::prelude::NaiveDateTime; +use naphtha::{ + barrel::{types, DatabaseSqlMigration, Migration}, + model, + DatabaseUpdateHandler, +}; + +// The model attribute automatically adds: +// +// use schema::*; +// #[derive(Debug, Queryable, Identifiable, AsChangeset, Associations)] +// #[table_name = "persons"] +#[model(table_name = "persons")] +pub struct Person { + id: i32, + pub description: Option, + pub updated_at: NaiveDateTime, +} + +pub mod schema { + table! { + persons (id) { + id -> Int4, + description -> Nullable, + updated_at -> Timestamp, + } + } +} + +impl naphtha::DatabaseModel for Person { + type PrimaryKey = i32; + fn primary_key(&self) -> Self::PrimaryKey { + self.id + } + + fn set_primary_key(&mut self, value: &Self::PrimaryKey) { + self.id = *value; + } + + fn default_primary_key() -> Self::PrimaryKey { + 0 + } + + fn table_name() -> &'static str { + "persons" + } +} + +impl DatabaseUpdateHandler for Person { + fn before_update(&mut self) { + self.updated_at = chrono::Utc::now().naive_utc(); + } + + fn after_update(&mut self) {} +} + +#[cfg(any(feature = "barrel-full", feature = "barrel-sqlite",))] +impl DatabaseSqlMigration for Person { + fn migration_up(migration: &mut Migration) { + use naphtha::DatabaseModel; + migration.create_table_if_not_exists(Self::table_name(), |t| { + t.add_column("id", types::primary()); + t.add_column("description", types::text().nullable(true)); + t.add_column("updated_at", types::custom("timestamp")); + }); + } + + fn migration_down(migration: &mut Migration) { + use naphtha::DatabaseModel; + migration.drop_table_if_exists(Self::table_name()); + } +} + +fn main() { + use naphtha::{ + barrel::DatabaseSqlMigrationExecutor, + DatabaseConnect, + DatabaseModel, + }; + + let db = DatabaseConnection::connect(":memory:").unwrap(); + + // create the table if not existent + // This method can be used on startup of your application to make sure + // your database schema is always up to date. + match Person::execute_migration_up(&db) { + Ok(_) => (), + Err(msg) => println!("Could not create table: {}", msg.to_string()), + }; + + let mut p = Person { + id: Person::default_primary_key(), + description: Some("The new person is registered".into()), + updated_at: chrono::Utc::now().naive_utc(), + }; + + p.insert(&db); + // id member is set to the correct number given by the database. + + // do a custom query to the database + let res = db.custom::>(|c| { + use schema::persons::dsl::*; + persons.filter(id.eq(1)).first(c) + }); + println!("{:#?}", res); + + p.remove(&db); + // p not available anymore +} diff --git a/naphtha/src/barrel.rs b/naphtha/src/barrel.rs new file mode 100644 index 0000000..1713a1e --- /dev/null +++ b/naphtha/src/barrel.rs @@ -0,0 +1,28 @@ +pub use {crate::DatabaseConnection, barrel_dep::*}; + +/// Provides an interface for the migration functions of the table belonging +/// to your model. +/// +/// *Requires any of the barrel features `barrel-full`, `barrel-sqlite`, `barrel-pg` or +/// `barrel-mysql`* +pub trait DatabaseSqlMigration { + /// Defines the creation of a table. + fn migration_up(migration: &mut Migration); + /// Defines the deletion of a table. + fn migration_down(migration: &mut Migration); +} + +/// Gets implemented automatically when `barrel` feature is enabled. +pub trait DatabaseSqlMigrationExecutor +where + Self: DatabaseSqlMigration, +{ + /// Executes the creation of the table. + fn execute_migration_up( + conn: &DatabaseConnection, + ) -> Result; + /// Executes the deletion of the table. + fn execute_migration_down( + conn: &DatabaseConnection, + ) -> Result; +} diff --git a/naphtha/src/database_impl/mod.rs b/naphtha/src/database_impl/mod.rs new file mode 100644 index 0000000..bd9b004 --- /dev/null +++ b/naphtha/src/database_impl/mod.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "sqlite")] +mod sqlite; + +#[cfg(feature = "sqlite")] +pub use sqlite::*; diff --git a/naphtha/src/database_impl/sqlite.rs b/naphtha/src/database_impl/sqlite.rs new file mode 100644 index 0000000..9e05a74 --- /dev/null +++ b/naphtha/src/database_impl/sqlite.rs @@ -0,0 +1,38 @@ +use { + crate::{DatabaseConnect, DatabaseConnection}, + diesel::{Connection, SqliteConnection}, + std::sync::{Arc, Mutex}, +}; + +impl From for DatabaseConnection { + fn from(c: SqliteConnection) -> Self { + DatabaseConnection::(Arc::new(Mutex::new(c))) + } +} + +impl From>> + for DatabaseConnection +{ + fn from(c: Arc>) -> Self { + DatabaseConnection::(c) + } +} + +impl DatabaseConnect + for DatabaseConnection +{ + fn connect( + database_url: &str, + ) -> Result, String> { + let connection = match Connection::establish(database_url) { + Ok(c) => c, + Err(msg) => { + return Err(format!( + "Connection to database \"{}\" could not be established: {}", + database_url, msg + )) + } + }; + Ok(DatabaseConnection(Arc::new(Mutex::new(connection)))) + } +} diff --git a/naphtha/src/lib.rs b/naphtha/src/lib.rs new file mode 100644 index 0000000..177e836 --- /dev/null +++ b/naphtha/src/lib.rs @@ -0,0 +1,214 @@ +#![deny(missing_docs)] +//! This library provides several traits in order to make database access a lot +//! easier. In addition when using `naphtha`, it is possible to change +//! the database that is used for specific models in your application without +//! the requirement of changing any code. +//! +//! It implements the most common operations on a database like `insert`, `update` +//! and `remove` for you, while also providing the ability to send custom queries +//! to the database. +//! In addition to that, when using the `barrel-XXX` features, you can write your +//! SQL migrations and use them in your application during runtime. +//! See the [examples](#examples) below. +//! +//! ## Features overview +//! +//! * Most common function implementations `insert`, `update`, `remove` for your +//! models. +//! * [DatabaseUpdateHandler](DatabaseUpdateHandler) enables you to change the models values before and +//! after the `update` transaction to the database. +//! * Change database on specific model in your application without the need to +//! change your code. +//! * Possibility to query a model from the database by using one of its member *(NOT FINISHED YET)*. +//! * Integrated [barrel] for writing your SQL migrations. +//! * Thread safe handling of the database connection. +//! +//! ## Supported databases +//! +//! * SQlite3 (using [diesel](diesel) under the hood). +//! +//! ## Examples +//! +//! In this chapter, minimal usages are shown. Please have a look at the examples +//! in the repository for more and detailed use. +//! +//! ### Connecting to a database +//! +//! ```rust +//! use naphtha::{DatabaseConnection, DatabaseConnect}; +//! let db = DatabaseConnection::connect(":memory:"); +//! // do some database work +//! ``` +//! +//! ### Defining a model and use database connection +//! +//! ```rust +//! #[macro_use] +//! extern crate diesel; +//! #[macro_use] +//! extern crate naphtha; +//! +//! use {naphtha::{model, DatabaseModel, DatabaseUpdateHandler}, diesel::table}; +//! #[cfg(any(feature = "barrel-full", feature = "barrel-sqlite"))] +//! use naphtha::barrel::{types, DatabaseSqlMigration, Migration}; +//! +//! #[model(table_name = "persons")] +//! pub struct Person { +//! id: i32, +//! pub description: Option, +//! } +//! +//! pub mod schema { +//! table! { +//! persons (id) { +//! id -> Int4, +//! description -> Nullable, +//! } +//! } +//! } +//! +//! impl DatabaseModel for Person { +//! type PrimaryKey = i32; +//! fn primary_key(&self) -> Self::PrimaryKey { +//! self.id +//! } +//! +//! fn set_primary_key(&mut self, value: &Self::PrimaryKey) { +//! self.id = *value; +//! } +//! +//! fn default_primary_key() -> Self::PrimaryKey { +//! 0 +//! } +//! +//! fn table_name() -> &'static str { +//! "persons" +//! } +//! } +//! +//! // do not implement custom changes before and after the update transaction +//! // to the database. +//! impl naphtha::DatabaseUpdateHandler for Person {} +//! +//! #[cfg(any( +//! feature = "barrel-full", +//! feature = "barrel-sqlite", +//! ))] +//! impl DatabaseSqlMigration for Person { +//! fn migration_up(migration: &mut Migration) { +//! use naphtha::DatabaseModel; +//! migration.create_table_if_not_exists(Self::table_name(), |t| { +//! t.add_column("id", types::primary()); +//! t.add_column("description", types::text().nullable(true)); +//! t.add_column("updated_at", types::custom("timestamp")); +//! }); +//! } +//! +//! fn migration_down(migration: &mut Migration) { +//! use naphtha::DatabaseModel; +//! migration.drop_table_if_exists(Self::table_name()); +//! } +//! } +//! +//! fn main() { +//! use naphtha::{DatabaseConnection, DatabaseConnect}; +//! let db = DatabaseConnection::connect(":memory:").unwrap(); +//! let mut p = Person { +//! id: Person::default_primary_key(), +//! description: Some("The new person is registered".into()), +//! }; +//! p.insert(&db); +//! // id member is set to the correct number given by the database. +//! +//! // do a custom query to the database +//! db.custom::>(|c| { +//! use schema::{persons, persons::dsl::*}; +//! persons.filter(id.eq(1)).first(c) +//! }); +//! +//! p.remove(&db); +//! // p not available anymore +//! } +//! ``` +//! +//! ## Upcoming +//! +//! * The query by methods will be implemented. +//! * More databases will be implemented, at least PostgreSQL and MySql. +use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; + +/// Defines your `struct` as a model and implements the required traits for +/// interacting with the database. Currently only *named* `struct` member are +/// supported. +pub use naphtha_proc_macro::model; + +#[cfg(any( + feature = "barrel-full", + feature = "barrel-sqlite", +))] +/// Re-exports the [barrel] crate including small additions required by naphtha. +pub mod barrel; +mod database_impl; +mod tests; + +/// Thin wrapper around a [Connection](diesel::Connection). +pub struct DatabaseConnection(Arc>); + +impl DatabaseConnection { + /// Aquires a lock to the wrapped connection. + pub fn lock( + &self, + ) -> Result, PoisonError>> { + self.0.lock() + } + + /// Executes the custom function to the database instance. + pub fn custom(&self, query: fn(&T) -> R) -> R { + let c = self.0.lock().expect("Could not aquire connection lock!"); + query(&*c) + } +} + +/// Contains functions database connection handling. +pub trait DatabaseConnect { + /// Establishes a new connection to the given database string. + fn connect(database_url: &str) -> Result, String>; +} + +/// Defines the relation of the model to the database. +pub trait DatabaseModel { + /// Defines the primary key type on the database table. + type PrimaryKey; + + /// Returns the primary key value. + fn primary_key(&self) -> Self::PrimaryKey; + /// Sets the primary key value. + fn set_primary_key(&mut self, value: &Self::PrimaryKey); + /// Gets the default primary key value. + fn default_primary_key() -> Self::PrimaryKey; + /// Returns the table name related to the model. + fn table_name() -> &'static str; +} + +/// Defines functions to modify the stored model instance on the database. +pub trait DatabaseModelModifier +where + Self: DatabaseUpdateHandler, +{ + /// Inserts `self` to the given database. + /// *Updates the `primary_key` to the one that has been assigned by the database*. + fn insert(&mut self, conn: &DatabaseConnection) -> bool; + /// Removes `self` from the database, selects by `id`. + fn remove(self, conn: &DatabaseConnection) -> bool; + /// Updates `self` on the given database. + /// *Updates the `updated_at` member if available before updating the database.*. + fn update(&mut self, conn: &DatabaseConnection) -> bool; +} + +/// Contains methods that are called during updating the model to the database. +pub trait DatabaseUpdateHandler { + /// This method is called before the transaction to the database takes place. + fn before_update(&mut self) {} + /// This method is called after the transaction to the database took place. + fn after_update(&mut self) {} +} diff --git a/naphtha/src/tests/mod.rs b/naphtha/src/tests/mod.rs new file mode 100644 index 0000000..a992a27 --- /dev/null +++ b/naphtha/src/tests/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "sqlite")] +mod sqlite; diff --git a/naphtha/src/tests/sqlite.rs b/naphtha/src/tests/sqlite.rs new file mode 100644 index 0000000..4feaf03 --- /dev/null +++ b/naphtha/src/tests/sqlite.rs @@ -0,0 +1,21 @@ +#[test] +fn from_connection() { + let c: diesel::SqliteConnection = + diesel::Connection::establish(":memory:").unwrap(); + let _db = crate::DatabaseConnection::from(c); +} + +#[test] +fn from_arc_mutex_connection() { + use std::sync::{Arc, Mutex}; + let c: diesel::SqliteConnection = + diesel::Connection::establish(":memory:").unwrap(); + let c = Arc::new(Mutex::new(c)); + let _db = crate::DatabaseConnection::from(c); +} + +#[test] +fn connect() { + use crate::{DatabaseConnect, DatabaseConnection}; + let _db = DatabaseConnection::connect(":memory:"); +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..c0bf015 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +max_width = 80 +format_strings = false +imports_layout = "HorizontalVertical" +reorder_imports = true diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -