Idiomatic way to set values of a struct with field validation

Hi there :slightly_smiling_face:

In my learning journey of Rust I am playing with Struct and field validation.

I think my question would have been perfectly follows as question in this post : best-practices-on-setters-and-accessor-methods-in-general but it has been closed....

I have a struct like this:

pub struct Config { 
   pub field1: String,
   pub field2: String
}

As all fields are public I can't check if both are empty and if they are empty to return any error.

So I have decided to implement a struct like this instead:

pub struct Config { 
  field1: String,
  field2: String
}

pub type Result<Config> = std::result::Result<Config, MyError>;

impl Config {

    pub fn new(field1: String, field2: String) -> Result<Self> {
         Self::assert_field_not_empty(& field1)?;
         Self::assert_field_not_empty(& field2)?;

        Ok(
            Self {
                field1,
                field2,
               }
          )
      }

    pub fn set_field1(mut self, field1: String) -> Result<()> {
           Self::assert_field_not_empty(& field1)?;
           self.field1 = field1;
           Ok(())
    }

    pub fn get_field1(&self) -> &String {
           &self.field1
    } 

   pub fn set_field1(mut self, field1: String) -> Result<()> {
          Self::assert_field_not_empty(& field1)?;
          self.field1 = field1;
         Ok(())
    }

    pub fn get_field2(&self) -> &String {
          &self.field2
    } 

   fn assert_field_not_empty(field: &String) -> Result<()>{
        if field.is_empty() {
            return Err(MyError::FieldIsEmpty);
        }
        Ok(())
     }
   }

impl Default for Config {
    fn default() -> Self { 
       Self{
            field1: "default_value_for_field1".to_string(),
            field2: "default_value_for_field2".to_string(),
        }
    }
}

The following code doesn't compile:

let config: Config = Default::default();
config.set_field1("new value".to_string())?;

Error:

 |   let config: Config = Default::default();
 |    ------ move occurs because `config` has type `config::Config`, which does not implement the `Copy` trait
 |
 |   config.set_field1"test_a".to_string())?;
 |   ------ value moved here

But this does:

let config: Config = Default::default();
config.clone().set_field1("new value".to_string())?;

What would be the right way to do what I describe above?

While writing this post I came across the Crate Validator. Maybe this would be the perfect way to take instead of implement all this validation by myself?

Thank you!

mut self will consume self here when you call set_field1. Try making this a &mut self and declaing your variable as mutable (let mut config: Config = Default::default()).

2 Likes

@jofas , thank you ! that what I need !

I had also to set a lifetime on the struct.

Is it common to use a struct with setter and validation like I did?
Do you see any other improvement(s) I can apply?

I don't know if its common, but it is a simple and easy pattern you follow. For bigger projects you might need to restructure your approach a little (i.e. once you share certain validations between structs).

It might also be worth checking out the validator crate you've linked. If you can catch all your validations with it, it might enhance your code by making it more declarative. With your approach I'd always be afraid that I forget validation somewhere. I.e. in your example, your implementation of Default is not validated which may cause bugs in your code that are hard to find.

Instead of mutable setters, I think it is more common to use stateless setters where you consume the old struct and return a new one. Also passing &String is generally discouraged in favour of &str. Here's an update to your snippet:

pub struct Config {
    field1: String,
    field2: String,
}

pub enum MyError {
    FieldIsEmpty
}

pub type Result<Config> = std::result::Result<Config, MyError>;

impl Config {
    pub fn new(field1: String, field2: String) -> Result<Self> {
        Self::assert_field_not_empty(&field1)?;
        Self::assert_field_not_empty(&field2)?;

        Ok(Self { field1, field2 })
    }

    pub fn with_field1(mut self, field1: String) -> Result<Self> {
        Self::assert_field_not_empty(&field1)?;
        self.field1 = field1;
        Ok(self)
    }

    pub fn field1(&self) -> &str {
        &self.field1
    }

    pub fn with_field2(mut self, field2: String) -> Result<Self> {
        Self::assert_field_not_empty(&field2)?;
        self.field2 = field2;
        Ok(self)
    }

    pub fn get_field2(&self) -> &str {
        &self.field2
    }

    fn assert_field_not_empty(field: &str) -> Result<()> {
        if field.is_empty() {
            return Err(MyError::FieldIsEmpty);
        }
        Ok(())
    }
}

impl Default for Config {
    fn default() -> Self {
        Self::new(
            "default_value_for_field1".to_string(),
            "default_value_for_field2".to_string(),
        )
        .unwrap()
    }
}

Playground.

Instead of letting users set a field and remembering to validate the value after the fact, idiomatic Rust will prefer to make invalid states unrepresentable.

For example, a std::string::String can only ever contain UTF-8 data so it's not possible to encounter encoding issues. Similarly, a std::ffi::CStr is guaranteed to point to a null-terminated sequence of bytes and it has safe constructors like CStr::from_bytes_with_null() which will make sure the &[u8] slice you pass it has that null terminator.

In general, if there is some invariant you would like to keep (e.g. make sure the provided string isn't empty), then create a new type with constructors that will enforce that requirement.

pub struct NonEmptyString(String);

impl NonEmptyString {
  pub fn new(s: String) -> Result<Self, EmptyStringError> {
    if s.is_empty() {
      Err(EmptyStringError);
    }
    Ok(NonEmptyString(s))
  }

  pub fn as_str(&self) -> &str { &self.0 }
}

struct EmptyStringError;

impl std::error::Error for EmptyStringError {}

From there, you can enforce your config is valid by using NonEmptyString everywhere.

pub struct Config {
  pub field1: NonEmptyString,
}

There's no need for validation code on Config because the fact that someone has a NonEmptyString means you can rely on it not being empty.

There's a nice term for this mentality - "Parse, don't validate".

3 Likes

Thank you @Michael-F-Bryan I had something like your proposal in mind. I forgive to put it here :slight_smile:

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.