Skip to content

Commit

Permalink
Add support for serde-json, time, uuid, improve docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Einliterflasche committed Aug 30, 2023
1 parent b8a9382 commit c028cd9
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 79 deletions.
6 changes: 6 additions & 0 deletions pg-worm-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ quote = "1.0.27"
syn = { version = "2.0.15", features = ["derive"] }
darling = "0.20"
postgres-types = "0.2"
convert_case = "0.6.0"

[features]
serde-json = []
time = []
uuid = []
63 changes: 49 additions & 14 deletions pg-worm-derive/src/parse.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use convert_case::Casing;
use darling::{ast::Data, Error, FromDeriveInput, FromField};
use postgres_types::Type;
use proc_macro2::TokenStream;
Expand All @@ -21,13 +22,15 @@ pub struct ModelField {
column_name: Option<String>,
#[darling(default)]
auto: bool,
#[darling(skip)]
auto_gen_stmt: String,
#[darling(default)]
primary_key: bool,
#[darling(default)]
unique: bool,
#[darling(skip)]
nullable: bool,
#[darling(default)]
#[darling(skip)]
array: bool,
}

Expand All @@ -43,7 +46,7 @@ impl ModelInput {
return table_name.clone();
}

self.ident.to_string().to_lowercase()
self.ident.to_string().to_case(convert_case::Case::Snake)
}

/// Get an iterator over the input struct's fields.
Expand Down Expand Up @@ -192,10 +195,10 @@ impl ModelInput {
let column_names = self.all_fields().map(|i| i.column_name());

quote!(
impl TryFrom<pg_worm::Row> for #ident {
impl TryFrom<pg_worm::pg::Row> for #ident {
type Error = pg_worm::Error;

fn try_from(row: pg_worm::Row) -> Result<#ident, Self::Error> {
fn try_from(row: pg_worm::pg::Row) -> Result<#ident, Self::Error> {
let res = #ident {
#(
#field_idents: row.try_get(#column_names)?
Expand All @@ -218,7 +221,7 @@ impl ModelInput {
let n_fields = self.all_fields().count();

quote!(
pub const COLUMNS: [&'static dyn Deref<Target = Column>; #n_fields] = [
pub const COLUMNS: [&'static dyn Deref<Target = pg_worm::query::Column>; #n_fields] = [
#(
&#ident::#field_idents
),*
Expand Down Expand Up @@ -330,19 +333,32 @@ impl ModelField {

// Extract relevant type from the path
let syn::Type::Path(path) = ty else {
spanned_error!("unsupported type", &ty)
spanned_error!("pg-worm: unsupported type", &ty)
};
let path = &path.path;
let Some(last_seg) = path.segments.last() else {
spanned_error!("invalid path (needs at least one segment)", &ty)
spanned_error!("pg-worm: invalid type path (needs at least one segment)", &ty)
};

match last_seg.ident.to_string().as_str() {
// If it's an Option<T>, set the field nullable
"Option" => field.nullable = true,
"Option" => {
field.nullable = true;
}
// If it's a Vec<T>, set the field to be an array
"Vec" => field.array = true,
"Vec" => {
field.array = true;
}
_ => (),
};

if field.auto {
field.auto_gen_stmt = match last_seg.ident.to_string().as_str() {
#[cfg(feature = "uuid")]
"Uuid" => "DEFAULT gen_random_uuid()",
"i16" | "i32" | "i64" => "GENERATED ALWAYS AS IDENTITY",
_ => spanned_error!("pg-worm: `auto` is only available for integers and uuid (with the `uuid` feature enabled)", &ty)
}.to_string();
}

Ok(field)
Expand Down Expand Up @@ -404,12 +420,31 @@ impl ModelField {

Ok(match id.to_string().as_ref() {
"String" => Type::TEXT,
"i16" => Type::INT2,
"i32" => Type::INT4,
"i64" => Type::INT8,
"f32" => Type::FLOAT4,
"f64" => Type::FLOAT8,
"bool" => Type::BOOL,
_ => spanned_error!("pg-worm: unsupported type, check docs", &ty),
// `serde_json` support
#[cfg(feature = "serde-json")]
"Value" => Type::JSONB,
// `time` support
#[cfg(feature = "time")]
"Date" => Type::DATE,
#[cfg(feature = "time")]
"Time" => Type::TIME,
#[cfg(feature = "time")]
"PrimitiveDateTime" => Type::TIMESTAMP,
#[cfg(feature = "time")]
"OffsetDateTime" => Type::TIMESTAMPTZ,
// `uuid` support
#[cfg(feature = "uuid")]
"Uuid" => Type::UUID,
_ => spanned_error!(
"pg-worm: unsupported type. did you forget to enable a feature?",
&ty
),
})
}

Expand All @@ -423,17 +458,17 @@ impl ModelField {
// This macro allows adding an arg to the list
// under a given condition.
macro_rules! arg {
($cond:expr, $sql:literal) => {
($cond:expr, $sql:expr) => {
if $cond {
args.push($sql.to_string());
}
};
}

// Add possible args
arg!(self.array, "ARRAY");
arg!(self.array, "[]");
arg!(self.primary_key, "PRIMARY KEY");
arg!(self.auto, "GENERATED ALWAYS AS IDENTITY");
arg!(self.auto, self.auto_gen_stmt);
arg!(self.unique, "UNIQUE");
arg!(!(self.primary_key || self.nullable), "NOT NULL");

Expand Down Expand Up @@ -477,7 +512,7 @@ impl ModelField {

quote!(
#[allow(non_upper_case_globals)]
pub const #ident: pg_worm::TypedColumn<#rs_type> = pg_worm::TypedColumn::new(#table_name, #col_name)
pub const #ident: pg_worm::query::TypedColumn<#rs_type> = pg_worm::query::TypedColumn::new(#table_name, #col_name)
#(#props)*;
)
}
Expand Down
13 changes: 9 additions & 4 deletions pg-worm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ repository = "https://github.com/Einliterflasche/pg-worm"
homepage = "https://github.com/Einliterflasche/pg-worm"

[dependencies]
# Core dependencies
thiserror = "1.0"
deadpool-postgres = "0.10"
tokio-postgres = "0.7"
tokio-postgres = { version = "0.7", default-features = false }
async-trait = "0.1"
futures = "0.3"

pg-worm-derive = { version = "0.6" }
futures-util = { version = "0.3", default-features = false }
pg-worm-derive = { version = "0.6", path = "../pg-worm-derive" }

[dev-dependencies]
tokio = { version = "1", features = ["full"] }

[features]
serde-json = ["tokio-postgres/with-serde_json-1", "pg-worm-derive/serde-json"]
uuid = ["tokio-postgres/with-uuid-1", "pg-worm-derive/uuid"]
time = ["tokio-postgres/with-time-0_3", "pg-worm-derive/time"]
103 changes: 88 additions & 15 deletions pg-worm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ Currently, the following methods are implemented:

Function | Description | Availability
---------|-------------|-------------
`eq` | Checks for equality. | Any type
`eq` | Checks for equality. | Any type
`gt`, `gte`, `lt`, `lte` | Check whether this column's value is greater than, etc than some other value. | Any type which implements [`PartialOrd`](https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html). Note: it's not guaranteed that Postgres supports these operator for a type just because it's `PartialOrd`. Be sure to check the Postgres documentation for your type beforehand.
`null`, `not_null` | Checks whether a column is `NULL`. | Any `Option<T>`. All other types are not `NULL`able and thus guaranteed not to be `NULL`.
`contains`, `contains_not`, `contains_all`, `conatains_none`, `contains_any` | Array operations. Check whether this column's array contains a value, a value _not_, or any/all/none values of another array. | Any `Vec<T>`.
Expand All @@ -152,8 +152,8 @@ Book::select()

Operator/Method | Description
----------------|------------
`!`, `.not()` | Negate a filter using a locigal `NOT`
`&`, `.and()` | Combine two filters using a logical `AND`
`!`, `.not()` | Negate a filter using a locigal `NOT`
`&`, `.and()` | Combine two filters using a logical `AND`
`\|\|`, `.or()` | Combine two filters using a logical `OR`


Expand Down Expand Up @@ -216,23 +216,96 @@ async fn foo() -> Result<(), Box<dyn std::error::Error>> {
```

## Supported types
The following is a list of all supported (Rust) types and which PostgreSQL type they are mapped to.
Rust type | PostgreSQL type
----------|---------------------
`bool` | `bool`
`i32` | `int4`
`i64` | `int8`
`f32` | `float4`
`f64` | `float8`
`String` | `TEXT`
`Option<T>`* | `T` (but the column becomes `nullable`)
`Vec<T>`* | `T[]`
The following is a list of supported (Rust) types and which PostgreSQL type they are mapped to.

Rust type | PostgreSQL type
-------------|---------------------
`bool` | `BOOL`
`i16` | `INT2`
`i32` | `INT4`
`i64` | `INT8`
`f32` | `FLOAT4`
`f64` | `FLOAT8`
`String` | `TEXT`
`Option<T>`* | `T` (but the column becomes `NULLABLE`)
`Vec<T>`* | `T[]`

_*`T` must be another supported type. Nesting and mixing `Option`/`Vec` is currently not supported._

### JSON, timestamps and more
are supported, too. To use them activate the respective feature, like so:

```toml
# Cargo.toml
[dependencies]
pg-worm = { version = "latest-version", features = ["foo"] }
```

Here is a list of the supported features/types with their respective PostgreSQL type:

* `"serde-json"` for [`serde_json`](https://crates.io/crates/serde_json) `v1.0`
Rust type | PostgreSQL type
----------|----------------
`Value` | `JSONB`
* `"time"` for [`time`](https://crates.io/crates/time/0.3.0) `v3.0`
Rust type | PostgreSQL type
--------------------|----------------
`Date` | `DATE`
`Time` | `TIME`
`PrimitiveDateTime` | `TIMESTAMP`
`OffsetDateTime` | `TIMESTAMP WITH TIME ZONE`

* `"uuid"` for [`uuid`](https://crates.io/crates/uuid) `v1.0`
Rust type | PostgreSQL type
----------|----------------
`Uuid` | `UUID`

## `derive` options

You can configure some options for you `Model`.
This is done by using one of the two attributes `pg-worm` exposes.

### The `#[table]` attribute

The `#[table]` attribute can be used to pass configurations to a `Model` which affect the respective table itself.

```rust
use pg_worm::prelude::*;

#[derive(Model)]
#[table(table_name = "book_list")]
struct Book {
id: i64
}
```

Option | Meaning | Usage | Default
-------|---------|-------|--------
`table_name` | Set the table's name | `table_name = "new_table_name"` | The `struct`'s name converted to snake case using [this crate](https://crates.io/crates/convert_case).

### The `#[column]` attribute

The `#[column]` attribute can be used to pass configurations to a `Model`'s field which affect the respective column.

```rust
use pg_worm::prelude::*;

#[derive(Model)]
struct Book {
#[column(primary_key, auto)]
id: i64
}
```

Option | Meaning | Usage | Default
-------|---------|-------|--------
`column_name` | Set this column's name. | `#[column(column_name = "new_column_name")]` | The fields's name converted to snake case using [this crate](https://crates.io/crates/convert_case).
`primary_key` | Make this column the primary key. Only use this once per `Model`. If you want this column to be auto generated use `auto` as well. | `#[column(primary_key)]` | `false`
`auto` | Make this column auto generated. Works only for `i16`, `i32` and `i64`, as well as `Uuid` *if* the `"uuid"` feature has been enabled *and* you use PostgreSQL version 13 or later. | `#[column(auto)]` | `false`


## MSRV
The minimum supported rust version is `1.70` as this crate uses the recently introduced `OnceLock` from the standard library.

## License
This project is dual-licensed under the MIT and Apache 2.0 licenses.

Loading

0 comments on commit c028cd9

Please sign in to comment.