Skip to content

Commit d6b2eb6

Browse files
authored
feat(orm): add support for unique columns (#62)
1 parent c7bbf69 commit d6b2eb6

File tree

16 files changed

+279
-35
lines changed

16 files changed

+279
-35
lines changed

Cargo.lock

+4-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ darling = "0.20"
3535
derive_builder = "0.20"
3636
derive_more = { version = "1", features = ["full"] }
3737
env_logger = "0.11"
38-
fake = { version = "2", features = ["derive", "chrono"] }
38+
fake = { version = "3", features = ["derive", "chrono"] }
3939
flareon = { path = "flareon" }
4040
flareon_codegen = { path = "flareon-codegen" }
4141
flareon_macros = { path = "flareon-macros" }

examples/admin/src/main.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ impl FlareonApp for HelloApp {
2222
}
2323

2424
async fn init(&self, context: &mut AppContext) -> flareon::Result<()> {
25-
DatabaseUser::create_user(context.database(), "admin", "admin").await?;
25+
// TODO use transaction
26+
let user = DatabaseUser::get_by_username(context.database(), "admin").await?;
27+
if user.is_none() {
28+
DatabaseUser::create_user(context.database(), "admin", "admin").await?;
29+
}
30+
2631
Ok(())
2732
}
2833

flareon-cli/src/migration_generator.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
77

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

@@ -534,6 +534,9 @@ impl Repr for Field {
534534
tokens = quote! { #tokens.primary_key() }
535535
}
536536
tokens = quote! { #tokens.set_null(<#ty as ::flareon::db::DatabaseField>::NULLABLE) };
537+
if self.unique {
538+
tokens = quote! { #tokens.unique() }
539+
}
537540
tokens
538541
}
539542
}

flareon-codegen/src/model.rs

+100-1
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,22 @@ pub enum ModelType {
2323
#[darling(forward_attrs(allow, doc, cfg), supports(struct_named))]
2424
pub struct ModelOpts {
2525
pub ident: syn::Ident,
26+
pub generics: syn::Generics,
2627
pub data: darling::ast::Data<darling::util::Ignored, FieldOpts>,
2728
}
2829

2930
impl ModelOpts {
31+
pub fn new_from_derive_input(input: &syn::DeriveInput) -> Result<Self, darling::error::Error> {
32+
let opts = Self::from_derive_input(input)?;
33+
if !opts.generics.params.is_empty() {
34+
return Err(
35+
darling::Error::custom("generics in models are not supported")
36+
.with_span(&opts.generics),
37+
);
38+
}
39+
Ok(opts)
40+
}
41+
3042
/// Get the fields of the struct.
3143
///
3244
/// # Panics
@@ -79,10 +91,11 @@ impl ModelOpts {
7991
}
8092

8193
#[derive(Debug, Clone, FromField)]
82-
#[darling(attributes(form))]
94+
#[darling(attributes(model))]
8395
pub struct FieldOpts {
8496
pub ident: Option<syn::Ident>,
8597
pub ty: syn::Type,
98+
pub unique: darling::util::Flag,
8699
}
87100

88101
impl FieldOpts {
@@ -108,6 +121,7 @@ impl FieldOpts {
108121
auto_value: is_auto,
109122
primary_key: is_primary_key,
110123
null: false,
124+
unique: self.unique.is_present(),
111125
}
112126
}
113127
}
@@ -136,4 +150,89 @@ pub struct Field {
136150
pub auto_value: bool,
137151
pub primary_key: bool,
138152
pub null: bool,
153+
pub unique: bool,
154+
}
155+
156+
#[cfg(test)]
157+
mod tests {
158+
use syn::parse_quote;
159+
160+
use super::*;
161+
162+
#[test]
163+
fn model_args_default() {
164+
let args: ModelArgs = Default::default();
165+
assert_eq!(args.model_type, ModelType::Application);
166+
assert!(args.table_name.is_none());
167+
}
168+
169+
#[test]
170+
fn model_type_default() {
171+
let model_type: ModelType = Default::default();
172+
assert_eq!(model_type, ModelType::Application);
173+
}
174+
175+
#[test]
176+
fn model_opts_fields() {
177+
let input: syn::DeriveInput = parse_quote! {
178+
struct TestModel {
179+
id: i32,
180+
name: String,
181+
}
182+
};
183+
let opts = ModelOpts::new_from_derive_input(&input).unwrap();
184+
let fields = opts.fields();
185+
assert_eq!(fields.len(), 2);
186+
assert_eq!(fields[0].ident.as_ref().unwrap().to_string(), "id");
187+
assert_eq!(fields[1].ident.as_ref().unwrap().to_string(), "name");
188+
}
189+
190+
#[test]
191+
fn model_opts_as_model() {
192+
let input: syn::DeriveInput = parse_quote! {
193+
struct TestModel {
194+
id: i32,
195+
name: String,
196+
}
197+
};
198+
let opts = ModelOpts::new_from_derive_input(&input).unwrap();
199+
let args = ModelArgs::default();
200+
let model = opts.as_model(&args).unwrap();
201+
assert_eq!(model.name.to_string(), "TestModel");
202+
assert_eq!(model.table_name, "test_model");
203+
assert_eq!(model.fields.len(), 2);
204+
assert_eq!(model.field_count(), 2);
205+
}
206+
207+
#[test]
208+
fn model_opts_as_model_migration() {
209+
let input: syn::DeriveInput = parse_quote! {
210+
#[model(model_type = "migration")]
211+
struct TestModel {
212+
id: i32,
213+
name: String,
214+
}
215+
};
216+
let opts = ModelOpts::new_from_derive_input(&input).unwrap();
217+
let args = ModelArgs::from_meta(&input.attrs.first().unwrap().meta).unwrap();
218+
let err = opts.as_model(&args).unwrap_err();
219+
assert_eq!(
220+
err.to_string(),
221+
"migration model names must start with an underscore"
222+
);
223+
}
224+
225+
#[test]
226+
fn field_opts_as_field() {
227+
let input: syn::Field = parse_quote! {
228+
#[model(unique)]
229+
name: String
230+
};
231+
let field_opts = FieldOpts::from_field(&input).unwrap();
232+
let field = field_opts.as_field();
233+
assert_eq!(field.field_name.to_string(), "name");
234+
assert_eq!(field.column_name, "name");
235+
assert_eq!(field.ty, parse_quote!(String));
236+
assert!(field.unique);
237+
}
139238
}

flareon-macros/src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ pub fn model(args: TokenStream, input: TokenStream) -> TokenStream {
101101
return TokenStream::from(Error::from(e).write_errors());
102102
}
103103
};
104-
let ast = parse_macro_input!(input as syn::DeriveInput);
105-
let token_stream = impl_model_for_struct(&attr_args, &ast);
104+
let mut ast = parse_macro_input!(input as syn::DeriveInput);
105+
let token_stream = impl_model_for_struct(&attr_args, &mut ast);
106106
token_stream.into()
107107
}
108108

flareon-macros/src/model.rs

+43-8
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
use darling::ast::NestedMeta;
2-
use darling::{FromDeriveInput, FromMeta};
2+
use darling::FromMeta;
33
use flareon_codegen::model::{Field, Model, ModelArgs, ModelOpts};
44
use proc_macro2::{Ident, TokenStream};
55
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
6+
use syn::punctuated::Punctuated;
7+
use syn::Token;
68

79
use crate::flareon_ident;
810

911
#[must_use]
10-
pub(super) fn impl_model_for_struct(args: &[NestedMeta], ast: &syn::DeriveInput) -> TokenStream {
12+
pub(super) fn impl_model_for_struct(
13+
args: &[NestedMeta],
14+
ast: &mut syn::DeriveInput,
15+
) -> TokenStream {
1116
let args = match ModelArgs::from_list(args) {
1217
Ok(v) => v,
1318
Err(e) => {
1419
return e.write_errors();
1520
}
1621
};
1722

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

33-
quote!(#ast #builder)
38+
let attrs = &ast.attrs;
39+
let vis = &ast.vis;
40+
let ident = &ast.ident;
41+
42+
// Filter out our helper attributes so they don't get passed to the struct
43+
let fields = match &mut ast.data {
44+
syn::Data::Struct(data) => &mut data.fields,
45+
_ => panic!("Only structs are supported"),
46+
};
47+
let fields = remove_helper_field_attributes(fields);
48+
49+
quote!(
50+
#(#attrs)*
51+
#vis struct #ident {
52+
#fields
53+
}
54+
#builder
55+
)
56+
}
57+
58+
fn remove_helper_field_attributes(fields: &mut syn::Fields) -> &Punctuated<syn::Field, Token![,]> {
59+
match fields {
60+
syn::Fields::Named(fields) => {
61+
for field in &mut fields.named {
62+
field.attrs.retain(|a| !a.path().is_ident("model"));
63+
}
64+
&fields.named
65+
}
66+
_ => panic!("Only named fields are supported"),
67+
}
3468
}
3569

3670
#[derive(Debug)]
@@ -77,19 +111,20 @@ impl ModelBuilder {
77111
let ty = &field.ty;
78112
let index = self.fields_as_columns.len();
79113
let column_name = &field.column_name;
80-
let is_auto = field.auto_value;
81-
let is_null = field.null;
82114

83115
{
84116
let mut field_as_column = quote!(#orm_ident::Column::new(
85117
#orm_ident::Identifier::new(#column_name)
86118
));
87-
if is_auto {
119+
if field.auto_value {
88120
field_as_column.append_all(quote!(.auto()));
89121
}
90-
if is_null {
122+
if field.null {
91123
field_as_column.append_all(quote!(.null()));
92124
}
125+
if field.unique {
126+
field_as_column.append_all(quote!(.unique()));
127+
}
93128
self.fields_as_columns.push(field_as_column);
94129
}
95130

flareon-macros/tests/compile_tests.rs

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ fn attr_model() {
1313
t.compile_fail("tests/ui/attr_model_migration_invalid_name.rs");
1414
t.compile_fail("tests/ui/attr_model_tuple.rs");
1515
t.compile_fail("tests/ui/attr_model_enum.rs");
16+
t.compile_fail("tests/ui/attr_model_generic.rs");
1617
}
1718

1819
#[rustversion::attr(not(nightly), ignore)]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use flareon::db::model;
2+
3+
#[model]
4+
struct MyModel<T> {
5+
id: i32,
6+
some_data: T,
7+
}
8+
9+
fn main() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
error: generics in models are not supported
2+
--> tests/ui/attr_model_generic.rs:4:15
3+
|
4+
4 | struct MyModel<T> {
5+
| ^^^

flareon/Cargo.toml

+1-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ mime_guess.workspace = true
3131
mockall.workspace = true
3232
password-auth.workspace = true
3333
pin-project-lite.workspace = true
34-
rand = { workspace = true, optional = true }
3534
regex.workspace = true
3635
sea-query-binder.workspace = true
3736
sea-query.workspace = true
@@ -50,7 +49,6 @@ tower-sessions.workspace = true
5049
async-stream.workspace = true
5150
fake.workspace = true
5251
futures.workspace = true
53-
rand.workspace = true
5452

5553
[package.metadata.cargo-machete]
5654
ignored = [
@@ -63,4 +61,4 @@ ignored = [
6361
]
6462

6563
[features]
66-
fake = ["dep:fake", "dep:rand"]
64+
fake = ["dep:fake"]

0 commit comments

Comments
 (0)