crud_api_endpoint/
api.rs

1use crate::Endpoint;
2use darling::{
3  ast::{Data, Fields},
4  FromDeriveInput, FromField, FromMeta, FromVariant,
5};
6use proc_macro2::TokenStream;
7use quote::quote;
8use syn::{GenericArgument, Ident, PathArguments, Type};
9
10#[derive(Debug, FromField, Clone)]
11#[darling(attributes(api))]
12pub struct ApiField {
13  pub ident: Option<Ident>,
14  pub ty: Type,
15  /// the field won't appears when display as the table
16  #[darling(default)]
17  pub table_skip: bool,
18  /// Format of the field
19  pub table_format: Option<FieldFormat>,
20}
21
22#[derive(Debug, FromMeta, Clone)]
23pub enum FieldFormat {
24  /// Format a string as datetime
25  Date {
26    /// Format of the date.
27    ///
28    /// The format is specified here:
29    /// <https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html#specifiers>
30    ///
31    /// # Example
32    ///
33    /// ```sh
34    /// #[api(format(date(format = "%F %T")))]
35    /// ```
36    format: String,
37  },
38}
39
40#[derive(Debug, FromVariant)]
41#[darling(attributes(api))]
42#[allow(dead_code)]
43pub struct ApiVariant {
44  ident: Ident,
45  //  discriminant: Option<syn::Expr>,
46  fields: darling::ast::Fields<Type>,
47  //  attrs: Vec<syn::Attribute>,
48}
49
50#[derive(Debug, FromDeriveInput)]
51#[darling(attributes(api, suggest), forward_attrs(derive))]
52pub struct Api {
53  pub ident: Ident,
54  pub data: Data<ApiVariant, ApiField>,
55  pub attrs: Vec<syn::Attribute>,
56
57  #[darling(default)]
58  #[darling(multiple)]
59  pub endpoint: Vec<Endpoint>,
60
61  /// returns a stream of bytes for this struct
62  #[darling(rename = "stream")]
63  #[darling(default)]
64  pub result_is_stream: bool,
65}
66
67#[rustfmt::skip::macros(quote)]
68pub fn table_impl<T: Into<ApiField> + Clone>(
69  struct_ident: &Ident,
70  data: &Data<ApiVariant, T>,
71  is_pretty: bool,
72) -> TokenStream {
73  let (headers, table_convertions) = match data {
74    Data::Enum(_) => {
75      todo!("Générer les entetes et les données pour chaque variant");
76      //      (vec![], quote!())
77    }
78    Data::Struct(Fields { fields, .. }) => {
79      let headers: Vec<proc_macro2::TokenStream> = fields
80        .iter()
81        .filter(|field| {
82          let field: ApiField = (*field).clone().into();
83          !field.table_skip
84        })
85        .map(|field| {
86          let field: ApiField = (*field).clone().into();
87          let f = field
88            .ident
89            .as_ref()
90            .expect("Field without ident")
91            .to_string();
92          quote! {#f.to_string()}
93        })
94        .collect();
95
96      let table_convertions: proc_macro2::TokenStream = fields
97        .iter()
98        .filter(|field| {
99          let field: ApiField = (*field).clone().into();
100          !field.table_skip
101        })
102        .map(|field| {
103          let field: ApiField = (*field).clone().into();
104          let fname = field.ident.as_ref().expect("Field without ident");
105          // I'm hardcoding std::Display
106          let unformated_value = if is_option_vec(&field.ty) {
107            // WARNING: Not all Vec<T> have the join method !
108            quote!{self.#fname .as_ref().unwrap() .join(", ").replace('\n', "\\n")}
109          } else if is_option(&field.ty) {
110            quote! {self.#fname .clone().unwrap_or_default().to_string().replace('\n', "\\n")}
111          } else if is_vec(&field.ty) {
112            quote!{self.#fname .join(", ").replace('\n', "\\n")}
113          } else {
114            quote! {self.#fname .to_string().replace('\n', "\\n")}
115          };
116
117          let formated_value = if let Some(FieldFormat::Date { format }) = field.table_format {
118            quote!(#unformated_value.parse::<chrono::DateTime<chrono::Utc>>()
119		    .into_diagnostic().wrap_err("Can't parse Date")?
120		    .format(#format).to_string()
121	    )
122          } else {
123            unformated_value
124          };
125
126          quote!(#formated_value ,)
127        })
128        .collect();
129      (headers, table_convertions)
130    }
131  };
132
133  let to_output = if is_pretty {
134    quote!{
135	  fn to_output(&self) -> miette::Result<String>
136	  where Self:crud_pretty_struct::PrettyPrint
137	{
138	    use is_terminal::IsTerminal;
139	    self.pretty(std::io::stdout().is_terminal(), None, None)
140	  }
141      }
142  } else {
143    quote!{
144	fn to_output(&self) -> miette::Result<String>
145	where Self: Serialize
146	{
147	    use miette::IntoDiagnostic;
148	    serde_yaml::to_string(self).into_diagnostic()
149	}
150      }
151  };
152
153  quote! {impl crud_api::Api for #struct_ident {
154      fn to_table_header(&self) -> Vec<String> {
155	  vec![#(#headers),*]
156      }
157      fn to_table(&self) -> miette::Result<Vec<String>> {
158	  Ok(vec![#table_convertions])
159      }
160      #to_output
161  }}
162}
163
164fn is_option(ty: &Type) -> bool {
165  if let Type::Path(s) = ty {
166    if let Some(x) = s.path.segments.first() {
167      return x.ident.eq("Option");
168    }
169  }
170  false
171}
172fn is_vec(ty: &Type) -> bool {
173  if let Type::Path(s) = ty {
174    if let Some(x) = s.path.segments.first() {
175      return x.ident.eq("Vec");
176    }
177  }
178  false
179}
180
181fn is_option_vec(ty: &Type) -> bool {
182  // It a copy of strip_type without the recursivity
183  fn strip_type_no_rec(ty: &Type) -> &Type {
184    if is_option(ty) || is_vec(ty) {
185      if let Type::Path(s) = ty {
186        if let Some(segment) = s.path.segments.first() {
187          if let PathArguments::AngleBracketed(first_arg) = &segment.arguments {
188            if let GenericArgument::Type(result_type) = first_arg.args.first().unwrap() {
189              return result_type;
190            }
191          }
192        }
193      }
194    }
195    ty
196  }
197
198  if is_option(ty) {
199    is_vec(strip_type_no_rec(ty))
200  } else {
201    false
202  }
203}