Search code examples
rustrust-axum

How can I avoid duplication when I implement a trait for few similar types in Rust


I need to implement the axum::IntoResponse trait for serde structures because they come from another crate:

struct BucketInfoListAxum(BucketList);

impl IntoResponse for BucketInfoListAxum {
    fn into_response(self) -> Response {
        let mut headers = HeaderMap::new();
        headers.typed_insert(headers::ContentType::json());
        (
            StatusCode::OK,
            headers,
            serde_json::to_string(&self.0).unwrap(),
        )
            .into_response()
    }
}

struct ServerInfoAxum(ServerInfo);

impl IntoResponse for ServerInfoAxum {
    fn into_response(self) -> Response {
        let mut headers = HeaderMap::new();
        headers.typed_insert(headers::ContentType::json());
        (
            StatusCode::OK,
            headers,
            serde_json::to_string(&self.0).unwrap(),
        )
            .into_response()
    }
}

All the implementations are exactly the same, and I'm wondering if there is a more elegant way in Rust to avoid the duplication rather than extracting it into a function.


Solution

  • As was suggested, I've solved the problem with macros. I've created a separated crate with the following code in src/lib.rs:

    use proc_macro::TokenStream;
    use quote::quote;
    use syn;
    
    #[proc_macro_derive(IntoResponse)]
    pub fn into_response_derive(input: TokenStream) -> TokenStream {
        // Construct a representation of Rust code as a syntax tree
        // that we can manipulate
        let ast = syn::parse(input).unwrap();
    
        // Build the trait implementation
        impl_into_response(&ast)
    }
    
    fn impl_into_response(ast: &syn::DeriveInput) -> TokenStream {
        let name = &ast.ident;
        let gen = quote! {
            impl IntoResponse for #name {
                fn into_response(self) -> Response {
                    let mut headers = HeaderMap::new();
                    headers.typed_insert(headers::ContentType::json());
                    (
                        StatusCode::OK,
                        headers,
                        serde_json::to_string(&self.0).unwrap(),
                    )
                        .into_response()
                }
            }
        };
        gen.into()
    }
    
    

    Now I can use it in my code:

    #[derive(IntoResponse)]
    struct BucketInfoListAxum(BucketList);
    

    A little disadvantage of the approach is that I have to create a crate for the macros and use workspaces.