2025-07-22 11:11:39 +02:00
|
|
|
#!/usr/bin/env python3
|
2026-01-21 12:34:59 +01:00
|
|
|
# Copyright (C) 2025-2026 Whiterun LLC,
|
2025-07-22 11:11:39 +02:00
|
|
|
# This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.,
|
|
|
|
|
# A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
API Documentation Generator for Riften Labs Indexer
|
|
|
|
|
|
|
|
|
|
This tool parses routes from main.rs and generates API documentation
|
|
|
|
|
in markdown format for each mount point.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
import os
|
|
|
|
|
from typing import List, Optional
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class RouteInfo:
|
|
|
|
|
"""Information about a route"""
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
path: str
|
|
|
|
|
function_name: str
|
|
|
|
|
method: str = "GET"
|
|
|
|
|
parameters: List[str] = None
|
|
|
|
|
query_params: List[str] = None
|
|
|
|
|
path_params: List[str] = None
|
|
|
|
|
description: str = ""
|
|
|
|
|
status: str = "unstable"
|
|
|
|
|
is_deprecated: bool = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class MountInfo:
|
|
|
|
|
"""Information about a mount point"""
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
path: str
|
|
|
|
|
routes: List[RouteInfo]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RustRouteParser:
|
|
|
|
|
"""Parser for Rust route definitions"""
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
def __init__(self, main_rs_path: str):
|
|
|
|
|
self.main_rs_path = main_rs_path
|
|
|
|
|
self.mounts: List[MountInfo] = []
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
def parse_mounts(self) -> List[MountInfo]:
|
|
|
|
|
"""Parse all mount points from main.rs"""
|
2025-07-23 08:47:52 +02:00
|
|
|
with open(self.main_rs_path, "r") as f:
|
2025-07-22 11:11:39 +02:00
|
|
|
content = f.read()
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Find all mount blocks - simpler approach
|
|
|
|
|
# Look for .mount( followed by path and routes![
|
|
|
|
|
mount_pattern = r'\.mount\(\s*"([^"]+)"\s*,\s*routes!\[(.*?)\]'
|
|
|
|
|
matches = list(re.finditer(mount_pattern, content, re.DOTALL))
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
for match in matches:
|
|
|
|
|
mount_path = match.group(1)
|
|
|
|
|
routes_content = match.group(2)
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
routes = self._parse_routes(routes_content)
|
|
|
|
|
mount_info = MountInfo(path=mount_path, routes=routes)
|
|
|
|
|
self.mounts.append(mount_info)
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
return self.mounts
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
def _parse_routes(self, routes_content: str) -> List[RouteInfo]:
|
|
|
|
|
"""Parse individual routes from a routes! macro"""
|
|
|
|
|
routes = []
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Split by commas and clean up
|
2025-07-23 08:47:52 +02:00
|
|
|
route_items = [
|
|
|
|
|
item.strip() for item in routes_content.split(",") if item.strip()
|
|
|
|
|
]
|
|
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
for item in route_items:
|
|
|
|
|
# Remove any trailing commas and whitespace
|
2025-07-23 08:47:52 +02:00
|
|
|
item = item.strip().rstrip(",")
|
2025-07-22 11:11:39 +02:00
|
|
|
if item:
|
|
|
|
|
route_info = self._parse_single_route(item)
|
|
|
|
|
if route_info:
|
|
|
|
|
routes.append(route_info)
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
return routes
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
def _parse_single_route(self, route_item: str) -> Optional[RouteInfo]:
|
|
|
|
|
"""Parse a single route item"""
|
|
|
|
|
# Extract function name (remove module prefix if present)
|
2025-07-23 08:47:52 +02:00
|
|
|
if "::" in route_item:
|
|
|
|
|
function_name = route_item.split("::")[-1]
|
2025-07-22 11:11:39 +02:00
|
|
|
else:
|
|
|
|
|
function_name = route_item
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# For now, we'll get the actual route path and parameters from the function definition
|
|
|
|
|
# This is a simplified approach - in a real implementation you'd want to parse the actual function signatures
|
|
|
|
|
return RouteInfo(
|
|
|
|
|
path="", # Will be filled in by function signature parsing
|
|
|
|
|
function_name=function_name,
|
|
|
|
|
parameters=[],
|
|
|
|
|
query_params=[],
|
2025-07-23 08:47:52 +02:00
|
|
|
path_params=[],
|
2025-07-22 11:11:39 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FunctionSignatureParser:
|
|
|
|
|
"""Parser for Rust function signatures to extract route information"""
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
def __init__(self, src_dir: str):
|
|
|
|
|
self.src_dir = src_dir
|
|
|
|
|
self.rpc_modules = {}
|
|
|
|
|
self._load_rpc_modules()
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
def _load_rpc_modules(self):
|
|
|
|
|
"""Load all RPC module files"""
|
2025-07-23 08:47:52 +02:00
|
|
|
rpc_dir = os.path.join(self.src_dir, "rpc")
|
2025-07-22 11:11:39 +02:00
|
|
|
if not os.path.exists(rpc_dir):
|
|
|
|
|
return
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Load files directly in rpc directory
|
|
|
|
|
for file in os.listdir(rpc_dir):
|
2025-07-23 08:47:52 +02:00
|
|
|
if file.endswith(".rs") and file != "mod.rs":
|
2025-07-22 11:11:39 +02:00
|
|
|
module_name = file[:-3] # Remove .rs extension
|
|
|
|
|
file_path = os.path.join(rpc_dir, file)
|
|
|
|
|
self.rpc_modules[module_name] = file_path
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Load files in subdirectories
|
|
|
|
|
for subdir in os.listdir(rpc_dir):
|
|
|
|
|
subdir_path = os.path.join(rpc_dir, subdir)
|
|
|
|
|
if os.path.isdir(subdir_path):
|
|
|
|
|
# Look for mod.rs files in subdirectories
|
2025-07-23 08:47:52 +02:00
|
|
|
mod_rs_path = os.path.join(subdir_path, "mod.rs")
|
2025-07-22 11:11:39 +02:00
|
|
|
if os.path.exists(mod_rs_path):
|
|
|
|
|
self.rpc_modules[subdir] = mod_rs_path
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
def get_function_signature(self, function_name: str) -> Optional[RouteInfo]:
|
|
|
|
|
"""Get function signature and route information for a given function name"""
|
|
|
|
|
for module_name, file_path in self.rpc_modules.items():
|
|
|
|
|
route_info = self._parse_function_in_file(file_path, function_name)
|
|
|
|
|
if route_info:
|
|
|
|
|
return route_info
|
|
|
|
|
return None
|
2025-07-23 08:47:52 +02:00
|
|
|
|
|
|
|
|
def _parse_function_in_file(
|
|
|
|
|
self, file_path: str, function_name: str
|
|
|
|
|
) -> Optional[RouteInfo]:
|
2025-07-22 11:11:39 +02:00
|
|
|
"""Parse a specific function in a file"""
|
|
|
|
|
try:
|
2025-07-23 08:47:52 +02:00
|
|
|
with open(file_path, "r") as f:
|
2025-07-22 11:11:39 +02:00
|
|
|
content = f.read()
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Look for the function definition
|
2025-07-23 08:47:52 +02:00
|
|
|
func_pattern = rf"pub fn {function_name}\s*\([^)]*\)\s*->[^{{]*{{"
|
2025-07-22 11:11:39 +02:00
|
|
|
func_match = re.search(func_pattern, content)
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
if not func_match:
|
|
|
|
|
return None
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Extract description, status, and deprecated flag from comments above the function
|
2025-07-23 08:47:52 +02:00
|
|
|
description, status, is_deprecated = self._extract_function_description(
|
|
|
|
|
content, function_name
|
|
|
|
|
)
|
|
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Skip deprecated functions
|
|
|
|
|
if is_deprecated:
|
|
|
|
|
return None
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Find the #[get("...")] attribute before the function
|
|
|
|
|
get_attr_pattern = rf'#\[get\("([^"]+)"\)\]\s*pub fn {function_name}'
|
|
|
|
|
get_match = re.search(get_attr_pattern, content)
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
if get_match:
|
|
|
|
|
route_path = get_match.group(1)
|
|
|
|
|
route_info = self._parse_route_path(route_path, function_name)
|
|
|
|
|
route_info.description = description
|
|
|
|
|
route_info.status = status
|
|
|
|
|
route_info.is_deprecated = is_deprecated
|
|
|
|
|
return route_info
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# If no #[get] attribute found, return None - we can't determine the route
|
|
|
|
|
return None
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Error parsing {file_path}: {e}")
|
|
|
|
|
return None
|
2025-07-23 08:47:52 +02:00
|
|
|
|
|
|
|
|
def _extract_function_description(
|
|
|
|
|
self, content: str, function_name: str
|
|
|
|
|
) -> tuple[str, str, bool]:
|
2025-07-22 11:11:39 +02:00
|
|
|
"""Extract description, status, and deprecated flag from comments above the function"""
|
|
|
|
|
# Look for the function definition - simpler pattern
|
2025-07-23 08:47:52 +02:00
|
|
|
func_pattern = rf"pub fn {function_name}"
|
2025-07-22 11:11:39 +02:00
|
|
|
func_match = re.search(func_pattern, content)
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
if not func_match:
|
|
|
|
|
return "no description", "unstable", False
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Get the position of the function
|
|
|
|
|
func_start = func_match.start()
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Look backwards from the function for comments
|
|
|
|
|
# Find consecutive /// comment lines before the function
|
2025-07-23 08:47:52 +02:00
|
|
|
lines = content[:func_start].split("\n")
|
2025-07-22 11:11:39 +02:00
|
|
|
comment_lines = []
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Go backwards from the function to find comment lines
|
|
|
|
|
for line in reversed(lines):
|
|
|
|
|
line = line.strip()
|
2025-07-23 08:47:52 +02:00
|
|
|
if line.startswith("///"):
|
2025-07-22 11:11:39 +02:00
|
|
|
comment_lines.insert(0, line[3:].strip()) # Remove /// and strip
|
2025-07-23 08:47:52 +02:00
|
|
|
elif line.startswith("pub fn"):
|
2025-07-22 11:11:39 +02:00
|
|
|
break
|
2025-07-23 08:47:52 +02:00
|
|
|
elif line and not line.startswith("///") and not line.startswith("#["):
|
2025-07-22 11:11:39 +02:00
|
|
|
break
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
if comment_lines:
|
|
|
|
|
# Filter out status lines and clean up the description
|
|
|
|
|
filtered_lines = []
|
|
|
|
|
for line in comment_lines:
|
|
|
|
|
# Skip status lines (they're handled separately)
|
|
|
|
|
if "Status:" in line:
|
|
|
|
|
continue
|
|
|
|
|
# Keep empty lines to preserve paragraph structure
|
|
|
|
|
if not line.strip():
|
|
|
|
|
filtered_lines.append("")
|
|
|
|
|
else:
|
|
|
|
|
# Preserve indentation by removing only the /// prefix
|
2025-07-23 08:47:52 +02:00
|
|
|
cleaned_line = line[3:] if line.startswith("///") else line
|
2025-07-22 11:11:39 +02:00
|
|
|
filtered_lines.append(cleaned_line.rstrip())
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Join filtered lines, preserving original structure
|
2025-07-23 08:47:52 +02:00
|
|
|
description = "\n".join(filtered_lines).strip()
|
|
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Check for deprecated functions
|
2025-07-23 08:47:52 +02:00
|
|
|
is_deprecated = any("deprecated" in line.lower() for line in comment_lines)
|
|
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Check for status
|
|
|
|
|
status = "unstable" # default
|
|
|
|
|
for line in comment_lines:
|
|
|
|
|
if "Status: Stable" in line:
|
|
|
|
|
status = "stable"
|
|
|
|
|
break
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
return description, status, is_deprecated
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
return "no description", "unstable", False
|
|
|
|
|
|
|
|
|
|
def _parse_route_path(self, route_path: str, function_name: str) -> RouteInfo:
|
|
|
|
|
"""Parse route path to extract parameters"""
|
|
|
|
|
path_params = []
|
|
|
|
|
query_params = []
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Split path and query parts
|
2025-07-23 08:47:52 +02:00
|
|
|
if "?" in route_path:
|
|
|
|
|
path_part, query_part = route_path.split("?", 1)
|
2025-07-22 11:11:39 +02:00
|
|
|
else:
|
|
|
|
|
path_part = route_path
|
|
|
|
|
query_part = ""
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Extract path parameters (e.g., <token_id>)
|
2025-07-23 08:47:52 +02:00
|
|
|
path_param_pattern = r"<([^>]+)>"
|
2025-07-22 11:11:39 +02:00
|
|
|
path_params = re.findall(path_param_pattern, path_part)
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Extract query parameters (e.g., <token_id>&<timestamp>)
|
|
|
|
|
if query_part:
|
2025-07-23 08:47:52 +02:00
|
|
|
query_param_pattern = r"<([^>]+)>"
|
2025-07-22 11:11:39 +02:00
|
|
|
query_params = re.findall(query_param_pattern, query_part)
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Clean up the route path for display
|
|
|
|
|
display_path = path_part
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
return RouteInfo(
|
|
|
|
|
path=display_path,
|
|
|
|
|
function_name=function_name,
|
|
|
|
|
method="GET",
|
|
|
|
|
path_params=path_params,
|
|
|
|
|
query_params=query_params,
|
2025-07-23 08:47:52 +02:00
|
|
|
parameters=path_params + query_params,
|
2025-07-22 11:11:39 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class APIDocGenerator:
|
|
|
|
|
"""Generate API documentation in markdown format"""
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
def __init__(self, output_dir: str = "apidoc"):
|
|
|
|
|
self.output_dir = output_dir
|
|
|
|
|
os.makedirs(output_dir, exist_ok=True)
|
2025-07-23 08:47:52 +02:00
|
|
|
|
|
|
|
|
def generate_docs(
|
|
|
|
|
self, mounts: List[MountInfo], function_parser: FunctionSignatureParser
|
|
|
|
|
):
|
2025-07-22 11:11:39 +02:00
|
|
|
"""Generate documentation for all mounts"""
|
|
|
|
|
# Generate index page
|
|
|
|
|
self._generate_index(mounts)
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Generate individual mount pages
|
|
|
|
|
for mount in mounts:
|
|
|
|
|
self._generate_mount_page(mount, function_parser)
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
def _generate_index(self, mounts: List[MountInfo]):
|
|
|
|
|
"""Generate the main index page"""
|
|
|
|
|
index_content = """# Riften Labs Indexer API Documentation
|
|
|
|
|
|
|
|
|
|
This document provides API documentation for the Riften Labs Indexer service.
|
|
|
|
|
|
|
|
|
|
Note that endpoints marked at unsable are subject to change or may be removed.
|
|
|
|
|
If you are using unstable endpoints, let us know in the telegram channel so that we can prioritize them.
|
|
|
|
|
|
|
|
|
|
## Available Endpoints
|
|
|
|
|
|
|
|
|
|
"""
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
for mount in mounts:
|
2025-07-23 08:47:52 +02:00
|
|
|
mount_name = mount.path.strip("/").replace("/", "_") or "root"
|
|
|
|
|
mount_display = mount.path if mount.path else "/"
|
2025-07-22 11:11:39 +02:00
|
|
|
index_content += f"- [{mount_display}]({mount_name}.md) - {len(mount.routes)} endpoints\n"
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
index_content += """
|
|
|
|
|
|
|
|
|
|
## Getting Started
|
|
|
|
|
|
|
|
|
|
All endpoints return JSON responses. Most endpoints support GET requests only.
|
|
|
|
|
|
|
|
|
|
### Authentication
|
|
|
|
|
|
|
|
|
|
Currently, no authentication is required for API access.
|
|
|
|
|
|
|
|
|
|
"""
|
2025-07-23 08:47:52 +02:00
|
|
|
|
|
|
|
|
with open(os.path.join(self.output_dir, "index.md"), "w") as f:
|
2025-07-22 11:11:39 +02:00
|
|
|
f.write(index_content)
|
2025-07-23 08:47:52 +02:00
|
|
|
|
|
|
|
|
def _generate_mount_page(
|
|
|
|
|
self, mount: MountInfo, function_parser: FunctionSignatureParser
|
|
|
|
|
):
|
2025-07-22 11:11:39 +02:00
|
|
|
"""Generate documentation for a specific mount point"""
|
2025-07-23 08:47:52 +02:00
|
|
|
mount_name = mount.path.strip("/").replace("/", "_") or "root"
|
|
|
|
|
mount_display = mount.path if mount.path else "/"
|
|
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
content = f"""# {mount_display} Endpoints
|
|
|
|
|
|
|
|
|
|
Base URL: `{mount_display}`
|
|
|
|
|
|
|
|
|
|
## Available Endpoints
|
|
|
|
|
|
|
|
|
|
"""
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Get detailed route information
|
|
|
|
|
detailed_routes = []
|
|
|
|
|
for route in mount.routes:
|
|
|
|
|
detailed_route = function_parser.get_function_signature(route.function_name)
|
|
|
|
|
if detailed_route:
|
|
|
|
|
detailed_routes.append(detailed_route)
|
|
|
|
|
# Skip routes without #[get] attributes - they can't be documented properly
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Sort routes alphabetically by their path
|
|
|
|
|
detailed_routes.sort(key=lambda x: x.path)
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
for route in detailed_routes:
|
2025-07-23 08:47:52 +02:00
|
|
|
content += self._generate_route_doc(mount, route)
|
|
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Add navigation
|
|
|
|
|
content += f"""
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
[← Back to Index](index.md)
|
|
|
|
|
"""
|
2025-07-23 08:47:52 +02:00
|
|
|
|
|
|
|
|
with open(os.path.join(self.output_dir, f"{mount_name}.md"), "w") as f:
|
2025-07-22 11:11:39 +02:00
|
|
|
f.write(content)
|
2025-07-23 08:47:52 +02:00
|
|
|
|
|
|
|
|
def generate_example_uri(self, mount: MountInfo, route: RouteInfo) -> str:
|
|
|
|
|
|
|
|
|
|
example_params = {
|
|
|
|
|
"token": "b79bfc8246b5fc4707e7c7dedcb6619ef1ab91f494a790c20b0f4c422ed95b92",
|
|
|
|
|
"category": "b79bfc8246b5fc4707e7c7dedcb6619ef1ab91f494a790c20b0f4c422ed95b92",
|
|
|
|
|
"pkh": "36c0020dd39e7cd66c21f237dc53d384661a557f",
|
|
|
|
|
"start": 1716537600,
|
|
|
|
|
"end": 1716624000,
|
|
|
|
|
"stepsize": 3600,
|
|
|
|
|
"token_id": "b79bfc8246b5fc4707e7c7dedcb6619ef1ab91f494a790c20b0f4c422ed95b92",
|
|
|
|
|
"timestamp": 1716537600,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tpl_title = f"{route.path}"
|
|
|
|
|
tpl = f"https://indexer.cauldron.quest{mount.path}{route.path}"
|
|
|
|
|
|
|
|
|
|
for param in route.path_params:
|
|
|
|
|
if param not in example_params:
|
|
|
|
|
print(
|
|
|
|
|
f"Warn: Could not create example URI; missing placeholder for {param} in {route.path}"
|
|
|
|
|
)
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
tpl = tpl.replace(f"<{param}>", str(example_params[param]))
|
|
|
|
|
tpl_title = tpl_title.replace(f"<{param}>", str(example_params[param]))
|
|
|
|
|
|
|
|
|
|
return f"""
|
|
|
|
|
**Example URI:** [{tpl_title}]({tpl})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def _generate_route_doc(self, mount: MountInfo, route: RouteInfo) -> str:
|
2025-07-22 11:11:39 +02:00
|
|
|
"""Generate documentation for a single route"""
|
2025-07-23 08:47:52 +02:00
|
|
|
full_path = route.path if route.path.startswith("/") else f"/{route.path}"
|
|
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Build the complete route including query parameters
|
|
|
|
|
complete_route = full_path
|
|
|
|
|
if route.query_params:
|
2025-07-23 08:47:52 +02:00
|
|
|
complete_route += "?" + "&".join(
|
|
|
|
|
f"<{param}>" for param in route.query_params
|
|
|
|
|
)
|
|
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
doc = f"""### `{complete_route}`
|
|
|
|
|
|
|
|
|
|
**Endpoint:** `{route.method} {full_path}`
|
|
|
|
|
|
|
|
|
|
**Status:** {route.status.title()}
|
|
|
|
|
|
|
|
|
|
"""
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
if route.description and route.description != "no description":
|
|
|
|
|
doc += f"**Description:** {route.description}\n\n"
|
|
|
|
|
|
2025-07-23 08:47:52 +02:00
|
|
|
doc += self.generate_example_uri(mount, route)
|
2025-07-22 11:11:39 +02:00
|
|
|
|
2025-07-23 08:47:52 +02:00
|
|
|
return doc
|
2025-07-22 11:11:39 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
"""Main function to generate API documentation"""
|
|
|
|
|
# Configuration
|
|
|
|
|
main_rs_path = "src/main.rs"
|
|
|
|
|
src_dir = "src"
|
|
|
|
|
output_dir = "apidoc"
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
print("🔍 Parsing routes from main.rs...")
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Parse mounts from main.rs
|
|
|
|
|
route_parser = RustRouteParser(main_rs_path)
|
|
|
|
|
mounts = route_parser.parse_mounts()
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
print(f"📁 Found {len(mounts)} mount points:")
|
|
|
|
|
for mount in mounts:
|
|
|
|
|
print(f" - {mount.path}: {len(mount.routes)} routes")
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Initialize function signature parser
|
|
|
|
|
function_parser = FunctionSignatureParser(src_dir)
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
print("📝 Generating API documentation...")
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# Generate documentation
|
|
|
|
|
doc_generator = APIDocGenerator(output_dir)
|
|
|
|
|
doc_generator.generate_docs(mounts, function_parser)
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
print(f"✅ API documentation generated in '{output_dir}/' directory")
|
|
|
|
|
print("📄 Generated files:")
|
2025-07-23 08:47:52 +02:00
|
|
|
|
2025-07-22 11:11:39 +02:00
|
|
|
# List generated files
|
|
|
|
|
for file in os.listdir(output_dir):
|
2025-07-23 08:47:52 +02:00
|
|
|
if file.endswith(".md"):
|
2025-07-22 11:11:39 +02:00
|
|
|
print(f" - {output_dir}/{file}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2025-07-23 08:47:52 +02:00
|
|
|
main()
|