refactor: rename module to docgen & implement tests for package

This commit is contained in:
JasterV 2026-02-05 19:37:58 +01:00
commit 124bd94467
10 changed files with 541 additions and 285 deletions

24
Cargo.lock generated
View file

@ -340,8 +340,10 @@ version = "0.7.0"
dependencies = [ dependencies = [
"clap", "clap",
"colored", "colored",
"granc_core 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "granc_core",
"prost-build",
"serde_json", "serde_json",
"tempfile",
"tokio", "tokio",
] ]
@ -364,26 +366,6 @@ dependencies = [
"tonic-reflection", "tonic-reflection",
] ]
[[package]]
name = "granc_core"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e87c4196f9cdf96a69381385c7029aab768d4d7bc6b1ff90654bf6c3eb19d2"
dependencies = [
"futures-util",
"http",
"http-body",
"prost",
"prost-reflect",
"prost-types",
"serde_json",
"thiserror",
"tokio",
"tokio-stream",
"tonic",
"tonic-reflection",
]
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.4.13" version = "0.4.13"

View file

@ -19,5 +19,8 @@ tokio = { version = "1.49.0" }
prost = "0.14" prost = "0.14"
prost-reflect = "0.16.3" prost-reflect = "0.16.3"
prost-types = "0.14" prost-types = "0.14"
prost-build = "0.14"
tonic = "0.14" tonic = "0.14"
tonic-prost = "0.14.3"
tonic-reflection = "0.14" tonic-reflection = "0.14"
tonic-prost-build = "0.14"

View file

@ -5,10 +5,10 @@ publish = false
[dependencies] [dependencies]
bytes = "1" bytes = "1"
prost = "0.14" prost = { workspace = true }
tonic = "0.14" tonic = { workspace = true }
prost-types = "0.14" prost-types = { workspace = true }
tonic-prost = "0.14.3" tonic-prost = { workspace = true }
[build-dependencies] [build-dependencies]
tonic-prost-build = "0.14" tonic-prost-build = { workspace = true }

View file

@ -37,6 +37,33 @@ pub enum Descriptor {
} }
impl Descriptor { impl Descriptor {
/// Returns the name (e.g.,`MyMessage`) of the inner descriptor
pub fn name(&self) -> &str {
match self {
Descriptor::MessageDescriptor(v) => v.name(),
Descriptor::ServiceDescriptor(v) => v.name(),
Descriptor::EnumDescriptor(v) => v.name(),
}
}
/// Returns the full_name (e.g.,`my.package.v1.MyMessage`) of the inner descriptor
pub fn full_name(&self) -> &str {
match self {
Descriptor::MessageDescriptor(v) => v.full_name(),
Descriptor::ServiceDescriptor(v) => v.full_name(),
Descriptor::EnumDescriptor(v) => v.full_name(),
}
}
/// Returns the package name (e.g.,`my.package.v1`) of the inner descriptor
pub fn package_name(&self) -> &str {
match self {
Descriptor::MessageDescriptor(v) => v.package_name(),
Descriptor::ServiceDescriptor(v) => v.package_name(),
Descriptor::EnumDescriptor(v) => v.package_name(),
}
}
/// Returns the inner [`MessageDescriptor`] if this variant is `MessageDescriptor`. /// Returns the inner [`MessageDescriptor`] if this variant is `MessageDescriptor`.
pub fn message_descriptor(&self) -> Option<&MessageDescriptor> { pub fn message_descriptor(&self) -> Option<&MessageDescriptor> {
match self { match self {

View file

@ -16,6 +16,10 @@ version = "0.7.0"
[dependencies] [dependencies]
clap = { version = "4.5.56", features = ["derive"] } clap = { version = "4.5.56", features = ["derive"] }
colored = "3.1.1" colored = "3.1.1"
granc_core = "0.6.0" granc_core = { path = "../granc-core" }
serde_json = { workspace = true } serde_json = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[dev-dependencies]
prost-build = { workspace = true }
tempfile = "3"

2
granc/src/docgen.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod markdown;
mod package;

View file

@ -0,0 +1,200 @@
use super::package::{Package, Packages};
use crate::formatter::FormattedString;
use granc_core::prost_reflect::{EnumDescriptor, Kind, MessageDescriptor, ServiceDescriptor};
use std::fs;
use std::path::PathBuf;
pub fn generate(output_dir: PathBuf, service: ServiceDescriptor) -> std::io::Result<()> {
// Disable colors for plain text generation
colored::control::set_override(false);
if !output_dir.exists() {
fs::create_dir_all(&output_dir)?;
}
let packages = Packages::from(service.clone());
for package in packages.values() {
let filename = format!("{}.md", package.name);
let path = output_dir.join(&filename);
let out = generate_package_file(package)?;
fs::write(path, out)?;
println!("Generated: {}", filename);
}
let path = output_dir.join("index.md");
let out = generate_index(&service, &packages)?;
fs::write(path, out)?;
println!("Generated: index.md");
// Restore colors
colored::control::unset_override();
Ok(())
}
fn generate_index(
entry_service: &ServiceDescriptor,
packages: &Packages,
) -> std::io::Result<String> {
let mut out = String::new();
out.push_str(&format!("# Documentation: `{}`\n\n", entry_service.name()));
let svc_package = entry_service.package_name();
let svc_link = format!("{}.md#{}", svc_package, entry_service.name());
out.push_str("## Entry Point\n\n");
out.push_str(&format!(
"- [**Service: {}**]({})\n",
entry_service.name(),
svc_link
));
out.push_str("\n## Namespaces\n\n");
// Collect package names (Google packages included)
let mut package_names: Vec<_> = packages.names().collect();
package_names.sort();
if package_names.is_empty() {
out.push_str("*None*\n");
} else {
for name in package_names {
out.push_str(&format!("- [{}]({}.md)\n", name, name));
}
}
Ok(out)
}
fn generate_package_file(package: &Package) -> std::io::Result<String> {
let mut out = String::new();
out.push_str(&format!("# Namespace: `{}`\n\n", package.name));
// 1. Services (Always on top)
let mut services = package.services.clone();
services.sort_by(|a, b| a.name().cmp(b.name()));
for service in services {
write_anchor(&mut out, service.name());
out.push_str(&format!("## {}\n\n", service.name()));
write_service_content(&mut out, &service);
out.push_str("---\n\n");
}
// 2. Messages
let mut messages = package.messages.clone();
messages.sort_by(|a, b| a.name().cmp(b.name()));
for message in messages {
write_anchor(&mut out, message.name());
out.push_str(&format!("## {}\n\n", message.name()));
write_message_content(&mut out, &message);
out.push_str("---\n\n");
}
// 3. Enums
let mut enums = package.enums.clone();
enums.sort_by(|a, b| a.name().cmp(b.name()));
for enum_desc in enums {
write_anchor(&mut out, enum_desc.name());
out.push_str(&format!("## {}\n\n", enum_desc.name()));
write_enum_content(&mut out, &enum_desc);
out.push_str("---\n\n");
}
Ok(out)
}
fn write_anchor(out: &mut String, name: &str) {
out.push_str(&format!("<a id=\"{}\"></a>\n", name));
}
fn write_service_content(out: &mut String, service: &ServiceDescriptor) {
out.push_str("**Type**: `Service`\n\n");
out.push_str(&format!("**Full Name**: `{}`\n\n", service.full_name()));
out.push_str("### Definition\n\n```protobuf\n");
out.push_str(&FormattedString::from(service.clone()).0);
out.push_str("\n```\n\n");
out.push_str("### Methods\n\n");
for method in service.methods() {
out.push_str(&format!("#### `{}`\n\n", method.name()));
let input = method.input();
let output = method.output();
let input_link = resolve_link(input.package_name(), input.name());
let output_link = resolve_link(output.package_name(), output.name());
out.push_str(&format!("- Request: [{}]({})\n", input.name(), input_link));
out.push_str(&format!(
"- Response: [{}]({})\n",
output.name(),
output_link
));
out.push('\n');
}
}
fn write_message_content(out: &mut String, message: &MessageDescriptor) {
out.push_str("**Type**: `Message`\n\n");
out.push_str(&format!("**Full Name**: `{}`\n\n", message.full_name()));
out.push_str("### Definition\n\n```protobuf\n");
out.push_str(&FormattedString::from(message.clone()).0);
out.push_str("\n```\n\n");
out.push_str("### Dependencies\n\n");
let mut has_deps = false;
for field in message.fields() {
match field.kind() {
Kind::Message(m) => {
has_deps = true;
let link = resolve_link(m.package_name(), m.name());
out.push_str(&format!(
"- Field `{}`: [{}]({})\n",
field.name(),
m.name(),
link
));
}
Kind::Enum(e) => {
has_deps = true;
let link = resolve_link(e.package_name(), e.name());
out.push_str(&format!(
"- Field `{}`: [{}]({})\n",
field.name(),
e.name(),
link
));
}
_ => {}
}
}
if !has_deps {
out.push_str("*None*\n");
}
out.push('\n');
}
fn write_enum_content(out: &mut String, enum_desc: &EnumDescriptor) {
out.push_str("**Type**: `Enum`\n\n");
out.push_str(&format!("**Full Name**: `{}`\n\n", enum_desc.full_name()));
out.push_str("### Definition\n\n```protobuf\n");
out.push_str(&FormattedString::from(enum_desc.clone()).0);
out.push_str("\n```\n\n");
}
fn resolve_link(package: &str, name: &str) -> String {
// Always link to local file + anchor
format!("{}.md#{}", package, name)
}

292
granc/src/docgen/package.rs Normal file
View file

@ -0,0 +1,292 @@
use granc_core::{
client::Descriptor,
prost_reflect::{EnumDescriptor, Kind, MessageDescriptor, ServiceDescriptor},
};
use std::collections::{HashMap, hash_map::Keys};
pub(crate) struct Package {
pub name: String,
pub services: Vec<ServiceDescriptor>,
pub messages: Vec<MessageDescriptor>,
pub enums: Vec<EnumDescriptor>,
}
impl Package {
fn new(name: String) -> Self {
Package {
name,
services: vec![],
messages: vec![],
enums: vec![],
}
}
fn push_descriptor(&mut self, descriptor: Descriptor) {
match descriptor {
Descriptor::MessageDescriptor(v) => self.messages.push(v),
Descriptor::ServiceDescriptor(v) => self.services.push(v),
Descriptor::EnumDescriptor(v) => self.enums.push(v),
}
}
}
impl From<Descriptor> for Package {
fn from(value: Descriptor) -> Self {
let package_name = value.package_name().to_string();
let mut package = Package::new(package_name);
package.push_descriptor(value);
package
}
}
pub(crate) struct Packages(HashMap<String, Package>);
impl Packages {
pub fn values(&self) -> std::collections::hash_map::Values<'_, String, Package> {
self.0.values()
}
pub fn names(&self) -> Keys<'_, String, Package> {
self.0.keys()
}
}
impl From<ServiceDescriptor> for Packages {
fn from(value: ServiceDescriptor) -> Self {
// Collect all reachable descriptors (Messages, Enums) from the Service methods
let mut descriptors: HashMap<String, Descriptor> = value
.methods()
.flat_map(|m| [m.input(), m.output()])
.fold(HashMap::new(), |mut acc, d| {
let message_name = d.full_name().to_string();
if acc.contains_key(&message_name) {
return acc;
}
acc.insert(message_name, Descriptor::MessageDescriptor(d.clone()));
collect_message_dependencies(acc, &d)
});
// Insert the Service itself
descriptors.insert(
value.full_name().to_string(),
Descriptor::ServiceDescriptor(value),
);
// Group into Packages
let packages: HashMap<_, Package> =
descriptors
.into_values()
.fold(HashMap::new(), |mut acc, descriptor| {
let package_name = descriptor.package_name();
match acc.get_mut(package_name) {
Some(package) => package.push_descriptor(descriptor),
None => {
let _ = acc.insert(package_name.to_string(), Package::from(descriptor));
}
}
acc
});
Packages(packages)
}
}
fn collect_message_dependencies(
descriptors: HashMap<String, Descriptor>,
message: &MessageDescriptor,
) -> HashMap<String, Descriptor> {
message
.fields()
.fold(descriptors, |mut acc, field| match field.kind() {
Kind::Message(m) => {
let message_name = m.full_name().to_string();
if acc.contains_key(&message_name) {
return acc;
}
acc.insert(message_name, Descriptor::MessageDescriptor(m.clone()));
collect_message_dependencies(acc, &m)
}
Kind::Enum(e) => {
acc.insert(e.full_name().to_string(), Descriptor::EnumDescriptor(e));
acc
}
_ => acc,
})
}
#[cfg(test)]
mod tests {
use super::*;
use granc_core::prost_reflect::DescriptorPool;
use std::fs;
/// Helper to compile proto strings into a DescriptorPool at runtime.
///
/// # Arguments
/// * `files` - A list of tuples (filename, content). E.g. `[("test.proto", "syntax=...")]`
fn compile_protos(files: &[(&str, &str)]) -> DescriptorPool {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let proto_dir = temp_dir.path().join("protos");
fs::create_dir(&proto_dir).expect("Failed to create protos dir");
let mut proto_paths = Vec::new();
for (name, content) in files {
let path = proto_dir.join(name);
fs::write(&path, content).expect("Failed to write proto file");
proto_paths.push(path);
}
let descriptor_path = temp_dir.path().join("descriptor.bin");
// Compile using prost_build
let mut config = prost_build::Config::new();
config.file_descriptor_set_path(&descriptor_path);
// We set out_dir to temp_dir because we don't care about the generated Rust code,
// we only want the descriptor set.
config.out_dir(temp_dir.path());
config
.compile_protos(&proto_paths, &[proto_dir])
.expect("Failed to compile protos");
let bytes = fs::read(descriptor_path).expect("Failed to read descriptor set");
DescriptorPool::decode(bytes.as_slice()).expect("Failed to decode descriptor pool")
}
#[test]
fn test_package_collection_with_deduplication() {
let proto = r#"
syntax = "proto3";
package test;
enum Status {
UNKNOWN = 0;
OK = 1;
}
message Request {
Status status = 1;
}
message Response {
Status status = 1;
}
service MyService {
rpc DoSomething(Request) returns (Response);
}
"#;
let pool = compile_protos(&[("test.proto", proto)]);
let service = pool
.get_service_by_name("test.MyService")
.expect("Service not found");
// --- Act ---
let packages = Packages::from(service);
// --- Assert ---
let test_package = packages.0.get("test").expect("Package 'test' missing");
// Verify Services
assert_eq!(test_package.services.len(), 1);
assert_eq!(test_package.services[0].name(), "MyService");
// Verify Messages
assert_eq!(test_package.messages.len(), 2);
let msg_names: Vec<_> = test_package.messages.iter().map(|m| m.name()).collect();
assert!(msg_names.contains(&"Request"));
assert!(msg_names.contains(&"Response"));
// Verify Enums (Deduplication Check)
assert_eq!(
test_package.enums.len(),
1,
"Enum should appear exactly once"
);
assert_eq!(test_package.enums[0].name(), "Status");
}
#[test]
fn test_circular_dependency_handling() {
let proto = r#"
syntax = "proto3";
package cycle;
message NodeA {
NodeB child = 1;
}
message NodeB {
NodeA parent = 1;
}
service Cycler {
rpc Cycle(NodeA) returns (NodeA);
}
"#;
let pool = compile_protos(&[("cycle.proto", proto)]);
let service = pool
.get_service_by_name("cycle.Cycler")
.expect("Service not found");
// --- Act ---
let packages = Packages::from(service);
// --- Assert ---
let pkg = packages.0.get("cycle").expect("Package 'cycle' missing");
assert_eq!(pkg.messages.len(), 2);
let names: Vec<_> = pkg.messages.iter().map(|m| m.name()).collect();
assert!(names.contains(&"NodeA"));
assert!(names.contains(&"NodeB"));
}
#[test]
fn test_multi_file_imports() {
let common_proto = r#"
syntax = "proto3";
package common;
message Shared {
string id = 1;
}
"#;
let app_proto = r#"
syntax = "proto3";
package app;
import "common.proto";
service AppService {
rpc Get(common.Shared) returns (common.Shared);
}
"#;
let pool = compile_protos(&[("common.proto", common_proto), ("app.proto", app_proto)]);
let service = pool
.get_service_by_name("app.AppService")
.expect("Service not found");
let packages = Packages::from(service);
// Assert Package 'app'
let app_pkg = packages.0.get("app").expect("Package 'app' missing");
assert_eq!(app_pkg.services.len(), 1);
// Assert Package 'common' (Ensures traversal follows imports)
let common_pkg = packages.0.get("common").expect("Package 'common' missing");
assert_eq!(common_pkg.messages.len(), 1);
assert_eq!(common_pkg.messages[0].name(), "Shared");
}
}

View file

@ -1,251 +0,0 @@
// granc/src/docs.rs
use crate::formatter::FormattedString;
use granc_core::client::Descriptor;
use granc_core::prost_reflect::{EnumDescriptor, Kind, MessageDescriptor, ServiceDescriptor};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::PathBuf;
#[derive(Default)]
struct Package {
services: Vec<ServiceDescriptor>,
messages: Vec<MessageDescriptor>,
enums: Vec<EnumDescriptor>,
}
pub struct DocsGenerator {
output_dir: PathBuf,
visited: HashSet<String>,
}
impl DocsGenerator {
pub fn new(output_dir: PathBuf) -> Self {
Self {
output_dir,
visited: HashSet::new(),
}
}
/// Entry point for documentation generation.
pub fn generate(&mut self, service: &ServiceDescriptor) -> std::io::Result<()> {
// Force colored output OFF so we get plain text for the markdown files
colored::control::set_override(false);
if !self.output_dir.exists() {
fs::create_dir_all(&self.output_dir)?;
}
let _packages = collect_descriptors(service.clone());
// 1. Generate the Service page and recursively all dependencies
self.generate_service(service)?;
// 2. Generate the Index (Table of Contents)
self.generate_index(service)?;
// Restore colored output for the CLI
colored::control::unset_override();
Ok(())
}
fn generate_index(&self, service: &ServiceDescriptor) -> std::io::Result<()> {
let path = self.output_dir.join("index.md");
let mut out = String::new();
out.push_str(&format!("# Documentation: `{}`\n\n", service.name()));
out.push_str("## Entry Point\n\n");
out.push_str(&format!(
"- [**Service Definition: {}**]({}.md)\n",
service.name(),
service.full_name()
));
out.push_str("\n## Messages & Enums\n\n");
let mut types: Vec<_> = self.visited.iter().collect();
types.sort();
if types.is_empty() {
out.push_str("*None*\n");
} else {
for name in types {
out.push_str(&format!("- [{}]({}.md)\n", name, name));
}
}
fs::write(path, out)?;
println!("Generated: index.md");
Ok(())
}
fn generate_service(&mut self, service: &ServiceDescriptor) -> std::io::Result<()> {
let filename = format!("{}.md", service.full_name());
let path = self.output_dir.join(&filename);
let mut out = String::new();
out.push_str(&format!("# Service: `{}`\n\n", service.name()));
// 1. Protobuf Definition
out.push_str("## Definition\n\n```protobuf\n");
out.push_str(&FormattedString::from(service.clone()).0);
out.push_str("\n```\n\n");
// 2. Methods List
out.push_str("## Methods\n\n");
for method in service.methods() {
out.push_str(&format!("### `{}`\n\n", method.name()));
let input = method.input();
let output = method.output();
out.push_str(&format!("- Request: [{0}]({0}.md)\n", input.full_name()));
out.push_str(&format!("- Response: [{0}]({0}.md)\n", output.full_name()));
out.push('\n');
// Queue recursion for dependencies
self.queue_message(input);
self.queue_message(output);
}
fs::write(path, out)?;
println!("Generated: {}", filename);
Ok(())
}
fn generate_message(&mut self, message: MessageDescriptor) -> std::io::Result<()> {
let filename = format!("{}.md", message.full_name());
let path = self.output_dir.join(&filename);
let mut out = String::new();
out.push_str(&format!("# Message: `{}`\n\n", message.name()));
// Definition
out.push_str("## Definition\n\n```protobuf\n");
out.push_str(&FormattedString::from(message.clone()).0);
out.push_str("\n```\n\n");
// Dependencies
out.push_str("## Dependencies\n\n");
let mut has_deps = false;
for field in message.fields() {
match field.kind() {
Kind::Message(m) => {
has_deps = true;
out.push_str(&format!(
"- Field `{0}`: [{1}]({1}.md)\n",
field.name(),
m.full_name()
));
self.queue_message(m);
}
Kind::Enum(e) => {
has_deps = true;
out.push_str(&format!(
"- Field `{}`: [{}]({}.md)\n",
field.name(),
e.full_name(),
e.full_name()
));
self.queue_enum(e);
}
_ => {}
}
}
if !has_deps {
out.push_str("*None*\n");
}
fs::write(path, out)?;
println!("Generated: {}", filename);
Ok(())
}
fn generate_enum(&mut self, enum_desc: EnumDescriptor) -> std::io::Result<()> {
let filename = format!("{}.md", enum_desc.full_name());
let path = self.output_dir.join(&filename);
let mut out = String::new();
out.push_str(&format!("# Enum: `{}`\n\n", enum_desc.name()));
out.push_str("## Definition\n\n```protobuf\n");
out.push_str(&FormattedString::from(enum_desc).0);
out.push_str("\n```\n");
fs::write(path, out)?;
println!("Generated: {}", filename);
Ok(())
}
fn queue_message(&mut self, message: MessageDescriptor) {
let name = message.full_name().to_string();
if !self.visited.insert(name) {
return;
}
if let Err(e) = self.generate_message(message) {
eprintln!("Failed to generate docs for message: {}", e);
}
}
fn queue_enum(&mut self, enum_desc: EnumDescriptor) {
let name = enum_desc.full_name().to_string();
if !self.visited.insert(name) {
return;
}
if let Err(e) = self.generate_enum(enum_desc) {
eprintln!("Failed to generate docs for enum: {}", e);
}
}
}
fn collect_descriptors(entrypoint: ServiceDescriptor) -> HashMap<String, Descriptor> {
let descriptors: HashMap<_, _> = [].into();
let mut descriptors = collect_service_dependencies(descriptors, &entrypoint);
descriptors.insert(
entrypoint.full_name().to_string(),
Descriptor::ServiceDescriptor(entrypoint),
);
descriptors
}
fn collect_service_dependencies(
descriptors: HashMap<String, Descriptor>,
service: &ServiceDescriptor,
) -> HashMap<String, Descriptor> {
service
.methods()
.flat_map(|m| [m.input(), m.output()])
.fold(descriptors, |acc, d| {
let mut descriptors = collect_message_dependencies(acc, &d);
descriptors.insert(d.full_name().to_string(), Descriptor::MessageDescriptor(d));
descriptors
})
}
fn collect_message_dependencies(
descriptors: HashMap<String, Descriptor>,
message: &MessageDescriptor,
) -> HashMap<String, Descriptor> {
message
.fields()
.fold(descriptors, |mut acc, field| match field.kind() {
Kind::Message(m) => {
let mut descriptors = collect_message_dependencies(acc, &m);
descriptors.insert(m.full_name().to_string(), Descriptor::MessageDescriptor(m));
descriptors
}
Kind::Enum(e) => {
acc.insert(field.full_name().to_string(), Descriptor::EnumDescriptor(e));
acc
}
_ => acc,
})
}

View file

@ -8,7 +8,7 @@
//! 3. **Execution**: Delegates request processing to `GrancClient`. //! 3. **Execution**: Delegates request processing to `GrancClient`.
//! 4. **Presentation**: Formats and prints data. //! 4. **Presentation**: Formats and prints data.
mod cli; mod cli;
mod docs; mod docgen;
mod formatter; mod formatter;
use clap::Parser; use clap::Parser;
@ -17,8 +17,6 @@ use formatter::{FormattedString, GenericError};
use granc_core::client::{Descriptor, DynamicRequest, DynamicResponse, GrancClient}; use granc_core::client::{Descriptor, DynamicRequest, DynamicResponse, GrancClient};
use std::process; use std::process;
use crate::docs::DocsGenerator;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let args = Cli::parse(); let args = Cli::parse();
@ -55,15 +53,14 @@ async fn main() {
output, output,
} => { } => {
let descriptor = describe(symbol.clone(), source.value()).await; let descriptor = describe(symbol.clone(), source.value()).await;
let service_descriptor = descriptor let service_descriptor = descriptor
.service_descriptor() .service_descriptor()
.cloned()
.ok_or_else(|| GenericError("The symbol must be a Service", symbol)) .ok_or_else(|| GenericError("The symbol must be a Service", symbol))
.unwrap_or_exit(); .unwrap_or_exit();
let mut generator = DocsGenerator::new(output); docgen::markdown::generate(output, service_descriptor)
generator
.generate(service_descriptor)
.map_err(|e| GenericError("Failed to generate docs", e)) .map_err(|e| GenericError("Failed to generate docs", e))
.unwrap_or_exit(); .unwrap_or_exit();