Convert string into semver struct

I'm an absolute beginner, thus forgive me any beginner mistakes.
Since I do want to learn, I prefer to write my own code instead of using existing crates.

I have created a simple struct which represents a version (semver).
Thus, it contains the 'major', 'minor' and 'patch' fields.

#[derive(Debug, PartialEq)]
pub struct Version {
    major: u8,
    minor: u8,
    patch: u8,
}

The goal of the function which I have created and for which I'm seeking a Code Review is a function which allows a &str instance to be converted into a Version instance.
Off course, it must fail when the string does NOT matches the pattern major.minor.patch template.

Therefore, I have created a Trait which can perform this check and do the conversion:

trait SplitIntoWithSize<T> {
    fn split_into(self, char: char, max_len: usize) -> Result<Vec<T>, ()>;
}

And here's the implementation on the &str type:

impl SplitIntoWithSize<u8> for &str {
    fn split_into(self, char: char, len: usize) -> Result<Vec<u8>, ()> {
        let mut retval = vec![0; len];

        let parts = self.split(char).collect::<Vec<Self>>();

        if parts.len() != len {
            return Err(());
        }

        for (idx, elem) in parts.iter().enumerate() {
            match elem.parse::<u8>() {
                Ok(result) => retval[idx] = result,
                Err(_) => return Err(()),
            };
        }

        Ok(retval)
    }
}

I then implemented the TryFrom trait on a &str to actually perform this conversion:

impl TryFrom<&str> for Version {
    type Error = ();

    fn try_from(input: &str) -> Result<Version, Self::Error> {
        match input.split_into_u8('.', 3) {
            Ok(parts) => Ok(Version::new(parts[0], parts[1], parts[2])),
            Err(_) => Err(()),
        }
    }
}

And off course we we have the unit tests:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_new_version() {
        struct UTCase {
            major: u8,
            minor: u8,
            patch: u8,
        }

        [
            UTCase {
                major: 1,
                minor: 2,
                patch: 3,
            },
            UTCase {
                major: 3,
                minor: 4,
                patch: 5,
            },
        ]
        .iter()
        .for_each(|case| {
            let version = Version::new(case.major, case.minor, case.patch);

            assert_eq!(version.major, case.major);
            assert_eq!(version.minor, case.minor);
            assert_eq!(version.patch, case.patch);
        });
    }

    #[test]
    fn version_from_valid_string() {
        struct UTCase<'a> {
            input: &'a str,
            version: Version,
        }

        [
            UTCase {
                input: "0.0.0",
                version: Version::new(0, 0, 0),
            },
            UTCase {
                input: "1.2.3",
                version: Version::new(1, 2, 3),
            },
        ]
        .iter()
        .for_each(|case| {
            let version = Version::try_from(case.input);

            assert_eq!(true, version.is_ok());
            assert_eq!(version.unwrap(), case.version);
        });
    }

    #[test]
    fn version_from_invalid_string() {
        struct UTCase<'a> {
            input: &'a str,
        }

        [
            UTCase { input: "0.0.0.0" },
            UTCase { input: "0" },
            UTCase { input: "256.0.0" },
        ]
        .iter()
        .for_each(|case| {
            let version = Version::try_from(case.input);

            assert_eq!(true, version.is_err());
        });
    }
}

What's your opinion based on the code above?
Things which I should have done in another way?

You could avoid a lot of work by using the iterator from split :

playground

The error type could be better in my version, but it gets the point across, with that you should be able to report errors by writing more match cases and enriching the parse errors. It's also more idiomatic to use FromStr rather that TryFrom<&str>, generally not always.

Thanks for the reply. But there's one concept still missing.
In my case, providing 4 numbers (1.2.3.4) would yield an error. Now that's not the case anymore.
Is there any easy way with the iterator to check that there are no more elements?

match (input.next(), input.next(), input.next(), input.next()) {
    (Some(major), Some(minor), Some(patch), None) => Ok(Version {
        major: major.parse()?,
        minor: minor.parse()?,
        patch: patch.parse()?,
    }),
    _ => Err(VersionError),
}
5 Likes

JFYI, the semver crate was recently re-written by dtolnay, and it is a beautiful piece of Rust to study: GitHub - dtolnay/semver: Parser and evaluator for Cargo's flavor of Semantic Versioning.

3 Likes

Just so you know, if you’re intending to parse versions from crates.io, they’ll need to be u64s. Even u32 is not large enough, I tried :sweat_smile:

9 Likes