feat: implement built-in API documentation system

- Introduce `DocRegistry` for managing API documentation
- Add automatic discovery of markdown documentation from templates
- Implement a new web-based documentation viewer at `/docs`
- Include basic markdown to HTML conversion logic
- Register core HeroServer API documentation and an example 'comments' API
This commit is contained in:
Mahmoud-Emad
2025-09-16 12:54:16 +03:00
parent e9e11ee407
commit 3669edf24e
9 changed files with 1507 additions and 117 deletions

View File

@@ -1,9 +1,26 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.hero.heroserver
import freeflowuniverse.herolib.schemas.openrpc
import os
mut server := heroserver.new_server(heroserver.ServerConfig{
port: 8080
auth_config: heroserver.AuthConfig{}
})!
// Register the comments API with documentation
script_dir := os.dir(@FILE)
openrpc_path := os.join_path(script_dir, 'openrpc.json')
spec := openrpc.new(path: openrpc_path)!
handler := openrpc.new_handler(openrpc_path)!
server.register_api('comments', spec, handler)
// Setup documentation site
server.setup_docs_site() or { println('Warning: Failed to setup documentation site: ${err}') }
println('Server starting on http://localhost:8080')
println('Documentation available at: http://localhost:8080/docs')
println('Comments API available at: http://localhost:8080/api/comments')
server.start()!

View File

@@ -1,18 +0,0 @@
import freeflowuniverse.herolib.hero.heroserver
fn testsuite_begin() {
// a clean start
// os.rm('./db')! //TODO: was giving issues
}
fn test_heroserver_new() {
// Create server
mut server := heroserver.new_server(heroserver.ServerConfig{
port: 8080
auth_config: heroserver.AuthConfig{}
})!
// Test that server was created successfully
assert server.config.port == 8080
assert true
}

View File

@@ -0,0 +1,94 @@
# HeroServer Documentation System
The HeroServer features a built-in documentation system that automatically discovers and serves API documentation with a clean web interface.
## Adding New Documentation
1. **Create a markdown file** in `lib/hero/heroserver/templates/pages/`:
```bash
# Example: lib/hero/heroserver/templates/pages/calendar.md
```
2. **Write your documentation** using standard markdown:
```markdown
# Calendar API
A comprehensive calendar management service.
## Endpoints
### Create Calendar
**Endpoint:** `POST /calendars`
**Request:**
```json
{
"name": "My Calendar"
}
```
**Response:**
```json
{
"id": "cal_123",
"name": "My Calendar"
}
```
3. **Restart the server** - Documentation available at `http://localhost:8080/docs/calendar`
## Features
- **Auto-Discovery**: Scans `templates/pages/` for `.md` files on startup
- **Built-in Viewer**: Professional web interface with sidebar navigation
- **Dynamic Rendering**: Markdown converted to HTML on-demand
- **Zero Configuration**: Just add markdown files and they appear automatically
## Documentation Structure
Your markdown files should follow this structure:
```markdown
# API Name
Brief description of your API.
## Overview
Detailed overview...
## Endpoints
### Create Resource
**Endpoint:** `POST /resources`
**Request:**
```json
{
"name": "example"
}
```
**Response:**
```json
{
"id": "123",
"name": "example"
}
```
## Error Handling
Standard HTTP status codes and error responses...
## Accessing Documentation
- **Main Index**: `http://localhost:8080/docs` (redirects to first available API)
- **Specific API**: `http://localhost:8080/docs/{api_name}`
- **Example**: `http://localhost:8080/docs/calendar`

View File

@@ -4,23 +4,360 @@ import freeflowuniverse.herolib.schemas.openrpc
import os
import freeflowuniverse.herolib.schemas.jsonschema
// Generate HTML documentation for handler type
pub fn (s HeroServer) generate_documentation(handler_type string, handler openrpc.Handler) !string {
spec := s.handler_registry.get_spec(handler_type) or {
return error('No spec found for handler type: ${handler_type}')
// Documentation registry for managing multiple API docs
pub struct DocRegistry {
pub mut:
apis map[string]ApiDocInfo
templates_dir string // Directory where user-provided markdown templates are stored
discovered bool // Flag to prevent multiple template discoveries
}
pub struct ApiDocInfo {
pub:
name string
title string
description string
version string
spec openrpc.OpenRPC
handler openrpc.Handler
}
pub fn new_doc_registry() &DocRegistry {
templates_dir := os.join_path(os.dir(@FILE), 'templates', 'pages')
return &DocRegistry{
templates_dir: templates_dir
}
}
// Register an API for documentation
pub fn (mut dr DocRegistry) register_api(name string, spec openrpc.OpenRPC, handler openrpc.Handler) {
dr.apis[name] = ApiDocInfo{
name: name
title: spec.info.title
description: spec.info.description
version: spec.info.version
spec: spec
handler: handler
}
}
// Register the core HeroServer API documentation
pub fn (mut dr DocRegistry) register_core_api() {
// Create a minimal OpenRPC spec for the core API
core_spec := openrpc.OpenRPC{
openrpc: '1.2.6'
info: openrpc.Info{
title: 'HeroServer Core API'
description: 'Core authentication and system endpoints for HeroServer'
version: '1.0.0'
}
methods: []
}
// Load and process template
template_path := os.join_path(@VMODROOT, 'lib/hero/heroserver/templates/doc.md')
dr.apis['heroserver'] = ApiDocInfo{
name: 'heroserver'
title: 'HeroServer Core API'
description: 'Core authentication and system endpoints for HeroServer'
version: '1.0.0'
spec: core_spec
handler: openrpc.Handler{
specification: core_spec
}
}
}
// Discover and register documentation from template files
pub fn (mut dr DocRegistry) discover_template_docs() ! {
// Prevent multiple discoveries
if dr.discovered {
return
}
dr.discovered = true
if !os.exists(dr.templates_dir) {
println('Templates directory does not exist: ${dr.templates_dir}')
return
}
// Get all .md files in the templates directory
files := os.ls(dr.templates_dir) or {
println('Failed to read templates directory: ${err}')
return
}
for file in files {
if file.ends_with('.md') && file != 'doc.md' {
// Extract API name from filename (remove .md extension)
api_name := file.replace('.md', '')
// Skip if already registered (e.g., heroserver_core_api.md becomes heroserver)
if api_name == 'heroserver_core_api' {
continue
}
// Read the markdown file to extract title and description
file_path := os.join_path(dr.templates_dir, file)
content := os.read_file(file_path) or {
println('Failed to read template file ${file}: ${err}')
continue
}
// Extract title and description from markdown
title, description := dr.extract_doc_metadata(content, api_name)
// Create a minimal spec for template-based docs
template_spec := openrpc.OpenRPC{
openrpc: '1.2.6'
info: openrpc.Info{
title: title
description: description
version: '1.0.0'
}
methods: []
}
// Register the template-based API
dr.apis[api_name] = ApiDocInfo{
name: api_name
title: title
description: description
version: '1.0.0'
spec: template_spec
handler: openrpc.Handler{
specification: template_spec
}
}
println('Discovered documentation template: ${api_name} (${title})')
}
}
}
// Extract title and description from markdown content
fn (dr DocRegistry) extract_doc_metadata(content string, fallback_name string) (string, string) {
lines := content.split('\n')
mut title := fallback_name.replace('_', ' ').title()
mut description := 'API documentation for ${title}'
for line in lines {
trimmed := line.trim_space()
// Look for the first H1 heading as title
if trimmed.starts_with('# ') && title == fallback_name.replace('_', ' ').title() {
title = trimmed[2..].trim_space()
}
// Look for description in the first paragraph after title
if trimmed.len > 0 && !trimmed.starts_with('#') && !trimmed.starts_with('**')
&& !trimmed.starts_with('```') && description == 'API documentation for ${title}' {
description = trimmed
break
}
}
return title, description
}
// Setup documentation system
pub fn (mut s HeroServer) setup_docs_site() ! {
// Discover documentation templates first
s.doc_registry.discover_template_docs()!
println('Documentation system ready with ${s.doc_registry.apis.len} APIs')
println('Available documentation:')
for name, api_info in s.doc_registry.apis {
println(' - ${api_info.title} (${name})')
}
}
// Generate HTML documentation viewer for a specific API
pub fn (s HeroServer) generate_docs_viewer(api_name string) !string {
// Load the HTML template
template_path := os.join_path(os.dir(@FILE), 'templates/docs_viewer.html')
template_content := os.read_file(template_path) or {
return error('Failed to read docs viewer template: ${err}')
}
// Generate sidebar items
mut sidebar_items := ''
for name, api_info in s.doc_registry.apis {
active_class := if name == api_name { ' class="active"' } else { '' }
sidebar_items += '<li><a href="/docs/${name}"${active_class}>${api_info.title}</a></li>\n'
}
// Get the markdown content for the requested API
content := s.get_api_markdown_content(api_name) or {
return error('Failed to get markdown content for ${api_name}: ${err}')
}
// Convert markdown to HTML (simple conversion for now)
html_content := s.markdown_to_html(content)
// Get the API title
api_info := s.doc_registry.apis[api_name] or { return error('API ${api_name} not found') }
// Replace template variables
mut result := template_content
result = result.replace('@{title}', api_info.title)
result = result.replace('@{sidebar_items}', sidebar_items)
result = result.replace('@{content}', html_content)
return result
}
// Get markdown content for an API
fn (s HeroServer) get_api_markdown_content(api_name string) !string {
// Check if there's a custom template file for this API
custom_template_path := os.join_path(s.doc_registry.templates_dir, '${api_name}.md')
if os.exists(custom_template_path) {
return os.read_file(custom_template_path) or {
return error('Failed to read custom template for ${api_name}: ${err}')
}
}
// For core HeroServer API, use the special template
if api_name == 'heroserver' {
core_template_path := os.join_path(os.dir(@FILE), 'templates/heroserver_core_api.md')
return os.read_file(core_template_path) or {
return error('Failed to read core API template: ${err}')
}
}
// For OpenRPC-based APIs, generate from spec
spec := s.handler_registry.get_spec(api_name) or {
return error('No documentation found for ${api_name}')
}
// Load and process the generic template
template_path := os.join_path(os.dir(@FILE), 'templates/doc.md')
template_content := os.read_file(template_path) or {
return error('Failed to read documentation template: ${err}')
}
// Process template with spec data
doc_content := process_doc_template(template_content, spec, handler_type)
return process_doc_template(template_content, spec, api_name)
}
// Return HTML with Bootstrap and markdown processing
return generate_html_wrapper(doc_content, handler_type)
// Simple markdown to HTML converter (basic implementation)
fn (s HeroServer) markdown_to_html(markdown string) string {
mut html := markdown
// Process headers more carefully
html = s.process_headers(html)
// Convert code blocks and process line by line
lines := html.split('\n')
mut result_lines := []string{}
mut in_code_block := false
for line in lines {
if line.starts_with('```') {
if in_code_block {
result_lines << '</code></pre>'
in_code_block = false
} else {
code_lang := line[3..].trim_space()
result_lines << '<pre><code class="language-${code_lang}">'
in_code_block = true
}
} else if in_code_block {
// Escape HTML in code blocks
escaped_line := line.replace('&', '&amp;').replace('<', '&lt;').replace('>',
'&gt;')
result_lines << escaped_line
} else {
// Process regular markdown
mut processed_line := line
// Process inline code first (before other replacements)
processed_line = s.process_inline_code(processed_line)
// Bold text
processed_line = processed_line.replace('**', '<strong>').replace('**', '</strong>')
// Paragraphs
if processed_line.trim_space() == '' {
processed_line = '</p><p>'
}
result_lines << processed_line
}
}
html = result_lines.join('\n')
// Wrap in paragraph tags
html = '<p>' + html + '</p>'
// Clean up empty paragraphs and code elements
html = html.replace('<p></p>', '')
html = html.replace('<p></p><p>', '<p>')
html = html.replace('<code></code>', '')
return html
}
// Process inline code with proper backtick handling
fn (s HeroServer) process_inline_code(line string) string {
mut result := line
mut in_code := false
mut chars := result.runes()
mut new_chars := []rune{}
mut i := 0
for i < chars.len {
if chars[i] == `\`` {
if in_code {
// Closing backtick
new_chars << '</code>'.runes()
in_code = false
} else {
// Opening backtick
new_chars << '<code>'.runes()
in_code = true
}
} else {
new_chars << chars[i]
}
i++
}
// If we ended with an unclosed code tag, close it
if in_code {
new_chars << '</code>'.runes()
}
return new_chars.string()
}
// Process headers with better detection to avoid false positives
fn (s HeroServer) process_headers(content string) string {
lines := content.split('\n')
mut result_lines := []string{}
for line in lines {
mut processed_line := line
// Only process lines that start with # and have space after
if line.starts_with('# ') && line.len > 2 {
processed_line = '<h1>' + line[2..].trim_space() + '</h1>'
} else if line.starts_with('## ') && line.len > 3 {
processed_line = '<h2>' + line[3..].trim_space() + '</h2>'
} else if line.starts_with('### ') && line.len > 4 {
processed_line = '<h3>' + line[4..].trim_space() + '</h3>'
} else if line.starts_with('#### ') && line.len > 5 {
processed_line = '<h4>' + line[5..].trim_space() + '</h4>'
}
result_lines << processed_line
}
return result_lines.join('\n')
}
// Legacy function for backward compatibility - now redirects to markdown generation
pub fn (s HeroServer) generate_documentation(handler_type string, handler openrpc.Handler) !string {
return 'Documentation is now served via built-in viewer at http://localhost:8080/docs'
}
// Process the markdown template with OpenRPC spec data
@@ -81,12 +418,3 @@ fn generate_method_doc(method openrpc.Method) string {
return doc
}
// Generate HTML wrapper with Bootstrap
fn generate_html_wrapper(markdown_content string, handler_type string) string {
template_path := os.join_path(@VMODROOT, 'lib/hero/heroserver/templates/doc_wrapper.html')
mut template_content := os.read_file(template_path) or { return 'Template not found' }
template_content = template_content.replace('@{handler_type}', handler_type)
template_content = template_content.replace('@{markdown_content}', markdown_content)
return template_content
}

View File

@@ -3,6 +3,7 @@ module heroserver
import veb
import freeflowuniverse.herolib.schemas.jsonrpc
import freeflowuniverse.herolib.hero.crypt
import freeflowuniverse.herolib.schemas.openrpc
pub struct ServerConfig {
pub mut:
@@ -16,6 +17,7 @@ pub mut:
auth_manager &AuthManager
handler_registry &HandlerRegistry
age_client &crypt.AGEClient
doc_registry &DocRegistry
}
pub struct Context {
@@ -67,16 +69,57 @@ pub fn (mut s HeroServer) api(mut ctx Context, handler_type string) veb.Result {
return ctx.json(response)
}
// Documentation endpoints
@['/doc/:handler_type'; get]
pub fn (mut s HeroServer) doc(mut ctx Context, handler_type string) veb.Result {
handler := s.handler_registry.get(handler_type) or { return ctx.not_found() }
// Documentation index endpoint - redirects to first available API
@['/docs'; get]
pub fn (mut s HeroServer) docs_index(mut ctx Context) veb.Result {
// Setup documentation site if not already done
s.setup_docs_site() or { return ctx.server_error('Documentation setup failed: ${err}') }
doc_html := s.generate_documentation(handler_type, handler) or {
return ctx.server_error('Documentation generation failed')
// Redirect to the first available API documentation (preferably heroserver)
if 'heroserver' in s.doc_registry.apis {
return ctx.redirect('/docs/heroserver')
}
return ctx.html(doc_html)
// If no heroserver, redirect to the first available API
for name, _ in s.doc_registry.apis {
return ctx.redirect('/docs/${name}')
}
// If no APIs available, show a message
return ctx.html('<h1>No API documentation available</h1><p>No APIs have been registered yet.</p>')
}
// Documentation viewer for specific APIs
@['/docs/:api_name'; get]
pub fn (mut s HeroServer) docs_api(mut ctx Context, api_name string) veb.Result {
// Setup documentation site if not already done
s.setup_docs_site() or { return ctx.server_error('Documentation setup failed: ${err}') }
// Check if the API exists
if api_name !in s.doc_registry.apis {
return ctx.not_found()
}
// Generate and return the documentation viewer
html_content := s.generate_docs_viewer(api_name) or {
return ctx.server_error('Failed to generate documentation: ${err}')
}
return ctx.html(html_content)
}
// Setup documentation site endpoint
@['/docs/setup'; post]
pub fn (mut s HeroServer) docs_setup(mut ctx Context) veb.Result {
s.setup_docs_site() or { return ctx.server_error('Documentation setup failed: ${err}') }
return ctx.json('{"status": "success", "message": "Documentation site setup completed", "url": "http://localhost:8080/docs"}')
}
// Register an API with both handler and documentation
pub fn (mut s HeroServer) register_api(name string, spec openrpc.OpenRPC, handler openrpc.Handler) {
s.handler_registry.register(name, handler, spec)
s.doc_registry.register_api(name, spec, handler)
}
// new_server creates a new HeroServer instance
@@ -84,11 +127,16 @@ pub fn new_server(config ServerConfig) !&HeroServer {
mut auth_manager := new_auth_manager(config.auth_config)
mut handler_registry := new_handler_registry()
mut age_client := crypt.new_age_client(crypt.AGEClientConfig{})!
mut doc_registry := new_doc_registry()
// Register the core HeroServer API documentation
doc_registry.register_core_api()
return &HeroServer{
config: config
auth_manager: auth_manager
handler_registry: handler_registry
age_client: age_client
doc_registry: doc_registry
}
}

View File

@@ -1,74 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@{handler_type} API Documentation</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100;
padding: 48px 0 0;
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
.main {
margin-left: 240px;
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
<div class="sidebar-sticky pt-3">
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>@{handler_type} API</span>
</h6>
<ul class="nav flex-column" id="toc">
<!-- Table of contents will be generated by JavaScript -->
</ul>
</div>
</nav>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-4 main">
<div id="content">
<!-- Markdown content will be rendered here -->
</div>
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Render markdown content
const markdownContent = `@{markdown_content}`;
document.getElementById('content').innerHTML = marked.parse(markdownContent);
// Generate table of contents
const headers = document.querySelectorAll('#content h1, #content h2, #content h3');
const toc = document.getElementById('toc');
headers.forEach((header, index) => {
const id = 'header-' + index;
header.id = id;
const li = document.createElement('li');
li.className = 'nav-item';
const a = document.createElement('a');
a.className = 'nav-link';
a.href = '#' + id;
a.textContent = header.textContent;
a.style.paddingLeft = (header.tagName === 'H2' ? '20px' : header.tagName === 'H3' ? '40px' : '10px');
li.appendChild(a);
toc.appendChild(li);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,350 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@{title} - HeroServer Documentation</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.5;
color: #333;
background-color: #f8f9fa;
font-size: 14px;
}
.container {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 280px;
background: #fff;
border-right: 1px solid #e1e5e9;
padding: 20px 0;
overflow-y: auto;
position: fixed;
height: 100vh;
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
}
.sidebar h2 {
padding: 0 20px 15px;
font-size: 16px;
color: #1a1a1a;
border-bottom: 1px solid #e1e5e9;
margin-bottom: 15px;
font-weight: 600;
}
.sidebar ul {
list-style: none;
}
.sidebar li {
margin: 0;
}
.sidebar a {
display: block;
padding: 8px 20px;
color: #666;
text-decoration: none;
transition: all 0.2s ease;
border-left: 3px solid transparent;
font-size: 13px;
}
.sidebar a:hover {
background-color: #f1f3f4;
color: #1976d2;
border-left-color: #1976d2;
}
.sidebar a.active {
background-color: #e3f2fd;
color: #1976d2;
border-left-color: #1976d2;
font-weight: 500;
}
.content {
flex: 1;
margin-left: 280px;
padding: 30px;
max-width: calc(100% - 280px);
}
.content-wrapper {
max-width: 900px;
background: #fff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
h1 {
color: #1a1a1a;
margin-bottom: 16px;
font-size: 28px;
font-weight: 600;
line-height: 1.3;
}
h2 {
color: #1a1a1a;
margin: 24px 0 12px;
font-size: 20px;
font-weight: 600;
border-bottom: 1px solid #e1e5e9;
padding-bottom: 6px;
}
/* Major section headers get stronger dividers */
h2.major-section {
border-bottom: 2px solid #e1e5e9;
}
/* Minor section headers get light dividers */
h2.minor-section {
border-bottom: 1px solid #f0f0f0;
}
h3 {
color: #333;
margin: 20px 0 10px;
font-size: 16px;
font-weight: 600;
}
h4 {
color: #333;
margin: 16px 0 8px;
font-size: 14px;
font-weight: 600;
}
p {
margin-bottom: 12px;
color: #555;
font-size: 14px;
line-height: 1.6;
}
code {
background: #f1f3f4;
padding: 2px 5px;
border-radius: 3px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
color: #d73a49;
}
pre {
background: #f8f8f8;
border: 1px solid #e1e5e9;
border-radius: 6px;
padding: 12px;
overflow-x: auto;
margin: 12px 0;
font-size: 12px;
}
pre code {
background: none;
padding: 0;
color: #333;
font-size: 12px;
}
ul,
ol {
margin: 12px 0;
padding-left: 24px;
}
li {
margin-bottom: 4px;
font-size: 14px;
line-height: 1.5;
}
blockquote {
border-left: 4px solid #1976d2;
margin: 12px 0;
padding: 12px 16px;
background: #f8f9fa;
color: #555;
font-size: 14px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 13px;
}
th,
td {
border: 1px solid #e1e5e9;
padding: 8px 10px;
text-align: left;
}
th {
background: #f8f9fa;
font-weight: 600;
font-size: 12px;
}
.badge {
display: inline-block;
padding: 2px 6px;
background: #1976d2;
color: white;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
margin-left: 6px;
}
.endpoint {
background: #e8f5e8;
border: 1px solid #4caf50;
border-radius: 4px;
padding: 6px 10px;
margin: 6px 0;
font-family: monospace;
font-weight: 600;
color: #2e7d32;
font-size: 12px;
}
.method-post {
border-color: #ff9800;
background: #fff3e0;
color: #f57c00;
}
.method-get {
border-color: #4caf50;
background: #e8f5e8;
color: #2e7d32;
}
.method-put {
border-color: #2196f3;
background: #e3f2fd;
color: #1976d2;
}
.method-delete {
border-color: #f44336;
background: #ffebee;
color: #d32f2f;
}
@media (max-width: 768px) {
.sidebar {
width: 100%;
position: relative;
height: auto;
}
.content {
margin-left: 0;
max-width: 100%;
padding: 15px;
}
.content-wrapper {
padding: 15px;
}
h1 {
font-size: 24px;
}
h2 {
font-size: 18px;
}
h3 {
font-size: 15px;
}
p,
li {
font-size: 13px;
}
}
</style>
</head>
<body>
<div class="container">
<nav class="sidebar">
<h2>📚 API Documentation</h2>
<ul>
@{sidebar_items}
</ul>
</nav>
<main class="content">
<div class="content-wrapper">
<div id="markdown-content">
@{content}
</div>
</div>
</main>
</div>
<script>
// Add active class to current page
const currentPath = window.location.pathname;
const links = document.querySelectorAll('.sidebar a');
links.forEach(link => {
if (link.getAttribute('href') === currentPath) {
link.classList.add('active');
}
});
// Add method classes to endpoints and manage header dividers
document.addEventListener('DOMContentLoaded', function () {
// Style code blocks for HTTP methods
const codeBlocks = document.querySelectorAll('code');
codeBlocks.forEach(code => {
const text = code.textContent.trim();
if (text.match(/^(GET|POST|PUT|DELETE|PATCH)\s+\//)) {
const method = text.split(' ')[0].toLowerCase();
code.classList.add('endpoint', `method-${method}`);
}
});
// Manage header dividers - only add to major sections
const h2Elements = document.querySelectorAll('h2');
h2Elements.forEach(h2 => {
const text = h2.textContent.toLowerCase();
const majorSections = ['overview', 'authentication', 'endpoints', 'api', 'examples', 'error handling', 'rate limiting'];
if (majorSections.some(section => text.includes(section))) {
h2.classList.add('major-section');
} else {
// Remove default border for minor sections
h2.style.borderBottom = 'none';
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,297 @@
# HeroServer Core API Documentation
The core HeroServer API provides authentication and system endpoints that are required before accessing any registered APIs.
**Version:** 1.0.0
## Overview
The HeroServer Core API handles user authentication using Ed25519 public key cryptography and provides system information endpoints. All other APIs require authentication through these core endpoints.
### Base URL
```
http://localhost:8080
```
---
## Authentication Endpoints
### Register Public Key
Register your Ed25519 public key with the server to enable authentication.
**Endpoint:** `POST /register`
**Request Body:**
```json
{
"public_key": "your_ed25519_public_key_in_base64"
}
```
**Response:**
```json
{
"status": "success",
"message": "Public key registered successfully"
}
```
**Example:**
```bash
curl -X POST http://localhost:8080/register \
-H "Content-Type: application/json" \
-d '{"public_key": "MCowBQYDK0OAQEiA..."}'
```
---
### Request Authentication Challenge
Request a challenge that must be signed with your private key.
**Endpoint:** `POST /authreq`
**Request Body:**
```json
{
"public_key": "your_ed25519_public_key_in_base64"
}
```
**Response:**
```json
{
"challenge": "base64_encoded_challenge_to_sign"
}
```
**Example:**
```bash
curl -X POST http://localhost:8080/authreq \
-H "Content-Type: application/json" \
-d '{"public_key": "MCowBQYDK0OAQEiA..."}'
```
---
### Authenticate with Signed Challenge
Submit the signed challenge to receive a session key for API access.
**Endpoint:** `POST /auth`
**Request Body:**
```json
{
"public_key": "your_ed25519_public_key_in_base64",
"signature": "base64_encoded_signature_of_challenge"
}
```
**Response:**
```json
{
"session_key": "your_session_key_for_api_calls",
"expires_at": "2024-01-01T12:00:00Z"
}
```
**Example:**
```bash
curl -X POST http://localhost:8080/auth \
-H "Content-Type: application/json" \
-d '{
"public_key": "MCowBQYDK0OAQEiA...",
"signature": "signature_of_challenge..."
}'
```
---
## Documentation Endpoints
### Documentation Index
View all available APIs and their documentation.
**Endpoint:** `GET /docs`
**Response:** HTML page with list of all registered APIs
**Example:**
```bash
curl http://localhost:8080/docs
```
---
### API-Specific Documentation
View documentation for a specific registered API.
**Endpoint:** `GET /docs/{api_name}`
**Parameters:**
- `api_name`: The name of the registered API (e.g., "comments")
**Response:** HTML documentation page for the specified API
**Example:**
```bash
curl http://localhost:8080/docs/comments
```
---
## API Access Endpoints
### Call Registered API Methods
Once authenticated, call methods on registered APIs using JSON-RPC 2.0.
**Endpoint:** `POST /api/{api_name}`
**Headers:**
```
Content-Type: application/json
Authorization: your_session_key_here
```
**Request Body (JSON-RPC 2.0):**
```json
{
"jsonrpc": "2.0",
"method": "method_name",
"params": {
"param1": "value1",
"param2": "value2"
},
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"data": "method_response_data"
},
"id": 1
}
```
**Example:**
```bash
curl -X POST http://localhost:8080/api/comments \
-H "Content-Type: application/json" \
-H "Authorization: your_session_key" \
-d '{
"jsonrpc": "2.0",
"method": "add_comment",
"params": {"content": "Hello World!"},
"id": 1
}'
```
---
## Error Handling
### Authentication Errors
| Code | Message | Description |
|------|---------|-------------|
| `400` | Bad Request | Invalid request format or missing required fields |
| `401` | Unauthorized | Invalid or missing authentication credentials |
| `403` | Forbidden | Valid credentials but insufficient permissions |
| `404` | Not Found | Requested endpoint or resource not found |
| `500` | Internal Server Error | Server-side error occurred |
### JSON-RPC Errors
| Code | Message | Description |
|------|---------|-------------|
| `-32700` | Parse error | Invalid JSON was received |
| `-32600` | Invalid Request | The JSON sent is not a valid Request object |
| `-32601` | Method not found | The method does not exist |
| `-32602` | Invalid params | Invalid method parameter(s) |
| `-32603` | Internal error | Internal JSON-RPC error |
---
## Security Considerations
### Ed25519 Key Management
- **Generate secure keys**: Use a cryptographically secure random number generator
- **Store private keys safely**: Never share or transmit private keys
- **Rotate keys regularly**: Consider periodic key rotation for enhanced security
### Session Management
- **Session expiration**: Session keys have a limited lifetime
- **Secure transmission**: Always use HTTPS in production
- **Key storage**: Store session keys securely on the client side
### Rate Limiting
- **Authentication endpoints**: Limited to prevent brute force attacks
- **API endpoints**: Rate limited per session to ensure fair usage
- **Documentation endpoints**: Publicly accessible but may have basic rate limits
---
## Complete Authentication Flow Example
Here's a complete example of the authentication flow using curl:
```bash
# 1. Register your public key
curl -X POST http://localhost:8080/register \
-H "Content-Type: application/json" \
-d '{"public_key": "your_public_key_here"}'
# 2. Request authentication challenge
CHALLENGE=$(curl -X POST http://localhost:8080/authreq \
-H "Content-Type: application/json" \
-d '{"public_key": "your_public_key_here"}' | jq -r '.challenge')
# 3. Sign the challenge (using your preferred signing method)
SIGNATURE=$(echo "$CHALLENGE" | your_signing_tool)
# 4. Authenticate and get session key
SESSION_KEY=$(curl -X POST http://localhost:8080/auth \
-H "Content-Type: application/json" \
-d "{\"public_key\": \"your_public_key_here\", \"signature\": \"$SIGNATURE\"}" | jq -r '.session_key')
# 5. Use session key for API calls
curl -X POST http://localhost:8080/api/comments \
-H "Content-Type: application/json" \
-H "Authorization: $SESSION_KEY" \
-d '{
"jsonrpc": "2.0",
"method": "add_comment",
"params": {"content": "Hello from HeroServer!"},
"id": 1
}'
```

View File

@@ -0,0 +1,348 @@
# Comments API
A simple service for managing comments and discussions.
**Version:** 1.0.0
## Overview
The Comments API provides a straightforward way to manage comments, replies, and discussions. It supports threaded conversations, moderation features, and real-time updates.
### Base URL
```
http://localhost:8080/api/comments
```
---
## Authentication
All Comments API endpoints require authentication through the HeroServer Core API. Please refer to the [authentication documentation](./heroserver.md#authentication-flow) for details.
---
## Endpoints
### Create Comment
Create a new comment.
**Endpoint:** `POST /comments`
**Request Body:**
```json
{
"content": "This is a great article! Thanks for sharing.",
"author": "john_doe",
"parent_id": null,
"metadata": {
"article_id": "article_123",
"section": "introduction"
}
}
```
**Response:**
```json
{
"id": "comment_456789",
"content": "This is a great article! Thanks for sharing.",
"author": "john_doe",
"parent_id": null,
"metadata": {
"article_id": "article_123",
"section": "introduction"
},
"status": "published",
"created_at": "2024-01-20T10:00:00Z",
"updated_at": "2024-01-20T10:00:00Z",
"replies_count": 0
}
```
### List Comments
Retrieve comments with optional filtering.
**Endpoint:** `GET /comments`
**Query Parameters:**
- `article_id` (optional): Filter by article ID
- `author` (optional): Filter by author
- `status` (optional): Filter by status (`published`, `pending`, `hidden`)
- `parent_id` (optional): Filter by parent comment ID (for replies)
- `limit` (optional): Maximum number of comments to return (default: 50)
- `offset` (optional): Number of comments to skip (default: 0)
**Response:**
```json
{
"comments": [
{
"id": "comment_456789",
"content": "This is a great article! Thanks for sharing.",
"author": "john_doe",
"parent_id": null,
"metadata": {
"article_id": "article_123",
"section": "introduction"
},
"status": "published",
"created_at": "2024-01-20T10:00:00Z",
"updated_at": "2024-01-20T10:00:00Z",
"replies_count": 2
}
],
"total": 1,
"has_more": false
}
```
### Get Comment
Retrieve a specific comment by ID.
**Endpoint:** `GET /comments/{comment_id}`
**Response:** Comment object (same as create response)
### Update Comment
Update an existing comment.
**Endpoint:** `PUT /comments/{comment_id}`
**Request Body:**
```json
{
"content": "Updated comment content",
"status": "published"
}
```
**Response:** Updated comment object
### Delete Comment
Delete a comment (soft delete - marks as hidden).
**Endpoint:** `DELETE /comments/{comment_id}`
**Response:**
```json
{
"message": "Comment deleted successfully"
}
```
### Reply to Comment
Create a reply to an existing comment.
**Endpoint:** `POST /comments/{comment_id}/replies`
**Request Body:**
```json
{
"content": "Great point! I totally agree.",
"author": "jane_smith"
}
```
**Response:** Reply comment object with `parent_id` set to the original comment
---
## Comment Status
Comments can have the following statuses:
- **published**: Comment is visible to all users
- **pending**: Comment is awaiting moderation
- **hidden**: Comment has been hidden by moderators
---
## Threading
The Comments API supports threaded conversations:
- **Top-level comments**: Have `parent_id` set to `null`
- **Replies**: Have `parent_id` set to the ID of the comment they're replying to
- **Nested replies**: Can reply to other replies, creating deep conversation threads
### Example Thread Structure
```
Comment A (parent_id: null)
├── Reply 1 (parent_id: comment_A_id)
│ └── Reply 1.1 (parent_id: reply_1_id)
└── Reply 2 (parent_id: comment_A_id)
```
---
## Moderation
### Moderate Comment
Update comment status for moderation.
**Endpoint:** `POST /comments/{comment_id}/moderate`
**Request Body:**
```json
{
"status": "hidden",
"reason": "inappropriate_content"
}
```
**Response:**
```json
{
"id": "comment_456789",
"status": "hidden",
"moderated_at": "2024-01-20T15:30:00Z",
"moderated_by": "moderator_123"
}
```
---
## Real-time Updates
The Comments API supports WebSocket connections for real-time comment updates:
**WebSocket Endpoint:** `ws://localhost:8080/api/comments/ws`
**Subscribe to Article Comments:**
```json
{
"action": "subscribe",
"article_id": "article_123"
}
```
**Receive New Comments:**
```json
{
"type": "new_comment",
"comment": {
"id": "comment_789",
"content": "New comment content",
"author": "user_456",
"created_at": "2024-01-20T16:00:00Z"
}
}
```
---
## Error Handling
The Comments API uses standard HTTP status codes:
**400 Bad Request:**
```json
{
"error": "invalid_request",
"message": "Comment content cannot be empty",
"details": {
"field": "content",
"code": "required"
}
}
```
**404 Not Found:**
```json
{
"error": "not_found",
"message": "Comment not found",
"details": {
"comment_id": "comment_invalid"
}
}
```
**403 Forbidden:**
```json
{
"error": "forbidden",
"message": "You can only edit your own comments",
"details": {
"comment_id": "comment_456789",
"owner": "other_user"
}
}
```
---
## Rate Limiting
The Comments API implements rate limiting to prevent spam:
- **Comment creation**: 10 comments per minute per user
- **Comment updates**: 20 updates per minute per user
- **Comment retrieval**: 100 requests per minute per user
---
## Examples
### Creating a Comment Thread
```bash
# Create top-level comment
curl -X POST http://localhost:8080/api/comments \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "Great article about API design!",
"author": "developer_123",
"metadata": {"article_id": "api_design_101"}
}'
# Reply to the comment
curl -X POST http://localhost:8080/api/comments/comment_456789/replies \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "I agree! The examples are very helpful.",
"author": "reader_456"
}'
```
### Retrieving Article Comments
```bash
curl "http://localhost:8080/api/comments?article_id=api_design_101&status=published" \
-H "Authorization: Bearer YOUR_TOKEN"
```
---
## Support
For questions and support:
- **Documentation**: [https://docs.heroserver.com/comments](https://docs.heroserver.com/comments)
- **GitHub**: [https://github.com/heroserver/comments-api](https://github.com/heroserver/comments-api)
- **Email**: comments-support@heroserver.com