Generate subgraph documentation

ROH: 2023-02-10

Introduction


Code


From your terminal/command line, create a new cargo project and open it with VSCode.


cargo new subgraph_documenter_2000
cd subgraph_documenter_2000
code .



With VSCode now open, click Cargo.toml in the sidebar then add the following dependencies (below [dependencies]). Make sure to save your changes.


reqwest = { version = "0.11.13", features = ["json", "blocking"] }
serde = { version = "1.0.152",  features = ["derive"]}
serde_yaml = "0.9.16"
graphql-parser = "0.4.0"
tokio = { version = "1.23.0", features = ["full"] }


  • serde is a “framework for serializing and deserializing Rust data structures efficiently and generically”
  • reqwest “provides a convenient, higher-level HTTP Client”
  • tokio is an “event-driven, non-blocking I/O platform for writing asynchronous applications with the Rust programming language”
  • serde_yaml
  • graphql-parser



Open src/main.rs in VSCode, delete the main function, and add the following use statements at the top of the file


use std::collections::HashMap;
use std::error::Error;
use std::fs::File;
use std::io::Write;
use std::string::String;

use serde::Deserialize;

use graphql_parser::schema::parse_schema;
use graphql_parser::schema::Definition::{SchemaDefinition, TypeDefinition, TypeExtension, DirectiveDefinition};
use graphql_parser::schema::Document;
use graphql_parser::schema::TypeDefinition::{Scalar, Object, Interface, Union, Enum, InputObject};
use graphql_parser::query::Type;



Next let’s define a few struct statements, then save your modifications.


#[allow(non_snake_case)]
#[derive(Debug, Deserialize)]
struct SubgraphManifest {
    dataSources: Vec<DataSource>,
    description: String,
    repository: String,
    specVersion: String,
    schema: SchemaAddress,
}

#[allow(non_snake_case)]
#[derive(Debug, Deserialize)]
struct SchemaAddress {
    file: HashMap<String, String>,
}

#[allow(non_snake_case)]
#[derive(Debug, Deserialize)]
struct DataSource {
    kind: String,
    mapping: Mapping,
    name: String,
    network: String,
    source: Source,
}

#[allow(non_snake_case)]
#[derive(Debug, Deserialize)]
struct Mapping {
    abis: serde_yaml::Sequence,
    apiVersion: String,
    entities: serde_yaml::Sequence,
    eventHandlers: serde_yaml::Sequence,
    file: HashMap<String, String>,
    kind: String,
    language: String,
}

#[allow(non_snake_case)]
#[derive(Debug, Deserialize)]
struct Source {
    abi: String,
    address: String,
    startBlock: u32,
}

#[allow(non_snake_case)]
#[derive(Debug)]
struct DefintionResult<'a> {
    name: String,
    description: Option<String>,
    fields: Vec<DefinitionField<'a>>,
    values: Vec<DefinitionValue>,
}

#[allow(non_snake_case)]
#[derive(Debug)]
struct DefinitionField<'a> {
    name: String,
    description: Option<String>,
    field_type: Type<'a, String>,
}

#[allow(non_snake_case)]
#[derive(Debug)]
struct DefinitionValue {
    name: String,
    description: Option<String>,
}



Let’s define a few helper functions to streamline main


fn get_manifest(qm_hash: &str) -> Result<SubgraphManifest, Box<dyn Error>> {
    let manifest_url = format!("https://ipfs.io/ipfs/{}", qm_hash);
    let manifest_response = reqwest::blocking::get(manifest_url)?
    .text()?;
    let manifest_data: SubgraphManifest = serde_yaml::from_str(&manifest_response)?;
    Ok(manifest_data)
}

fn get_schema<'a>(qm_hash: &str) -> Result<Document<'static, String>, Box<dyn Error>> {
    let schema_url = format!("https://ipfs.io/{}", qm_hash);
    let schema_response = reqwest::blocking::get(schema_url)?
    .text()?;
    let ast = parse_schema::<String>(&schema_response)?.into_static();
    Ok(ast)
}

fn get_ast_definitions<'a>(ast: &Document<'static, String>) -> Result<Vec<DefintionResult<'static>>, Box<dyn Error>> {
    let mut results: Vec<DefintionResult> = Vec::new();
    for definition in &ast.definitions {
        match definition {
            TypeDefinition(Object(o)) => {
                let mut tmp_fields: Vec<DefinitionField> = Vec::new();
                for field in &o.fields {
                    tmp_fields.push(
                        DefinitionField {
                            name: field.name.clone(),
                            description: field.description.clone(),
                            field_type: field.field_type.clone(),
                        }
                    );
                }
                let mut tmp_values: Vec<DefinitionValue> = Vec::new();
                tmp_values.push(
                    DefinitionValue {
                        name: "None".to_string(),
                        description: None,
                    }
                );
                results.push(
                    DefintionResult {
                        name: o.name.clone(),
                        description: o.description.clone(),
                        fields: tmp_fields,
                        values: tmp_values,
                    }
                );
            },
            TypeDefinition(Enum(e)) => {
                let mut tmp_values: Vec<DefinitionValue> = Vec::new();
                for value in &e.values {
                    tmp_values.push(
                        DefinitionValue {
                            name: value.name.clone(),
                            description: value.description.clone(),
                        }
                    );
                }
                let mut tmp_fields: Vec<DefinitionField> = Vec::new();
                tmp_fields.push(
                    DefinitionField {
                        name: "None".to_string(),
                        description: None,
                        field_type: Type::NamedType("None".to_string()),
                    }
                );
                results.push(
                    DefintionResult {
                        name: e.name.clone(),
                        description: e.description.clone(),
                        fields: tmp_fields,
                        values: tmp_values,
                    }
                );
            },
            // Work on interface next
            TypeDefinition(Interface(i)) => {
                let mut tmp_fields: Vec<DefinitionField> = Vec::new();
                for field in &i.fields {
                    tmp_fields.push(
                        DefinitionField {
                            name: field.name.clone(),
                            description: field.description.clone(),
                            field_type: field.field_type.clone(),
                        }
                    );
                }
                let mut tmp_values: Vec<DefinitionValue> = Vec::new();
                tmp_values.push(
                    DefinitionValue {
                        name: "None".to_string(),
                        description: None,
                    }
                );
                results.push(
                    DefintionResult {
                        name: i.name.clone(),
                        description: i.description.clone(),
                        fields: tmp_fields,
                        values: tmp_values,
                    }
                );
            },
            TypeDefinition(Scalar(s)) => {
                println!("Scalar Coming soon");
            },
            TypeDefinition(Union(u)) => {
                println!("Union Coming soon");
            },
            TypeDefinition(InputObject(io)) => {
                println!("InputObject Coming soon");
            },
            SchemaDefinition(_) | TypeExtension(_) | DirectiveDefinition(_) => todo!(),
        }
    }
    Ok(results)
}

fn generate_overview_page(manifest: &SubgraphManifest) -> Result<(), Box<dyn Error>> {
    let path = "overview.md";

    let mut markdown = String::from("# Subgraph Overview\n");
    markdown.push_str(&format!("{}\n", &manifest.description));
    markdown.push_str("\n");
    markdown.push_str(&format!("{}\n", &manifest.repository));
    markdown.push_str("\n");

    markdown.push_str("| Smart Contract | Address | \n");
    markdown.push_str("| --- | --- | \n");

    for source in &manifest.dataSources {
        markdown.push_str(&format!("| {} | {} | \n", source.name, source.source.address));
    }

    markdown.push_str("\n");

    markdown.push_str("An [API key](https://thegraph.com/docs/en/querying/managing-api-keys/) needed is needed to query our Ethereum subgraph, as it's based on [The Graph](https://thegraph.com/)'s decentralized network. Replace [api-key] with your API key in the API endpoint. [Here](https://thegraph.com/docs/en/studio/managing-api-keys/) is a good guide on how to manage your API keys and set indexer preferences.\n");
    markdown.push_str("* [Creating an API Key Video Tutorial](https://www.youtube.com/watch?v=UrfIpm-Vlgs)\n");


    let mut output = File::create(path)?;
    write!(output, "{}", &markdown)?;
    Ok(())
}

fn generate_entities_page(schema_definitions: &Vec<DefintionResult<'static>>) -> Result<(), Box<dyn Error>> {
    let path = "entities.md";

    let mut markdown = String::from("# Subgraph Entities\n");

    for def in schema_definitions {
        markdown.push_str(&format!("* [{}](#{})\n", def.name, def.name.to_lowercase()));
    }

    markdown.push_str("\n");

    for def in schema_definitions {
        markdown.push_str(&format!("## {}\n", def.name));
        match &def.description {
            Some(d) => markdown.push_str(&format!("{}\n", d)),
            None => (),
        }
        markdown.push_str("\n");

        markdown.push_str("| Field/Value | Type | Description | \n");
        markdown.push_str("| --- | --- | --- | \n");

        // if fields not the null placeholder
        // add fields to entities table
        for field in &def.fields {
            if field.name != "None" {

                match &field.description {
                    Some(d) => markdown.push_str(&format!("| {} | {} | {} | \n", field.name, field.field_type, d)),
                    None => markdown.push_str(&format!("| {} | {} | | \n", field.name, field.field_type)),
                }
            }
        }

        // if values not the null placeholder
        // add values to entities table
        for value in &def.values {
            if value.name != "None" {
                match &value.description {
                    Some(d) => markdown.push_str(&format!("| {} | | {} | \n", value.name, d)),
                    None => markdown.push_str(&format!("| {} | | | \n", value.name)),
                }
            }
        }

        markdown.push_str("\n\n\n");

    }

    let mut output = File::create(path)?;
    write!(output, "{}", &markdown)?;
    Ok(())
}



Finally we can now update our main function as such


#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>>{
    let manifest_qm_hash = "QmbW34MGRyp7LWkpDyKXDLWsKrN8iqZrNAMjGTYHN2zHa1";
    let manifest: SubgraphManifest = get_manifest(manifest_qm_hash)?;

    let schema_qm_hash = manifest.schema.file.get("/").unwrap();
    let schema = get_schema(schema_qm_hash)?;
    let schema_definitions = get_ast_definitions(&schema)?;

    generate_overview_page(&manifest)?;
    generate_entities_page(&schema_definitions)?;

    println!("Subgraph documentation is ready");

    Ok(())
}



Now that the main function is complete, save your changes and run the program from the integrated terminal in VSCode.

cargo run


Open overview.md and entities.md to see your subgraph documentation.


Closing thoughts


The subgraph docs are pretty nice, but is there anything you would change in the “production” version?

  • How might you refine entity types?



Reference code


use std::collections::HashMap;
use std::error::Error;
use std::fs::File;
use std::io::{Write};
use std::string::String;

use serde::Deserialize;

use graphql_parser::schema::parse_schema;
use graphql_parser::schema::Definition::{SchemaDefinition, TypeDefinition, TypeExtension, DirectiveDefinition};
use graphql_parser::schema::Document;
use graphql_parser::schema::TypeDefinition::{Scalar, Object, Interface, Union, Enum, InputObject};
use graphql_parser::query::Type;

#[allow(non_snake_case)]
#[derive(Debug, Deserialize)]
struct SubgraphManifest {
    dataSources: Vec<DataSource>,
    description: String,
    repository: String,
    specVersion: String,
    schema: SchemaAddress,
}

#[allow(non_snake_case)]
#[derive(Debug, Deserialize)]
struct SchemaAddress {
    file: HashMap<String, String>,
}

#[allow(non_snake_case)]
#[derive(Debug, Deserialize)]
struct DataSource {
    kind: String,
    mapping: Mapping,
    name: String,
    network: String,
    source: Source,
}

#[allow(non_snake_case)]
#[derive(Debug, Deserialize)]
struct Mapping {
    abis: serde_yaml::Sequence,
    apiVersion: String,
    entities: serde_yaml::Sequence,
    eventHandlers: serde_yaml::Sequence,
    file: HashMap<String, String>,
    kind: String,
    language: String,
}

#[allow(non_snake_case)]
#[derive(Debug, Deserialize)]
struct Source {
    abi: String,
    address: String,
    startBlock: u32,
}

#[allow(non_snake_case)]
#[derive(Debug)]
struct DefintionResult<'a> {
    name: String,
    description: Option<String>,
    fields: Vec<DefinitionField<'a>>,
    values: Vec<DefinitionValue>,
}

#[allow(non_snake_case)]
#[derive(Debug)]
struct DefinitionField<'a> {
    name: String,
    description: Option<String>,
    field_type: Type<'a, String>,
}

#[allow(non_snake_case)]
#[derive(Debug)]
struct DefinitionValue {
    name: String,
    description: Option<String>,
}

fn get_manifest(qm_hash: &str) -> Result<SubgraphManifest, Box<dyn Error>> {
    let manifest_url = format!("https://ipfs.io/ipfs/{}", qm_hash);
    let manifest_response = reqwest::blocking::get(manifest_url)?
    .text()?;
    let manifest_data: SubgraphManifest = serde_yaml::from_str(&manifest_response)?;
    Ok(manifest_data)
}

fn get_schema<'a>(qm_hash: &str) -> Result<Document<'static, String>, Box<dyn Error>> {
    let schema_url = format!("https://ipfs.io/{}", qm_hash);
    let schema_response = reqwest::blocking::get(schema_url)?
    .text()?;
    let ast = parse_schema::<String>(&schema_response)?.into_static();
    Ok(ast)
}

fn get_ast_definitions<'a>(ast: &Document<'static, String>) -> Result<Vec<DefintionResult<'static>>, Box<dyn Error>> {
    let mut results: Vec<DefintionResult> = Vec::new();
    for definition in &ast.definitions {
        match definition {
            TypeDefinition(Object(o)) => {
                let mut tmp_fields: Vec<DefinitionField> = Vec::new();
                for field in &o.fields {
                    tmp_fields.push(
                        DefinitionField {
                            name: field.name.clone(),
                            description: field.description.clone(),
                            field_type: field.field_type.clone(),
                        }
                    );
                }
                let mut tmp_values: Vec<DefinitionValue> = Vec::new();
                tmp_values.push(
                    DefinitionValue {
                        name: "None".to_string(),
                        description: None,
                    }
                );
                results.push(
                    DefintionResult {
                        name: o.name.clone(),
                        description: o.description.clone(),
                        fields: tmp_fields,
                        values: tmp_values,
                    }
                );
            },
            TypeDefinition(Enum(e)) => {
                let mut tmp_values: Vec<DefinitionValue> = Vec::new();
                for value in &e.values {
                    tmp_values.push(
                        DefinitionValue {
                            name: value.name.clone(),
                            description: value.description.clone(),
                        }
                    );
                }
                let mut tmp_fields: Vec<DefinitionField> = Vec::new();
                tmp_fields.push(
                    DefinitionField {
                        name: "None".to_string(),
                        description: None,
                        field_type: Type::NamedType("None".to_string()),
                    }
                );
                results.push(
                    DefintionResult {
                        name: e.name.clone(),
                        description: e.description.clone(),
                        fields: tmp_fields,
                        values: tmp_values,
                    }
                );
            },
            // Work on interface next
            TypeDefinition(Interface(i)) => {
                let mut tmp_fields: Vec<DefinitionField> = Vec::new();
                for field in &i.fields {
                    tmp_fields.push(
                        DefinitionField {
                            name: field.name.clone(),
                            description: field.description.clone(),
                            field_type: field.field_type.clone(),
                        }
                    );
                }
                let mut tmp_values: Vec<DefinitionValue> = Vec::new();
                tmp_values.push(
                    DefinitionValue {
                        name: "None".to_string(),
                        description: None,
                    }
                );
                results.push(
                    DefintionResult {
                        name: i.name.clone(),
                        description: i.description.clone(),
                        fields: tmp_fields,
                        values: tmp_values,
                    }
                );
            },
            TypeDefinition(Scalar(s)) => {
                println!("Scalar Coming soon");
            },
            TypeDefinition(Union(u)) => {
                println!("Union Coming soon");
            },
            TypeDefinition(InputObject(io)) => {
                println!("InputObject Coming soon");
            },
            SchemaDefinition(_) | TypeExtension(_) | DirectiveDefinition(_) => todo!(),
        }
    }
    Ok(results)
}

fn generate_overview_page(manifest: &SubgraphManifest) -> Result<(), Box<dyn Error>> {
    let path = "overview.md";

    let mut markdown = String::from("# Subgraph Overview\n");
    markdown.push_str(&format!("{}\n", &manifest.description));
    markdown.push_str("\n");
    markdown.push_str(&format!("{}\n", &manifest.repository));
    markdown.push_str("\n");

    markdown.push_str("| Smart Contract | Address | \n");
    markdown.push_str("| --- | --- | \n");

    for source in &manifest.dataSources {
        markdown.push_str(&format!("| {} | {} | \n", source.name, source.source.address));
    }

    markdown.push_str("\n");

    markdown.push_str("An [API key](https://thegraph.com/docs/en/querying/managing-api-keys/) needed is needed to query our Ethereum subgraph, as it's based on [The Graph](https://thegraph.com/)'s decentralized network. Replace [api-key] with your API key in the API endpoint. [Here](https://thegraph.com/docs/en/studio/managing-api-keys/) is a good guide on how to manage your API keys and set indexer preferences.\n");
    markdown.push_str("* [Creating an API Key Video Tutorial](https://www.youtube.com/watch?v=UrfIpm-Vlgs)\n");


    let mut output = File::create(path)?;
    write!(output, "{}", &markdown)?;
    Ok(())
}

fn generate_entities_page(schema_definitions: &Vec<DefintionResult<'static>>) -> Result<(), Box<dyn Error>> {
    let path = "entities.md";

    let mut markdown = String::from("# Subgraph Entities\n");

    for def in schema_definitions {
        markdown.push_str(&format!("* [{}](#{})\n", def.name, def.name.to_lowercase()));
    }

    markdown.push_str("\n");

    for def in schema_definitions {
        markdown.push_str(&format!("## {}\n", def.name));
        match &def.description {
            Some(d) => markdown.push_str(&format!("{}\n", d)),
            None => (),
        }
        markdown.push_str("\n");

        markdown.push_str("| Field/Value | Type | Description | \n");
        markdown.push_str("| --- | --- | --- | \n");

        // if fields not the null placeholder
        // add fields to entities table
        for field in &def.fields {
            if field.name != "None" {

                match &field.description {
                    Some(d) => markdown.push_str(&format!("| {} | {} | {} | \n", field.name, field.field_type, d)),
                    None => markdown.push_str(&format!("| {} | {} | | \n", field.name, field.field_type)),
                }
            }
        }

        // if values not the null placeholder
        // add values to entities table
        for value in &def.values {
            if value.name != "None" {
                match &value.description {
                    Some(d) => markdown.push_str(&format!("| {} | | {} | \n", value.name, d)),
                    None => markdown.push_str(&format!("| {} | | | \n", value.name)),
                }
            }
        }

        markdown.push_str("\n\n\n");

    }

    let mut output = File::create(path)?;
    write!(output, "{}", &markdown)?;
    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>>{
    let manifest_qm_hash = "QmbW34MGRyp7LWkpDyKXDLWsKrN8iqZrNAMjGTYHN2zHa1";
    let manifest: SubgraphManifest = get_manifest(manifest_qm_hash)?;

    let schema_qm_hash = manifest.schema.file.get("/").unwrap();
    let schema = get_schema(schema_qm_hash)?;
    let schema_definitions = get_ast_definitions(&schema)?;

    generate_overview_page(&manifest)?;
    generate_entities_page(&schema_definitions)?;

    println!("Subgraph documentation is ready");

    Ok(())
}