libskyfall_macros 0.1.0

Quantum-Safe P2P Communication System
Documentation
#![allow(dead_code)]
use std::collections::HashMap;

use darling::FromMeta;
use proc_macro2::TokenStream;
use syn::{ Attribute, FnArg, Ident, ImplItemFn, Pat, PatIdent, PatType };

#[derive(Clone, Debug, FromMeta)]
pub struct RouteArgs {
    pub path: String,

    #[darling(default)]
    pub selector: Option<String>,

    #[darling(default)]
    pub data: Option<Ident>,

    #[darling(default)]
    pub channel: Option<Ident>,

    #[darling(default)]
    pub request: Option<Ident>,

    #[darling(default)]
    pub about: Option<String>
}

pub fn parse_route_attr_validator(args: TokenStream, item: TokenStream) -> manyhow::Result {
    let args = crate::parse_meta::<RouteArgs>(args)?;
    let method = syn::parse2::<ImplItemFn>(item.clone())?;
    if method.sig.asyncness.is_none() {
        return Err(crate::error("Route handlers should be asynchronous."));
    }

    match method.sig.inputs.first() {
        Some(&FnArg::Typed(_)) => {
            return Err(crate::error("Route handlers should at least reference &self/&mut self"));
        }
        None => {
            return Err(crate::error("Route handlers should at least reference &self/&mut self"));
        }
        _ => (),
    }

    if let Some(sel) = args.selector.clone() {
        crate::as_valid(sel)?;
    }

    if args.path.starts_with("/") && !args.path.ends_with("/") {
        for segment in args.path.trim_matches('/').split("/") {
            if segment.starts_with(":") {
                if let Err(reason) = syn::parse_str::<Ident>(segment.trim_start_matches(':')) {
                    return Err(
                        crate::error(
                            format!("Capture segment must be a valid identifier: {reason:?}")
                        )
                    );
                }
            } else if segment.len() == 0 {
                return Err(crate::error(format!("Empty path parts are not allowed.")));
            }
        }
    } else {
        return Err(crate::error("Paths must start with a leading / and not contain a trailing /"));
    }

    Ok(item)
}

#[derive(Clone, Debug)]
pub struct RouteInfo {
    pub handler_id: String,
    pub prefix: String,
    pub selector: String,
    pub method_name: String,
    pub fields: HashMap<String, usize>,
    pub captures: Vec<String>,
    pub data_field: Option<String>,
    pub channel_field: Option<String>,
    pub request_field: Option<String>,
    pub about: Option<String>,
    pub path: String,
    pub full_path: String
}

pub fn parse_route(
    handler_id: String,
    prefix: String,
    attr: Attribute,
    method: ImplItemFn
) -> manyhow::Result<RouteInfo> {
    let args = RouteArgs::from_meta(&attr.meta)?;
    let selector = crate::as_valid(args.selector.unwrap_or(method.sig.ident.to_string()))?;

    let all_fields: HashMap<String, usize> = method.sig.inputs
        .iter()
        .enumerate()
        .filter_map(|(index, f)| {
            if let FnArg::Typed(PatType { pat, .. }) = f.clone() {
                match *pat {
                    Pat::Ident(PatIdent { ident, .. }) => Some((ident.to_string(), index - 1)),
                    _ => panic!("Unprocessable method argument: {:?}", f.clone()),
                }
            } else {
                None
            }
        })
        .collect();

    let mut captures: Vec<String> = Vec::new();
    for segment in args.path.clone().trim_matches('/').split("/") {
        if all_fields.contains_key(&segment.trim_start_matches(':').to_string()) {
            captures.push(segment.trim_start_matches(':').to_string());
        }
    }

    Ok(RouteInfo {
        handler_id,
        prefix: prefix.clone(),
        fields: all_fields,
        captures,
        data_field: args.data.clone().and_then(|f| Some(f.to_string())),
        channel_field: args.channel.clone().and_then(|f| Some(f.to_string())),
        request_field: args.request.clone().and_then(|f| Some(f.to_string())),
        selector,
        method_name: method.sig.ident.to_string(),
        about: args.about.clone(),
        path: args.path.clone(),
        full_path: format!("{prefix}/{}", args.path.trim_matches('/'))
    })
}