salvo-oapi-macros 0.91.1

Macros for salvo-oapi
Documentation
use std::ops::Deref;

use syn::{Attribute, Expr, Lit, Meta};

const DOC_ATTRIBUTE_TYPE: &str = "doc";

/// CommentAttributes holds Vec of parsed doc comments
#[derive(Debug)]
pub(crate) struct CommentAttributes(pub(crate) Vec<String>);

impl CommentAttributes {
    /// Creates new [`CommentAttributes`] instance from [`Attribute`] slice filtering out all
    /// other attributes which are not `doc` comments
    pub(crate) fn from_attributes(attributes: &[Attribute]) -> Self {
        let mut docs = attributes
            .iter()
            .filter_map(|attr| {
                if !matches!(attr.path().get_ident(), Some(ident) if ident == DOC_ATTRIBUTE_TYPE) {
                    return None;
                }
                if let Meta::NameValue(name_value) = &attr.meta
                    && let Expr::Lit(ref doc_comment) = name_value.value
                    && let Lit::Str(ref doc) = doc_comment.lit
                {
                    let mut doc = doc.value();
                    doc.truncate(doc.trim_end().len());
                    return Some(doc);
                }
                None
            })
            .collect::<Vec<_>>();

        let min_indent = docs
            .iter()
            .filter(|s| !s.is_empty())
            .map(|s| s.len() - s.trim_start_matches(' ').len())
            .min()
            .unwrap_or(0);

        for line in &mut docs {
            if !line.is_empty() {
                line.drain(..min_indent);
            }
        }

        Self(docs)
    }

    /// Returns found `doc comments` as formatted `String` joining them all with `\n` _(new line)_.
    pub(crate) fn as_formatted_string(&self) -> String {
        self.join("\n")
    }
}

impl Deref for CommentAttributes {
    type Target = Vec<String>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

#[cfg(test)]
mod tests {
    use quote::quote;

    use super::*;

    fn parse_attrs(tokens: proc_macro2::TokenStream) -> Vec<Attribute> {
        let item: syn::ItemStruct = syn::parse2(tokens).unwrap();
        item.attrs
    }

    #[test]
    fn test_comment_attributes_empty() {
        let attrs = parse_attrs(quote! {
            struct Foo;
        });
        let comments = CommentAttributes::from_attributes(&attrs);
        assert!(comments.is_empty());
    }

    #[test]
    fn test_comment_attributes_single_doc() {
        let attrs = parse_attrs(quote! {
            /// This is a doc comment
            struct Foo;
        });
        let comments = CommentAttributes::from_attributes(&attrs);
        assert_eq!(comments.len(), 1);
        assert_eq!(comments[0], "This is a doc comment");
    }

    #[test]
    fn test_comment_attributes_multiple_docs() {
        let attrs = parse_attrs(quote! {
            /// First line
            /// Second line
            /// Third line
            struct Foo;
        });
        let comments = CommentAttributes::from_attributes(&attrs);
        assert_eq!(comments.len(), 3);
        assert_eq!(comments[0], "First line");
        assert_eq!(comments[1], "Second line");
        assert_eq!(comments[2], "Third line");
    }

    #[test]
    fn test_comment_attributes_trims_whitespace() {
        let attrs = parse_attrs(quote! {
            ///   Padded with spaces
            struct Foo;
        });
        let comments = CommentAttributes::from_attributes(&attrs);
        assert_eq!(comments.len(), 1);
        assert_eq!(comments[0], "Padded with spaces");
    }

    #[test]
    fn test_comment_attributes_filters_non_doc() {
        let attrs = parse_attrs(quote! {
            /// Doc comment
            #[derive(Debug)]
            #[allow(dead_code)]
            struct Foo;
        });
        let comments = CommentAttributes::from_attributes(&attrs);
        assert_eq!(comments.len(), 1);
        assert_eq!(comments[0], "Doc comment");
    }

    #[test]
    fn test_comment_attributes_as_formatted_string() {
        let attrs = parse_attrs(quote! {
            /// First line
            /// Second line
            struct Foo;
        });
        let comments = CommentAttributes::from_attributes(&attrs);
        let formatted = comments.as_formatted_string();
        assert_eq!(formatted, "First line\nSecond line");
    }

    #[test]
    fn test_comment_attributes_deref() {
        let attrs = parse_attrs(quote! {
            /// Test
            struct Foo;
        });
        let comments = CommentAttributes::from_attributes(&attrs);
        // Test that we can use Vec methods through Deref
        let first: Option<&String> = comments.first();
        assert_eq!(first.map(|s| s.as_str()), Some("Test"));
    }

    #[test]
    fn test_comment_attributes_debug() {
        let attrs = parse_attrs(quote! {
            /// Test
            struct Foo;
        });
        let comments = CommentAttributes::from_attributes(&attrs);
        let debug_str = format!("{comments:?}");
        assert!(debug_str.contains("CommentAttributes"));
    }
}