crud-api-derive 0.1.7

CLI generator for your API. derive Api implementation
Documentation
use crate::gen_clap_declarations::strip_var;
use crud_api_endpoint::{endpoints, Emap, Endpoint};
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;

#[rustfmt::skip::macros(quote)]
fn match_endpoint(
  ep: &Endpoint,
  arg_ident: &Ident,
  ids: &[(Ident, TokenStream)],
) -> proc_macro2::TokenStream {
  let (paylay_decl, payload) = if let Some(payload_struct) = &ep.payload_struct {
    let payload_type = Ident::new(payload_struct, Span::call_site());
    let error_context = format!("Can't read payload for '{payload_struct}'");
    (
      quote! {
          let payload = <#payload_type>::from_clap_matches(#arg_ident)
              .context(#error_context)?;
          log::trace!("Payload: {:#?}",payload);
      },
      quote! {Some(payload)},
    )
  } else {
    (quote! {}, quote! {None::<()>})
  };

  let (query_dec, query_args) = if let Some(query_struct) = &ep.query_struct {
    let query_type = Ident::new(query_struct, Span::call_site());
    let error_context = format!("Can't read query for '{query_struct}'");
    (
      quote! {
          let query = <#query_type>::from_clap_matches(#arg_ident)
              .context(#error_context)?;
          log::trace!("Query: {:?}",query);
      },
      quote! {Some(query)},
    )
  } else {
    (quote! {}, quote! {None::<()>})
  };

  let uri = &ep.route;
  let urif = format!("{{}}{uri}");
  let ids: TokenStream = ids
    .iter()
    .map(|(ident, _dec)| quote!(, #ident=#ident))
    .collect();
  let result = Ident::new(&ep.result_struct, Span::call_site());
  let result_type = if ep.result_multiple {
    quote! {Vec<#result>}
  } else {
    quote! {#result}
  };

  let output_format = if ep.cli_no_output {
    quote!(None)
  } else {
    quote!(crud_api::clap_match_output_format(#arg_ident))
  };

  let result_output = if ep.result_multiple {
    quote! {#result :: output_multiple(&result, #output_format )?;}
  } else {
    quote! {result.output(#output_format)?;}
  };
  let method = Ident::new(&ep.method, Span::call_site());
  let status = Ident::new(&ep.result_ok_status, Span::call_site());
  let ko_status: TokenStream = ep
    .result_ko_status
    .iter()
    .map(|s| {
      let status = Ident::new(&s.status, Span::call_site());
      let msg = &s.message;
      quote!{
	    h.insert(hyper::StatusCode::#status, #msg.into());
	}
    })
    .collect();
  let ko_status_map = if ko_status.is_empty() {
    quote!(&std::collections::HashMap::new())
  } else {
    quote!{
	&{
	    let mut h = std::collections::HashMap::new();
	    #ko_status
	    h
	}
    }
  };

  let extra_action = if let Some(extra_action) = &ep.extra_action {
    let action_function = Ident::new(extra_action, Span::call_site());
    quote!(#action_function(&result, &settings)?;)
  } else {
    quote!()
  };

  let extra_headers = if ep.extra_header.is_empty() {
    quote!()
  } else {
    let eh: Vec<proc_macro2::TokenStream> = ep
      .extra_header
      .iter()
      .map(|h| {
        let key = &h.key;
        let value = &h.value;
        quote!(extra_headers.push(crud_api::http::Header{key:#key, value:#value});)
      })
      .collect();

    quote!{
	  let mut extra_headers = extra_headers.clone();
	  #(#eh)*
      }
  };

  let auth = if ep.no_auth {
    quote!(None)
  } else {
    quote!(Some(&auth))
  };

  let transform_type = if let Some(transform_from) = &ep.transform_from {
    let ty: syn::Type = syn::parse_str(transform_from).expect("Can't parse type in transform_from");
    quote!(Some(std::marker::PhantomData::<#ty>))
  } else {
    quote!(None::<std::marker::PhantomData<crud_api::DummyTryFrom>>)
  };

  let query_and_print = if ep.result_is_stream {
    quote!(crud_api::http::HTTPApi::new(format!(#urif,base_url #ids),
				     hyper::Method::#method,
				     hyper::StatusCode::#status,
				     #ko_status_map,
				     #auth,
				     &extra_headers)
	   .stream(#payload,
		   #query_args,
		   #arg_ident.get_one::<String>("output_file").cloned()).await?;
    )
  } else {
    quote!(
        let result:#result_type =
	    crud_api::http::HTTPApi::new(format!(#urif,base_url #ids),
				      hyper::Method::#method,
				      hyper::StatusCode::#status,
				      #ko_status_map,
				      #auth,
				      &extra_headers)
	    .query(#payload, #query_args, #transform_type).await?;
	#extra_action
        #result_output
    )
  };

  quote! {
      #query_dec
      #paylay_decl
      #extra_headers
      #query_and_print
    }
}

#[rustfmt::skip::macros(quote)]
fn argmatches_rec(
  endpoints_map: &Emap,
  last_match: Option<Ident>,
  ids: Vec<(Ident, TokenStream)>,
) -> Vec<(proc_macro2::TokenStream, Option<proc_macro2::TokenStream>)> {
  endpoints_map
    .iter()
    .map(|(segment, node)| {
      if let Some(var) = strip_var(segment) {
        let var_ident = Ident::new(var, Span::call_site());
        let new_ids = if let Some(arg_ident) = &last_match {
          let vardec = quote!{
	      if !#arg_ident.contains_id(#var) { miette::bail!("<{}> is required",#var)};
	      let #var_ident = #arg_ident.get_one::<String>(#var).cloned().unwrap();
	  };
          let mut ids_mut = ids.clone();
          ids_mut.push((var_ident.to_owned(), vardec));
          ids_mut
        } else {
          ids.clone()
        };
        let argmatches = argmatches_rec(&node.route, last_match.to_owned(), new_ids.clone());
        let argmatches: Vec<proc_macro2::TokenStream> =
          argmatches.iter().map(|(f, _)| f.to_owned()).collect();
        let var_do_query = if let Some(arg_ident) = &last_match {
          let endpoints = &node.endpoint;
          if endpoints.is_empty() {
            None
          } else {
            let do_query_var: TokenStream = endpoints
              .iter()
              .map(|ep| match_endpoint(ep, arg_ident, &new_ids))
              .collect();
            Some(quote!{
		  if #arg_ident.contains_id(#var) {
		      let #var_ident = #arg_ident.get_one::<String>(#var).cloned().unwrap();
		      #do_query_var
		  }
	      })
          }
        } else {
          None
        };
        (quote!(#(#argmatches)*), var_do_query)
      } else {
        let arg_ident = Ident::new(&format!("{segment}_arg"), Span::call_site());
        let argmatches = argmatches_rec(&node.route, Some(arg_ident.to_owned()), ids.to_owned());
        let do_query_when_id_matched: Vec<proc_macro2::TokenStream> = argmatches
          .iter()
          .filter(|(_, v)| v.is_some())
          .map(|(_, v)| {
            if let Some(v) = v {
              quote!(#v else)
            } else {
              quote!()
            }
          })
          .collect();
        let subcommands_matches: Vec<proc_macro2::TokenStream> =
          argmatches.iter().map(|(f, _)| f.to_owned()).collect();
        let endpoints = &node.endpoint;
        let do_query_when_no_ids: TokenStream = endpoints
          .iter()
          .map(|ep| match_endpoint(ep, &arg_ident, &ids))
          .collect();

        let ids_dec: TokenStream = ids.iter().map(|(_, dec)| dec.to_owned()).collect();

        let submatches = if subcommands_matches.is_empty() {
          quote!(
		#ids_dec
		#do_query_when_no_ids
	    )
        } else {
          // else: we have subcommands.
          quote!{
	      match #arg_ident.subcommand() {
		  #(#subcommands_matches)*
		  Some((_,_))=> commands.print_help().into_diagnostic()?,
		  None => {
		      #ids_dec
		      #(#do_query_when_id_matched)*
		      { #do_query_when_no_ids }
		  },
	      }
	  }
        };
        (
          quote!{
	      Some((#segment,#arg_ident)) => {
		  #submatches
	      },
	  },
          None,
        )
      }
    })
    .collect::<Vec<(proc_macro2::TokenStream, Option<proc_macro2::TokenStream>)>>()
}

pub(crate) fn argmatches() -> proc_macro2::TokenStream {
  let eps = endpoints();
  let matches = argmatches_rec(&eps, None, vec![]);
  let matches: proc_macro2::TokenStream = matches.iter().map(|(f, _)| f.to_owned()).collect();
  //  println!("{}", matches);

  quote! {#matches}
}