Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Derive Macros

Graph API's derive macros automatically generate code that integrates your custom types with the Graph API framework, making your graph operations type-safe and ergonomic. This section explains the types that are generated and how to use them effectively.

Overview

Graph API provides two primary derive macros:

  1. VertexExt - For vertex enum types
  2. EdgeExt - For edge enum types
    // Create a new graph with our model
    let mut graph = SimpleGraph::<Vertex, Edge>::new();

    // Add vertices of different types
    let alice = graph.add_vertex(Vertex::Person {
        name: "Alice".to_string(),
        username: "alice_dev".to_string(),
        biography: "Software engineer and graph enthusiast".to_string(),
        age: 29,
    });

    let project = graph.add_vertex(Vertex::Project {
        name: "GraphDB".to_string(),
    });

    // Add edges connecting vertices
    graph.add_edge(alice, project, Edge::Created);

    // Use type-safe projections for property access
    let person_info = graph
        .walk()
        .vertices(VertexSearch::scan())
        .filter_person() // Generated helper method
        .map(|v, _| {
            // Use type-safe projection
            let person = v.project::<Person<_>>().unwrap();

            // Return formatted string with person information
            format!(
                "Person: {} (@{})\n  Bio: {}\n  Age: {}",
                person.name(),
                person.username(),
                person.biography(),
                person.age()
            )
        })
        .collect::<Vec<_>>();

    // Print the collected information
    for info in person_info {
        println!("{}", info);
    }

Generated Types for VertexExt

When you apply #[derive(VertexExt)] to an enum, several useful types are generated:

1. Label Enum (VertexLabel)

A type-safe enum representing the variants of your vertex enum:

// Given this vertex definition
#[derive(VertexExt)]
enum Vertex {
    Person { /* fields */ },
    Project { /* fields */ },
    Comment { /* fields */ },
}

// This is generated
pub enum VertexLabel {
    Person,
    Project,
    Comment,
}

Purpose: Provides type-safe representations of vertex types, used for label-based indexing and filtering.

2. Index selectors

Selectors to make querying easier:

impl Vertex {
    // Find all Person vertices
    pub fn person() -> VertexSearch { /* ... */ }
    
    // Find Person vertices by indexed properties
    pub fn person_by_username(username: &str) -> VertexSearch { /* ... */ }
    pub fn person_by_biography(text: &str) -> VertexSearch { /* ... */ }
    pub fn person_by_age_range(range: Range<u8>) -> VertexSearch { /* ... */ }
}

Purpose: Enables efficient querying of vertices by label and properties.

3. Projection Types

For each variant with fields, two projection structs are generated:

// For Person variant
pub struct Person<'a, V> {
    // Immutable references to fields
    name: &'a String,
    username: &'a String,
    biography: &'a String,
    age: &'a u8,
    unique_id: &'a Uuid,
}

pub struct PersonMut<'a, V> {
    // Mutable references to fields
    name: &'a mut String,
    username: &'a mut String,
    biography: &'a mut String,
    age: &'a mut u8,
    unique_id: &'a mut Uuid,
}

Purpose: Provides type-safe access to variant fields without manually matching on enum variants.

4. Filter Extension Traits

Extension methods for the walker builder to filter by vertex type:

// On WalkerBuilder
// For all Person vertices
fn filter_person(self) -> Self;

// For Person vertices with custom criteria
fn filter_by_person<F>(self, filter: F) -> Self
where
    F: Fn(Person<Vertex>, &Context) -> bool;

Purpose: Enables type-safe filtering of vertices during traversals.

Generated Types for EdgeExt

When you apply #[derive(EdgeExt)] to an enum, similar types are generated:

1. Label Enum (EdgeLabel)

// Given this edge definition
#[derive(EdgeExt)]
pub enum Edge {
    Created,
    Follows,
    Liked { timestamp: String },
    Commented { timestamp: String },
}

// This is generated
pub enum EdgeLabel {
    Created,
    Follows,
    Liked,
    Commented,
}

Purpose: Provides type-safe representations of edge types, used for label-based indexing and filtering.

2. Index selectors

Selectors to make querying easier

impl Edge {
    // Find all Created edges
    pub fn created() -> EdgeSearch { /* ... */ }
    
    // Find all Follows edges
    pub fn follows() -> EdgeSearch { /* ... */ }
    
    // Find all Liked edges
    pub fn liked() -> EdgeSearch { /* ... */ }
    
    // Find all Commented edges
    pub fn commented() -> EdgeSearch { /* ... */ }
}

Purpose: Enables efficient querying of edges by label.

3. Projection Types

For each variant with fields, projection structs are generated:

// For Liked variant
pub struct Liked<'a, E> {
    timestamp: &'a String,
}

pub struct LikedMut<'a, E> {
    timestamp: &'a mut String,
}

Purpose: Provides type-safe access to edge properties.

4. Filter Extension Traits

Extension methods for the walker builder to filter by edge type:

// On WalkerBuilder
// For all Liked edges
fn filter_liked(self) -> Self;

// For Liked edges with custom criteria
fn filter_by_liked<F>(self, filter: F) -> Self
where
    F: Fn(Liked<Edge>, &Context) -> bool;

Purpose: Enables type-safe filtering of edges during traversals.

Using the Generated Types

Vertex Querying

// Find all Person vertices
let people = graph
    .walk()
    .vertices(Vertex::person())
    .collect::<Vec<_>>();

// Find Person vertices with a specific username
let user = graph
    .walk()
    .vertices(Vertex::person_by_username("bryn123"))
    .first()
    .unwrap();

// Find Person vertices in an age range
let adults = graph
    .walk()
    .vertices(Vertex::person_by_age_range(18..65))
    .collect::<Vec<_>>();

Edge Traversal

// Find outgoing Created edges from a person
let created_projects = graph
    .walk()
    .vertices_by_id([person_id])
    .edges(Edge::created().outgoing())
    .tail()  // Follow edges to target vertices
    .collect::<Vec<_>>();

Type-Safe Filtering

// Find Person vertices that match specific criteria
let experienced_devs = graph
    .walk()
    .vertices(Vertex::person())
    .filter_by_person(|person, _| {
        person.age() > 25 && person.biography().contains("developer")
    })
    .collect::<Vec<_>>();

Projection for Type Safety

// Working with a vertex reference
if let Some(vertex_ref) = graph.vertex(vertex_id) {
    // Project to Person variant
    if let Some(person) = vertex_ref.project::<Person<_>>() {
        println!("Name: {}, Age: {}", person.name(), person.age());
    }
}

Index Attributes

You can apply these attributes to fields to control indexing behavior:

  • #[index] - Standard index for exact match queries
  • #[index(range)] - Range index for range queries
  • #[index(full_text)] - Full-text index for text search (String fields only)
#[derive(VertexExt)]
enum User {
    Profile {
        #[index]  // Standard index
        username: String,
        
        #[index(range)]  // Range queries possible
        age: u32,
        
        #[index(full_text)]  // Text search possible
        bio: String,
        
        // Not indexed
        email: String,
    }
}

Best Practices

  1. Selective Indexing: Only add indexes to fields you'll frequently query, as each index increases memory usage.

  2. Choose Appropriate Index Types:

    • Use #[index] for exact match lookups
    • Use #[index(range)] for numeric fields that need range queries
    • Use #[index(full_text)] for text fields that need substring or keyword search
  3. Use Type-Safe Methods:

    • Prefer filter_by_person() over generic filter() with manual pattern matching
    • Use the generated search methods (person_by_username(), etc.) for efficient queries
  4. Leverage Projections:

    • Use the projection types to safely access variant fields without repetitive pattern matching
    • This makes your code more maintainable and less error-prone
  5. Consider Query Performance:

    • Using an indexed search in the initial vertices() step is typically more efficient than scanning and then filtering
    • The more you can narrow your search using indexes, the better your graph traversal performance

For more detailed information on the derive macros, see the API Reference.