1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
//! # Macros for Atmosphere
//!
//! This crate provides a set of procedural macros to simplify and automate various tasks in
//! atmosphere. These macros enhance the developer experience by reducing boilerplate,
//! ensuring consistency, and integrating seamlessly with the framework's functionalities.
//!
//! This crate includes macros for deriving schema information from structs, handling table-related
//! attributes, and managing hooks within the framework. The macros are designed to be intuitive
//! and align with the framework's conventions, making them a powerful tool in the application
//! development process.

use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, ItemStruct};

mod derive;
mod hooks;
mod schema;

use schema::table::Table;

/// A derive macro that processes structs to automatically generate schema-related code. It reads
/// custom attributes and derives necessary traits and implementations for interacting with the
/// database.
///
/// Entity attributes:
///
/// - `#[table(schema = "schema_name", name = "table_name")]` - Set schema and table name
///
/// Field attributes:
///
/// - `#[sql(pk)]` - Mark a column as primary key
/// - `#[sql(fk -> OtherModel)]` - Mark a column as foreign key on `OtherModel`
/// - `#[sql(unique)]` - Mark a column as unique
/// - `#[sql(timestamp = [create|update|delete])]` - Mark a column as timestamp
/// - `#[sql(.., rename = "renamed_sql_col")]` - Rename a column in the generated sql
///
/// Usage:
///
/// ```
/// # use atmosphere::prelude::*;
/// #[derive(Schema)]
/// #[table(schema = "public", name = "user")]
/// struct User {
///     #[sql(pk)]
///     id: i32,
///     #[sql(unique)]
///     username: String,
/// }
///
/// #[derive(Schema)]
/// #[table(schema = "public", name = "post")]
/// struct Post {
///     #[sql(pk)]
///     id: i32,
///     #[sql(fk -> User, rename = "author_id")]
///     author: i32,
/// }
/// ```
#[proc_macro_derive(Schema, attributes(sql))]
pub fn schema(input: TokenStream) -> TokenStream {
    let table = parse_macro_input!(input as Table);
    derive::all(&table).into()
}

/// An attribute macro that stores metadata about the sql table.
/// Must be used after `#[derive(Schema)]`.
///
/// Keys:
///
/// - `schema` - sets schema name.
/// - `name` - sets table name.
///
/// Usage:
///
/// ```
/// # use atmosphere::prelude::*;
/// # #[derive(Schema)]
/// #[table(schema = "public", name = "user")]
/// # struct User {
/// #     #[sql(pk)]
/// #     id: i32,
/// #     #[sql(unique)]
/// #     username: String,
/// # }
/// ```
#[proc_macro_attribute]
pub fn table(_: TokenStream, input: TokenStream) -> TokenStream {
    let mut model = parse_macro_input!(input as ItemStruct);

    for ref mut field in model.fields.iter_mut() {
        let attribute = field
            .attrs
            .iter()
            .find(|a| a.path().is_ident(schema::column::attribute::PATH));

        let Some(attribute) = attribute else {
            continue;
        };

        let attribute: schema::column::attribute::Attribute = attribute.parse_args().unwrap();

        if let Some(rename) = attribute.renamed {
            struct Extract {
                rename: syn::Attribute,
            }

            impl syn::parse::Parse for Extract {
                fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
                    Ok(Self {
                        rename: input
                            .call(syn::Attribute::parse_outer)?
                            .into_iter()
                            .next()
                            .unwrap(),
                    })
                }
            }

            let Extract { rename } =
                syn::parse_str(&format!("#[sqlx(rename = \"{}\")]", rename)).unwrap();

            field.attrs.push(rename);
        }
    }

    let model = model.to_token_stream();

    quote! {
        #[derive(::atmosphere::sqlx::FromRow)]
        #model
    }
    .into()
}

/// An attribute macro for registering on a table. Must be used after `#[derive(Schema)]`.
///
/// Takes as argument a type which implements `Hook<Self>` for the entity type.
///
/// Usage:
///
/// ```
/// # use atmosphere::prelude::*;
/// # use atmosphere::hooks::*;
/// #[derive(Schema)]
/// #[table(schema = "public", name = "user")]
/// #[hooks(MyHook)]
/// struct User {
///     #[sql(pk)]
///     id: i32,
///     #[sql(unique)]
///     username: String,
/// }
///
/// struct MyHook;
///
/// impl Hook<User> for MyHook {
///     fn stage(&self) -> HookStage {
///         todo!()
///     }
/// }
/// ```
#[proc_macro_attribute]
pub fn hooks(attr: TokenStream, input: TokenStream) -> TokenStream {
    let model = parse_macro_input!(input as ItemStruct);
    let _ = parse_macro_input!(attr as hooks::Hooks);
    quote! { #model }.into()
}