Search code examples
rustrust-macros

How can I handle or test if a type is an Option in a Rust macro?


I am trying to create a macro to generate a struct which can be filled from a postgres-database. Now, as there are fields in the db which are nullable and non-nullable, I'd want to handle them differently in the macro.

Output should be a struct like this:

#[derive(Debug, Default)]
pub struct MyStruct {
    pub attrib_a: i64,
    pub attrib_b: Option<i64>,
}

impl MyStruct {
    pub fn get_row(&self, row: &postgres::rows::Row) -> MyStruct {
        MyStruct {
            // the non-nullable attrib_a, where I can for sure take the value out of the Option and assign it
            attrib_a: match row.get::<_, Option<i64>>("attrib_a") {
                Some(x) => x,
                None => 0,
            },

            // here for the nullable attrib_b I just want to return the Option as is
            attrib_b: row.get::<_, Option<i64>>("attrib_b"),
        }
    }
}

Here's the current macro code:

macro_rules! make_table_struct {
    ($tname: stmt => $sname: ident; $($fname: ident: $ftype: ty),+) => {

        #[derive(Debug, Clone, Default)]
        pub struct $sname {
            $(
                pub $fname: $ftype,
            )+
        }

        impl $sname
        {
            pub fn get_row(&self,row:&postgres::rows::Row)->$sname
            {
                $sname
                {
                    //How do I know if I have an Option here or not and act then accordingly?
                    $(
                        $fname: row.get::<_,Option<$ftype>>(stringify!($fname)),
                    )+
                }
            }
        }
    }
}

Macro-call:

make_table_struct! ("source_table_name" => MyStruct; attrib_a: i64, attrib_b: Option<i64>)

Solution

  • Here's a macro that takes a $fname and a type, which may be optional. If the type is optional, it generates another function than if is not:

    macro_rules! make_get_row {
        ($fname: ident, Option<$ftype: ty>) => {
            fn $fname(i: Option<$ftype>) -> i64 {
                i.unwrap_or(0)
            }
        };
        ($fname: ident, $ftype: ty) => {
            fn $fname(i: $ftype) -> i64 {
                i
            }
        };
    }
    
    make_get_row!(direct, i64);
    make_get_row!(via_op, Option<i64>);
    
    fn main() {
        direct(1);
        via_op(Some(1));
    }
    

    You should be able to use a (possibly adjusted) variant of this macro within make_table_struct.

    The following is not production quality, but might lead you somewhere:

    macro_rules! get_table_row {
        ($row: ident, nonnullable $fname:ident: $ftype:ty) => {
            $row.get::<_,$ftype>(stringify!($fname))
        };
        ($row: ident, nullable $fname:ident: $ftype:ty) => {
            match $row.get::<_,Option<Option<$ftype>>>(stringify!($fname)) { Some(x) => x, None => Some(0) }
        }
    }
    type nullable<T> = Option<T>;
    type nonnullable<T> = T;
    macro_rules! make_table_struct {
        ($tname:stmt => $sname:ident; $($nul: ident $fname:ident: $ftype:tt),+) => {
    
            #[derive(Debug, Clone, Default)]
            pub struct $sname {
                $(
                    pub $fname: $nul < $ftype >,
                )+
            }
    
            impl $sname {
                pub fn get_row(&self, row: &postgres::rows::Row) -> $sname {
                    $(
                        let $fname = get_table_row!(row, $nul $fname: $ftype);
                    )+
                    $sname {
                        $($fname,)+
                    }
                }
            }
        }
    }
    
    make_table_struct! ("source_table_name" => MyStruct; nonnullable attrib_a: i64, nullable attrib_b: i64);