Generic vs Closures for generating data storage id?

Hello,

I must convert an optional string slice into a MongoDB ObjectId or a UUID. The conversion is trivial, but I am unsure if I have the best implementation below. Basically, I have a make_id() function that validates and selects how to provide the id.

The first solution, below, uses two closures, which either parse the id or generate one as an ObjectId or a Uuid.

The second solution, below, uses generics.

Although both solutions work, I am curious whether one is more safe or has advantages over the other. I'd appreciate your comments.

Thanks for your interest and time.
Mike

CLOSURE VERSION

[derive(Debug)]
pub enum DataId {
    InMemory(Uuid),
    MongoDb(ObjectId),
}

impl DataId {}

#[tokio::main]
async fn main() {
    let uuid = "142c458f-2728-4961-b4c5-6514c7491691";
    let oid = "6507fdd16c27fffd69308982";

    // Get a UUID from a string slice.
    let result_uuid = make_id(Some(uuid), parse_uuid, generate_uuid).unwrap();
    println!("Parsed UUID '{}' to '{:?}'", uuid, result_uuid);

    // Get an ObjectId from a string slice.
    let result_objectid = make_id(Some(oid), parse_objectid, generate_objectid).unwrap();
    println!("Parsed ObjectId '{}' to '{:?}'", oid, result_objectid);
}

// Create an instance of a data storage id specific for an inmemory store or a MongoDb database.
// id: The value of a unique id for a project in storage as a string slice.
// parser: A closure that parses the UUID or ObjectId.
// generator: A closure that generates a new UUID or ObjectId.
// returns: the data store-specific id.
fn make_id(
    id: Option<&str>,
    parser: fn(id: &str) -> Result<DataId, EntityError>,
    generator: fn() -> DataId,
) -> Result<DataId, EntityError> {
    return if let Some(id) = id {
        match parser(id) {
            Ok(did) => Ok(did),
            Err(e) => Err(e),
        }
    } else {
        // No id provided, so we will make one.
        Ok(generator())
    };
}

fn parse_uuid(id: &str) -> Result<DataId, EntityError> {
    match Uuid::parse_str(id) {
        Ok(uid) => Ok(DataId::InMemory(uid)),
        Err(e) => Err(EntityError::Application {
            message: "failed to parse the uuid".to_string(),
            err: Some(BoxError::try_from(e).unwrap()),
        }),
    }
}

fn parse_objectid(id: &str) -> Result<DataId, EntityError> {
    match ObjectId::parse_str(id) {
        Ok(oid) => Ok(DataId::MongoDb(oid)),
        Err(e) => Err(EntityError::Application {
            message: "failed to parse the object id".to_string(),
            err: Some(BoxError::try_from(e).unwrap()),
        }),
    }
}

fn generate_uuid() -> DataId {
    DataId::InMemory(Uuid::new_v4())
}

fn generate_objectid() -> DataId {
    DataId::MongoDb(ObjectId::new())
}

GENERIC VERSION

// Represents the unique id for an entity in a data store.
//    where T is either UuidType or ObjectIdType.
// Defines two methods that must be implemented:
//    generator() to create a new Uuid or ObjectId.
//    parser() to parse a string slice id and produce either a Uuid or ObjectId.
trait DataIdType<T> {
    fn generator() -> T;
    fn parser(id: &str) -> Result<T, EntityError>;
}

// UuidType: Define and implement DataIdType<Uuid>.
#[derive(Debug)]
struct UuidType {}
impl DataIdType<Uuid> for UuidType {
    fn generator() -> Uuid {
        Uuid::new_v4()
    }

    fn parser(id: &str) -> Result<Uuid, EntityError> {
        match Uuid::parse_str(id) {
            Ok(uid) => Ok(uid),
            Err(e) => Err(EntityError::Application {
                message: "failed to parse the uuid".to_string(),
                err: Some(BoxError::try_from(e).unwrap()),
            }),
        }
    }
}

// ObjectIdType: Define and implement DataIdType<ObjectId>.
#[derive(Debug)]
struct ObjectIdType {}
impl DataIdType<ObjectId> for ObjectIdType {
    fn generator() -> ObjectId {
        ObjectId::new()
    }

    fn parser(id: &str) -> Result<ObjectId, EntityError> {
        match ObjectId::parse_str(id) {
            Ok(oid) => Ok(oid),
            Err(e) => Err(EntityError::Application {
                message: "failed to parse the objectid".to_string(),
                err: Some(BoxError::try_from(e).unwrap()),
            }),
        }
    }
}

// Create a data storage id as either a Uuid or ObjectId.
//    where T is either UuidType or ObjectIdType; and
//          V is either Uuid     or ObjectId
// id: The unique, string-slice value id for an entity in storage.
// returns: the data store specific id.
fn make_id<T,V>(id: Option<&str>) -> Result<V, EntityError> where T:DataIdType<V> {

    return if let Some(id) = id {
        match T::parser(id) {
            Ok(did) => Ok(did),
            Err(e) => Err(e),
        }
    } else {
        // No id provided, so we will make one.
        Ok(T::generator())
    };
}

fn main() {

    let uuid = "142c458f-2728-4961-b4c5-6514c7491691";
    let objectid = "6507fdd16c27fffd69308982";

    let parsed_uuid = make_id::<UuidType, Uuid>(Some(uuid)).unwrap();
    let parsed_objectid = make_id::<ObjectIdType, ObjectId>(Some(objectid)).unwrap();

    println!("Parsed UUID '{}' to '{:?}'", uuid, parsed_uuid);
    println!("Parsed ObjectId '{}' to '{:?}'", objectid, parsed_objectid);

    let generated_uuid = make_id::<UuidType, Uuid>(None).unwrap();
    let generated_objectid = make_id::<ObjectIdType, ObjectId>(None).unwrap();

    println!("Generated UUID '{:?}'", generated_uuid);
    println!("Generated ObjectId '{}'", generated_objectid);
}

I'm struggling to understand what that is. It seems like you could simplify the code just by having two functions (make_uuid and make_object_id) then flattening everything.

In other words, what purpose does make_id serve other than to factor out a single if?

2 Likes

and that if let can be replaced with a one-liner using combinator map_or_else:

fn make_id(
    id: Option<&str>,
    parser: fn(id: &str) -> Result<DataId, EntityError>,
    generator: fn() -> DataId,
) -> Result<DataId, EntityError> {
    id.map_or_else(|| Ok(generator()), parser)
}

if, the parser function returns different Err, use the ? operator, assuming the error types can be converted (if not, Result::map_err is always available):

id.map_or_else(|| Ok(generator()), |id| Ok(parser(id)?))

If the question is whether the return type should be a generic or an enum, then it should probably be a geneoc. There's no reason why the individual ID types would nees to know about each other.