├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE.md ├── README.md ├── pg_mapper_derive ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE.md └── src │ └── lib.rs ├── rustfmt.toml └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | **/target 3 | **/.history 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.9 2 | - Updated dependency versions, removed unnecessary examples dir 3 | 4 | # 0.1.8 5 | - Fixed Display impl for Error causing stackoverflow 6 | 7 | # 0.1.7 8 | - refactored std::Error impl, replacing deprecated 'fn description' with 'fn source' 9 | 10 | # 0.1.6 11 | - remove unwrap from error handling, replaced with a general unknown variant 12 | 13 | # 0.1.5 14 | - fully qualified the Result type within the macro definitions 15 | 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tokio-pg-mapper" 3 | version = "0.2.0" 4 | description = "Proc-macro library used to map a tokio-postgres row to a Rust type (struct)" 5 | authors = ["Darin Gordon ", "Zeyla Hellyer "] 6 | repository = "https://www.github.com/Dowwie/tokio-postgres-mapper" 7 | homepage = "https://www.github.com/Dowwie/tokio-postgres-mapper" 8 | license = "ISC" 9 | keywords = ["tokio", "postgres", "mapper"] 10 | edition = "2018" 11 | 12 | [lib] 13 | doctest = false 14 | 15 | [dependencies] 16 | tokio-postgres = "0.7" 17 | tokio-pg-mapper-derive = { version = "0.2.0", optional = true } 18 | 19 | [features] 20 | derive = ["tokio-pg-mapper-derive"] 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | 3 | Copyright (c) 2020, Darin Gordon , 4 | Copyright (c) 2017, Zeyla Hellyer 5 | 6 | 7 | Permission to use, copy, modify, and/or distribute this software for any purpose 8 | with or without fee is hereby granted, provided that the above copyright notice 9 | and this permission notice appear in all copies. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 12 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 13 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 14 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 15 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 16 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 17 | THIS SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tokio-pg-mapper 2 | 3 | `tokio_postgres-mapper` is a proc-macro designed to make mapping from postgresql 4 | tables to structs simple. 5 | 6 | ### Why? 7 | 8 | It can be frustrating to write a lot of boilerplate and, ultimately, duplicated 9 | code for mapping from postgres Rows into structs. 10 | 11 | For example, this might be what someone would normally write: 12 | 13 | ```rust 14 | extern crate postgres; 15 | 16 | use postgres::rows::Row; 17 | 18 | pub struct User { 19 | pub id: i64, 20 | pub name: String, 21 | pub email: Option, 22 | } 23 | 24 | impl From for User { 25 | fn from(row: Row) -> Self { 26 | Self { 27 | id: row.get("id"), 28 | name: row.get("name"), 29 | email: row.get("email"), 30 | } 31 | } 32 | } 33 | 34 | // code to execute a query here and get back a row 35 | let user = User::from(row); // this can panic 36 | ``` 37 | 38 | This becomes worse when manually implementating using the non-panicking 39 | `get_opt` method variant. 40 | 41 | Using this crate, the boilerplate is removed, and panicking and non-panicking 42 | implementations are derived: 43 | 44 | ```rust 45 | extern crate tokio_pg_mapper_derive; 46 | extern crate tokio_pg_mapper; 47 | 48 | use tokio_pg_mapper::FromTokioPostgresRow; 49 | use tokio_pg_mapper_derive::PostgresMapper; 50 | 51 | #[derive(PostgresMapper)] 52 | pub struct User { 53 | pub id: i64, 54 | pub name: String, 55 | pub email: Option, 56 | } 57 | 58 | // Code to execute a query here and get back a row might now look like: 59 | let stmt = "SELECT * FROM user WHERE username = $1 AND password = $2"; 60 | 61 | let result = client.query_one(stmt, &[&5, "asdf"]).await?; 62 | let user = User::from_row(result).unwrap(); // or from_row_ref(&result) 63 | 64 | 65 | ``` 66 | 67 | 68 | ### The two crates 69 | 70 | This repository contains two crates: `postgres-mapper` which contains an `Error` 71 | enum and traits for converting from a `tokio-postgres` `Row` 72 | without panicking, and `postgres-mapper-derive` which contains the proc-macro. 73 | 74 | 75 | ### Installation 76 | 77 | Install `tokio-pg-mapper-derive` and `tokio-pg-mapper` from crates.io 78 | 79 | 80 | ### License 81 | 82 | ISC. 83 | -------------------------------------------------------------------------------- /pg_mapper_derive/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.6 2 | - bump up sync, quote and tokio-postgres to 0.7 3 | # 0.1.5 4 | - fully qualified the Result type within the macro definitions 5 | -------------------------------------------------------------------------------- /pg_mapper_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tokio-pg-mapper-derive" 3 | version = "0.2.0" 4 | description = "Proc-macro library used to map a tokio-postgres row to a Rust type (struct)" 5 | authors = ["Darin Gordon ", "Zeyla Hellyer "] 6 | repository = "https://www.github.com/Dowwie/tokio-postgres-mapper" 7 | homepage = "https://www.github.com/Dowwie/tokio-postgres-mapper" 8 | license = "ISC" 9 | keywords = ["tokio", "postgres", "mapper"] 10 | edition = "2018" 11 | 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | quote = "1.0.8" 18 | syn = { version = "1.0.54", features = ["full"] } 19 | tokio-postgres = "0.7" 20 | -------------------------------------------------------------------------------- /pg_mapper_derive/LICENSE.md: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | 3 | Copyright (c) 2020, Darin Gordon , 4 | Copyright (c) 2017, Zeyla Hellyer 5 | 6 | 7 | Permission to use, copy, modify, and/or distribute this software for any purpose 8 | with or without fee is hereby granted, provided that the above copyright notice 9 | and this permission notice appear in all copies. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 12 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 13 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 14 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 15 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 16 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 17 | THIS SOFTWARE. 18 | -------------------------------------------------------------------------------- /pg_mapper_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | #[macro_use] 3 | extern crate quote; 4 | extern crate syn; 5 | 6 | use proc_macro::TokenStream; 7 | 8 | use syn::{ 9 | Data, DataStruct, DeriveInput, Ident, ImplGenerics, Item, 10 | Meta::{List, NameValue}, 11 | NestedMeta::Meta, 12 | TypeGenerics, WhereClause, 13 | }; 14 | 15 | #[proc_macro_derive(PostgresMapper, attributes(pg_mapper))] 16 | pub fn postgres_mapper(input: TokenStream) -> TokenStream { 17 | let mut ast: DeriveInput = syn::parse(input).expect("Couldn't parse item"); 18 | 19 | impl_derive(&mut ast) 20 | } 21 | 22 | fn impl_derive(ast: &mut DeriveInput) -> TokenStream { 23 | let name = &ast.ident; 24 | let table_name = parse_table_attr(&ast); 25 | 26 | let (impl_generics, ty_generics, where_clause) = &ast.generics.split_for_impl(); 27 | 28 | let s = match ast.data { 29 | Data::Struct(ref s) => s, 30 | _ => panic!("Enums or Unions can not be mapped"), 31 | }; 32 | 33 | let tokio_pg_mapper = impl_tokio_pg_mapper( 34 | s, 35 | name, 36 | &table_name, 37 | impl_generics, 38 | ty_generics, 39 | where_clause, 40 | ); 41 | 42 | let tokens = quote! { 43 | #tokio_pg_mapper 44 | }; 45 | 46 | tokens.into() 47 | } 48 | 49 | fn impl_tokio_pg_mapper( 50 | s: &DataStruct, 51 | name: &Ident, 52 | table_name: &str, 53 | impl_generics: &ImplGenerics, 54 | ty_generics: &TypeGenerics, 55 | where_clause: &Option<&WhereClause>, 56 | ) -> Item { 57 | let fields = s.fields.iter().map(|field| { 58 | let ident = field.ident.as_ref().unwrap(); 59 | let ty = &field.ty; 60 | 61 | let row_expr = format!(r##"{}"##, ident); 62 | quote! { 63 | #ident:row.try_get::<&str,#ty>(#row_expr)? 64 | } 65 | }); 66 | 67 | let ref_fields = s.fields.iter().map(|field| { 68 | let ident = field.ident.as_ref().unwrap(); 69 | let ty = &field.ty; 70 | 71 | let row_expr = format!(r##"{}"##, ident); 72 | quote! { 73 | #ident:row.try_get::<&str,#ty>(&#row_expr)? 74 | } 75 | }); 76 | 77 | let table_columns = s 78 | .fields 79 | .iter() 80 | .map(|field| { 81 | let ident = field 82 | .ident 83 | .as_ref() 84 | .expect("Expected structfield identifier"); 85 | format!(" {0}.{1} ", table_name, ident) 86 | }) 87 | .collect::>() 88 | .join(", "); 89 | 90 | let columns = s 91 | .fields 92 | .iter() 93 | .map(|field| { 94 | let ident = field 95 | .ident 96 | .as_ref() 97 | .expect("Expected structfield identifier"); 98 | format!(" {} ", ident) 99 | }) 100 | .collect::>() 101 | .join(", "); 102 | 103 | let tokens = quote! { 104 | impl #impl_generics tokio_pg_mapper::FromTokioPostgresRow for #name #ty_generics #where_clause { 105 | fn from_row(row: tokio_postgres::row::Row) -> ::std::result::Result { 106 | Ok(Self { 107 | #(#fields),* 108 | }) 109 | } 110 | 111 | fn from_row_ref(row: &tokio_postgres::row::Row) -> ::std::result::Result { 112 | Ok(Self { 113 | #(#ref_fields),* 114 | }) 115 | } 116 | 117 | fn sql_table() -> String { 118 | #table_name.to_string() 119 | } 120 | 121 | fn sql_table_fields() -> String { 122 | #table_columns.to_string() 123 | } 124 | 125 | fn sql_fields() -> String { 126 | #columns.to_string() 127 | } 128 | } 129 | }; 130 | 131 | syn::parse_quote!(#tokens) 132 | } 133 | 134 | fn get_mapper_meta_items(attr: &syn::Attribute) -> Option> { 135 | if attr.path.segments.len() == 1 && attr.path.segments[0].ident == "pg_mapper" { 136 | match attr.parse_meta() { 137 | Ok(List(ref meta)) => Some(meta.nested.iter().cloned().collect()), 138 | _ => { 139 | panic!("declare table name: #[pg_mapper(table = \"foo\")]"); 140 | } 141 | } 142 | } else { 143 | None 144 | } 145 | } 146 | 147 | fn get_lit_str<'a>( 148 | attr_name: Option<&Ident>, 149 | lit: &'a syn::Lit, 150 | ) -> ::std::result::Result<&'a syn::LitStr, ()> { 151 | if let syn::Lit::Str(ref lit) = *lit { 152 | Ok(lit) 153 | } else { 154 | if let Some(val) = attr_name { 155 | panic!("expected pg_mapper {:?} attribute to be a string", val); 156 | } else { 157 | panic!("expected pg_mapper attribute to be a string"); 158 | } 159 | #[allow(unreachable_code)] 160 | Err(()) 161 | } 162 | } 163 | 164 | fn parse_table_attr(ast: &DeriveInput) -> String { 165 | // Parse `#[pg_mapper(table = "foo")]` 166 | let mut table_name: Option = None; 167 | 168 | for meta_items in ast.attrs.iter().filter_map(get_mapper_meta_items) { 169 | for meta_item in meta_items { 170 | match meta_item { 171 | // Parse `#[pg_mapper(table = "foo")]` 172 | Meta(NameValue(ref m)) if m.path.is_ident("table") => { 173 | if let Ok(s) = get_lit_str(m.path.get_ident(), &m.lit) { 174 | table_name = Some(s.value()); 175 | } 176 | } 177 | Meta(_) => { 178 | panic!(format!("unknown pg_mapper container attribute",)) 179 | } 180 | _ => { 181 | panic!("unexpected literal in pg_mapper container attribute"); 182 | } 183 | } 184 | } 185 | } 186 | 187 | table_name.expect("declare table name: #[pg_mapper(table = \"foo\")]") 188 | } 189 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 89 2 | reorder_imports = false 3 | reorder_modules = false 4 | edition = "2018" 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # tokio-pg-mapper 2 | //! 3 | //! `tokio-pg-mapper` is a proc-macro designed to make mapping from postgresql 4 | //! tables to structs simple. 5 | //! 6 | //! ### Why? 7 | //! 8 | //! It can be frustrating to write a lot of boilerplate and, ultimately, duplicated 9 | //! code for mapping from postgres Rows into structs. 10 | //! 11 | //! For example, this might be what someone would normally write: 12 | //! 13 | //! ```rust 14 | //! use postgres::row::Row; 15 | //! 16 | //! pub struct User { 17 | //! pub id: i64, 18 | //! pub name: String, 19 | //! pub email: Option, 20 | //! } 21 | //! 22 | //! impl From for User { 23 | //! fn from(row: Row) -> Self { 24 | //! Self { 25 | //! id: row.get("id"), 26 | //! name: row.get("name"), 27 | //! email: row.get("email"), 28 | //! } 29 | //! } 30 | //! } 31 | //! 32 | //! // code to execute a query here and get back a row 33 | //! let user = User::from_row(row); // returns Result 34 | //! ``` 35 | //! 36 | //! 37 | //! ### The two crates 38 | //! 39 | //! This repository contains two crates: `tokio-pg-mapper` which contains an `Error` 40 | //! enum and traits for converting from `tokio-postgres` `Row` 41 | //! without panicking, and `pg-mapper-derive` which contains the proc-macro. 42 | //! 43 | //! `pg-mapper-derive` has 3 features that can be enabled (where T is the 44 | //! struct being derived with the provided `TokioPostgresMapper` proc-macro): 45 | //! 46 | //! `impl FromTokioPostgresRow<::tokio_postgres::row::Row> for T` and 47 | //! `impl FromTokioPostgresRow<&::tokio_postgres::row::Row> for T` implementations 48 | //! - `pg-mapper` which, for each of the above features, implements 49 | //! `pg-mapper`'s `FromTokioPostgresRow` trait 50 | //! 51 | //! 52 | //! This will derive implementations for converting from owned and referenced 53 | //! `tokio-postgres::row::Row`s, as well as implementing `pg-mapper`'s 54 | //! `FromTokioPostgresRow` trait for non-panicking conversions. 55 | #[cfg(feature = "derive")] 56 | #[allow(unused_imports)] 57 | pub extern crate tokio_pg_mapper_derive; 58 | 59 | #[cfg(feature = "derive")] 60 | #[doc(hidden)] 61 | pub use tokio_pg_mapper_derive::*; 62 | 63 | use tokio_postgres::row::Row as TokioRow; 64 | 65 | use std::error::Error as StdError; 66 | use std::fmt::{Display, Formatter, Result as FmtResult}; 67 | 68 | /// Trait containing various methods for converting from a `tokio-postgres` Row 69 | /// to a mapped type. 70 | /// 71 | /// When using the `pg_mapper_derive` crate's `TokioPostgresMapper` proc-macro, 72 | /// this will automatically be implemented on types. 73 | /// 74 | /// The [`from_row`] method exists for consuming a `Row` - useful 75 | /// for iterator mapping - while [`from_row_ref`] exists for borrowing 76 | /// a `Row`. 77 | pub trait FromTokioPostgresRow: Sized { 78 | /// Converts from a `tokio-postgres` `Row` into a mapped type, consuming the 79 | /// given `Row`. 80 | /// 81 | /// # Errors 82 | /// 83 | /// Returns [`Error::ColumnNotFound`] if the column in a mapping was not 84 | /// found. 85 | /// 86 | /// Returns [`Error::Conversion`] if there was an error converting the row 87 | /// column to the requested type. 88 | /// 89 | /// [`Error::ColumnNotFound`]: enum.Error.html#variant.ColumnNotFound 90 | /// [`Error::Conversion`]: enum.Error.html#variant.Conversion 91 | fn from_row(row: TokioRow) -> Result; 92 | 93 | /// Converts from a `tokio-postgres` `Row` into a mapped type, borrowing the 94 | /// given `Row`. 95 | /// 96 | /// # Errors 97 | /// 98 | /// Returns [`Error::ColumnNotFound`] if the column in a mapping was not 99 | /// found. 100 | /// 101 | /// Returns [`Error::Conversion`] if there was an error converting the row 102 | /// column into the requested type. 103 | /// 104 | /// [`Error::ColumnNotFound`]: enum.Error.html#variant.ColumnNotFound 105 | /// [`Error::Conversion`]: enum.Error.html#variant.Conversion 106 | fn from_row_ref(row: &TokioRow) -> Result; 107 | 108 | /// Get the name of the annotated sql table name. 109 | /// 110 | /// Example: 111 | /// 112 | /// The following will return the String " user ". 113 | /// Note the extra spaces on either side to avoid incorrect formatting. 114 | /// 115 | /// ``` 116 | /// #[derive(PostgresMapper)] 117 | /// #[pg_mapper(table = "user")] 118 | /// pub struct User { 119 | /// pub id: i64, 120 | /// pub email: Option, 121 | /// } 122 | /// ``` 123 | fn sql_table() -> String; 124 | 125 | 126 | /// Get a list of the field names, excluding table name prefix. 127 | /// 128 | /// Example: 129 | /// 130 | /// The following will return the String " id, email ". 131 | /// Note the extra spaces on either side to avoid incorrect formatting. 132 | /// 133 | /// ``` 134 | /// #[derive(PostgresMapper)] 135 | /// #[pg_mapper(table = "user")] 136 | /// pub struct User { 137 | /// pub id: i64, 138 | /// pub email: Option, 139 | /// } 140 | /// ``` 141 | /// 142 | fn sql_fields() -> String; 143 | 144 | /// Get a list of the field names, including table name prefix. 145 | /// 146 | /// We also expect an attribute tag #[pg_mapper(table = "foo")] 147 | /// so that a scoped list of fields can be generated. 148 | /// 149 | /// Example: 150 | /// 151 | /// The following will return the String " user.id, user.email ". 152 | /// Note the extra spaces on either side to avoid incorrect formatting. 153 | /// 154 | /// ``` 155 | /// #[derive(PostgresMapper)] 156 | /// #[pg_mapper(table = "user")] 157 | /// pub struct User { 158 | /// pub id: i64, 159 | /// pub email: Option, 160 | /// } 161 | /// ``` 162 | /// 163 | fn sql_table_fields() -> String; 164 | } 165 | 166 | /// General error type returned throughout the library. 167 | #[derive(Debug)] 168 | pub enum Error { 169 | /// A column in a row was not found. 170 | ColumnNotFound, 171 | /// An error from the `tokio-postgres` crate while converting a type. 172 | Conversion(Box), 173 | /// Used in a scenario where tokios_postgres::Error::into_source returns None 174 | UnknownTokioPG(String) 175 | } 176 | 177 | impl From for Error { 178 | fn from(err: tokio_postgres::Error) -> Self { 179 | let reason = err.to_string(); 180 | if let Some(source) = err.into_source() { 181 | source.into() 182 | } else { 183 | Error::UnknownTokioPG(reason) 184 | } 185 | } 186 | } 187 | 188 | impl From> for Error { 189 | fn from(err: Box) -> Self { 190 | Error::Conversion(err) 191 | } 192 | } 193 | 194 | impl Display for Error { 195 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 196 | match self { 197 | Error::ColumnNotFound => f.write_str("Tokio-postgres-mapper: Column not found"), 198 | Error::UnknownTokioPG(reason) => f.write_str(reason), 199 | Error::Conversion(err) => f.write_str(err.to_string().as_str()), 200 | } 201 | } 202 | } 203 | 204 | impl StdError for Error { 205 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 206 | match *self { 207 | Error::Conversion(ref inner) => inner.source(), 208 | _ => None 209 | } 210 | } 211 | } 212 | --------------------------------------------------------------------------------