Derive Macros
The Graph API provides derive macros to make working with your graph model types straightforward and type-safe. These macros generate code that integrates your custom types with the Graph API framework.
Overview
There are two primary derive macros:
VertexExt
- For vertex enum typesEdgeExt
- For edge enum types
These macros generate:
- Label enums for type-safe queries
- Index enums for efficient property lookups
- Helper methods for traversing and querying the graph
- Projection types for type-safe access to variant fields
- Walker builder filter extensions for type-safe filtering
VertexExt Derive Macro
Generated Types
When you apply #[derive(VertexExt)]
to an enum, the following types are generated:
- VertexLabel enum - Contains variants matching your enum's variants
- VertexIndex enum - Contains variants for each indexed field
- Projection structs - For accessing fields in a type-safe way
Example
use graph_api_derive::VertexExt;
use uuid::Uuid;
#[derive(Debug, Clone, VertexExt)]
pub enum Vertex {
Person {
#[index]
name: String,
#[index(range)]
age: u64,
#[index(full_text)]
biography: String,
unique_id: Uuid, // Not indexed
},
Project {
name: String,
},
Tag, // Unit variant
}
This generates:
// Label enum
pub enum VertexLabel {
Person,
Project,
Tag,
}
// Index enum with methods
pub enum VertexIndex {
PersonName,
PersonAge,
PersonBiography,
}
// Projection structs (simplified)
pub struct Person<'a, V> {
name: &'a String,
age: &'a u64,
biography: &'a String,
unique_id: &'a Uuid,
}
pub struct PersonMut<'a, V, L> {
name: &'a mut String,
age: &'a mut u64,
biography: &'a mut String,
unique_id: &'a mut Uuid,
}
Generated Methods
The derive macro generates several methods on the VertexIndex
enum:
Label-based Querying
For each enum variant, a method is generated to query by label:
// Query for all Person vertices
Vertex::person() -> VertexSearch<'_, Graph>
Property-based Querying
For each indexed field, methods are generated for exact matching:
// Query for Person vertices with a specific name
Vertex::person_by_name(value: & str) -> VertexSearch<'_, Graph>
// Query for Person vertices with a specific age
Vertex::person_by_age(value: u64) -> VertexSearch<'_, Graph>
Range-based Querying
For fields with the #[index(range)]
attribute:
// Query for Person vertices with age in a range
Vertex::person_by_age_range(range: Range<u64>) -> VertexSearch<'_, Graph>
Full-text Querying
For fields with the #[index(full_text)]
attribute:
// Query for Person vertices with matching text in biography
Vertex::person_by_biography(search: & str) -> VertexSearch<'_, Graph>
EdgeExt Derive Macro
Generated Types
When you apply #[derive(EdgeExt)]
to an enum, similar types are generated:
- EdgeLabel enum - Contains variants matching your enum's variants
- EdgeIndex enum - Contains variants for each indexed field
- Projection structs - For accessing fields in a type-safe way
Example
use graph_api_derive::EdgeExt;
#[derive(Debug, Clone, EdgeExt)]
pub enum Edge {
Knows {
since: u32,
},
Created,
Rated(Rating),
}
This generates:
// Label enum
pub enum EdgeLabel {
Knows,
Created,
Rated,
}
// Index enum with methods
pub enum EdgeIndex {
// EdgeIndex variants (if indexed fields exist)
}
Generated Methods
The EdgeIndex enum offers methods for edge traversal:
// Query for all Knows edges
EdgeIndex::knows() -> EdgeSearch<'_, Graph>
// Specify outgoing direction
EdgeIndex::knows().outgoing()
// Specify incoming direction
EdgeIndex::knows().incoming()
// Limit result count
EdgeIndex::knows().limit(n)
Walker Builder Filter Extensions
The derive macros also generate filter extension methods on the walker builders to simplify filtering based on vertex/edge types.
For Unit Variants
For unit variants (without fields), a single filter method is generated:
// Filter for all instances of the unit variant
fn filter_tag(self) -> /* walker builder */
Usage example:
// Get all Tag vertices
let tags = graph
.walk()
.vertices(VertexSearch::scan())
.filter_tag()
.collect::<Vec<_ > > ();
For Named Fields Variants
For variants with named fields, two filter methods are generated:
// 1. Filter for all instances of this variant
fn filter_person(self) -> /* walker builder */
// 2. Filter with custom logic using the projected fields
fn filter_by_person<F>(self, filter: F) -> /* walker builder */
where
F: Fn(Person<Graph::Vertex>, &Context) -> bool
Usage example:
// Get all Person vertices
let all_persons = graph
.walk()
.vertices(VertexSearch::scan())
.filter_person()
.collect::<Vec<_ > > ();
// Get Person vertices with specific criteria
let adults = graph
.walk()
.vertices(VertexSearch::scan())
.filter_by_person( | person, _ | person.age() > = 18)
.collect::<Vec<_ > > ();
For Tuple Variants
For tuple variants, similar filter methods are generated:
// 1. Filter for all instances of this variant
fn filter_rated(self) -> /* walker builder */
// 2. Filter with custom logic using the tuple fields
fn filter_by_rated<F>(self, filter: F) -> /* walker builder */
where
F: Fn(&Rating, &Context) -> bool
Usage example:
// Get edges with high ratings
let high_ratings = graph
.walk()
.vertices_by_id([person_id])
.edges(EdgeSearch::scan())
.filter_by_rated( | rating, _ | rating.stars > = 4)
.collect::<Vec<_ > > ();
Benefits of Filter Extensions
These filter extensions provide several advantages:
- Type Safety - The closures receive strongly typed projections
- Code Clarity - Filters are expressive and self-documenting
- IDE Support - Better autocompletion for variant fields
- Context Access - Access to the walker's context object
- Pattern Matching - No need for manual pattern matching
Using Generated Types
In Graph Queries
The generated types integrate with the Graph walker pattern:
// Find all person vertices
let people = graph
.walk()
.vertices(Vertex::person())
.collect::<Vec<_ > > ();
// Find people with a specific name
let named_people = graph
.walk()
.vertices(Vertex::person_by_name("Bryn"))
.collect::<Vec<_ > > ();
// Find people in an age range
let adults = graph
.walk()
.vertices(Vertex::person_by_age_range(18..65))
.collect::<Vec<_ > > ();
// Find outgoing 'knows' edges from a vertex
let friends = graph
.walk()
.vertices_by_id([person_id])
.edges(EdgeIndex::knows().outgoing())
.collect::<Vec<_ > > ();
Combined with Filter Extensions
Filter extensions can be combined with other walker steps:
// Find adults named "Bryn" with a complex filter
let result = graph
.walk()
.vertices(Vertex::person())
.filter_by_person( | person, _ | {
person.age() > = 18 & & person.name().contains("Bryn")
})
.collect::<Vec<_ > > ();
// Find friendship edges created before 2000
let old_friendships = graph
.walk()
.vertices_by_id([person_id])
.edges(EdgeIndex::knows().outgoing())
.filter_by_knows( | knows, _ | knows.since() < 2000)
.collect::<Vec<_ > > ();
Type Constraints
When using these types, your Graph type needs to implement appropriate support:
fn example<G>(graph: &G)
where
G: Graph<Vertex=Vertex, Edge=Edge>,
G::SupportsVertexLabelIndex: Supported,
{
// Now you can use label-based indexes
graph.walk().vertices(Vertex::person())...
}
Index Attributes
You can use these attributes on struct fields:
#[index]
- Basic indexing for efficient lookups#[index(range)]
- Enables range queries#[index(full_text)]
- Enables text search (String fields only)
Best Practices
-
Use the appropriate index type for your query pattern:
- Use label index for type filtering
- Use property index for exact matches
- Use range index for numeric ranges
- Use full-text for searching text content
-
Apply indexes sparingly:
- Each index adds memory overhead
- Only index fields you'll query frequently, it's OK to filter once you are on the graph.
-
Consider the query planner:
- Using an index in a vertices() step is typically more efficient than filtering the entire graph
- Combining indices with other walker steps can create efficient traversal patterns
-
Use filter extensions for type-safety:
- Prefer
filter_by_person()
overfilter()
with manual pattern matching - Leverage the projection types for field access
- Use specific filter methods for clearer, more maintainable code
- Prefer