Need some help with conversion of types

I have made the following newtype:

/// Create the Name newtype
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)]
#[sqlx(transparent)]
pub struct Name(String);

/// Implement the Name type
impl Name {
    pub fn new(name: &str) -> Result<Self, Vec<&'static str>> {
        let mut errors = Vec::new();
        
        // Name must be between 3 and 50 characters
        if !validate_length(name, Some(3), Some(50), None) {
            errors.push("Name must be between 3 and 50 characters");
        }
        
        // Name must start with a letter
        if !name.chars().next().map_or(false, |c| c.is_alphabetic()) {
            errors.push("Name must start with a letter");
        }

        // All Name rules comply, return the Name
        if errors.is_empty() {
            Ok(Self(name.to_string()))
        } else {
            Err(errors)
        }
    }
}

// Convert String to a Name type
impl From<String> for Name {
    fn from(value: String) -> Self {
        Self(value)
    }
}

// Implement the Display trait for Name
impl fmt::Display for Name {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

Now, I need to convert a var that is being set using an env to convert to the Name type. I have no clue how to do this:

// admin user
    let admin_user_name= env::var("ADMIN_USER_NAME")
        .unwrap_or_else(|_| "admin".to_string());

So, admin_user_name must be of type Name.

Since you have implemented the From<String> trait, you can declare the type of the variable and use .into(). The compiler will know it has to convert from String to Name:

let admin_user_name: Name = env::var("ADMIN_USER_NAME")
    .unwrap_or("admin".to_string())
    .into();

or you can use an explicit Name::from():

let admin_user_name = Name::from(env::var("ADMIN_USER_NAME")
    .unwrap_or("admin".to_string()));

If you need to use it in a place the compiler knows to be Name, the into() is the easiest way to do it. For example, as a function argument of type Name.

PS: Note that the unwrap_or I used is evaluated every time, even if env::var returned Ok. The method you used, unwrap_or_else only call the function if env::var is not Ok, which can be desirable if the function does more than directly return a String (e.g. if there are side-effects or if it's heavier to compute).

1 Like

The expression above gives you a String, so this is the type of admin_user_name. I assume you want to call Name::new. But your Name::new(name: &str) function takes a parameter of type &str.

You can coerce a String to &str by prefixing the expression with & or by using two variables:

    let admin_user_string = env::var("ADMIN_USER_NAME")
        .unwrap_or_else(|_| "admin".to_string());
    let admin_user_name = Name::new(&admin_user_string);

Note that this will create an intermediate String, which is then converted to another new String, which wastes an allocation. To avoid that you may want to change Name::new(name: &str) to Name::new(name: String).

2 Likes

That worked! thank you!!

1 Like

Another thing worth mentioning is that your implementation of From<String> isn't calling the new method, which means that it would create a Name that hasn't gone through the validation that you created for it.

3 Likes

There were a few good remarks here, so I made a 2nd version.

The problem with your initial code is that, as @firebits.io said, the From implementation doesn't use your validation code. And since From doesn't allow for errors, you should use TryFrom instead. Also, as @jumpnbrownweasel noticed, it's more appropriate here to use a String instead of an &str in new().

I declared a struct for the error, to avoid repetitions and to make changes easier. The TryForm then looks like this (the whole code is in the playground at the bottom):

#[derive(Debug)]
pub struct ErrorName(Vec<&'static str>);

// Convert String to a Name type
impl TryFrom<String> for Name {
    type Error = ErrorName;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Name::new(value)
    }
}

You can then manage any error with this pattern (in the playground, I put another form that gives default values in case of errors, but the one below is better IMO):

// manages the possible error:
let admin_user_name = Name::try_from(env::var("ADMIN_USER_NAME").unwrap_or("admin".to_string()));
match admin_user_name {
    Ok(name) => { println!("admin_user_name = {name}"); }
    Err(e) => { println!("## ERROR: {}", e.0.join(", ")); }
}

(I removed some of the non-std derives of Name in the playground)