Generate subgraph documentation
Generate subgraph documentation
ROH: 2023-02-10
Introduction
Code
From your terminal/command line, create a new cargo
project and open it with VSCode.
- If you don’t already have Rust and
cargo
installed, here’s the official installation guide to help you get up and running. - This tutorial assumes you are using Visual Studio Code editor (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(())
}