From 8852f0b7090c612f1d04a60becb55bbe56441eda Mon Sep 17 00:00:00 2001 From: Melody Horn / boringcactus Date: Wed, 23 Jun 2021 21:07:56 -0600 Subject: start impling diesel::Queryable for Models --- Cargo.toml | 4 +- examples/tutorial02/main.rs | 37 ++++++++++++++ examples/tutorial02/polls/models.rs | 4 +- src/db/backend.rs | 8 ++- tosin-macros/src/lib.rs | 97 ++++++++++++++++++++++++++----------- 5 files changed, 118 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 76282f6..6b9d7fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,12 +8,12 @@ edition = "2018" [dependencies] barrel = { version = "0.6.5", features = ["sqlite3"] } -diesel = { version = "1.4.4", features = ["sqlite"] } +chrono = "0.4" +diesel = { version = "1.4.4", features = ["sqlite", "chrono"] } hyper = { version = "0.14", features = ["full"] } quote = "1.0" structopt = "0.3.21" syn = { version = "1.0", features = ["full", "extra-traits"] } -time = "0.2.27" tokio = { version = "1", features = ["full"] } tosin-macros = { version = "0.1.0", path = "./tosin-macros" } warp = "0.3" diff --git a/examples/tutorial02/main.rs b/examples/tutorial02/main.rs index 8f0eebe..bac20e1 100644 --- a/examples/tutorial02/main.rs +++ b/examples/tutorial02/main.rs @@ -25,3 +25,40 @@ fn settings() -> Settings { } tosin::main!(urls(), settings()); + +#[cfg(test)] +mod test { + use super::*; + use diesel::prelude::*; + + #[test] + fn test_models() { + use polls::models::{Choice, Question, choice, question}; + + let settings = settings(); + let connection = settings.database.connect().unwrap(); + + // get the list of all questions + let all_questions = question::table.load::(&connection).unwrap(); + assert!(all_questions.is_empty()); + + // make a new one + let mut q = Question::new( + "What's new?".to_string(), + time::PrimitiveDateTime::now(), + ); + // save it + q.save_mut(); + // it's got an id now! + assert!(q.id().is_some()); + // it's still got all the same fields! + assert_eq!(q.question_text(), "What's new?"); + // we can change them! + q.set_question_text("What's up?"); + q.save_mut(); + + // it should be in the list now, too!! + let all_questions = question::table.load::(&connection).unwrap(); + assert_eq!(all_questions, vec![q]); + } +} diff --git a/examples/tutorial02/polls/models.rs b/examples/tutorial02/polls/models.rs index fb8be8f..d62f219 100644 --- a/examples/tutorial02/polls/models.rs +++ b/examples/tutorial02/polls/models.rs @@ -1,6 +1,6 @@ use tosin::db::models::{Model, Id, gather}; -#[derive(Model)] +#[derive(Model, PartialEq, Debug)] pub struct Question { id: Option, #[model(max_length=200)] @@ -9,7 +9,7 @@ pub struct Question { pub_date: time::PrimitiveDateTime, } -#[derive(Model)] +#[derive(Model, PartialEq, Debug)] pub struct Choice { id: Option, #[model(Question, on_delete=Cascade)] diff --git a/src/db/backend.rs b/src/db/backend.rs index 5f35dd9..5950747 100644 --- a/src/db/backend.rs +++ b/src/db/backend.rs @@ -1,8 +1,14 @@ use barrel::backend::SqlGenerator; pub use diesel::connection::Connection; +pub trait UsableBackend: diesel::backend::Backend where *const str: diesel::deserialize::FromSql {} +impl UsableBackend for diesel::sqlite::Sqlite {} + +pub trait UsableConnection: Connection where ::Backend: UsableBackend {} +impl UsableConnection for diesel::sqlite::SqliteConnection {} + pub trait Connectable { - type Connection: Connection; + type Connection: UsableConnection; type SqlGenerator: SqlGenerator; fn connect(&self) -> diesel::ConnectionResult; } diff --git a/tosin-macros/src/lib.rs b/tosin-macros/src/lib.rs index c9e352c..131888c 100644 --- a/tosin-macros/src/lib.rs +++ b/tosin-macros/src/lib.rs @@ -18,9 +18,35 @@ pub fn model_derive(input: TokenStream) -> TokenStream { impl_model(&ast) } -// TODO clean all this shit up +struct FieldInfo { + tosin_field: T, + diesel_column: T, + diesel_type: T, +} + +struct FieldsInfo { + tosin_fields: Vec, + diesel_columns: Vec, + diesel_types: Vec, +} -fn to_field_spec(field: &syn::Field) -> (impl quote::ToTokens, impl quote::ToTokens) { +impl std::iter::FromIterator> for FieldsInfo { + fn from_iter>>(iter: I) -> Self { + let mut result = Self { + tosin_fields: vec![], + diesel_columns: vec![], + diesel_types: vec![], + }; + for info in iter { + result.tosin_fields.push(info.tosin_field); + result.diesel_columns.push(info.diesel_column); + result.diesel_types.push(info.diesel_type); + } + result + } +} + +fn to_field_spec(field: &syn::Field) -> FieldInfo { fn parse_type(ty: &str) -> syn::Type { syn::parse_str(ty).unwrap() } @@ -41,42 +67,48 @@ fn to_field_spec(field: &syn::Field) -> (impl quote::ToTokens, impl quote::ToTok }) .collect(); if field_type == &parse_type("Option") { - ( - quote! { ::tosin::db::models::Field::IntField { name: stringify!(#field_name) } }, - quote! { #field_name -> Integer } - ) + FieldInfo { + tosin_field: quote! { ::tosin::db::models::Field::IntField { name: stringify!(#field_name) } }, + diesel_column: quote! { #field_name -> Integer }, + diesel_type: quote! { ::tosin::db::sql_types::Integer }, + } } else if field_type == &parse_type("Id") { // TODO foreign key constraint - ( - quote! { ::tosin::db::models::Field::IntField { name: stringify!(#field_name) } }, - quote! { #field_name -> Integer } - ) + FieldInfo { + tosin_field: quote! { ::tosin::db::models::Field::IntField { name: stringify!(#field_name) } }, + diesel_column: quote! { #field_name -> Integer }, + diesel_type: quote! { ::tosin::db::sql_types::Integer }, + } } else if field_type == &parse_type("usize") { // TODO default - ( - quote! { ::tosin::db::models::Field::IntField { name: stringify!(#field_name) } }, - quote! { #field_name -> Integer } - ) + FieldInfo { + tosin_field: quote! { ::tosin::db::models::Field::IntField { name: stringify!(#field_name) } }, + diesel_column: quote! { #field_name -> Integer }, + diesel_type: quote! { ::tosin::db::sql_types::Integer }, + } } else if field_type == &parse_type("String") { let max_length = model_options.iter() .find(|(name, _value)| name.get_ident().map_or(false, |path| path == "max_length")) .map(|(_name, value)| value); if let Some(max_length) = max_length { - ( - quote! { ::tosin::db::models::Field::CharField { name: stringify!(#field_name), max_length: Some(#max_length) } }, - quote! { #field_name -> Text } - ) + FieldInfo { + tosin_field: quote! { ::tosin::db::models::Field::CharField { name: stringify!(#field_name), max_length: Some(#max_length) } }, + diesel_column: quote! { #field_name -> Text }, + diesel_type: quote! { ::tosin::db::sql_types::Text } + } } else { - ( - quote! { ::tosin::db::models::Field::CharField { name: stringify!(#field_name), max_length: None } }, - quote! { #field_name -> Text } - ) + FieldInfo { + tosin_field: quote! { ::tosin::db::models::Field::CharField { name: stringify!(#field_name), max_length: None } }, + diesel_column: quote! { #field_name -> Text }, + diesel_type: quote! { ::tosin::db::sql_types::Text } + } } } else if field_type == &parse_type("time::PrimitiveDateTime") { - ( - quote! { ::tosin::db::models::Field::DateTimeField { name: stringify!(#field_name) } }, - quote! { #field_name -> Timestamp } - ) + FieldInfo { + tosin_field: quote! { ::tosin::db::models::Field::DateTimeField { name: stringify!(#field_name) } }, + diesel_column: quote! { #field_name -> Timestamp }, + diesel_type: quote! { ::tosin::db::sql_types::Timestamp } + } } else { use quote::ToTokens; panic!("can't handle {}", field.to_token_stream()) @@ -91,7 +123,8 @@ fn impl_model(ast: &syn::DeriveInput) -> TokenStream { } else { panic!("not on a struct"); }; - let (tosin_fields, diesel_columns): (Vec<_>, Vec<_>) = ast_data.fields.iter().map(to_field_spec).unzip(); + let real_types: Vec<_> = ast_data.fields.iter().map(|field| &field.ty).collect(); + let FieldsInfo { tosin_fields, diesel_columns, diesel_types } = ast_data.fields.iter().map(to_field_spec).collect(); let gen = quote! { impl #name { pub const META: ::tosin::db::models::ModelMeta = ::tosin::db::models::ModelMeta { @@ -100,6 +133,16 @@ fn impl_model(ast: &syn::DeriveInput) -> TokenStream { }; } + impl<__DB: tosin::db::diesel_backend::Backend, __ST> tosin::db::Queryable<__ST, __DB> for #name + where (#(#real_types),*): tosin::db::Queryable<__ST, __DB> { + type Row = <(#(#real_types),*) as tosin::db::Queryable<__ST, __DB>>::Row; + + fn build(row: Self::Row) -> Self { + let row: (#(#real_types),*) = tosin::db::Queryable::build(row); + todo!() + } + } + // this means users need #[macro_use] extern crate diesel; but fuck doing it ourselves table! { #lowercase_name { -- cgit v1.2.3