Skip to content

Commit 4b7f5f7

Browse files
authored
Implement TryFromForm for all types (#24)
* impl TryFromRow and TryFromUuid for Pathfinder types Created two macros to derive the two traits on a given struct, because the general process of getting a row from the database and making a struct from it is mostly the same for all types, with some odd cases. These are covered with a number of attributes that inform how the traits are implemented. In the future, some of those attributes should be able to be removed in favor of parsing the field's type, but that's too much work for the current deadline we have, and more attributes is easier to implement. There is no guarantee the generated code even works: that's the upcoming commit(s). [TVRN-10] * migration to diesel pt 1 This commit adds most, if not all, of the diesel-related annotations for types in tavern_pathfinder. Due to needing to `use` enum types derived in tavern_pathfinder, the diesel migrations are stored in tavern_server. So far, this also simpilifies the amount of custom macros needed in tavern_derive. Committing now as a good checkpoint to use moving forward, starting with creating a patch file for `schema.rs` to add the custom enum types. * migration to diesel pt 2 Hundreds of compiler errors have been fixed (though many were the same errors in multiple contexts). Currently all that remains is implementing the tavern-related DB traits on objects, which the custom derive macros will help implement them on the DB____ types, which helps with implementing the actual Pathfinder types. Among other changes, of note is the addition of a patch file for schema.rs to add imports for the enums created by diesel-derive-enum. The biggest change is that all of the code was merged under tavern_server. While we wanted the Pathfinder types in a separate library, using that with Diesel just added too much complexity. * run cargo fmt * diesel migration: implement database traits Not all operations are implemented on Effect and Feat, but the required Get* methods are, automatically. `class.rs` and `character.rs` have not yet been touched. * (mostly) finish db impl for types This commit finishes enough impls that the server compiles without errors. Some types almost certainly need to have Insert/Update/Delete implemented for them, but that can wait until they are needed. * implement TryFromForm for all datatypes
1 parent 2396345 commit 4b7f5f7

File tree

12 files changed

+1227
-128
lines changed

12 files changed

+1227
-128
lines changed

tavern_derive/src/lib.rs

+55-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ pub fn impl_enum_display(input: TokenStream) -> TokenStream {
166166
input.ident.span(),
167167
"this macro only works on enums with all unit variants"
168168
)
169-
.into();
169+
.into()
170170
};
171171

172172
for v in input.variants.pairs() {
@@ -206,6 +206,60 @@ pub fn impl_enum_display(input: TokenStream) -> TokenStream {
206206
result.into()
207207
}
208208

209+
#[proc_macro_derive(FromStr)]
210+
pub fn impl_enum_from_str(input: TokenStream) -> TokenStream {
211+
let input = syn::parse_macro_input!(input as syn::DeriveInput);
212+
let name = &input.ident;
213+
214+
let input = if let Data::Enum(e) = &input.data {
215+
e
216+
} else {
217+
return compile_error_args!(
218+
input.ident.span(),
219+
"this macro only works on enums with all unit variants"
220+
)
221+
.into();
222+
};
223+
224+
for v in input.variants.pairs() {
225+
match v.value().fields {
226+
Fields::Unit => {}
227+
_ => {
228+
return compile_error_args!(
229+
v.value().ident.span(),
230+
"this macro only works on enums with all unit variants"
231+
)
232+
.into()
233+
}
234+
}
235+
}
236+
237+
let var_words = input
238+
.variants
239+
.pairs()
240+
.map(|v| v.value().ident.to_string().to_case(Case::Lower));
241+
242+
let var = input.variants.pairs().zip(var_words).map(|(v, s)| {
243+
let v = &v.value().ident;
244+
quote! { if val == #s { std::result::Result::<_,_>::Ok(#name::#v) } else }
245+
});
246+
247+
let error_str = format!("invalid value for {}: {}", name, "{}");
248+
249+
let result = quote! {
250+
impl std::str::FromStr for #name {
251+
type Err = crate::status::Error;
252+
fn from_str(val: &str) -> std::result::Result<Self, Self::Err> {
253+
#(#var)* {
254+
std::result::Result::<_,_>::Err(Self::Err::new(format!(#error_str, val)))
255+
}
256+
}
257+
}
258+
};
259+
260+
result.into()
261+
}
262+
209263
#[proc_macro_derive(GetById, attributes(table_name, tavern))]
210264
pub fn derive_get_by_id(input: TokenStream) -> TokenStream {
211265
let input = syn::parse_macro_input!(input as DeriveInput);

tavern_server/src/auth.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -311,8 +311,8 @@ impl TryFrom<Form> for User {
311311
type Error = Rejection;
312312

313313
fn try_from(form: Form) -> Result<Self, Self::Error> {
314-
let username = forms::get_form_text_field(&form, FIELD_USERNAME)?;
315-
let email = forms::get_form_text_field(&form, FIELD_EMAIL)?;
314+
let username = forms::get_required_form_text_field(&form, FIELD_USERNAME)?;
315+
let email = forms::get_required_form_text_field(&form, FIELD_EMAIL)?;
316316
let is_admin = form
317317
.get(FIELD_IS_ADMIN)
318318
.map(|field| {
@@ -469,7 +469,7 @@ struct RegistrationInfo {
469469
impl TryFrom<Form> for RegistrationInfo {
470470
type Error = Rejection;
471471
fn try_from(form: Form) -> Result<Self, Self::Error> {
472-
let password = forms::get_form_text_field(&form, FIELD_PASSWORD)?;
472+
let password = forms::get_required_form_text_field(&form, FIELD_PASSWORD)?;
473473

474474
let info = RegistrationInfo {
475475
user: User::try_from(form)?,

tavern_server/src/db.rs

+28-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use crate::status;
1+
use crate::status::{self, Error as StatusError};
22
use diesel::prelude::*;
33
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
4-
use diesel::result;
4+
use diesel::result::Error as DieselError;
55
use lazy_static::lazy_static;
66
use std::fmt::{self, Display};
77
use std::sync::Arc;
@@ -12,6 +12,7 @@ use warp::filters::BoxedFilter;
1212
use warp::{Filter, Rejection};
1313

1414
pub use tavern_derive::*;
15+
use nebula_status::{Status, StatusCode};
1516

1617
#[cfg(test)]
1718
mod tests {
@@ -51,9 +52,10 @@ lazy_static! {
5152

5253
embed_migrations!();
5354

54-
pub async fn init() -> Result<(), Error> {
55-
let mut conn = get_connection().await?;
56-
embedded_migrations::run(&conn).map_err(Error::Migration)
55+
pub async fn init() -> Result<(), String> {
56+
let conn = get_connection().await
57+
.map_err(|err| format!("error while getting connection: {:#?}", err))?;
58+
embedded_migrations::run(&conn).map_err(|err| format!("error while running migrations: {:#?}", err))
5759
}
5860

5961
async fn get_filter_connection() -> Result<Connection, Rejection> {
@@ -72,6 +74,7 @@ pub struct PostgreSQLOpt {
7274
long = "db-host",
7375
env = "TAVERN_DB_HOST",
7476
help = "the domain name or IP address of the database host"
77+
7578
)]
7679
host: String,
7780
#[structopt(
@@ -140,18 +143,35 @@ pub enum Error {
140143
Connection(::r2d2::Error),
141144
InvalidValues(Vec<String>),
142145
Migration(diesel_migrations::RunMigrationsError),
143-
RunQuery(result::Error),
146+
RunQuery(DieselError),
144147
NoRows,
145148
UserUnauthorized(Uuid),
146149
Other(String),
147150
}
148151

149-
impl From<result::Error> for Error {
150-
fn from(err: result::Error) -> Self {
152+
impl From<DieselError> for Error {
153+
fn from(err: DieselError) -> Self {
151154
Error::RunQuery(err)
152155
}
153156
}
154157

158+
impl From<Error> for Rejection {
159+
fn from(err: Error) -> Self {
160+
match err {
161+
Error::InvalidValues(list) => {
162+
let error = StatusError::new(format!("invalid values for {}", list.join(", ")));
163+
Status::with_data(&StatusCode::BAD_REQUEST, error).into()
164+
},
165+
Error::UserUnauthorized(id) => {
166+
Status::new(&StatusCode::UNAUTHORIZED).into()
167+
},
168+
err => {
169+
status::server_error_into_rejection(err.to_string())
170+
}
171+
}
172+
}
173+
}
174+
155175
impl Display for Error {
156176
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157177
match self {

tavern_server/src/forms.rs

+72-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
use crate::status::Error;
1+
use crate::status::{self, Error};
22
use nebula_form::Form;
33
use nebula_status::{Status, StatusCode};
44
use warp::Rejection;
5+
use uuid::Uuid;
6+
use std::str::FromStr;
7+
use crate::db::{Connection, GetById, Error as DBError};
8+
use diesel::result::Error as DieselError;
59

610
#[cfg(test)]
711
mod tests {
@@ -32,15 +36,15 @@ mod tests {
3236
#[test]
3337
fn get_form_text_field_works() {
3438
let form = generate_form();
35-
let value =
36-
super::get_form_text_field(&form, FIELD_TEXT_FOO).expect("this should not fail");
39+
let value: String =
40+
super::get_required_form_text_field(&form, FIELD_TEXT_FOO).expect("this should not fail");
3741
assert_eq!(&value, VALUE_TEXT_FOO);
3842
}
3943

4044
#[test]
4145
fn get_file_field_as_text_fails() {
4246
let form = generate_form();
43-
match super::get_form_text_field(&form, FIELD_FILE_BAZ) {
47+
match super::get_required_form_text_field::<String>(&form, FIELD_FILE_BAZ) {
4448
Ok(_) => assert!(false, "file as text should not have returned successfully"),
4549
Err(_) => assert!(true),
4650
}
@@ -49,7 +53,7 @@ mod tests {
4953
#[test]
5054
fn get_missing_field_as_text_fails() {
5155
let form = generate_form();
52-
match super::get_form_text_field(&form, FIELD_MISSING_OOPS) {
56+
match super::get_required_form_text_field::<String>(&form, FIELD_MISSING_OOPS) {
5357
Ok(_) => assert!(false, "getting a missing field should not succeed"),
5458
Err(_) => assert!(true),
5559
}
@@ -88,10 +92,68 @@ pub(crate) fn field_is_invalid_error(field_name: &str) -> Rejection {
8892

8993
/// Retrieve the specified *text* field from the form or return a relevant
9094
/// error.
91-
pub(crate) fn get_form_text_field(form: &Form, field_name: &str) -> Result<String, Rejection> {
95+
pub(crate) fn get_optional_form_text_field<T: FromStr>(form: &Form, field_name: &str) -> Result<Option<T>, Rejection> {
9296
form.get(field_name)
93-
.ok_or_else(|| missing_field_error(field_name))?
94-
.as_text()
95-
.map(|txt| txt.to_string())
96-
.ok_or_else(|| field_is_file_error(field_name))
97+
.map(|val| {
98+
val.as_text()
99+
.map(|txt| {
100+
txt.parse()
101+
})
102+
.transpose()
103+
.map_err(|_| field_is_file_error(field_name))
104+
}).transpose()
105+
.map(|opt| opt.flatten())
97106
}
107+
108+
/// Retrieve the specified *text* field from the form or return a relevant
109+
/// error.
110+
pub(crate) fn get_required_form_text_field<T: FromStr>(form: &Form, field_name: &str) -> Result<T, Rejection> {
111+
get_optional_form_text_field(form, field_name)?
112+
.ok_or_else(|| missing_field_error(field_name))
113+
}
114+
115+
pub(crate) fn value_by_id<T: GetById>(id: Uuid, conn: &Connection) -> Result<T, Rejection> {
116+
T::db_get_by_id(&id, conn)
117+
.map_err(|err| {
118+
match err {
119+
DBError::RunQuery(err) => {
120+
match err {
121+
DieselError::NotFound => {
122+
let error = Error::new(format!{"invalid id: {}", id});
123+
Status::with_data(&StatusCode::BAD_REQUEST, error).into()
124+
},
125+
err => status::server_error_into_rejection(err.to_string())
126+
}
127+
},
128+
err => status::server_error_into_rejection(err.to_string()),
129+
}
130+
})
131+
}
132+
133+
pub(crate) fn valid_id<T: GetById>(id: Uuid, conn: &Connection) -> Result<Uuid, Rejection> {
134+
value_by_id::<T>(id, conn)
135+
.map(|_| id)
136+
}
137+
138+
pub(crate) fn valid_id_or_new<T: GetById>(id: Option<Uuid>, conn: &Connection) -> Result<Uuid, Rejection> {
139+
match id {
140+
None => Ok(Uuid::new_v4()),
141+
Some(id) => valid_id::<T>(id, conn)
142+
}
143+
}
144+
145+
pub(crate) fn db_error_to_rejection(err: DBError, field: &str) -> Rejection {
146+
match err {
147+
DBError::RunQuery(err) => {
148+
match err {
149+
DieselError::NotFound => field_is_invalid_error(field),
150+
err => Rejection::from(DBError::RunQuery(err)),
151+
}
152+
},
153+
err => Rejection::from(err),
154+
}
155+
}
156+
157+
pub trait TryFromForm {
158+
fn try_from_form(conn: &Connection, form: Form, this_id: Option<Uuid>, parent_id: Option<Uuid>) -> Result<Self, Rejection> where Self: Sized;
159+
}

0 commit comments

Comments
 (0)