cido-macros 0.2.0

Macros for generating code that enables easier interfacing with cido
Documentation
pub use crate::parse::EventsEnum;
use crate::parse::deps::*;
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};

type Result = syn::Result<TokenStream>;

pub fn event_handler_codegen(events_enum: &EventsEnum) -> Result {
  let event_handler = gen_event_handler(events_enum)?;

  crate::parse::embed_generated_code(
    events_enum.embed_generated_code,
    &events_enum.name,
    event_handler,
    "event_handler",
  )
}

fn gen_event_handler(events_enum: &EventsEnum) -> Result {
  let cidomap = &events_enum.cidomap;
  let enum_name = &events_enum.name;

  let event_parse_trigger_body = gen_parse_trigger_body(events_enum)?;
  let event_handler_body = gen_event_handler_fn_body(events_enum);
  let event_preprocessor_body = gen_event_preprocessor_fn_body(events_enum)?;
  let event_source_body = gen_event_source_body(events_enum)?;
  let event_error_ty = quote! { <#cidomap as ::cido::__internal::Cidomap>::Error };

  let default_handler: syn::Type = syn::parse_quote!(CidoParsingErrorHandler);
  let (handler_type, handler_body) = match &events_enum.error_handler {
    crate::parse::ErrorFlow::Break => (
      default_handler,
      Some(quote! {
        async move { ::core::ops::ControlFlow::Break(error) }.boxed()
      }),
    ),
    crate::parse::ErrorFlow::Continue => (
      default_handler,
      Some(quote! {
        async move { ::core::ops::ControlFlow::Continue(error) }.boxed()
      }),
    ),
    crate::parse::ErrorFlow::HandlerFn(func) => (
      default_handler,
      Some(quote! {
        async move { #func(ctx, error).await }.boxed()
      }),
    ),
    crate::parse::ErrorFlow::HandlerTrait(name) => (name.clone(), None),
  };

  let handler_impl = if let Some(handler_body) = handler_body {
    quote! {
      #[derive(Default)]
      pub struct CidoParsingErrorHandler;

      impl ::cido::__internal::ParsingContext<#enum_name> for CidoParsingErrorHandler {
        #[allow(unused)]
        fn handle_error<'a>(
          &'a mut self,
          ctx: impl ::cido::__internal::ParseContext<
            'a,
            <#enum_name as ::cido::__internal::EventHandler>::Cidomap
          >,
          error: #event_error_ty,
        ) -> ::cido::__internal::futures::future::BoxFuture<'a, ::core::ops::ControlFlow<
          #event_error_ty,
          #event_error_ty
        >>
        {
          use ::cido::__internal::futures::future::FutureExt;
          #handler_body
        }
      }
    }
  } else {
    quote! {}
  };

  let (cacheless_name, enum_def) = gen_event_enum(events_enum)?;

  let event_handler_impl = quote! {
    #enum_def

    #handler_impl

    impl ::cido::__internal::EventHandler for #enum_name {
      type Cidomap = #cidomap;
      type Cacheless = #cacheless_name;
      type ContextHandler = #handler_type;

      fn parse_trigger(
        handler: &mut Self::ContextHandler,
        trigger: &<<Self::Cidomap as ::cido::__internal::Cidomap>::Network as ::cido::__internal::Network>::Trigger,
        filter: &<<Self::Cidomap as ::cido::__internal::Cidomap>::Network as ::cido::__internal::Network>::TriggerFilter,
      ) -> ::core::result::Result<Self::Cacheless, #event_error_ty> {
        #event_parse_trigger_body
      }

      async fn event_handler<'a>(
        self,
        ctx: ::cido::__internal::Context<'a, Self::Cidomap>,
        meta: ::cido::__internal::MetaEvent<Self::Cidomap>,
      ) ->
      ::core::result::Result<(), #event_error_ty>
      {
        #event_handler_body
      }

      async fn event_source<'a>(
        &self,
        ctx: ::cido::__internal::GeneratorContext<'a, Self::Cidomap>,
        meta: &'a ::cido::__internal::MetaEvent<Self::Cidomap>,
      ) ->
      ::core::result::Result<(), #event_error_ty>
      {
        #event_source_body
      }

      fn event_preprocessor<'a>(
        ctx: &'a ::cido::__internal::PreprocessingContext<Self::Cidomap>,
        meta: ::cido::__internal::MetaEvent<Self::Cidomap>,
        event: Self::Cacheless,
      ) -> ::cido::__internal::either::Either<
        ::cido::__internal::futures::future::BoxFuture<'a, ::core::result::Result<(cido::__internal::MetaEvent<Self::Cidomap>, Self), #event_error_ty>>,
        ::core::result::Result<(cido::__internal::MetaEvent<Self::Cidomap>, Self), #event_error_ty>
      >
      {
        #event_preprocessor_body
      }
    }
  };
  Ok(event_handler_impl)
}

fn gen_event_handler_fn_body(events_enum: &EventsEnum) -> TokenStream {
  let event_handlers = events_enum.events.iter().map(|def| {
    let callable = &def.config.func;
    let variant = &def.name;
    let variant_name = variant.to_string();
    quote! {
      Self::#variant(ev, cache) => {
        Handler::<_, _, ::std::result::Result<(), <Self::Cidomap as ::cido::prelude::Cidomap>::Error>>::call(#callable, (ctx, meta, Event(ev), Cache(cache)))
        .meter_ms(#variant_name, 100)
        .await
        .map_err(|e| {
          ::cido::__internal::tracing::error!("Error while running {}: {}", #variant_name, e);
          e
        })?;
      }
    }
  });

  quote! {
    use ::cido::__internal::tracing::Instrument;
    use ::cido::__internal::FutureMeter;
    use ::cido::__internal::{Cache, Event, Handler};
    use ::core::convert::TryInto;
    let span = ::cido::__internal::tracing::debug_span!("event_matcher").or_current();
    async {
      match self {
        #(#event_handlers),*
      }
      ::core::result::Result::Ok(())
    }
    .instrument(span)
    .await
  }
}

fn gen_event_preprocessor_fn_body(events_enum: &EventsEnum) -> syn::Result<TokenStream> {
  let variants = events_enum.events.iter().map(|ev_def| {
    let ev_variant = &ev_def.name;
    let ev_type = &ev_def.ty;
    match ev_def.config.preprocessor.as_ref() {
      Some(p) => {
        let cache_type = &p.cache;
        let preprocessor_fn = &p.func;
        quote! {
          Self::Cacheless::#ev_variant(ev) => {
            // Guarantee that each future will be created in order, even though they'll be polled together
            // this allows us to make some optimizations as needed in the preprocessor implementations.
            // This requires us to call the `call` function outside of an async block so that we know that
            // any future initialization is done.

            // make the event unpin so that we can move the box around while leaving a reference that the
            // future can use.
            let boxed_meta_event = ::std::boxed::Box::new((meta, ev));
            let ev_ref = unsafe {&* (&boxed_meta_event.1 as *const #ev_type)};
            let meta_ref = unsafe {&* (&boxed_meta_event.0 as *const ::cido::prelude::MetaEvent<Self::Cidomap>)};
            let f = Handler::<_, _, Result<#cache_type, <Self::Cidomap as ::cido::prelude::Cidomap>::Error>>::call(
              #preprocessor_fn, (ctx, meta_ref, Event(ev_ref), ())
            );
            ::cido::__internal::either::Either::Left(::std::boxed::Box::pin(async move {
              let cache = f.await?;
              // f is complete so we can now move the boxed event
              let (meta, ev) = *boxed_meta_event;
              ::core::result::Result::Ok((meta, Self::#ev_variant(ev, cache)))
            }))
          }
        }
      }
      None => {
        quote! {
          Self::Cacheless::#ev_variant(ev) => {
            ::cido::__internal::either::Either::Right(::core::result::Result::Ok((meta, Self::#ev_variant(ev, ()))))
          }
        }
      }
    }
  });

  let fn_body = quote! {
    use ::core::convert::Into;
    use ::cido::__internal::{Cache, Event, Handler};
    match event {
      #(#variants)*
    }
  };
  Ok(fn_body)
}

fn gen_event_source_body(events_enum: &EventsEnum) -> Result {
  let generators = events_enum
    .events
    .iter()
    .filter_map(|e| e.config.generator.as_ref().map(|g| (e, g)));

  let mut variants = vec![];
  for (ev_def, source_gen) in generators {
    let ev_variant = &ev_def.name;
    let match_stmt = quote! {
      Self::#ev_variant(ev, cache) => {
        Handler::<_, _, Result<(), <Self::Cidomap as ::cido::prelude::Cidomap>::Error>>::call(#source_gen, (ctx, meta, Event(ev), Cache(cache))).await
      },
    };
    variants.push(match_stmt);
  }

  Ok(quote! {
    use ::core::convert::TryInto;
    use ::cido::__internal::futures::FutureExt;
    use ::cido::__internal::{Cache, Event, Handler};
    ::cido::__internal::tracing::trace!("Running event spawner for {}", meta.event_order());
    match self {
      #(#variants)*
      _ => {
        ::core::result::Result::Ok(())
      }
    }
  })
}

fn gen_parse_trigger_body(events_enum: &EventsEnum) -> Result {
  let try_from_triggers = events_enum.events.iter().map(|e| {
    let ev_variant = &e.name;
    let ty = &e.ty;
    quote! {
      if let ::core::option::Option::Some(v) = #ty::try_from_trigger(trigger, filter) {
        return v.map(Self::Cacheless::#ev_variant)
          .map_err(|err| ::std::boxed::Box::new(err) as ::std::boxed::Box<dyn ::std::error::Error + ::core::marker::Send + ::core::marker::Sync>)
          .map_err(::core::convert::Into::into);
      }
    }
  });
  Ok(quote! {
    use ::cido::__internal::TryFromTrigger;
    use ::core::convert::Into;
    #(#try_from_triggers)*
    let error: ::std::boxed::Box<dyn ::std::error::Error + ::core::marker::Send + ::core::marker::Sync> = ::std::format!("Unable to find trigger parser for {:?}", trigger).into();
    ::core::result::Result::Err(error.into())
  })
}

pub fn gen_event_enum(events_enum: &EventsEnum) -> syn::Result<(Ident, TokenStream)> {
  let event_enum_name = &events_enum.name;
  let event_enum_name_cacheless = format_ident!("{event_enum_name}Cacheless");

  let variants_cacheless = events_enum.events.iter().map(|e| {
    let name = &e.name;
    let ty = &e.ty;
    quote! {#name(#ty),}
  });

  let variants = events_enum.events.iter().map(|e| {
    let name = &e.name;
    let ty = &e.ty;
    let cache = e
      .config
      .preprocessor
      .as_ref()
      .map(|p| {
        let cache = &p.cache;
        quote! {#cache}
      })
      .unwrap_or_else(|| quote! {()});
    quote! {#name(#ty, #cache),}
  });

  let must_use_cacheless = format!("{event_enum_name_cacheless} must be used");
  let must_use = format!("{event_enum_name} must be used");

  let defs = quote! {
    #[must_use = #must_use_cacheless]
    #[derive(Debug)]
    pub enum #event_enum_name_cacheless {
      #(#variants_cacheless)*
    }

    #[derive(Debug)]
    #[must_use = #must_use]
    pub enum #event_enum_name {
      #(#variants)*
    }
  };

  Ok((event_enum_name_cacheless, defs))
}