Files
herolib/lib/hero/heroserver/doc_model.v
Mahmoud-Emad 63c2efc921 feat: Group API methods and improve TOC
- Add method grouping by model/actor prefix
- Introduce DocMethodGroup struct for grouped methods
- Refactor TOC to display methods by groups
- Add collapsible sections for method groups and methods
- Improve CSS for better presentation of grouped content
2025-10-21 16:43:43 +03:00

342 lines
9.6 KiB
V

module heroserver
import incubaid.herolib.schemas.openrpc
import incubaid.herolib.schemas.jsonschema
// DocSpec is the main object passed to the documentation template.
pub struct DocSpec {
pub mut:
info openrpc.Info
methods []DocMethod
objects []DocObject
method_groups []DocMethodGroup // Grouped methods by model/actor prefix
auth_info AuthDocInfo
base_url string // Dynamic base URL for examples
}
// DocMethodGroup represents a group of methods for the same model/actor
pub struct DocMethodGroup {
pub mut:
name string // e.g., "calendar", "calendar_event", "user"
display_name string // e.g., "Calendar", "Calendar Event", "User"
methods []DocMethod
}
// DocObject represents a logical grouping of methods.
pub struct DocObject {
pub mut:
name string
description string
methods []DocMethod
}
// DocMethod holds the information for a single method to be displayed.
pub struct DocMethod {
pub mut:
name string
summary string
description string
params []DocParam
result DocParam
example_request string
example_response string
endpoint_url string
curl_example string
}
// DocParam represents a parameter or result in the documentation
pub struct DocParam {
pub mut:
name string
description string
type_info string
required bool
example string
}
// AuthDocInfo contains authentication flow information
pub struct AuthDocInfo {
pub mut:
enabled bool
steps []AuthStep
}
// AuthStep represents a single step in the authentication flow
pub struct AuthStep {
pub mut:
number int
title string
method string
endpoint string
description 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.
// 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.
pub fn doc_spec_from_openrpc(openrpc_spec openrpc.OpenRPC, handler_type string) !DocSpec {
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')
}
mut doc_spec := DocSpec{
info: openrpc_spec.info
base_url: config.base_url
auth_info: create_auth_info_with_config(config.auth_enabled)
}
// Process all methods - inflate each method to resolve $ref references
for method in openrpc_spec.methods {
// Inflate the method to resolve all schema references
inflated_method := openrpc_spec.inflate_method(method)
doc_method := process_method(inflated_method, config)!
doc_spec.methods << doc_method
}
// Group methods by their model/actor prefix
doc_spec.method_groups = group_methods_by_prefix(doc_spec.methods)
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)!
example_request := generate_request_example(doc_params)!
// Convert result
doc_result := process_result(method.result)!
// Example is always generated by extract_example_from_schema
example_response := doc_result.example
endpoint_url := '${config.base_url}/api/${config.handler_type}'
curl_example := generate_curl_example(method.name, example_request, endpoint_url)
doc_method := DocMethod{
name: method.name
summary: method.summary
description: method.description
params: doc_params
result: doc_result
example_response: example_response
example_request: example_request
endpoint_url: endpoint_url
curl_example: curl_example
}
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 := extract_example_from_schema(param.schema)
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 := extract_example_from_schema(result.schema)
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.
// Returns detailed type string (e.g., 'string', 'array[integer]', 'object') for better example generation.
fn extract_type_from_schema(schema_ref jsonschema.SchemaRef) string {
schema := match schema_ref {
jsonschema.Schema {
schema_ref
}
jsonschema.Reference {
return 'reference'
}
}
if schema.typ.len > 0 {
// For arrays, include the item type if available
if schema.typ == 'array' {
if items := schema.items {
// Handle single schema reference (most common case)
if items is jsonschema.SchemaRef {
item_type := extract_type_from_schema(items)
return 'array[${item_type}]'
}
// Handle array of schema references (tuple validation)
if items is []jsonschema.SchemaRef {
if items.len > 0 {
item_type := extract_type_from_schema(items[0])
return 'array[${item_type}]'
}
}
}
}
// For objects with additionalProperties, include the value type
if schema.typ == 'object' {
if additional := schema.additional_properties {
value_type := extract_type_from_schema(additional)
return 'object[${value_type}]'
}
}
return schema.typ
}
return 'unknown'
}
// Create authentication documentation info
fn create_auth_info() AuthDocInfo {
return AuthDocInfo{
enabled: true
steps: [
AuthStep{
number: 1
title: 'Register Public Key'
method: 'POST'
endpoint: '/auth/register'
description: 'Register your public key with the server'
example: '{\n "pubkey": "your_public_key_here"\n}'
},
AuthStep{
number: 2
title: 'Request Challenge'
method: 'POST'
endpoint: '/auth/authreq'
description: 'Request an authentication challenge'
example: '{\n "pubkey": "your_public_key_here"\n}'
},
AuthStep{
number: 3
title: 'Submit Signature'
method: 'POST'
endpoint: '/auth/auth'
description: 'Sign the challenge and submit for authentication'
example: '{\n "pubkey": "your_public_key_here",\n "signature": "signed_challenge"\n}'
},
AuthStep{
number: 4
title: 'Use Session Key'
method: 'ALL'
endpoint: '/api/{handler}/{method}'
description: 'Include session key in Authorization header for all API calls'
example: 'Authorization: Bearer {session_key}'
},
]
}
}
// group_methods_by_prefix groups methods by their model/actor prefix (the part before the dot)
// For example: "calendar.get", "calendar.set" -> group "calendar"
// "calendar_event.get" -> group "calendar_event"
fn group_methods_by_prefix(methods []DocMethod) []DocMethodGroup {
mut groups_map := map[string][]DocMethod{}
// Group methods by prefix
for method in methods {
prefix := extract_method_prefix(method.name)
if prefix !in groups_map {
groups_map[prefix] = []DocMethod{}
}
groups_map[prefix] << method
}
// Convert map to array of DocMethodGroup
mut groups := []DocMethodGroup{}
for prefix, group_methods in groups_map {
groups << DocMethodGroup{
name: prefix
display_name: format_display_name(prefix)
methods: group_methods
}
}
// Sort groups alphabetically by name
groups.sort(a.name < b.name)
return groups
}
// extract_method_prefix extracts the model/actor prefix from a method name
// Examples: "calendar.get" -> "calendar"
// "calendar_event.set" -> "calendar_event"
// "user.delete" -> "user"
fn extract_method_prefix(method_name string) string {
parts := method_name.split('.')
if parts.len > 0 {
return parts[0]
}
return method_name
}
// format_display_name converts a prefix to a human-readable display name
// Examples: "calendar" -> "Calendar"
// "calendar_event" -> "Calendar Event"
// "chat_message" -> "Chat Message"
fn format_display_name(prefix string) string {
// Split by underscore and capitalize each word
words := prefix.split('_')
mut capitalized := []string{}
for word in words {
if word.len > 0 {
capitalized << word[0].ascii_str().to_upper() + word[1..]
}
}
return capitalized.join(' ')
}