arcly-stream-macros 0.4.0

Procedural macros for arcly-stream: #[protocol] and #[derive(MediaSink)] ergonomics.
Documentation
//! Procedural macros for `arcly-stream`.
//!
//! These are ergonomics, not load-bearing — everything they generate can be
//! written by hand. They are gated behind the `macros` feature of the parent
//! crate and expand against the stable `arcly_stream::__macro_support` surface.

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields, Index, LitStr};

/// Derive [`MediaSink`] for a wrapper struct by **delegating** to one inner
/// field that already implements it. The delegate is the field marked
/// `#[sink]`, or the first field if none is marked.
///
/// ```ignore
/// use arcly_stream::prelude::*;
///
/// #[derive(MediaSinkDerive)]
/// struct LoggingSink {
///     #[sink]
///     inner: NullSink,
///     label: String,
/// }
/// ```
#[proc_macro_derive(MediaSink, attributes(sink))]
pub fn derive_media_sink(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    let (impl_g, ty_g, where_c) = input.generics.split_for_impl();

    let fields = match &input.data {
        Data::Struct(s) => &s.fields,
        _ => {
            return syn::Error::new_spanned(name, "MediaSink can only be derived for structs")
                .to_compile_error()
                .into()
        }
    };

    let accessor = match pick_delegate(fields) {
        Ok(tokens) => tokens,
        Err(e) => return e.to_compile_error().into(),
    };

    quote! {
        #[::arcly_stream::__macro_support::async_trait]
        impl #impl_g ::arcly_stream::traits::MediaSink for #name #ty_g #where_c {
            async fn send_frame(
                &mut self,
                frame: ::arcly_stream::MediaFrame,
            ) -> ::arcly_stream::Result<()> {
                ::arcly_stream::traits::MediaSink::send_frame(&mut self.#accessor, frame).await
            }
            async fn flush(&mut self) -> ::arcly_stream::Result<()> {
                ::arcly_stream::traits::MediaSink::flush(&mut self.#accessor).await
            }
        }
    }
    .into()
}

/// Picks the delegate field: the one tagged `#[sink]`, else the first field.
fn pick_delegate(fields: &Fields) -> syn::Result<proc_macro2::TokenStream> {
    let marked = |attrs: &[syn::Attribute]| attrs.iter().any(|a| a.path().is_ident("sink"));
    match fields {
        Fields::Named(named) => {
            let chosen = named
                .named
                .iter()
                .find(|f| marked(&f.attrs))
                .or_else(|| named.named.first());
            match chosen {
                Some(f) => {
                    let ident = f.ident.as_ref().expect("named field has ident");
                    Ok(quote!(#ident))
                }
                None => Err(syn::Error::new_spanned(
                    &named.named,
                    "MediaSink derive needs at least one field to delegate to",
                )),
            }
        }
        Fields::Unnamed(unnamed) => {
            let idx = unnamed
                .unnamed
                .iter()
                .position(|f| marked(&f.attrs))
                .unwrap_or(0);
            if unnamed.unnamed.is_empty() {
                return Err(syn::Error::new_spanned(
                    &unnamed.unnamed,
                    "MediaSink derive needs at least one field to delegate to",
                ));
            }
            let index = Index::from(idx);
            Ok(quote!(#index))
        }
        Fields::Unit => Err(syn::Error::new_spanned(
            fields,
            "MediaSink cannot be derived for a unit struct (nothing to delegate to)",
        )),
    }
}

/// Attach a protocol name to a handler type. Generates an associated
/// `PROTOCOL_NAME` constant the `ProtocolHandler::name` impl can return.
///
/// ```ignore
/// #[protocol("rtmp")]
/// struct RtmpHandler { addr: std::net::SocketAddr }
///
/// #[async_trait]
/// impl ProtocolHandler for RtmpHandler {
///     fn name(&self) -> &'static str { Self::PROTOCOL_NAME }
///     // ...
/// }
/// ```
#[proc_macro_attribute]
pub fn protocol(attr: TokenStream, item: TokenStream) -> TokenStream {
    let lit = parse_macro_input!(attr as LitStr);
    let input = parse_macro_input!(item as DeriveInput);
    let name = &input.ident;
    let (impl_g, ty_g, where_c) = input.generics.split_for_impl();
    let value = lit.value();

    quote! {
        #input

        impl #impl_g #name #ty_g #where_c {
            /// The protocol name supplied via `#[protocol("…")]`.
            pub const PROTOCOL_NAME: &'static str = #value;
        }
    }
    .into()
}