1use proc_macro::TokenStream;
4use proc_macro2::{Span, TokenStream as TokenStream2};
5use quote::quote;
6use syn::ext::IdentExt;
7use syn::spanned::Spanned;
8use syn::{Data, DeriveInput, Error, Fields, Ident, LitStr, Result, Type, parse_macro_input};
9
10mod naming;
11
12#[proc_macro_derive(OrbitModel, attributes(orbit))]
14pub fn derive_orbit_model(input: TokenStream) -> TokenStream {
15 let input = parse_macro_input!(input as DeriveInput);
16 expand(&input)
17 .unwrap_or_else(Error::into_compile_error)
18 .into()
19}
20
21#[derive(Default)]
23struct Options {
24 table: Option<LitStr>,
25 primary_key: Option<LitStr>,
26}
27
28struct Column {
30 ident: Ident,
31 ty: Type,
32 name: String,
33 skip: bool,
34 fillable: bool,
35}
36
37fn expand(input: &DeriveInput) -> Result<TokenStream2> {
38 if !input.generics.params.is_empty() {
39 return Err(Error::new_spanned(
40 &input.generics,
41 "OrbitModel does not support generic structs",
42 ));
43 }
44 let Data::Struct(data) = &input.data else {
45 return Err(Error::new_spanned(
46 &input.ident,
47 "OrbitModel can only be derived for a struct",
48 ));
49 };
50 let Fields::Named(named) = &data.fields else {
51 return Err(Error::new_spanned(
52 &input.ident,
53 "OrbitModel needs a struct with named fields",
54 ));
55 };
56
57 let options = struct_options(input)?;
58 let columns = named.named.iter().map(column).collect::<Result<Vec<_>>>()?;
59
60 let table = match options.table {
62 Some(table) => {
63 if table.value().is_empty() {
64 return Err(Error::new_spanned(
65 &table,
66 "the table name must not be empty",
67 ));
68 }
69 table
70 }
71 None => LitStr::new(
72 &naming::table_name(&input.ident.unraw().to_string()),
73 input.ident.span(),
74 ),
75 };
76
77 let primary_key = match &options.primary_key {
78 Some(key) => {
79 let mapped = columns.iter().any(|c| !c.skip && c.name == key.value());
80 if !mapped {
81 return Err(Error::new_spanned(
82 key,
83 format!("no field maps to the primary key column `{}`", key.value()),
84 ));
85 }
86 key.value()
87 }
88 None => "id".to_owned(),
89 };
90
91 let name = &input.ident;
92 let fields = columns.iter().map(|c| {
93 let ident = &c.ident;
94 if c.skip {
95 quote!(#ident: ::core::default::Default::default())
96 } else {
97 let column = &c.name;
98 quote!(#ident: ::orbit_orm::__sqlx::Row::try_get(row, #column)?)
99 }
100 });
101 let fillable = columns.iter().filter(|c| c.fillable).map(|c| &c.name);
102 let decodes = columns.iter().filter(|c| !c.skip).map(|c| &c.ty);
105
106 Ok(quote! {
107 impl<'r, R> ::orbit_orm::__sqlx::FromRow<'r, R> for #name
108 where
109 R: ::orbit_orm::__sqlx::Row,
110 &'r ::core::primitive::str: ::orbit_orm::__sqlx::ColumnIndex<R>,
111 #(
112 #decodes: ::orbit_orm::__sqlx::Decode<'r, <R as ::orbit_orm::__sqlx::Row>::Database>
113 + ::orbit_orm::__sqlx::Type<<R as ::orbit_orm::__sqlx::Row>::Database>,
114 )*
115 {
116 fn from_row(row: &'r R) -> ::core::result::Result<Self, ::orbit_orm::__sqlx::Error> {
117 ::core::result::Result::Ok(Self { #(#fields),* })
118 }
119 }
120
121 impl ::orbit_orm::Model for #name {
122 const TABLE: &'static str = #table;
123 const PRIMARY_KEY: &'static str = #primary_key;
124 const FILLABLE: &'static [&'static str] = &[#(#fillable),*];
125 }
126 })
127}
128
129fn struct_options(input: &DeriveInput) -> Result<Options> {
131 let mut options = Options::default();
132 for attr in input.attrs.iter().filter(|a| a.path().is_ident("orbit")) {
133 attr.parse_nested_meta(|meta| {
134 if meta.path.is_ident("table") {
135 options.table = Some(meta.value()?.parse()?);
136 } else if meta.path.is_ident("primary_key") {
137 options.primary_key = Some(meta.value()?.parse()?);
138 } else {
139 return Err(meta.error("unknown key; a struct takes `table` and `primary_key`"));
140 }
141 Ok(())
142 })?;
143 }
144 Ok(options)
145}
146
147fn column(field: &syn::Field) -> Result<Column> {
149 let ident = field.ident.clone().expect("named fields have idents");
150 let mut name: Option<LitStr> = None;
151 let mut skip: Option<Span> = None;
152 let mut fillable: Option<Span> = None;
153
154 for attr in field.attrs.iter().filter(|a| a.path().is_ident("orbit")) {
155 attr.parse_nested_meta(|meta| {
156 if meta.path.is_ident("column") {
157 name = Some(meta.value()?.parse()?);
158 } else if meta.path.is_ident("skip") {
159 skip = Some(meta.path.span());
160 } else if meta.path.is_ident("fillable") {
161 fillable = Some(meta.path.span());
162 } else {
163 return Err(
164 meta.error("unknown key; a field takes `column`, `skip` and `fillable`")
165 );
166 }
167 Ok(())
168 })?;
169 }
170
171 if let Some(span) = skip {
172 if let Some(name) = &name {
173 return Err(Error::new_spanned(
174 name,
175 "`skip` cannot be combined with `column`",
176 ));
177 }
178 if fillable.is_some() {
179 return Err(Error::new(
180 span,
181 "`skip` cannot be combined with `fillable`",
182 ));
183 }
184 }
185 if let Some(name) = &name
186 && name.value().is_empty()
187 {
188 return Err(Error::new_spanned(
189 name,
190 "the column name must not be empty",
191 ));
192 }
193
194 let name = match &name {
195 Some(name) => name.value(),
196 None => ident.unraw().to_string(),
197 };
198 Ok(Column {
199 ident,
200 ty: field.ty.clone(),
201 name,
202 skip: skip.is_some(),
203 fillable: fillable.is_some(),
204 })
205}