Search code examples
rustnom

Optional field with strict format


I am trying to build nom parser to examine URLs with ID as UUID

rooms/e19c94cf-53eb-4048-9c94-7ae74ff6d912

I created the following:

extern crate uuid;
use uuid::Uuid;

named!(room_uuid<&str, Option<Uuid>>,
    do_parse!(
        tag_s!("rooms") >>
        id: opt!(complete!(preceded!(
            tag_s!("/"),
            map_res!(take_s!(36), FromStr::from_str)
        ))) >>

        (id)
    )
);

It handles almost all cases well:

assert_eq!(room_uuid("rooms"), Done("", None));
assert_eq!(room_uuid("rooms/"), Done("/", None));
assert_eq!(room_uuid("rooms/e19c94cf-53eb-4048-9c94-7ae74ff6d912"), Done("", Some(Uuid::parse_str("e19c94cf-53eb-4048-9c94-7ae74ff6d912").unwrap())));

Except cases where ID is not a valid UUID:

assert!(room_uuid("rooms/123").is_err()); # it fails
# room_uuid("rooms/123").to_result() => Ok(None)

As far as I understand it happens because opt! converts inner Err into None.

I would like to have ID as optional section but if it is present it should be a valid UUID.
Unfortunately, I don't understand how to combine both those things: optionality and strict format.


Solution

  • I've only started working with nom myself in the last couple of weeks but I found one way of solving this. It doesn't fit exclusively within a macro but it does give the correct behavior with one modification. I swallow the / rather than leave it dangling after when a UUID is not given.

    #[macro_use]
    extern crate nom;
    extern crate uuid;
    
    use std::str::FromStr;
    use nom::IResult;
    use uuid::Uuid;
    
    fn room_uuid(input: &str) -> IResult<&str, Option<Uuid>> {
        // Check that it starts with "rooms"
        let res = tag_s!(input, "rooms");
        let remaining = match res {
            IResult::Incomplete(i) => return IResult::Incomplete(i),
            IResult::Error(e) => return IResult::Error(e),
            IResult::Done(i, _) => i
        };
    
        // If a slash is not present, return early
        let optional_slash = opt!(remaining, tag_s!("/"));
        let remaining = match optional_slash {
            IResult::Error(_) |
            IResult::Incomplete(_) => return IResult::Done(remaining, None),
            IResult::Done(i, _) => i
        };
    
        // If something follows a slash, make sure
        // it's a valid UUID
        if remaining.len() > 0 {
            let res = complete!(remaining, map_res!(take_s!(36), FromStr::from_str));
            match res {
                IResult::Done(i, o) => IResult::Done(i, Some(o)),
                IResult::Error(e) => IResult::Error(e),
                IResult::Incomplete(n) => IResult::Incomplete(n)
            }
        } else {
            // This branch allows for "rooms/"
            IResult::Done(remaining, None)
        }
    }
    
    #[test]
    fn match_room_plus_uuid() {
        use nom::IResult::*;
    
        assert_eq!(room_uuid("rooms"), Done("", None));
        assert_eq!(room_uuid("rooms/"), Done("", None));
        assert_eq!(room_uuid("rooms/e19c94cf-53eb-4048-9c94-7ae74ff6d912"), Done("", Some(Uuid::parse_str("e19c94cf-53eb-4048-9c94-7ae74ff6d912").unwrap())));
        assert!(room_uuid("rooms/123").is_err());
    }