Skip to content

Commit

Permalink
feat(orm): add support for unique columns (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
m4tx authored Nov 10, 2024
1 parent c7bbf69 commit d6b2eb6
Show file tree
Hide file tree
Showing 16 changed files with 279 additions and 35 deletions.
9 changes: 4 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ darling = "0.20"
derive_builder = "0.20"
derive_more = { version = "1", features = ["full"] }
env_logger = "0.11"
fake = { version = "2", features = ["derive", "chrono"] }
fake = { version = "3", features = ["derive", "chrono"] }
flareon = { path = "flareon" }
flareon_codegen = { path = "flareon-codegen" }
flareon_macros = { path = "flareon-macros" }
Expand Down
7 changes: 6 additions & 1 deletion examples/admin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ impl FlareonApp for HelloApp {
}

async fn init(&self, context: &mut AppContext) -> flareon::Result<()> {
DatabaseUser::create_user(context.database(), "admin", "admin").await?;
// TODO use transaction
let user = DatabaseUser::get_by_username(context.database(), "admin").await?;
if user.is_none() {
DatabaseUser::create_user(context.database(), "admin", "admin").await?;
}

Ok(())
}

Expand Down
7 changes: 5 additions & 2 deletions flareon-cli/src/migration_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};

use anyhow::{bail, Context};
use cargo_toml::Manifest;
use darling::{FromDeriveInput, FromMeta};
use darling::FromMeta;
use flareon::db::migrations::{DynMigration, MigrationEngine};
use flareon_codegen::model::{Field, Model, ModelArgs, ModelOpts, ModelType};
use log::{debug, info};
Expand Down Expand Up @@ -494,7 +494,7 @@ struct ModelInSource {
impl ModelInSource {
fn from_item(item: ItemStruct, args: &ModelArgs) -> anyhow::Result<Self> {
let input: syn::DeriveInput = item.clone().into();
let opts = ModelOpts::from_derive_input(&input)
let opts = ModelOpts::new_from_derive_input(&input)
.map_err(|e| anyhow::anyhow!("cannot parse model: {}", e))?;
let model = opts.as_model(args)?;

Expand Down Expand Up @@ -534,6 +534,9 @@ impl Repr for Field {
tokens = quote! { #tokens.primary_key() }
}
tokens = quote! { #tokens.set_null(<#ty as ::flareon::db::DatabaseField>::NULLABLE) };
if self.unique {
tokens = quote! { #tokens.unique() }
}
tokens
}
}
Expand Down
101 changes: 100 additions & 1 deletion flareon-codegen/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,22 @@ pub enum ModelType {
#[darling(forward_attrs(allow, doc, cfg), supports(struct_named))]
pub struct ModelOpts {
pub ident: syn::Ident,
pub generics: syn::Generics,
pub data: darling::ast::Data<darling::util::Ignored, FieldOpts>,
}

impl ModelOpts {
pub fn new_from_derive_input(input: &syn::DeriveInput) -> Result<Self, darling::error::Error> {
let opts = Self::from_derive_input(input)?;
if !opts.generics.params.is_empty() {
return Err(
darling::Error::custom("generics in models are not supported")
.with_span(&opts.generics),
);
}
Ok(opts)
}

/// Get the fields of the struct.
///
/// # Panics
Expand Down Expand Up @@ -79,10 +91,11 @@ impl ModelOpts {
}

#[derive(Debug, Clone, FromField)]
#[darling(attributes(form))]
#[darling(attributes(model))]
pub struct FieldOpts {
pub ident: Option<syn::Ident>,
pub ty: syn::Type,
pub unique: darling::util::Flag,
}

impl FieldOpts {
Expand All @@ -108,6 +121,7 @@ impl FieldOpts {
auto_value: is_auto,
primary_key: is_primary_key,
null: false,
unique: self.unique.is_present(),
}
}
}
Expand Down Expand Up @@ -136,4 +150,89 @@ pub struct Field {
pub auto_value: bool,
pub primary_key: bool,
pub null: bool,
pub unique: bool,
}

#[cfg(test)]
mod tests {
use syn::parse_quote;

use super::*;

#[test]
fn model_args_default() {
let args: ModelArgs = Default::default();
assert_eq!(args.model_type, ModelType::Application);
assert!(args.table_name.is_none());
}

#[test]
fn model_type_default() {
let model_type: ModelType = Default::default();
assert_eq!(model_type, ModelType::Application);
}

#[test]
fn model_opts_fields() {
let input: syn::DeriveInput = parse_quote! {
struct TestModel {
id: i32,
name: String,
}
};
let opts = ModelOpts::new_from_derive_input(&input).unwrap();
let fields = opts.fields();
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].ident.as_ref().unwrap().to_string(), "id");
assert_eq!(fields[1].ident.as_ref().unwrap().to_string(), "name");
}

#[test]
fn model_opts_as_model() {
let input: syn::DeriveInput = parse_quote! {
struct TestModel {
id: i32,
name: String,
}
};
let opts = ModelOpts::new_from_derive_input(&input).unwrap();
let args = ModelArgs::default();
let model = opts.as_model(&args).unwrap();
assert_eq!(model.name.to_string(), "TestModel");
assert_eq!(model.table_name, "test_model");
assert_eq!(model.fields.len(), 2);
assert_eq!(model.field_count(), 2);
}

#[test]
fn model_opts_as_model_migration() {
let input: syn::DeriveInput = parse_quote! {
#[model(model_type = "migration")]
struct TestModel {
id: i32,
name: String,
}
};
let opts = ModelOpts::new_from_derive_input(&input).unwrap();
let args = ModelArgs::from_meta(&input.attrs.first().unwrap().meta).unwrap();
let err = opts.as_model(&args).unwrap_err();
assert_eq!(
err.to_string(),
"migration model names must start with an underscore"
);
}

#[test]
fn field_opts_as_field() {
let input: syn::Field = parse_quote! {
#[model(unique)]
name: String
};
let field_opts = FieldOpts::from_field(&input).unwrap();
let field = field_opts.as_field();
assert_eq!(field.field_name.to_string(), "name");
assert_eq!(field.column_name, "name");
assert_eq!(field.ty, parse_quote!(String));
assert!(field.unique);
}
}
4 changes: 2 additions & 2 deletions flareon-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ pub fn model(args: TokenStream, input: TokenStream) -> TokenStream {
return TokenStream::from(Error::from(e).write_errors());
}
};
let ast = parse_macro_input!(input as syn::DeriveInput);
let token_stream = impl_model_for_struct(&attr_args, &ast);
let mut ast = parse_macro_input!(input as syn::DeriveInput);
let token_stream = impl_model_for_struct(&attr_args, &mut ast);
token_stream.into()
}

Expand Down
51 changes: 43 additions & 8 deletions flareon-macros/src/model.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
use darling::ast::NestedMeta;
use darling::{FromDeriveInput, FromMeta};
use darling::FromMeta;
use flareon_codegen::model::{Field, Model, ModelArgs, ModelOpts};
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
use syn::punctuated::Punctuated;
use syn::Token;

use crate::flareon_ident;

#[must_use]
pub(super) fn impl_model_for_struct(args: &[NestedMeta], ast: &syn::DeriveInput) -> TokenStream {
pub(super) fn impl_model_for_struct(
args: &[NestedMeta],
ast: &mut syn::DeriveInput,
) -> TokenStream {
let args = match ModelArgs::from_list(args) {
Ok(v) => v,
Err(e) => {
return e.write_errors();
}
};

let opts = match ModelOpts::from_derive_input(ast) {
let opts = match ModelOpts::new_from_derive_input(ast) {
Ok(val) => val,
Err(err) => {
return err.write_errors();
Expand All @@ -30,7 +35,36 @@ pub(super) fn impl_model_for_struct(args: &[NestedMeta], ast: &syn::DeriveInput)
};
let builder = ModelBuilder::from_model(model);

quote!(#ast #builder)
let attrs = &ast.attrs;
let vis = &ast.vis;
let ident = &ast.ident;

// Filter out our helper attributes so they don't get passed to the struct
let fields = match &mut ast.data {
syn::Data::Struct(data) => &mut data.fields,
_ => panic!("Only structs are supported"),
};
let fields = remove_helper_field_attributes(fields);

quote!(
#(#attrs)*
#vis struct #ident {
#fields
}
#builder
)
}

fn remove_helper_field_attributes(fields: &mut syn::Fields) -> &Punctuated<syn::Field, Token![,]> {
match fields {
syn::Fields::Named(fields) => {
for field in &mut fields.named {
field.attrs.retain(|a| !a.path().is_ident("model"));
}
&fields.named
}
_ => panic!("Only named fields are supported"),
}
}

#[derive(Debug)]
Expand Down Expand Up @@ -77,19 +111,20 @@ impl ModelBuilder {
let ty = &field.ty;
let index = self.fields_as_columns.len();
let column_name = &field.column_name;
let is_auto = field.auto_value;
let is_null = field.null;

{
let mut field_as_column = quote!(#orm_ident::Column::new(
#orm_ident::Identifier::new(#column_name)
));
if is_auto {
if field.auto_value {
field_as_column.append_all(quote!(.auto()));
}
if is_null {
if field.null {
field_as_column.append_all(quote!(.null()));
}
if field.unique {
field_as_column.append_all(quote!(.unique()));
}
self.fields_as_columns.push(field_as_column);
}

Expand Down
1 change: 1 addition & 0 deletions flareon-macros/tests/compile_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ fn attr_model() {
t.compile_fail("tests/ui/attr_model_migration_invalid_name.rs");
t.compile_fail("tests/ui/attr_model_tuple.rs");
t.compile_fail("tests/ui/attr_model_enum.rs");
t.compile_fail("tests/ui/attr_model_generic.rs");
}

#[rustversion::attr(not(nightly), ignore)]
Expand Down
9 changes: 9 additions & 0 deletions flareon-macros/tests/ui/attr_model_generic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use flareon::db::model;

#[model]
struct MyModel<T> {
id: i32,
some_data: T,
}

fn main() {}
5 changes: 5 additions & 0 deletions flareon-macros/tests/ui/attr_model_generic.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: generics in models are not supported
--> tests/ui/attr_model_generic.rs:4:15
|
4 | struct MyModel<T> {
| ^^^
4 changes: 1 addition & 3 deletions flareon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ mime_guess.workspace = true
mockall.workspace = true
password-auth.workspace = true
pin-project-lite.workspace = true
rand = { workspace = true, optional = true }
regex.workspace = true
sea-query-binder.workspace = true
sea-query.workspace = true
Expand All @@ -50,7 +49,6 @@ tower-sessions.workspace = true
async-stream.workspace = true
fake.workspace = true
futures.workspace = true
rand.workspace = true

[package.metadata.cargo-machete]
ignored = [
Expand All @@ -63,4 +61,4 @@ ignored = [
]

[features]
fake = ["dep:fake", "dep:rand"]
fake = ["dep:fake"]
Loading

0 comments on commit d6b2eb6

Please sign in to comment.