feat: enhance server documentation and configuration

- Add HTML homepage and JSON handler info endpoints
- Implement markdown documentation generation for APIs
- Introduce auth_enabled flag for server configuration
- Improve documentation generation with dynamic base URLs
- Refactor server initialization and handler registration
This commit is contained in:
Mahmoud-Emad
2025-09-18 12:10:49 +03:00
parent 5eedae9717
commit e59ff8b63f
19 changed files with 974 additions and 1937 deletions

View File

@@ -5,9 +5,11 @@ import freeflowuniverse.herolib.hero.heromodels
import time import time
fn main() { fn main() {
// Start the server in a background thread // Start the server in a background thread with authentication disabled for testing
spawn fn () { spawn fn () {
rpc.start(port: 8080) or { panic('Failed to start HeroModels server: ${err}') } rpc.start(port: 8080, auth_enabled: false) or {
panic('Failed to start HeroModels server: ${err}')
}
}() }()
// Keep the main thread alive // Keep the main thread alive

View File

@@ -2,8 +2,6 @@ module rpc
import freeflowuniverse.herolib.schemas.openrpc import freeflowuniverse.herolib.schemas.openrpc
import freeflowuniverse.herolib.hero.heroserver import freeflowuniverse.herolib.hero.heroserver
import freeflowuniverse.herolib.schemas.jsonrpc
import freeflowuniverse.herolib.ui.console
import os import os
const openrpc_path = os.join_path(os.dir(@FILE), 'openrpc.json') const openrpc_path = os.join_path(os.dir(@FILE), 'openrpc.json')
@@ -73,21 +71,19 @@ pub fn new_heromodels_handler() !&openrpc.Handler {
@[params] @[params]
pub struct ServerArgs { pub struct ServerArgs {
pub mut: pub mut:
port int = 8080 port int = 8080
host string = 'localhost' host string = 'localhost'
auth_enabled bool = true
} }
pub fn start(args ServerArgs) ! { pub fn start(args ServerArgs) ! {
// Create a new heroserver instance // Create a new heroserver instance
mut server := heroserver.new(port: args.port, host: args.host)! mut server := heroserver.new(port: args.port, host: args.host, auth_enabled: args.auth_enabled)!
// Create and register the heromodels handler // Create and register the heromodels handler
handler := new_heromodels_handler()! handler := new_heromodels_handler()!
server.register_handler('heromodels', handler)! server.register_handler('heromodels', handler)!
console.print_green('Documentation available at: http://${args.host}:${args.port}/doc/heromodels/')
console.print_green('HeroModels API available at: http://${args.host}:${args.port}/api/heromodels')
// Start the server // Start the server
server.start()! server.start()!
} }

View File

@@ -8,6 +8,7 @@
{ {
"name": "comment_get", "name": "comment_get",
"summary": "Get a comment by ID", "summary": "Get a comment by ID",
"description": "Retrieve a comment by its unique ID. Returns the comment object.",
"params": [ "params": [
{ {
"name": "id", "name": "id",

View File

@@ -1,14 +1,53 @@
module heroserver module heroserver
import veb import veb
import freeflowuniverse.herolib.schemas.openrpc
import freeflowuniverse.herolib.schemas.jsonschema
// Home page handler - returns HTML homepage
@['/']
pub fn (mut server HeroServer) home_handler(mut ctx Context) veb.Result {
// Create a simple server info structure for the template
server_info := HomePageData{
base_url: get_base_url_from_context(ctx)
handlers: server.handlers
auth_enabled: server.auth_enabled
host: server.host
port: server.port
}
// Load and process the HTML template
html_content := $tmpl('templates/home.html')
return ctx.html(html_content)
}
// JSON server info handler
@['/json/:handler_type']
pub fn (mut server HeroServer) json_handler(mut ctx Context, handler_type string) veb.Result {
// Get the OpenRPC handler for the specified handler type
handler := server.handlers[handler_type] or { return ctx.not_found() }
// Create server info structure focused on this handler
server_info := create_handler_json_info(server, handler_type, handler, get_base_url_from_context(ctx))
return ctx.json(server_info)
}
@['/doc/:handler_type'] @['/doc/:handler_type']
pub fn (mut server HeroServer) doc_handler(mut ctx Context, handler_type string) veb.Result { pub fn (mut server HeroServer) doc_handler(mut ctx Context, handler_type string) veb.Result {
// Get the OpenRPC handler for the specified handler type // Get the OpenRPC handler for the specified handler type
handler := server.handlers[handler_type] or { return ctx.not_found() } handler := server.handlers[handler_type] or { return ctx.not_found() }
// Convert the OpenRPC specification to a DocSpec // Create dynamic configuration based on request
spec := doc_spec_from_openrpc(handler.specification, handler_type) or { config := DocConfig{
base_url: get_base_url_from_context(ctx)
handler_type: handler_type
auth_enabled: server.auth_enabled
}
// Convert the OpenRPC specification to a DocSpec with dynamic configuration
spec := doc_spec_from_openrpc_with_config(handler.specification, config) or {
return ctx.server_error('Failed to generate documentation: ${err}') return ctx.server_error('Failed to generate documentation: ${err}')
} }
@@ -17,3 +56,298 @@ pub fn (mut server HeroServer) doc_handler(mut ctx Context, handler_type string)
return ctx.html(html_content) return ctx.html(html_content)
} }
@['/md/:handler_type']
pub fn (mut server HeroServer) md_handler(mut ctx Context, handler_type string) veb.Result {
// Get the OpenRPC handler for the specified handler type
handler := server.handlers[handler_type] or { return ctx.not_found() }
// Generate markdown content from the OpenRPC specification
markdown_content := generate_markdown_from_openrpc(handler.specification, handler_type,
get_base_url_from_context(ctx)) or {
return ctx.server_error('Failed to generate markdown documentation: ${err}')
}
// Set content type to text/plain for markdown
ctx.set_content_type('text/plain; charset=utf-8')
return ctx.text(markdown_content)
}
// get_base_url_from_context extracts the base URL from the VEB context
fn get_base_url_from_context(ctx Context) string {
scheme := if ctx.get_header(.x_forwarded_proto) or { '' } == 'https' { 'https' } else { 'http' }
host := ctx.get_header(.host) or { 'localhost:8080' }
return '${scheme}://${host}'
}
// generate_markdown_from_openrpc generates markdown documentation from OpenRPC specification
fn generate_markdown_from_openrpc(spec openrpc.OpenRPC, handler_type string, base_url string) !string {
mut md := ''
// Title and description
md += '# ${spec.info.title}\n\n'
if spec.info.description.len > 0 {
md += '${spec.info.description}\n\n'
}
// Basic info
md += '**Version:** ${spec.info.version}\n'
md += '**Handler Type:** ${handler_type}\n'
md += '**Base URL:** ${base_url}\n\n'
// Overview
md += '## Overview\n\n'
md += 'This API provides JSON-RPC 2.0 endpoints for ${handler_type} operations.\n\n'
md += '**API Endpoint:** `${base_url}/api/${handler_type}`\n\n'
// Table of Contents
if spec.methods.len > 0 {
md += '## Table of Contents\n\n'
for method in spec.methods {
md += '- [${method.name}](#${method.name.to_lower().replace('_', '-')})\n'
}
md += '\n'
}
// Methods
if spec.methods.len > 0 {
md += '## API Methods\n\n'
for method in spec.methods {
md += generate_method_markdown(method, base_url, handler_type)
}
}
// Authentication section
md += '## Authentication\n\n'
md += 'All API requests use JSON-RPC 2.0 format and may require authentication depending on server configuration.\n\n'
md += '### Request Format\n\n'
md += '```json\n'
md += '{\n'
md += ' "jsonrpc": "2.0",\n'
md += ' "method": "method_name",\n'
md += ' "params": {\n'
md += ' "param1": "value1",\n'
md += ' "param2": "value2"\n'
md += ' },\n'
md += ' "id": 1\n'
md += '}\n'
md += '```\n\n'
// Error handling
md += '## Error Handling\n\n'
md += 'The API uses standard JSON-RPC 2.0 error codes:\n\n'
md += '- `-32700`: Parse error\n'
md += '- `-32600`: Invalid Request\n'
md += '- `-32601`: Method not found\n'
md += '- `-32602`: Invalid params\n'
md += '- `-32603`: Internal error\n\n'
return md
}
// generate_method_markdown generates markdown documentation for a single method
fn generate_method_markdown(method openrpc.Method, base_url string, handler_type string) string {
mut md := ''
// Method header
md += '### ${method.name}\n\n'
if method.summary.len > 0 {
md += '**Summary:** ${method.summary}\n\n'
}
if method.description.len > 0 {
md += '${method.description}\n\n'
}
// Parameters
if method.params.len > 0 {
md += '#### Parameters\n\n'
md += '| Name | Type | Required | Description |\n'
md += '|------|------|----------|-------------|\n'
for param in method.params {
if param is openrpc.ContentDescriptor {
param_desc := param as openrpc.ContentDescriptor
param_type := if param_desc.schema is jsonschema.Schema {
schema := param_desc.schema as jsonschema.Schema
schema.typ
} else {
'unknown'
}
required := if param_desc.required { 'Yes' } else { 'No' }
md += '| ${param_desc.name} | ${param_type} | ${required} | ${param_desc.description} |\n'
}
}
md += '\n'
}
// Result
if method.result is openrpc.ContentDescriptor {
result := method.result as openrpc.ContentDescriptor
md += '#### Returns\n\n'
md += '${result.description}\n\n'
}
// Example request
md += '#### Example Request\n\n'
md += '```bash\n'
md += 'curl -X POST ${base_url}/api/${handler_type} \\\n'
md += ' -H "Content-Type: application/json" \\\n'
md += " -d '{\n"
md += ' "jsonrpc": "2.0",\n'
md += ' "method": "${method.name}",\n'
md += ' "params": {\n'
// Add example parameters
if method.params.len > 0 {
for i, param in method.params {
if param is openrpc.ContentDescriptor {
param_desc := param as openrpc.ContentDescriptor
example_value := get_example_value_for_param(param_desc)
comma := if i < method.params.len - 1 { ',' } else { '' }
md += ' "${param_desc.name}": ${example_value}${comma}\n'
}
}
}
md += ' },\n'
md += ' "id": 1\n'
md += " }'\n"
md += '```\n\n'
return md
}
// get_example_value_for_param returns an example value for a parameter based on its type
fn get_example_value_for_param(param openrpc.ContentDescriptor) string {
if param.schema is jsonschema.Schema {
schema := param.schema as jsonschema.Schema
match schema.typ {
'string' { return '"example_string"' }
'integer', 'number' { return '123' }
'boolean' { return 'true' }
'array' { return '[]' }
'object' { return '{}' }
else { return '"example_value"' }
}
}
return '"example_value"'
}
// create_server_info_json creates a comprehensive JSON response about the server
fn create_server_info_json(server HeroServer, base_url string) ServerInfoJSON {
mut handlers := []HandlerInfoJSON{}
// Process each registered handler
for handler_name, handler in server.handlers {
mut methods := []MethodInfoJSON{}
// Extract methods from the OpenRPC specification
for method in handler.specification.methods {
methods << MethodInfoJSON{
name: method.name
summary: method.summary
description: method.description
}
}
handlers << HandlerInfoJSON{
name: handler_name
title: handler.specification.info.title
description: handler.specification.info.description
version: handler.specification.info.version
api_endpoint: '${base_url}/api/${handler_name}'
doc_endpoint: '${base_url}/doc/${handler_name}'
md_endpoint: '${base_url}/md/${handler_name}'
methods: methods
}
}
// Define server features
features := [
FeatureJSON{
title: 'JSON-RPC 2.0'
description: 'Full compliance with JSON-RPC 2.0 specification for reliable API communication'
icon: '🔗'
},
FeatureJSON{
title: 'Dynamic Documentation'
description: 'Auto-generated interactive documentation with curl examples and copy buttons'
icon: '📚'
},
FeatureJSON{
title: 'Secure Authentication'
description: 'Built-in cryptographic authentication with public key infrastructure'
icon: '🔐'
},
FeatureJSON{
title: 'Markdown Export'
description: 'Export API documentation as clean markdown for integration with other tools'
icon: '📝'
},
]
// Create endpoints information
endpoints := EndpointsJSON{
api_pattern: '/api/{handler_name}'
documentation_pattern: '/doc/{handler_name}'
markdown_pattern: '/md/{handler_name}'
home_json: '/'
home_html: '/home'
}
// Create quick start example
example_handler := if handlers.len > 0 { handlers[0].name } else { 'handler_name' }
quick_start := QuickStartJSON{
description: "All API endpoints use JSON-RPC 2.0 format. Here's a basic example:"
example: ExampleRequestJSON{
method: 'POST'
url: '${base_url}/api/${example_handler}'
headers: {
'Content-Type': 'application/json'
}
body: '{\n "jsonrpc": "2.0",\n "method": "method_name",\n "params": {\n "param1": "value1",\n "param2": "value2"\n },\n "id": 1\n}'
description: 'Replace method_name and params with actual values from the API documentation'
}
}
return ServerInfoJSON{
server_name: 'HeroServer'
version: '1.0.0'
description: 'Modern JSON-RPC 2.0 API Gateway with Dynamic Documentation'
base_url: base_url
host: server.host
port: server.port
auth_enabled: server.auth_enabled
handlers: handlers
endpoints: endpoints
features: features
quick_start: quick_start
}
}
// create_handler_json_info creates JSON response focused on a specific handler
fn create_handler_json_info(server HeroServer, handler_name string, handler &openrpc.Handler, base_url string) HandlerInfoJSON {
mut methods := []MethodInfoJSON{}
// Extract methods from the OpenRPC specification
for method in handler.specification.methods {
methods << MethodInfoJSON{
name: method.name
summary: method.summary
description: method.description
}
}
return HandlerInfoJSON{
name: handler_name
title: handler.specification.info.title
description: handler.specification.info.description
version: handler.specification.info.version
api_endpoint: '${base_url}/api/${handler_name}'
doc_endpoint: '${base_url}/doc/${handler_name}'
md_endpoint: '${base_url}/md/${handler_name}'
methods: methods
}
}

View File

@@ -10,6 +10,7 @@ pub mut:
methods []DocMethod methods []DocMethod
objects []DocObject objects []DocObject
auth_info AuthDocInfo auth_info AuthDocInfo
base_url string // Dynamic base URL for examples
} }
// DocObject represents a logical grouping of methods. // DocObject represents a logical grouping of methods.
@@ -31,7 +32,7 @@ pub mut:
example_call string example_call string
example_response string example_response string
endpoint_url string endpoint_url string
curl_example string // New field for curl command curl_example string
} }
// DocParam represents a parameter or result in the documentation // DocParam represents a parameter or result in the documentation
@@ -47,9 +48,11 @@ pub mut:
// AuthDocInfo contains authentication flow information // AuthDocInfo contains authentication flow information
pub struct AuthDocInfo { pub struct AuthDocInfo {
pub mut: pub mut:
steps []AuthStep enabled bool
steps []AuthStep
} }
// AuthStep represents a single step in the authentication flow
pub struct AuthStep { pub struct AuthStep {
pub mut: pub mut:
number int number int
@@ -60,81 +63,126 @@ pub mut:
example string example string
} }
// DocConfig holds configuration for documentation generation
pub struct DocConfig {
pub mut:
base_url string = 'http://localhost:8080'
handler_type string
auth_enabled bool = true
}
// doc_spec_from_openrpc converts an OpenRPC specification to a documentation-friendly DocSpec. // doc_spec_from_openrpc converts an OpenRPC specification to a documentation-friendly DocSpec.
// Processes all methods, parameters, and results with proper type extraction and example generation. // Processes all methods, parameters, and results with proper type extraction and example generation.
// Returns error if handler_type is empty or if OpenRPC spec is invalid. // Returns error if handler_type is empty or if OpenRPC spec is invalid.
pub fn doc_spec_from_openrpc(openrpc_spec openrpc.OpenRPC, handler_type string) !DocSpec { pub fn doc_spec_from_openrpc(openrpc_spec openrpc.OpenRPC, handler_type string) !DocSpec {
if handler_type.trim_space() == '' { return doc_spec_from_openrpc_with_config(openrpc_spec, DocConfig{
handler_type: handler_type
})
}
// doc_spec_from_openrpc_with_config converts an OpenRPC specification with custom configuration
pub fn doc_spec_from_openrpc_with_config(openrpc_spec openrpc.OpenRPC, config DocConfig) !DocSpec {
if config.handler_type.trim_space() == '' {
return error('handler_type cannot be empty') return error('handler_type cannot be empty')
} }
mut doc_spec := DocSpec{ mut doc_spec := DocSpec{
info: openrpc_spec.info info: openrpc_spec.info
auth_info: create_auth_info() base_url: config.base_url
auth_info: create_auth_info_with_config(config.auth_enabled)
} }
// Process all methods
for method in openrpc_spec.methods { for method in openrpc_spec.methods {
// Convert parameters doc_method := process_method(method, config)!
mut doc_params := []DocParam{}
for param in method.params {
if param is openrpc.ContentDescriptor {
type_info := extract_type_from_schema(param.schema)
example := generate_example_from_schema(param.schema, param.name)
doc_param := DocParam{
name: param.name
description: param.description
required: param.required
type_info: type_info
example: example
}
doc_params << doc_param
}
}
// Convert result
mut doc_result := DocParam{}
if method.result is openrpc.ContentDescriptor {
result_cd := method.result as openrpc.ContentDescriptor
type_info := extract_type_from_schema(result_cd.schema)
example := generate_example_from_schema(result_cd.schema, result_cd.name)
doc_result = DocParam{
name: result_cd.name
description: result_cd.description
required: result_cd.required
type_info: type_info
example: example
}
}
// Generate example call and response
example_call := generate_example_call(doc_params)
example_response := generate_example_response(doc_result)
// Generate JSON-RPC example call
jsonrpc_call := generate_jsonrpc_example_call(method.name, doc_params)
mut doc_method := DocMethod{
name: method.name
summary: method.summary
description: method.description
params: doc_params
result: doc_result
endpoint_url: '/api/${handler_type}'
example_call: example_call
example_response: example_response
curl_example: '' // Will be set later with proper base URL
}
// Generate curl example with localhost as default using JSON-RPC format
doc_method.curl_example = generate_curl_example_jsonrpc(method.name, doc_params,
'http://localhost:8080', handler_type)
doc_spec.methods << doc_method doc_spec.methods << doc_method
} }
return doc_spec return doc_spec
} }
// process_method converts a single OpenRPC method to a DocMethod
fn process_method(method openrpc.Method, config DocConfig) !DocMethod {
// Convert parameters
doc_params := process_parameters(method.params)!
// Convert result
doc_result := process_result(method.result)!
// Generate examples
example_call := generate_example_call(doc_params)
example_response := generate_example_response(doc_result)
doc_method := DocMethod{
name: method.name
summary: method.summary
description: method.description
params: doc_params
result: doc_result
endpoint_url: '${config.base_url}/api/${config.handler_type}'
example_call: example_call
example_response: example_response
curl_example: generate_curl_example_jsonrpc(method.name, doc_params, config.base_url,
config.handler_type)
}
return doc_method
}
// process_parameters converts OpenRPC parameters to DocParam array
fn process_parameters(params []openrpc.ContentDescriptorRef) ![]DocParam {
mut doc_params := []DocParam{}
for param in params {
if param is openrpc.ContentDescriptor {
type_info := extract_type_from_schema(param.schema)
example := generate_example_from_schema(param.schema, param.name)
doc_params << DocParam{
name: param.name
description: param.description
type_info: type_info
required: param.required
example: example
}
}
}
return doc_params
}
// process_result converts OpenRPC result to DocParam
fn process_result(result openrpc.ContentDescriptorRef) !DocParam {
mut doc_result := DocParam{}
if result is openrpc.ContentDescriptor {
type_info := extract_type_from_schema(result.schema)
example := generate_example_from_schema(result.schema, result.name)
doc_result = DocParam{
name: result.name
description: result.description
type_info: type_info
required: false // Results are never required
example: example
}
}
return doc_result
}
// create_auth_info_with_config creates authentication documentation based on configuration
fn create_auth_info_with_config(enabled bool) AuthDocInfo {
if !enabled {
return AuthDocInfo{
enabled: false
steps: []
}
}
return create_auth_info()
}
// extract_type_from_schema extracts the JSON Schema type from a SchemaRef. // extract_type_from_schema extracts the JSON Schema type from a SchemaRef.
// Returns the type string (e.g., 'string', 'object', 'array') or 'reference'/'unknown' for edge cases. // Returns the type string (e.g., 'string', 'object', 'array') or 'reference'/'unknown' for edge cases.
fn extract_type_from_schema(schema_ref jsonschema.SchemaRef) string { fn extract_type_from_schema(schema_ref jsonschema.SchemaRef) string {
@@ -205,7 +253,8 @@ fn generate_example_response(result DocParam) string {
// Create authentication documentation info // Create authentication documentation info
fn create_auth_info() AuthDocInfo { fn create_auth_info() AuthDocInfo {
return AuthDocInfo{ return AuthDocInfo{
steps: [ enabled: true
steps: [
AuthStep{ AuthStep{
number: 1 number: 1
title: 'Register Public Key' title: 'Register Public Key'

View File

@@ -2,6 +2,7 @@ module heroserver
import freeflowuniverse.herolib.crypt.herocrypt import freeflowuniverse.herolib.crypt.herocrypt
import freeflowuniverse.herolib.schemas.openrpc import freeflowuniverse.herolib.schemas.openrpc
import freeflowuniverse.herolib.ui.console
import veb import veb
// Create a new HeroServer instance // Create a new HeroServer instance
@@ -12,31 +13,42 @@ pub fn new(config HeroServerConfig) !&HeroServer {
} else { } else {
herocrypt.new_default()! herocrypt.new_default()!
} }
mut server := &HeroServer{ mut server := &HeroServer{
port: config.port port: config.port
host: config.host host: config.host
crypto_client: crypto_client crypto_client: crypto_client
sessions: map[string]Session{} sessions: map[string]Session{}
handlers: map[string]&openrpc.Handler{} handlers: map[string]&openrpc.Handler{}
challenges: map[string]AuthChallenge{} challenges: map[string]AuthChallenge{}
auth_enabled: config.auth_enabled
} }
console.print_header('HeroServer created on port ${server.port}')
return server return server
} }
// Register an OpenRPC handler // Register an OpenRPC handler
pub fn (mut server HeroServer) register_handler(handler_type string, handler &openrpc.Handler) ! { pub fn (mut server HeroServer) register_handler(handler_type string, handler &openrpc.Handler) ! {
server.handlers[handler_type] = handler server.handlers[handler_type] = handler
console.print_header('Registered handler: ${handler_type}')
} }
// Start the server // Start the server
pub fn (mut server HeroServer) start() ! { pub fn (mut server HeroServer) start() ! {
// Start VEB server // Start VEB server
handler_name := server.handlers.keys()[0]
console.print_green('Server starting on http://${server.host}:${server.port}')
console.print_green('HTML Homepage: http://${server.host}:${server.port}/')
console.print_green('JSON Info: http://${server.host}:${server.port}/json/${handler_name}')
console.print_green('Documentation: http://${server.host}:${server.port}/doc/${handler_name}')
console.print_green('Markdown Docs: http://${server.host}:${server.port}/md/${handler_name}')
console.print_green('API Endpoint: http://${server.host}:${server.port}/api/${handler_name}')
veb.run[HeroServer, Context](mut server, server.port) veb.run[HeroServer, Context](mut server, server.port)
} }
// Context struct for VEB // Context struct for VEB
pub struct Context { pub struct Context {
veb.Context veb.Context
} }

View File

@@ -8,8 +8,9 @@ import time
@[params] @[params]
pub struct HeroServerConfig { pub struct HeroServerConfig {
pub mut: pub mut:
port int = 9977 port int = 9977
host string = 'localhost' host string = 'localhost'
auth_enabled bool = true // Whether to enable authentication
// Optional crypto client, will create default if not provided // Optional crypto client, will create default if not provided
crypto_client ?&herocrypt.HeroCrypt crypto_client ?&herocrypt.HeroCrypt
} }
@@ -17,31 +18,43 @@ pub mut:
// Main server struct // Main server struct
pub struct HeroServer { pub struct HeroServer {
mut: mut:
port int port int
host string host string
crypto_client &herocrypt.HeroCrypt crypto_client &herocrypt.HeroCrypt
sessions map[string]Session // sessionkey -> Session sessions map[string]Session // sessionkey -> Session
handlers map[string]&openrpc.Handler // handlertype -> handler handlers map[string]&openrpc.Handler // handlertype -> handler
challenges map[string]AuthChallenge challenges map[string]AuthChallenge
pub mut:
auth_enabled bool = true // Whether authentication is required
} }
// Authentication challenge data // Authentication challenge data
pub struct AuthChallenge { pub struct AuthChallenge {
pub mut: pub mut:
pubkey string pubkey string
challenge string // unique hashed challenge challenge string // unique hashed challenge
created_at time.Time created_at time.Time
expires_at time.Time expires_at time.Time
} }
// Home page data for template rendering
pub struct HomePageData {
pub mut:
base_url string
handlers map[string]&openrpc.Handler
auth_enabled bool
host string
port int
}
// Active session data // Active session data
pub struct Session { pub struct Session {
pub mut: pub mut:
session_key string session_key string
pubkey string pubkey string
created_at time.Time created_at time.Time
last_activity time.Time last_activity time.Time
expires_at time.Time expires_at time.Time
} }
// Authentication request structures // Authentication request structures
@@ -62,7 +75,7 @@ pub:
pub struct AuthSubmitRequest { pub struct AuthSubmitRequest {
pub: pub:
pubkey string pubkey string
signature string // signed challenge signature string // signed challenge
} }
@@ -75,6 +88,72 @@ pub:
pub struct APIRequest { pub struct APIRequest {
pub: pub:
session_key string session_key string
method string method string
params map[string]string params map[string]string
} }
// JSON response structures for homepage
pub struct ServerInfoJSON {
pub:
server_name string
version string
description string
base_url string
host string
port int
auth_enabled bool
handlers []HandlerInfoJSON
endpoints EndpointsJSON
features []FeatureJSON
quick_start QuickStartJSON
}
pub struct HandlerInfoJSON {
pub:
name string
title string
description string
version string
api_endpoint string
doc_endpoint string
md_endpoint string
methods []MethodInfoJSON
}
pub struct MethodInfoJSON {
pub:
name string
summary string
description string
}
pub struct EndpointsJSON {
pub:
api_pattern string
documentation_pattern string
markdown_pattern string
home_json string
home_html string
}
pub struct FeatureJSON {
pub:
title string
description string
icon string
}
pub struct QuickStartJSON {
pub:
description string
example ExampleRequestJSON
}
pub struct ExampleRequestJSON {
pub:
method string
url string
headers map[string]string
body string
description string
}

View File

@@ -35,21 +35,24 @@ fn main() {
## API Endpoints ## API Endpoints
- **API Calls**: `POST /api/{handler_type}/{method_name}` - **HTML Homepage**: `GET /` - Returns HTML homepage with server information
- **Documentation**: `GET /doc/{handler_type}/` - **JSON Handler Info**: `GET /json/{handler_type}` - Returns handler information in JSON format
- **API Calls**: `POST /api/{handler_type}`
- **Documentation**: `GET /doc/{handler_type}`
- **Markdown Docs**: `GET /md/{handler_type}` - Returns documentation in markdown format
## Authentication Flow ## Authentication Flow
1. **Register Public Key**: `POST /auth/register` 1. **Register Public Key**: `POST /auth/register`
- Body: `{"pubkey": "your_public_key"}` - Body: `{"pubkey": "your_public_key"}`
2. **Request Challenge**: `POST /auth/authreq` 2. **Request Challenge**: `POST /auth/authreq`
- Body: `{"pubkey": "your_public_key"}` - Body: `{"pubkey": "your_public_key"}`
- Returns a unique challenge string. - Returns a unique challenge string.
3. **Submit Signature**: `POST /auth/auth` 3. **Submit Signature**: `POST /auth/auth`
- Sign the challenge from step 2 with your private key. - Sign the challenge from step 2 with your private key.
- Body: `{"pubkey": "your_public_key", "signature": "your_signature"}` - Body: `{"pubkey": "your_public_key", "signature": "your_signature"}`
- Returns a session key. - Returns a session key.
All subsequent API calls must include the session key in the `Authorization` header: All subsequent API calls must include the session key in the `Authorization` header:
`Authorization: Bearer {session_key}` `Authorization: Bearer {session_key}`

View File

@@ -225,7 +225,7 @@
<div class="card method-card" id="method-${method.name}"> <div class="card method-card" id="method-${method.name}">
<div class="card-header"> <div class="card-header">
<h5 class="mb-0">${method.name}</h5> <h5 class="mb-0">${method.name}</h5>
<div class="method-endpoint">POST ${method.endpoint_url}</div> <div class="method-endpoint mt-2 mb-2">POST ${method.endpoint_url}</div>
</div> </div>
<div class="card-body"> <div class="card-body">
@if method.summary.len > 0 @if method.summary.len > 0

View File

@@ -0,0 +1,393 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HeroServer - API Gateway</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
:root {
--google-blue: #1a73e8;
--google-blue-hover: #1557b0;
--google-gray: #5f6368;
--google-light-gray: #f8f9fa;
--google-border: #dadce0;
--google-text: #202124;
--google-text-secondary: #5f6368;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: var(--google-text);
line-height: 1.6;
}
.hero-section {
padding: 4rem 0;
margin-bottom: 3rem;
text-align: center;
}
.hero-section h1 {
font-weight: 300;
font-size: 3.5rem;
margin-bottom: 1.5rem;
color: var(--google-text);
}
.hero-section .lead {
font-weight: 400;
font-size: 1.25rem;
color: var(--google-text-secondary);
margin-bottom: 1rem;
}
.hero-section p {
color: var(--google-text-secondary);
font-size: 1rem;
}
.feature-card {
border: 1px solid var(--google-border);
border-radius: 8px;
padding: 1.5rem;
height: 100%;
background: white;
}
.feature-card h5 {
font-weight: 500;
color: var(--google-text);
margin-bottom: 1rem;
}
.feature-card p {
color: var(--google-text-secondary);
font-size: 0.95rem;
}
.endpoint-card {
border: 1px solid var(--google-border);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1rem;
background: white;
}
.endpoint-url {
background: var(--google-light-gray);
border: 1px solid var(--google-border);
padding: 0.75rem;
border-radius: 6px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 0.875rem;
margin-bottom: 0.75rem;
color: var(--google-text);
}
.badge-custom {
background: var(--google-blue);
color: white;
font-size: 0.75rem;
font-weight: 500;
padding: 0.4rem 0.8rem;
border-radius: 4px;
}
.btn-google-primary {
background: var(--google-blue);
border: 1px solid var(--google-blue);
color: white;
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 6px;
}
.btn-google-secondary {
background: white;
border: 1px solid var(--google-border);
color: var(--google-text);
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 6px;
}
.section-spacing {
margin-bottom: 3rem;
}
.code-block {
background: var(--google-light-gray);
border: 1px solid var(--google-border);
border-radius: 8px;
padding: 1.5rem;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 0.875rem;
overflow-x: auto;
}
.alert-google-info {
background: #e8f0fe;
border: 1px solid #d2e3fc;
border-radius: 8px;
padding: 1.5rem;
color: var(--google-text);
}
.alert-google-success {
background: #e6f4ea;
border: 1px solid #ceead6;
border-radius: 8px;
padding: 1.5rem;
color: var(--google-text);
}
.footer {
border-top: 1px solid var(--google-border);
padding: 3rem 0;
margin-top: 6rem;
color: var(--google-text-secondary);
}
h2 {
font-weight: 400;
color: var(--google-text);
margin-bottom: 2rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.text-muted {
color: var(--google-text-secondary) !important;
}
/* Ensure proper spacing and alignment */
.row {
margin-left: 0;
margin-right: 0;
}
.col-12,
.col-md-4,
.col-md-6 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
/* Fix button spacing */
.d-flex.gap-2 {
gap: 0.5rem !important;
}
/* Responsive improvements */
@media (max-width: 768px) {
.hero-section {
padding: 3rem 0;
margin-bottom: 2rem;
}
.hero-section h1 {
font-size: 2.5rem;
}
.feature-card,
.endpoint-card {
margin-bottom: 1rem;
padding: 1rem;
}
.section-spacing {
margin-bottom: 2rem;
}
}
</style>
</head>
<body>
<!-- Hero Section -->
<div class="hero-section">
<div class="container">
<div class="row">
<div class="col-12">
<h1>HeroServer</h1>
<p class="lead">Modern JSON-RPC 2.0 API Gateway with Dynamic Documentation</p>
<p class="mb-4">A powerful, secure, and developer-friendly API server built with V language</p>
</div>
</div>
</div>
</div>
<div class="container">
<!-- Features Section -->
<div class="row section-spacing">
<div class="col-12">
<h2 class="text-center">Key Features</h2>
</div>
<div class="col-md-4 mb-4">
<div class="feature-card text-center">
<h5>JSON-RPC 2.0</h5>
<p>Full compliance with JSON-RPC 2.0 specification for reliable API communication</p>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="feature-card text-center">
<h5>Dynamic Documentation</h5>
<p>Auto-generated interactive documentation with curl examples and copy buttons</p>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="feature-card text-center">
<h5>Secure Authentication</h5>
<p>Built-in cryptographic authentication with public key infrastructure</p>
</div>
</div>
</div>
<!-- Available Endpoints Section -->
<div class="row section-spacing">
<div class="col-12">
<h2 class="mb-2">Available API Endpoints</h2>
<p class="text-muted mb-3">Explore the available API handlers and their documentation</p>
</div>
</div>
<div class="row">
@for handler_name in server_info.handlers.keys()
<div class="col-md-6 mb-4">
<div class="endpoint-card">
<h5 class="mb-3">
<span class="badge-custom me-2">API</span>
${handler_name.title()}
</h5>
<div class="endpoint-url">
<strong>API Endpoint:</strong> /api/${handler_name}
</div>
<div class="endpoint-url">
<strong>Documentation:</strong> /doc/${handler_name}
</div>
<div class="endpoint-url">
<strong>JSON Info:</strong> /json/${handler_name}
</div>
<p class="text-muted mb-3">
JSON-RPC 2.0 API for ${handler_name} operations with full CRUD support
</p>
<div class="d-flex gap-2">
<a href="/doc/${handler_name}" class="btn btn-google-primary btn-sm">
View Documentation
</a>
<a href="/json/${handler_name}" class="btn btn-google-secondary btn-sm">
JSON Info
</a>
<a href="/md/${handler_name}" class="btn btn-google-secondary btn-sm">
Markdown Docs
</a>
<a href="/api/${handler_name}" class="btn btn-google-secondary btn-sm"
onclick="alert('This is a JSON-RPC endpoint. Use POST requests with JSON-RPC 2.0 format. See documentation for examples.'); return false;">
API Endpoint
</a>
</div>
</div>
</div>
@end
</div>
<!-- Quick Start Section -->
<div class="row section-spacing">
<div class="col-12">
<h2 class="mb-3">Quick Start</h2>
<div class="endpoint-card">
<h5 class="mb-2">Getting Handler Information</h5>
<p class="text-muted mb-3">Get detailed information about a specific API handler in JSON format:</p>
<div class="code-block">
<pre>curl ${server_info.base_url}/json/[handler_name]</pre>
</div>
<h5 class="mb-2 mt-4">Making API Calls</h5>
<p class="text-muted mb-3">All API endpoints use JSON-RPC 2.0 format. Here's a basic example:</p>
<div class="code-block">
<pre>curl -X POST ${server_info.base_url}/api/[handler_name] \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "method_name",
"params": {
"param1": "value1",
"param2": "value2"
},
"id": 1
}'</pre>
</div>
<div class="mt-3">
<small class="text-muted">
<strong>Tip:</strong> Visit the documentation pages for specific examples with
copy-to-clipboard functionality
</small>
</div>
</div>
</div>
</div>
@if server_info.auth_enabled
<!-- Authentication Section -->
<div class="row section-spacing">
<div class="col-12">
<h2 class="mb-3">Authentication</h2>
<div class="alert-google-info">
<h5 class="mb-2">Authentication Required</h5>
<p class="mb-0">
This server requires authentication. Please register your public key and obtain a session token
before making API calls. See the documentation for detailed authentication flow.
</p>
</div>
</div>
</div>
@else
<!-- No Authentication Notice -->
<div class="row section-spacing">
<div class="col-12">
<h2 class="mb-3">Authentication</h2>
<div class="alert-google-success">
<h5 class="mb-2">Open Access</h5>
<p class="mb-0">
Authentication is currently disabled. You can make API calls directly without authentication.
</p>
</div>
</div>
</div>
@end
</div>
<!-- Footer -->
<footer class="footer">
<div class="container text-center">
<p class="mb-0">
<strong>HeroServer</strong> - Built with V language •
<a href="https://github.com/freeflowuniverse/herolib" target="_blank" class="text-decoration-none">
View on GitHub
</a>
</p>
</div>
</footer>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.3/dist/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -1,94 +0,0 @@
# 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

@@ -1,117 +0,0 @@
module heroserver
import crypto.md5
import crypto.ed25519
import rand
import time
pub struct AuthConfig {
}
pub struct AuthManager {
mut:
registered_keys map[string]string // pubkey -> user_id
pending_auths map[string]AuthChallenge // challenge -> challenge_data
active_sessions map[string]Session // session_key -> session_data
}
pub struct AuthChallenge {
pub:
pubkey string
challenge string
created_at i64
expires_at i64
}
pub struct Session {
pub:
user_id string
pubkey string
created_at i64
expires_at i64
}
pub fn new_auth_manager(config AuthConfig) &AuthManager {
// Use config if needed, for now it's just passed
_ = config
return &AuthManager{}
}
// Register public key
pub fn (mut am AuthManager) register_pubkey(pubkey string) !string {
// Validate pubkey format
if pubkey.len != 64 { // ed25519 pubkey length
return error('Invalid public key format')
}
user_id := md5.hexhash(pubkey + time.now().unix().str())
am.registered_keys[pubkey] = user_id
return user_id
}
// Generate authentication challenge
pub fn (mut am AuthManager) create_auth_challenge(pubkey string) !string {
// Check if pubkey is registered
if pubkey !in am.registered_keys {
return error('Public key not registered')
}
// Generate unique challenge
random_data := rand.string(32)
challenge := md5.hexhash(pubkey + random_data + time.now().unix().str())
now := time.now().unix()
am.pending_auths[challenge] = AuthChallenge{
pubkey: pubkey
challenge: challenge
created_at: now
expires_at: now + 300 // 5 minutes
}
return challenge
}
// Verify signature and create session
pub fn (mut am AuthManager) verify_and_create_session(challenge string, signature string) !string {
// Get challenge data
auth_challenge := am.pending_auths[challenge] or {
return error('Invalid or expired challenge')
}
// Check expiration
if time.now().unix() > auth_challenge.expires_at {
am.pending_auths.delete(challenge)
return error('Challenge expired')
}
// Verify signature
pubkey_bytes := auth_challenge.pubkey.bytes()
challenge_bytes := challenge.bytes()
signature_bytes := signature.bytes()
ed25519.verify(pubkey_bytes, challenge_bytes, signature_bytes) or {
return error('Invalid signature')
}
// Create session
session_key := md5.hexhash(auth_challenge.pubkey + time.now().unix().str() + rand.string(16))
now := time.now().unix()
am.active_sessions[session_key] = Session{
user_id: am.registered_keys[auth_challenge.pubkey]
pubkey: auth_challenge.pubkey
created_at: now
expires_at: now + 3600 // 1 hour
}
// Clean up challenge
am.pending_auths.delete(challenge)
return session_key
}
// Validate session
pub fn (am AuthManager) validate_session(session_key string) bool {
session := am.active_sessions[session_key] or { return false }
return time.now().unix() < session.expires_at
}

View File

@@ -1,420 +0,0 @@
module heroserver
import freeflowuniverse.herolib.schemas.openrpc
import os
import freeflowuniverse.herolib.schemas.jsonschema
// 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: []
}
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
return process_doc_template(template_content, spec, api_name)
}
// 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
fn process_doc_template(template string, spec openrpc.OpenRPC, handler_type string) string {
mut content := template
// Replace template variables
content = content.replace('@{handler_type}', handler_type)
content = content.replace('@{spec.info.title}', spec.info.title)
content = content.replace('@{spec.info.description}', spec.info.description)
content = content.replace('@{spec.info.version}', spec.info.version)
// Generate methods documentation
mut methods_doc := ''
for method in spec.methods {
methods_doc += generate_method_doc(method)
}
content = content.replace('@{methods}', methods_doc)
return content
}
// Generate documentation for a single method
fn generate_method_doc(method openrpc.Method) string {
mut doc := '## ${method.name}\n\n'
if method.description.len > 0 {
doc += '${method.description}\n\n'
}
// Parameters
if method.params.len > 0 {
doc += '### Parameters\n\n'
for param in method.params {
// Handle both ContentDescriptor and Reference
if param is openrpc.ContentDescriptor {
if param.schema is jsonschema.Schema {
schema := param.schema as jsonschema.Schema
doc += '- **${param.name}** (${schema.typ}): ${param.description}\n'
}
}
}
doc += '\n'
}
// Result
if method.result is openrpc.ContentDescriptor {
result := method.result as openrpc.ContentDescriptor
doc += '### Returns\n\n'
doc += '${result.description}\n\n'
}
// Examples (would need to be added to OpenRPC spec or handled differently)
doc += '### Example\n\n'
doc += '```json\n'
doc += '// Request example would go here\n'
doc += '```\n\n'
return doc
}

View File

@@ -1,34 +0,0 @@
module heroserver
import freeflowuniverse.herolib.schemas.openrpc
pub struct HandlerRegistry {
mut:
handlers map[string]openrpc.Handler
specs map[string]openrpc.OpenRPC
}
pub fn new_handler_registry() &HandlerRegistry {
return &HandlerRegistry{}
}
// Register OpenRPC handler with type name
pub fn (mut hr HandlerRegistry) register(handler_type string, handler openrpc.Handler, spec openrpc.OpenRPC) {
hr.handlers[handler_type] = handler
hr.specs[handler_type] = spec
}
// Get handler by type
pub fn (hr HandlerRegistry) get(handler_type string) ?openrpc.Handler {
return hr.handlers[handler_type]
}
// Get OpenRPC spec by type
pub fn (hr HandlerRegistry) get_spec(handler_type string) ?openrpc.OpenRPC {
return hr.specs[handler_type]
}
// List all registered handler types
pub fn (hr HandlerRegistry) list_types() []string {
return hr.handlers.keys()
}

View File

@@ -1,142 +0,0 @@
module heroserver
import veb
import freeflowuniverse.herolib.schemas.jsonrpc
import freeflowuniverse.herolib.hero.crypt
import freeflowuniverse.herolib.schemas.openrpc
pub struct ServerConfig {
pub mut:
port int = 8080
auth_config AuthConfig
}
pub struct HeroServer {
pub mut:
config ServerConfig
auth_manager &AuthManager
handler_registry &HandlerRegistry
age_client &crypt.AGEClient
doc_registry &DocRegistry
}
pub struct Context {
veb.Context
}
// Start the server
pub fn (mut s HeroServer) start() ! {
veb.run[HeroServer, Context](mut s, s.config.port)
}
// Authentication endpoints
@['/register'; post]
pub fn (mut s HeroServer) register(mut ctx Context) veb.Result {
// Implementation for pubkey registration
return ctx.text('not implemented')
}
@['/authreq'; post]
pub fn (mut s HeroServer) authreq(mut ctx Context) veb.Result {
// Implementation for authentication request
return ctx.text('not implemented')
}
@['/auth'; post]
pub fn (mut s HeroServer) auth(mut ctx Context) veb.Result {
// Implementation for authentication verification
return ctx.text('not implemented')
}
// API endpoints
@['/api/:handler_type'; post]
pub fn (mut s HeroServer) api(mut ctx Context, handler_type string) veb.Result {
// Validate session
session_key := ctx.get_custom_header('Authorization') or { '' }
if !s.auth_manager.validate_session(session_key) {
return ctx.request_error('Invalid session')
}
// Get handler and process request
mut handler := s.handler_registry.get(handler_type) or { return ctx.not_found() }
request := jsonrpc.decode_request(ctx.req.data) or {
return ctx.request_error('Invalid JSON-RPC request')
}
response := handler.handle(request) or { return ctx.server_error('Handler error') }
return ctx.json(response)
}
// 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}') }
// Redirect to the first available API documentation (preferably heroserver)
if 'heroserver' in s.doc_registry.apis {
return ctx.redirect('/docs/heroserver')
}
// 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
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,30 +0,0 @@
# @{handler_type} API Documentation
@{spec.info.description}
**Version:** @{spec.info.version}
## Overview
This documentation provides details about the @{handler_type} API endpoints and their usage.
@{methods}
## Authentication
All API requests require a valid session key obtained through the authentication flow:
1. **Register**: Submit your public key to register
2. **Request Challenge**: Get an authentication challenge
3. **Authenticate**: Sign the challenge and submit for session key
4. **Use Session**: Include session key in subsequent API requests
## Error Handling
The API uses standard JSON-RPC 2.0 error codes:
- `-32700`: Parse error
- `-32600`: Invalid Request
- `-32601`: Method not found
- `-32602`: Invalid params
- `-32603`: Internal error

View File

@@ -1,350 +0,0 @@
<!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

@@ -1,297 +0,0 @@
# 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

@@ -1,348 +0,0 @@
# 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