Aurelia Peters
Aurelia Peters

Reputation: 2209

Function that generates a HashMap of Enum variants

I'm working with apollo_parser to parse a GraphQL query. It defines an enum, apollo_parser::ast::Definition, that has several variants including apollo_parser::ast::OperationDefintion and apollo_parser::ast::FragmentDefinition. I'd like to have a single Trait I can apply to apollo_parser::ast::Definition that provides a function definition_map that returns a HashMap mapping the operation name to the variant instance.

I've got as far as the trait, but I don't know how to implement it. Also, I don't know how to constrain T to be a variant of Definition.

trait Mappable {
  fn definition_map<T>(&self) -> HashMap<String, T>;
}

EDIT:

Here's a Rust-ish pseudocode implementation.

impl Mappable for Document {
  fn definition_map<T>(&self) -> HashMap<String, T> {
    let defs = Vec<T> = self.definitions
      .filter_map(|def: Definition| match def {
        T(foo) => Some(foo),
        _ => None
      }).collect();
    let map = HashMap::new();
    for def: T in definitions {
       map.insert(def.name(), def);
    }
    map
  }
}

and it would output

// From a document consisting of OperationDefinitions "operation1" and "operation2"
// and FragmentDefinitons "fragment1" and "fragment2"

{
  "operation1": OperationDefinition(...),
  "operation2": OperationDefinition(...),
}

{ 
  "fragment1": FragmentDefinition(...),
  "fragment2": FragmentDefinition(...)
}

Upvotes: 1

Views: 619

Answers (1)

kmdreko
kmdreko

Reputation: 60492

I don't know how to constrain T to be a variant of Definition.

There is no such thing in Rust. There's the name of the variant and the name of the type contained within that variant, there is no relationship between the two. The variants can be named whatever they want, and multiple variant can contain the same type. So there's no shorthand for pulling a T out of an enum which has a variant with a T.

You need to make your own trait that says how to get a T from a Definition:

trait TryFromDefinition {
    fn try_from_def(definition: Definition) -> Option<Self> where Self: Sized;
    fn name(&self) -> String;
}

And using that, your implementation is simple:

impl Mappable for Document {
    fn definition_map<T: TryFromDefinition>(&self) -> HashMap<String, T> {
        self.definitions()
            .filter_map(T::try_from_def)
            .map(|t| (t.name(), t))
            .collect()
    }
}

You just have to define TryFromDefinition for all the types you want to use:

impl TryFromDefinition for OperationDefinition {
    fn try_from_def(definition: Definition) -> Option<Self> {
        match definition {
            Definition::OperationDefinition(operation) => Some(operation),
            _ => None,
        }
    }

    fn name(&self) -> String {
        self.name().unwrap().ident_token().unwrap().text().into()
    }
}

impl TryFromDefinition for FragmentDefinition {
    fn try_from_def(definition: Definition) -> Option<Self> {
        match definition {
            Definition::FragmentDefinition(operation) => Some(operation),
            _ => None,
        }
    }

    fn name(&self) -> String {
        self.fragment_name().unwrap().name().unwrap().ident_token().unwrap().text().into()
    }
}

...

Some of this could probably be condensed using macros, but there's no normalized way that I can tell to get a name from a definition, so that would still have to be custom per type.

You should also decide how you want to handle definitions that don't have a name; you'd probably want to return Option<String> to avoid all those .unwrap()s, but I don't know how you'd want to put that in your HashMap.


Without knowing your whole workflow, I might suggest a different route instead:

struct Definitions {
    operations: HashMap<String, OperationDefinition>,
    fragments: HashMap<String, FragmentDefinition>,
    ...
}

impl Definitions {
    fn from_document(document: &Document) -> Self {
        let mut operations = HashMap::new();
        let mut fragments = HashMap::new();
        ...

        for definition in document.definitions() {
            match definition {
                Definition::OperationDefinition(operation) => {
                    let name: String = operation.name().unwrap().ident_token().unwrap().text().into();
                    operations.insert(name, operation);
                },
                Definition::FragmentDefinition(fragment) => {
                    let name: String = fragment.fragment_name().unwrap().name().unwrap().ident_token().unwrap().text().into();
                    fragments.insert(name, fragment);
                },
                ...
            }
        }

        Definitions { 
            operations,
            fragments,
            ...
        }
    }
}

Upvotes: 2

Related Questions