What is the most idiomatic way to create a struct based on another struct?

I am writing an API with Rust, I have a struct like this for one of my route:

#[derive(Debug, Serialize, Deserialize)]
struct Interval {
    interval: Option<u64>,
    id: Option<String>,
}

And in another route, I want to enforce all the fields and make sure they exists like this.

#[derive(Debug, Serialize, Deserialize)]
struct Interval {
    interval: u64,
    id: String,
}

What is the most idiomatic way to create a struct based on another struct? Or I should just use match or is_some to conditionally deal with the struct?

1 Like

If you go with different structs, and they have sensible defaults, implement From:

const DEFAULT_INTERVAL_INTERVAL: u64 = 42;
const DEFAULT_INTERVAL_ID: &'static str = "Karla";

impl From<MaybeInterval> for Interval {
    fn from(mi: MaybeInterval) -> Self {
        let interval = mi.interval.unwrap_or(DEFAULT_INTERVAL_INTERVAL);
        let id = mi.id.unwrap_or_else(|| DEFAULT_INTERVAL_ID.into());
        Self { interval, id }
    }
}

If they do not, implement TryFrom:

pub enum IntervalConversionError {
    Etc,
    EtCetera,
}

impl TryFrom<MaybeInterval> for Interval {
    type Error = IntervalConversionError;
    fn try_from(mi: MaybeInterval) -> Result<Self, Self::Error> {
        let interval = mi.interval.ok_or(IntervalConversionError::Etc)?;
        let id = mi.id.ok_or(IntervalConversionError::EtCetera)?;
        Ok(Self { interval, id })
    }
}

Playground.

I would probably just have defined a method:

impl MaybeInterval {
    fn try_into_interval(self) -> Option<Interval> {
        match (self.interval, self.id) {
            (Some(interval), Some(id)) => Some(Interval { interval, id }),
            _ => None,
        }
    }
}
1 Like

I'm not very experienced yet, so my proposal might not be idiomatic, but what about creating a trait to cover both cases?

If you have

struct IntervalOpt {
    interval: Option<u64>,
    id: Option<String>,
}

struct Interval {
    interval: u64,
    id: String,
}

you could define a common trait

trait IntervalGeneric {
    fn interval(&self) -> Option<u64>;
    fn id(&self) -> Option<&String>;
}

because both types allow you to retrieve an Option<u64> for the interval and an Option<&String> for the ID.

The trait could be implemented as follows:

impl IntervalGeneric for IntervalOpt {
    fn interval(&self) -> Option<u64> {
        self.interval
    }
    fn id(&self) -> Option<&String> {
        self.id.as_ref()
    }
}

impl IntervalGeneric for Interval {
    fn interval(&self) -> Option<u64> {
        Some(self.interval)
    }
    fn id(&self) -> Option<&String> {
        Some(&self.id)
    }
}

Then you can have functions that deal with both cases, such as:

fn print_interval_generic<T: IntervalGeneric>(value: T) {
    println!(
        "Interval = {}, ID = {}",
        value.interval().map_or("NONE".to_string(), |x| x.to_string()),
        value.id().map_or("NONE", |x| &x),
    );
}

fn main() {
    let v1 = Interval {
        interval: 1,
        id: "101".to_string(),
    };
    let v2 = IntervalOpt {
        interval: Some(10),
        id: Some("102".to_string()),
    };
    let v3 = IntervalOpt {
        interval: None,
        id: Some("103".to_string()),
    };
    print_interval_generic(v1);
    print_interval_generic(v2);
    print_interval_generic(v3);
}

(Note: I don't like the "NONE".to_string() there, as it's causing an unnecessary allocation in many cases, but this is just an example.)

But as I said, not sure if that's the best approach. Maybe it also depends on how your API is overall defined and designed.

I'd probably go with this, but with a solution which becomes a compiler error if you later add a field to those structs :

impl MaybeInterval {
    fn build(self) -> Option<Interval> {
        let Self { interval, id } = self;
        Some(Interval {
            interval: interval?,
            id: id?,
        })
    }
}

I'm wondering the same thing, all answers that have been given here imply to rewrite the fields name at least once, be it in another struct, trait or method.

Isn't there a way (e.g. #[derive]) to create a new struct removing the Option<> wrapper on all fields (besides creating our own derive macro) ?

This is not a normal or common idiom. It's not inherently wrong, but being explicit and what your types are is good. It's unusual to have fields with the same name and different types, and certainly not something I'd want to do automatically or implicitly.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.