refactor: Update JSON parsing and schema inflation

- Use `json2.decode[json2.Any]` instead of `json2.raw_decode`
- Add `@[required]` to procedure function signatures
- Improve error handling for missing JSONRPC fields
- Update `encode` to use `prettify: true`
- Add checks for missing schema and content descriptor references
This commit is contained in:
Mahmoud-Emad
2025-10-22 17:44:37 +03:00
parent 37f0aa0e96
commit 0bfb5cfdd0
16 changed files with 783 additions and 1069 deletions

View File

@@ -48,7 +48,7 @@ pub fn json_dict_get_any(r string, clean bool, key string) !json2.Any {
if r2.trim(' \n') == '' {
return error('Cannot do json2 raw decode in json_dict_get_any.\ndata was empty.')
}
data_raw := json2.raw_decode(r2) or {
data_raw := json2.decode[json2.Any](r2) or {
return error('Cannot do json2 raw decode in json_dict_get_any.\ndata:\n${r2}\nerror:${err}')
}
mut res := data_raw.as_map()
@@ -74,7 +74,7 @@ pub fn json_dict_filter_any(r string, clean bool, include []string, exclude []st
if r2.trim(' \n') == '' {
return error('Cannot do json2 raw decode in json_dict_filter_any.\ndata was empty.')
}
data_raw := json2.raw_decode(r2) or {
data_raw := json2.decode[json2.Any](r2) or {
return error('Cannot do json2 raw decode in json_dict_filter_any.\ndata:\n${r2}\nerror:${err}')
}
mut res := data_raw.as_map()
@@ -111,7 +111,7 @@ pub fn json_list_dict_get_any(r string, clean bool, key string) ![]json2.Any {
if r2.trim(' \n') == '' {
return error('Cannot do json2 raw decode in json_dict_get_any.\ndata was empty.')
}
data_raw := json2.raw_decode(r2) or {
data_raw := json2.decode[json2.Any](r2) or {
return error('Cannot do json2 raw decode in json_dict_get_any.\ndata:\n${r2}\nerror:${err}')
}
mut res_list := data_raw.arr()

View File

@@ -188,6 +188,15 @@ pub fn (mut server HeroServer) doc_handler(mut ctx Context, handler_type string)
return ctx.server_error('Failed to generate documentation: ${err}')
}
// Create server info for navbar
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 using the literal path
html_content := $tmpl('templates/doc.html')

View File

@@ -1,290 +1,59 @@
module heroserver
import x.json2
import incubaid.herolib.schemas.jsonschema
// ============================================================================
// Constants
// ============================================================================
// max_example_depth controls maximum recursion depth for nested schema examples
// to prevent infinite loops and keep examples readable
const max_example_depth = 4
// max_props_shallow controls maximum number of properties shown in object examples
// at shallow nesting levels (depth <= 2)
const max_props_shallow = 10
// max_props_deep controls maximum number of properties shown in object examples
// at deep nesting levels (depth > 2) to keep examples concise
const max_props_deep = 3
// ============================================================================
// JSON Formatting
// ============================================================================
// prettify_json takes a compact JSON string and returns it with pretty formatting.
// Uses V's built-in json2 module with 3-space indentation for consistent formatting.
// Falls back to the original string if parsing fails.
fn prettify_json(compact_json string) string {
parsed := json2.decode[json2.Any](compact_json) or { return compact_json }
return json2.encode(parsed, prettify: true, indent_string: ' ')
}
// ============================================================================
// Request Example Generation
// ============================================================================
// generate_curl_example creates a working curl command for a JSON-RPC method.
// Takes the method name, example request payload (params only), and endpoint URL.
// generate_curl_example creates a curl command for a JSON-RPC method.
// Returns a properly formatted curl command with JSON-RPC 2.0 wrapper.
pub fn generate_curl_example(method_name string, params_json string, endpoint_url string) string {
// Build the complete JSON-RPC request
mut jsonrpc_request := '{\n'
jsonrpc_request += ' "jsonrpc": "2.0",\n'
jsonrpc_request += ' "method": "${method_name}",\n'
jsonrpc_request += ' "params": '
// Add the params (already formatted with proper indentation)
// Need to indent each line by 2 spaces to align with the "params" key
params_lines := params_json.split('\n')
for i, line in params_lines {
if i == 0 {
jsonrpc_request += line
} else {
jsonrpc_request += '\n ' + line
}
}
// Add comma after params and then id field
jsonrpc_request += ',\n'
jsonrpc_request += ' "id": 1\n'
jsonrpc_request += '}'
// Escape single quotes for shell
jsonrpc_request := '{"jsonrpc":"2.0","method":"${method_name}","params":${params_json},"id":1}'
escaped_request := jsonrpc_request.replace("'", "'\\''")
// Build curl command
mut curl := "curl -X POST '${endpoint_url}' \\\n"
curl += " -H 'Content-Type: application/json' \\\n"
curl += " -d '${escaped_request}'"
return curl
return "curl -X POST '${endpoint_url}' -H 'Content-Type: application/json' -d '${escaped_request}'"
}
fn generate_request_example[T](model T) !string {
mut field_parts := []string{} // Build JSON manually to avoid type conflicts
for param in model {
// Use schema-generated examples (already populated by extract_example_from_schema)
// The example field contains dynamically generated values based on actual schema
if param.example.len == 0 || param.example.trim_space() == '' {
return error('Parameter "${param.name}" has no example - schema may be missing')
}
field_parts << '"${param.name}":${param.example}'
}
// Build compact JSON string
if field_parts.len == 0 {
// generate_request_example generates a JSON example from DocParam array.
// Extracts example values from the schema to create proper params.
// For single simple parameters, returns just the value.
// For multiple or complex parameters, returns a JSON object.
fn generate_request_example(params []DocParam) !string {
if params.len == 0 {
return '{}'
}
compact := '{${field_parts.join(',')}}'
// Single parameter with simple type (not object/array) - return just the value
if params.len == 1 {
example := params[0].example.trim_space()
if !example.starts_with('{') && !example.starts_with('[') {
return example
}
}
// Prettify using V's built-in json2 formatter
return prettify_json(compact)
// Multiple parameters or complex type - return JSON object
mut parts := []string{}
for param in params {
parts << '"${param.name}":${param.example}'
}
return '{${parts.join(',')}}'
}
// ============================================================================
// Schema-based Example Generation
// Schema Example Extraction
// ============================================================================
// These functions generate examples from JSON Schema objects (used for response examples)
// extract_example_from_schema extracts or generates an example value from a SchemaRef.
// After schema inflation, all references should be resolved to Schema objects.
// This function intelligently generates examples based on schema type and constraints.
// Returns a pretty-formatted JSON string for display in documentation.
// extract_example_from_schema extracts the example value from a schema.
// If no example is defined, returns an empty placeholder.
pub fn extract_example_from_schema(schema_ref jsonschema.SchemaRef) string {
compact := generate_example_from_schema_with_depth(schema_ref, 0, map[string]bool{})
return prettify_json(compact)
}
// generate_example_from_schema creates an example value for a parameter or result
pub fn generate_example_from_schema(schema_ref jsonschema.SchemaRef, param_name string) string {
compact := generate_example_from_schema_with_depth(schema_ref, 0, map[string]bool{})
return prettify_json(compact)
}
// generate_example_from_schema_with_depth recursively generates example values with depth limiting.
// Prevents infinite recursion from circular references by tracking depth and visited schemas.
// Max depth is controlled by max_example_depth constant to keep examples readable while showing structure.
fn generate_example_from_schema_with_depth(schema_ref jsonschema.SchemaRef, depth int, visited map[string]bool) string {
// Depth limit to prevent infinite recursion
// Return null for deeply nested structures to keep JSON valid
if depth > max_example_depth {
return 'null'
}
schema := match schema_ref {
jsonschema.Schema {
schema_ref
}
jsonschema.Reference {
// After inflation, references should be resolved
// If we still encounter a reference, return a placeholder
return '"<unresolved_reference>"'
}
jsonschema.Schema { schema_ref }
jsonschema.Reference { return '{}' }
}
// Check if schema has an explicit example - use it if available
// Note: json2.Any.str() returns '[]' or '{}' for empty/null values, so we filter those out
// We use json_str() instead of str() to get properly JSON-encoded values (with quotes for strings)
example_str := schema.example.json_str()
if example_str != '' && example_str != '[]' && example_str != '{}' && example_str != 'null' {
return example_str
}
// Track visited schemas by their ID to prevent circular references
schema_id := schema.id
if schema_id != '' {
if schema_id in visited {
return 'null'
}
}
// Create a new visited map for this branch
mut new_visited := visited.clone()
if schema_id != '' {
new_visited[schema_id] = true
}
// Infer type from schema structure if not explicitly set
schema_type := if schema.typ != '' {
schema.typ
} else if schema.properties.len > 0 {
'object'
} else if schema.items != none {
'array'
} else {
''
}
// Generate example based on schema type
match schema_type {
'string' {
// Use format hints if available
return match schema.format {
'date-time' {
'"2024-01-15T10:30:00Z"'
}
'date' {
'"2024-01-15"'
}
'time' {
'"10:30:00"'
}
'email' {
'"user@example.com"'
}
'uri', 'url' {
'"https://example.com"'
}
'uuid' {
'"550e8400-e29b-41d4-a716-446655440000"'
}
'ipv4' {
'"192.168.1.1"'
}
'ipv6' {
'"2001:0db8:85a3:0000:0000:8a2e:0370:7334"'
}
'hostname' {
'"example.com"'
}
else {
// Use schema title or description as hint, or generic placeholder
if schema.title != '' {
'"${schema.title}"'
} else if schema.description != '' && schema.description.len < 50 {
'"${schema.description}"'
} else {
'"Sample Text"'
}
}
}
}
'integer', 'number' {
// Use minimum/maximum if specified
if schema.minimum > 0 {
return schema.minimum.str()
}
if schema.maximum > 0 {
return schema.maximum.str()
}
return '42'
}
'boolean' {
return 'true'
}
'null' {
return 'null'
}
'array' {
// Generate array with one example item (compact format)
if items := schema.items {
item_example := if items is jsonschema.SchemaRef {
generate_example_from_schema_with_depth(items, depth + 1, new_visited)
} else if items is []jsonschema.SchemaRef {
if items.len > 0 {
generate_example_from_schema_with_depth(items[0], depth + 1, new_visited)
} else {
'null'
}
} else {
'null'
}
return '[${item_example}]'
}
return '[]'
}
'object' {
// Generate object with all properties (compact format)
if schema.properties.len > 0 {
mut props := []string{}
// Limit number of properties shown at deep levels
max_props := if depth > 2 { max_props_deep } else { max_props_shallow }
mut count := 0
for prop_name, prop_schema in schema.properties {
if count >= max_props {
break
}
prop_example := generate_example_from_schema_with_depth(prop_schema,
depth + 1, new_visited)
props << '"${prop_name}":${prop_example}'
count++
}
if props.len > 0 {
return '{${props.join(',')}}'
}
}
// Handle additionalProperties
if additional := schema.additional_properties {
value_example := generate_example_from_schema_with_depth(additional, depth + 1,
new_visited)
return '{"key":${value_example}}'
}
return '{}'
}
else {
// Handle oneOf - use first option
if schema.one_of.len > 0 {
return generate_example_from_schema_with_depth(schema.one_of[0], depth + 1,
new_visited)
}
// Unknown type
return 'null'
}
}
return '{}'
}

View File

@@ -8,470 +8,11 @@
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
/* CSS Variables for Light and Dark Themes */
:root {
/* Light mode colors */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
--text-primary: #212529;
--text-secondary: #6c757d;
--text-muted: #6c757d;
--border-color: #dee2e6;
--border-color-light: #e9ecef;
--link-color: #0d6efd;
--link-hover-color: #0a58ca;
--code-bg: #f8f9fa;
--code-text: #212529;
--card-bg: #ffffff;
--card-header-bg: #f8f9fa;
--method-group-bg: #f8f9fa;
--method-endpoint-bg: #e3f2fd;
--toc-bg: #f8f9fa;
--toc-group-bg: #f8f9fa;
--alert-info-bg: #cfe2ff;
--alert-info-border: #b6d4fe;
--alert-info-text: #084298;
--badge-bg: #0d6efd;
--badge-text: #ffffff;
}
/* Dark mode colors */
body.dark-mode {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #3a3a3a;
--text-primary: #e9ecef;
--text-secondary: #adb5bd;
--text-muted: #adb5bd;
--border-color: #495057;
--border-color-light: #3a3a3a;
--link-color: #6ea8fe;
--link-hover-color: #9ec5fe;
--code-bg: #2d2d2d;
--code-text: #e9ecef;
--card-bg: #2d2d2d;
--card-header-bg: #3a3a3a;
--method-group-bg: #2d2d2d;
--method-endpoint-bg: #1e3a5f;
--toc-bg: #2d2d2d;
--toc-group-bg: #3a3a3a;
--alert-info-bg: #052c65;
--alert-info-border: #084298;
--alert-info-text: #6ea8fe;
--badge-bg: #0d6efd;
--badge-text: #ffffff;
}
/* Smooth transitions for theme changes */
body,
.card,
.card-header,
.method-group-section,
.toc,
.toc-group,
.code-block,
.alert,
pre,
.method-endpoint {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
background-color: var(--bg-primary);
color: var(--text-primary);
}
.method-card {
margin-bottom: 1.5rem;
background-color: var(--card-bg);
border-color: var(--border-color);
}
.card-header {
background-color: var(--card-header-bg);
border-color: var(--border-color);
color: var(--text-primary);
}
.card-body {
background-color: var(--card-bg);
color: var(--text-primary);
}
.param-table {
font-size: 0.9rem;
color: var(--text-primary);
}
.param-table th,
.param-table td {
border-color: var(--border-color);
}
.code-block {
background-color: var(--code-bg);
border: 1px solid var(--border-color-light);
border-radius: 0.375rem;
padding: 1rem;
overflow-x: auto;
font-family: 'Courier New', monospace;
color: var(--code-text);
}
.code-block pre {
background-color: var(--code-bg);
color: var(--code-text);
margin: 0;
}
.object-section {
margin-bottom: 3rem;
}
.method-endpoint {
background-color: var(--method-endpoint-bg);
padding: 0.5rem;
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.9rem;
color: var(--text-primary);
}
.toc {
background-color: var(--toc-bg);
border: 1px solid var(--border-color);
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 2rem;
}
.toc ul {
margin-bottom: 0;
}
.toc a {
text-decoration: none;
color: var(--link-color);
}
.toc a:hover {
color: var(--link-hover-color);
}
pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
background-color: var(--code-bg);
color: var(--code-text);
}
.curl-section {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 0.375rem;
padding: 1rem;
margin-top: 1rem;
position: relative;
}
.copy-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: #007bff;
color: white;
border: none;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
cursor: pointer;
transition: background-color 0.2s;
}
.copy-button:hover {
background: #0056b3;
}
.copy-button.copied {
background: #28a745;
}
.curl-command {
font-family: 'Courier New', monospace;
font-size: 0.85rem;
margin: 0;
padding-right: 4rem;
}
.toc-header {
cursor: pointer;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
}
.toc-header:hover {
background-color: #e9ecef;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
margin: -0.25rem -0.5rem;
}
.toc-toggle-icon {
transition: transform 0.3s ease;
font-weight: bold;
font-size: 1.2rem;
}
.toc-toggle-icon.collapsed {
transform: rotate(-90deg);
}
/* Reusable collapsible section styles */
.collapsible-header {
cursor: pointer;
user-select: none;
transition: background-color 0.2s;
}
.collapsible-header:hover {
background-color: #f8f9fa;
}
.collapsible-toggle {
transition: transform 0.3s ease;
display: inline-block;
font-weight: bold;
margin-right: 0.5rem;
}
.collapsible-toggle.collapsed {
transform: rotate(-90deg);
}
.method-card .card-header {
cursor: pointer;
user-select: none;
}
.method-card .card-header:hover {
background-color: #f8f9fa;
}
.show-more-link {
cursor: pointer;
color: #007bff;
text-decoration: none;
font-size: 0.9rem;
display: inline-block;
margin-top: 0.5rem;
}
.show-more-link:hover {
text-decoration: underline;
}
/* Equal height for example request/response sections */
.examples-row {
display: flex;
flex-wrap: wrap;
}
.examples-row>[class*='col-'] {
display: flex;
flex-direction: column;
}
.examples-row .code-block {
flex: 1;
display: flex;
flex-direction: column;
}
.examples-row .code-block pre {
flex: 1;
}
/* Method group section styles */
.method-group-section {
margin-bottom: 2rem;
background-color: var(--method-group-bg);
border: 1px solid var(--border-color);
border-radius: 0.25rem;
padding: 1rem;
}
.method-group-header {
background-color: transparent;
border: none;
padding: 0.5rem 0;
margin-bottom: 1rem;
cursor: pointer;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
transition: opacity 0.2s ease;
}
.method-group-header:hover {
opacity: 0.8;
}
.method-group-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
}
.method-group-toggle {
transition: transform 0.3s ease;
font-size: 1.2rem;
color: var(--text-secondary);
}
.method-group-toggle.collapsed {
transform: rotate(-90deg);
}
.method-group-content {
padding: 0;
}
.method-group-content .method-card {
background-color: var(--card-bg);
}
/* TOC group styles */
.toc-group {
margin-bottom: 0.75rem;
background-color: var(--toc-group-bg);
border: 1px solid var(--border-color-light);
border-radius: 0.25rem;
padding: 0.5rem;
}
.toc-group-header {
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0;
cursor: pointer;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.25rem 0;
}
.toc-group-header:hover {
color: var(--text-secondary);
}
.toc-group-toggle {
transition: transform 0.3s ease;
font-size: 0.9rem;
color: var(--text-secondary);
margin-right: 0.5rem;
}
.toc-group-toggle.collapsed {
transform: rotate(-90deg);
}
.toc-group-methods {
margin-left: 1.5rem;
margin-top: 0.5rem;
margin-bottom: 0;
}
.toc-group-methods li {
margin-bottom: 0.25rem;
}
/* Theme toggle button */
.theme-toggle {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 1000;
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 50%;
width: 3rem;
height: 3rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
}
.theme-toggle:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.theme-toggle svg {
width: 1.5rem;
height: 1.5rem;
fill: var(--text-primary);
}
.text-muted {
color: var(--text-muted) !important;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--text-primary);
}
a {
color: var(--link-color);
}
a:hover {
color: var(--link-hover-color);
}
footer {
background-color: var(--bg-secondary) !important;
color: var(--text-primary);
border-top: 1px solid var(--border-color);
}
.badge {
background-color: var(--badge-bg) !important;
color: var(--badge-text) !important;
}
</style>
@include 'partials/theme_styles'
</head>
<body>
<!-- Theme Toggle Button -->
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode" title="Toggle dark/light mode">
<svg id="theme-icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="display: none;">
<path
d="M12 18C8.68629 18 6 15.3137 6 12C6 8.68629 8.68629 6 12 6C15.3137 6 18 8.68629 18 12C18 15.3137 15.3137 18 12 18ZM12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16ZM11 1H13V4H11V1ZM11 20H13V23H11V20ZM3.51472 4.92893L4.92893 3.51472L7.05025 5.63604L5.63604 7.05025L3.51472 4.92893ZM16.9497 18.364L18.364 16.9497L20.4853 19.0711L19.0711 20.4853L16.9497 18.364ZM19.0711 3.51472L20.4853 4.92893L18.364 7.05025L16.9497 5.63604L19.0711 3.51472ZM5.63604 16.9497L7.05025 18.364L4.92893 20.4853L3.51472 19.0711L5.63604 16.9497ZM23 11V13H20V11H23ZM4 11V13H1V11H4Z" />
</svg>
<svg id="theme-icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M10 7C10 10.866 13.134 14 17 14C18.9584 14 20.729 13.1957 21.9995 11.8995C22 11.933 22 11.9665 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C12.0335 2 12.067 2 12.1005 2.00049C10.8043 3.27098 10 5.04157 10 7ZM4 12C4 16.4183 7.58172 20 12 20C15.0583 20 17.7158 18.2839 19.062 15.7621C18.3945 15.9187 17.7035 16 17 16C12.0294 16 8 11.9706 8 7C8 6.29648 8.08133 5.60547 8.2379 4.938C5.71611 6.28423 4 8.9417 4 12Z" />
</svg>
</button>
@include 'partials/navbar'
<div class="container mt-4">
<!-- Header -->
@@ -531,27 +72,6 @@
</div>
</div>
<!-- Authentication Info -->
<!-- <div class="row mb-4">
<div class="col-12">
<div class="alert alert-info">
<h5>Authentication Required</h5>
<p>All API endpoints require authentication using a session key obtained through the authentication
flow:</p>
<ol>
@for step in spec.auth_info.steps
<li>
<strong>${step.title}</strong>:
<code>${step.method} ${step.endpoint}</code>
- ${step.description}
<div class="code-block mt-2">${step.example}</div>
</li>
@end
</ol>
</div>
</div>
</div> -->
<!-- Table of Contents -->
@if spec.methods.len > 0 || spec.objects.len > 0
<div class="row mb-4">
@@ -691,13 +211,21 @@
<div class="col-md-6">
<h6>Example Request:</h6>
<div class="code-block">
<pre>${method.example_request}</pre>
<button class="copy-button"
onclick="copyToClipboard('request-${method.name}', this)">
Copy
</button>
<pre id="request-${method.name}">${method.example_request}</pre>
</div>
</div>
<div class="col-md-6">
<h6>Example Response:</h6>
<div class="code-block">
<pre>${method.example_response}</pre>
<button class="copy-button"
onclick="copyToClipboard('response-${method.name}', this)">
Copy
</button>
<pre id="response-${method.name}">${method.example_response}</pre>
</div>
</div>
</div>
@@ -792,17 +320,25 @@
@end
<!-- Examples -->
<div class="row mt-3">
<div class="row mt-3 examples-row">
<div class="col-md-6">
<h6>Example Request:</h6>
<div class="code-block">
<pre>${method.example_request}</pre>
<button class="copy-button"
onclick="copyToClipboard('request-obj-${method.name}', this)">
Copy
</button>
<pre id="request-obj-${method.name}">${method.example_request}</pre>
</div>
</div>
<div class="col-md-6">
<h6>Example Response:</h6>
<div class="code-block">
<pre>${method.example_response}</pre>
<button class="copy-button"
onclick="copyToClipboard('response-obj-${method.name}', this)">
Copy
</button>
<pre id="response-obj-${method.name}">${method.example_response}</pre>
</div>
</div>
</div>
@@ -810,10 +346,10 @@
<!-- Curl Example -->
<div class="curl-section">
<h6>Curl Command:</h6>
<button class="copy-button" onclick="copyToClipboard('curl-${method.name}', this)">
📋 Copy
<button class="copy-button" onclick="copyToClipboard('curl-obj-${method.name}', this)">
Copy
</button>
<pre class="curl-command" id="curl-${method.name}">${method.curl_example}</pre>
<pre class="curl-command" id="curl-obj-${method.name}">${method.curl_example}</pre>
</div>
</div>
</div>
@@ -824,66 +360,14 @@
@end
@end
<!-- Footer -->
<footer class="mt-5 py-4 bg-light">
<div class="container text-center">
<p class="mb-0">Generated from OpenRPC specification • ${spec.info.title}
v${spec.info.version}</p>
</div>
</footer>
</div>
@include 'partials/footer'
<!-- Bootstrap JS Bundle (includes Popper) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Theme Toggle Functionality -->
<script>
// Theme management
function toggleTheme() {
const body = document.body;
const isDark = body.classList.toggle('dark-mode');
// Save preference to localStorage
localStorage.setItem('theme', isDark ? 'dark' : 'light');
// Update icon visibility
updateThemeIcon(isDark);
}
function updateThemeIcon(isDark) {
const sunIcon = document.getElementById('theme-icon-sun');
const moonIcon = document.getElementById('theme-icon-moon');
if (isDark) {
sunIcon.style.display = 'block';
moonIcon.style.display = 'none';
} else {
sunIcon.style.display = 'none';
moonIcon.style.display = 'block';
}
}
function initializeTheme() {
// Check localStorage for saved preference
const savedTheme = localStorage.getItem('theme');
// If no saved preference, check system preference
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
// Apply theme
const shouldBeDark = savedTheme === 'dark' || (!savedTheme && prefersDark);
if (shouldBeDark) {
document.body.classList.add('dark-mode');
}
// Update icon
updateThemeIcon(shouldBeDark);
}
// Initialize theme on page load
initializeTheme();
</script>
@include 'partials/theme_script'
<!-- Copy to Clipboard Functionality -->
<script>
@@ -1023,8 +507,132 @@
: 'Show less ▲';
}
// JSON Prettification
function prettifyJSON(jsonString) {
try {
// Try to parse and prettify the JSON
const parsed = JSON.parse(jsonString);
return JSON.stringify(parsed, null, 3); // 3 spaces indentation
} catch (e) {
// If parsing fails, return original string
return jsonString;
}
}
function prettifyCurlJSON(curlCommand) {
try {
// Extract JSON from curl -d '...' part
const match = curlCommand.match(/-d\s+'([^']+)'/);
if (match && match[1]) {
const jsonStr = match[1];
const parsed = JSON.parse(jsonStr);
const prettified = JSON.stringify(parsed, null, 2); // 2 spaces for curl
// Replace the compact JSON with prettified version
// Escape single quotes in the prettified JSON
const escapedPrettified = prettified.replace(/'/g, "'\\''");
return curlCommand.replace(jsonStr, escapedPrettified);
}
} catch (e) {
// If parsing fails, return original
}
return curlCommand;
}
function prettifyAllJSON() {
// Prettify example requests and responses
document.querySelectorAll('.code-block pre').forEach(pre => {
const content = pre.textContent.trim();
if (content && (content.startsWith('{') || content.startsWith('['))) {
pre.textContent = prettifyJSON(content);
}
});
// Prettify parameter examples in tables
document.querySelectorAll('.param-table code.text-muted').forEach(code => {
const content = code.textContent.trim();
if (content && (content.startsWith('{') || content.startsWith('['))) {
code.textContent = prettifyJSON(content);
}
});
// Prettify result examples
document.querySelectorAll('.alert-light code.text-muted').forEach(code => {
const content = code.textContent.trim();
if (content && (content.startsWith('{') || content.startsWith('['))) {
code.textContent = prettifyJSON(content);
}
});
// Prettify JSON in curl commands
document.querySelectorAll('.curl-command').forEach(pre => {
const content = pre.textContent.trim();
if (content.includes('curl') && content.includes('-d')) {
pre.textContent = prettifyCurlJSON(content);
}
});
}
// Scroll to method and expand it
function scrollToMethod(methodName) {
// Find the method card
const methodCard = document.getElementById('method-' + methodName);
if (!methodCard) return;
// Find which group this method belongs to
const methodGroup = methodCard.closest('.method-group-content');
if (methodGroup) {
const groupId = methodGroup.id;
const groupIdx = groupId.replace('group-', '');
// Expand the group if collapsed
const bsGroupCollapse = bootstrap.Collapse.getOrCreateInstance(methodGroup);
if (!methodGroup.classList.contains('show')) {
bsGroupCollapse.show();
const groupToggle = document.getElementById('group-toggle-' + groupIdx);
if (groupToggle) {
groupToggle.classList.remove('collapsed');
}
}
}
// Find the method body
const methodBody = methodCard.querySelector('[id^="method-body-"]');
if (methodBody) {
// Expand the method if collapsed
const bsMethodCollapse = bootstrap.Collapse.getOrCreateInstance(methodBody);
if (!methodBody.classList.contains('show')) {
bsMethodCollapse.show();
const bodyId = methodBody.id;
const toggleIcon = document.getElementById('toggle-' + bodyId.replace('method-body-', ''));
if (toggleIcon) {
toggleIcon.classList.remove('collapsed');
}
}
}
// Scroll to the method with smooth animation
setTimeout(() => {
methodCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 300); // Wait for collapse animations to complete
}
// Handle hash navigation on page load and hash change
function handleHashNavigation() {
const hash = window.location.hash;
if (hash && hash.startsWith('#method-')) {
const methodName = hash.replace('#method-', '');
scrollToMethod(methodName);
}
}
// Table of Contents toggle functionality
document.addEventListener('DOMContentLoaded', function () {
// Prettify all JSON on page load
prettifyAllJSON();
// Handle hash changes (when clicking TOC links)
window.addEventListener('hashchange', handleHashNavigation);
const tocContent = document.getElementById('tocContent');
const tocHeader = document.querySelector('.toc-header');
const tocIcon = document.querySelector('.toc-toggle-icon');
@@ -1095,6 +703,8 @@
});
// Initialize method cards - expand only the first method in the first group
// BUT: if there's a hash in the URL, don't expand the first method (hash navigation will handle it)
const hasHash = window.location.hash && window.location.hash.startsWith('#method-');
const methodBodies = document.querySelectorAll('[id^="method-body-"]');
methodBodies.forEach((body) => {
const bodyId = body.id;
@@ -1103,8 +713,8 @@
const methodIdx = parts[1];
const toggleIcon = document.getElementById('toggle-' + groupIdx + '-' + methodIdx);
if (groupIdx === '0' && methodIdx === '0') {
// First method in first group - expand it
if (!hasHash && groupIdx === '0' && methodIdx === '0') {
// First method in first group - expand it (only if no hash)
const bsCollapse = new bootstrap.Collapse(body, { toggle: false });
bsCollapse.show();
if (toggleIcon) {
@@ -1117,6 +727,15 @@
}
}
});
// Handle initial hash navigation AFTER all initialization is complete
// This ensures the hash navigation can properly expand the target method
if (hasHash) {
// Use setTimeout to ensure all Bootstrap collapse instances are initialized
setTimeout(() => {
handleHashNavigation();
}, 100);
}
});
</script>
</body>

View File

@@ -8,192 +8,11 @@
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
/* Smooth transitions for theme changes */
body,
.card,
.alert,
.btn,
.navbar,
.dropdown-menu {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
/* Dark mode overrides */
body.dark-mode {
background-color: #1a1a1a;
color: #e9ecef;
}
body.dark-mode .navbar {
background-color: #2d2d2d !important;
border-bottom-color: #495057;
}
body.dark-mode .card {
background-color: #2d2d2d;
border-color: #495057;
color: #e9ecef;
}
body.dark-mode .bg-light {
background-color: #3a3a3a !important;
color: #e9ecef;
}
body.dark-mode pre,
body.dark-mode code {
background-color: #3a3a3a;
color: #e9ecef;
border-color: #495057;
}
body.dark-mode .text-muted {
color: #adb5bd !important;
}
body.dark-mode .border,
body.dark-mode .border-top {
border-color: #495057 !important;
}
body.dark-mode .dropdown-menu {
background-color: #2d2d2d;
border-color: #495057;
}
body.dark-mode .dropdown-item {
color: #e9ecef;
}
body.dark-mode .dropdown-item:hover {
background-color: #3a3a3a;
color: #e9ecef;
}
body.dark-mode .dropdown-header {
color: #adb5bd;
}
body.dark-mode .navbar-toggler {
border-color: #495057;
}
body.dark-mode .navbar-toggler-icon {
filter: invert(1);
}
body.dark-mode .nav-link {
color: #adb5bd;
}
body.dark-mode .nav-link:hover {
color: #6ea8fe;
}
body.dark-mode .navbar-brand {
color: #e9ecef;
}
/* Theme toggle button in navbar */
.theme-toggle-navbar {
background-color: transparent;
border: 1px solid #dee2e6;
border-radius: 50%;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
margin-left: 1rem;
}
body.dark-mode .theme-toggle-navbar {
border-color: #495057;
}
.theme-toggle-navbar:hover {
background-color: #f8f9fa;
transform: scale(1.05);
}
body.dark-mode .theme-toggle-navbar:hover {
background-color: #3a3a3a;
}
.theme-toggle-navbar svg {
width: 1.25rem;
height: 1.25rem;
}
/* Responsive improvements */
@media (max-width: 768px) {
.theme-toggle-navbar {
margin-left: 0;
margin-top: 0.5rem;
}
}
</style>
@include 'partials/theme_styles'
</head>
<body>
<!-- Navigation Bar -->
<nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom sticky-top">
<div class="container">
<a class="navbar-brand fw-semibold" href="/">
HeroServer
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto align-items-center">
@if server_info.handlers.len > 0
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
API Documentation
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
@for handler_name, _ in server_info.handlers
<li>
<h6 class="dropdown-header">${handler_name.to_upper()}</h6>
</li>
<li><a class="dropdown-item" href="/doc/${handler_name}">Documentation</a></li>
<li><a class="dropdown-item" href="/json/${handler_name}">OpenRPC JSON</a></li>
<li><a class="dropdown-item" href="/md/${handler_name}">Markdown Docs</a></li>
@end
</ul>
</li>
@end
<li class="nav-item">
<a class="nav-link" href="#features">Features</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#endpoints">Endpoints</a>
</li>
<li class="nav-item">
<button class="theme-toggle-navbar" onclick="toggleTheme()" aria-label="Toggle dark mode"
title="Toggle dark/light mode">
<svg id="theme-icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
style="display: none;">
<path
d="M12 18C8.68629 18 6 15.3137 6 12C6 8.68629 8.68629 6 12 6C15.3137 6 18 8.68629 18 12C18 15.3137 15.3137 18 12 18ZM12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16ZM11 1H13V4H11V1ZM11 20H13V23H11V20ZM3.51472 4.92893L4.92893 3.51472L7.05025 5.63604L5.63604 7.05025L3.51472 4.92893ZM16.9497 18.364L18.364 16.9497L20.4853 19.0711L19.0711 20.4853L16.9497 18.364ZM19.0711 3.51472L20.4853 4.92893L18.364 7.05025L16.9497 5.63604L19.0711 3.51472ZM5.63604 16.9497L7.05025 18.364L4.92893 20.4853L3.51472 19.0711L5.63604 16.9497ZM23 11V13H20V11H23ZM4 11V13H1V11H4Z" />
</svg>
<svg id="theme-icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M10 7C10 10.866 13.134 14 17 14C18.9584 14 20.729 13.1957 21.9995 11.8995C22 11.933 22 11.9665 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C12.0335 2 12.067 2 12.1005 2.00049C10.8043 3.27098 10 5.04157 10 7ZM4 12C4 16.4183 7.58172 20 12 20C15.0583 20 17.7158 18.2839 19.062 15.7621C18.3945 15.9187 17.7035 16 17 16C12.0294 16 8 11.9706 8 7C8 6.29648 8.08133 5.60547 8.2379 4.938C5.71611 6.28423 4 8.9417 4 12Z" />
</svg>
</button>
</li>
</ul>
</div>
</div>
</nav>
@include 'partials/navbar'
<!-- Hero Section -->
<div class="py-5 text-center">
@@ -328,10 +147,10 @@
</div>
<!-- Available Endpoints Section -->
<div class="row mb-5" id="endpoints">
<div class="row mb-4" id="endpoints">
<div class="col-12">
<h2 class="mb-3">Available API Endpoints</h2>
<p class="text-muted mb-4">Explore the available API handlers and their documentation</p>
<p class="text-muted">Explore the available API handlers and their documentation</p>
</div>
</div>
@@ -362,7 +181,7 @@
</p>
<div class="d-flex flex-wrap gap-2">
<a href="/doc/${handler_name}" class="btn btn-primary btn-sm">
<a href="/doc/${handler_name}" class="btn btn-outline-secondary btn-sm">
View Documentation
</a>
<a href="/json/${handler_name}" class="btn btn-outline-secondary btn-sm">
@@ -447,69 +266,12 @@
@end
</div>
<!-- Footer -->
<footer class="border-top py-4 mt-5">
<div class="container text-center">
<p class="text-muted mb-0">
<strong>HeroServer</strong> - Built with V language •
<a href="https://github.com/incubaid/herolib" target="_blank" class="text-decoration-none">
View on GitHub
</a>
</p>
</div>
</footer>
@include 'partials/footer'
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Theme Toggle Functionality -->
<script>
// Theme management
function toggleTheme() {
const body = document.body;
const isDark = body.classList.toggle('dark-mode');
// Save preference to localStorage
localStorage.setItem('theme', isDark ? 'dark' : 'light');
// Update icon visibility
updateThemeIcon(isDark);
}
function updateThemeIcon(isDark) {
const sunIcon = document.getElementById('theme-icon-sun');
const moonIcon = document.getElementById('theme-icon-moon');
if (isDark) {
sunIcon.style.display = 'block';
moonIcon.style.display = 'none';
} else {
sunIcon.style.display = 'none';
moonIcon.style.display = 'block';
}
}
function initializeTheme() {
// Check localStorage for saved preference
const savedTheme = localStorage.getItem('theme');
// If no saved preference, check system preference
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
// Apply theme
const shouldBeDark = savedTheme === 'dark' || (!savedTheme && prefersDark);
if (shouldBeDark) {
document.body.classList.add('dark-mode');
}
// Update icon
updateThemeIcon(shouldBeDark);
}
// Initialize theme on page load
initializeTheme();
</script>
@include 'partials/theme_script'
</body>
</html>

View File

@@ -0,0 +1,12 @@
<!-- Footer -->
<footer class="border-top py-4 mt-5">
<div class="container text-center">
<p class="text-muted mb-0">
<strong>HeroServer</strong> - Built with V language •
<a href="https://github.com/incubaid/herolib" target="_blank" class="text-decoration-none">
View on GitHub
</a>
</p>
</div>
</footer>

View File

@@ -0,0 +1,55 @@
<!-- Navigation Bar -->
<nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom sticky-top">
<div class="container">
<a class="navbar-brand fw-semibold" href="/">
HeroServer
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto align-items-center">
@if server_info.handlers.len > 0
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
API Documentation
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
@for handler_name, _ in server_info.handlers
<li>
<h6 class="dropdown-header">${handler_name.to_upper()}</h6>
</li>
<li><a class="dropdown-item" href="/doc/${handler_name}">Documentation</a></li>
<li><a class="dropdown-item" href="/json/${handler_name}">OpenRPC JSON</a></li>
<li><a class="dropdown-item" href="/md/${handler_name}">Markdown Docs</a></li>
@end
</ul>
</li>
@end
<li class="nav-item">
<a class="nav-link" href="/#features">Features</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/#endpoints">Endpoints</a>
</li>
<li class="nav-item">
<button class="theme-toggle-navbar" onclick="toggleTheme()" aria-label="Toggle dark mode"
title="Toggle dark/light mode">
<svg id="theme-icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
style="display: none;">
<path
d="M12 18C8.68629 18 6 15.3137 6 12C6 8.68629 8.68629 6 12 6C15.3137 6 18 8.68629 18 12C18 15.3137 15.3137 18 12 18ZM12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16ZM11 1H13V4H11V1ZM11 20H13V23H11V20ZM3.51472 4.92893L4.92893 3.51472L7.05025 5.63604L5.63604 7.05025L3.51472 4.92893ZM16.9497 18.364L18.364 16.9497L20.4853 19.0711L19.0711 20.4853L16.9497 18.364ZM19.0711 3.51472L20.4853 4.92893L18.364 7.05025L16.9497 5.63604L19.0711 3.51472ZM5.63604 16.9497L7.05025 18.364L4.92893 20.4853L3.51472 19.0711L5.63604 16.9497ZM23 11V13H20V11H23ZM4 11V13H1V11H4Z" />
</svg>
<svg id="theme-icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M10 7C10 10.866 13.134 14 17 14C18.9584 14 20.729 13.1957 21.9995 11.8995C22 11.933 22 11.9665 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C12.0335 2 12.067 2 12.1005 2.00049C10.8043 3.27098 10 5.04157 10 7ZM4 12C4 16.4183 7.58172 20 12 20C15.0583 20 17.7158 18.2839 19.062 15.7621C18.3945 15.9187 17.7035 16 17 16C12.0294 16 8 11.9706 8 7C8 6.29648 8.08133 5.60547 8.2379 4.938C5.71611 6.28423 4 8.9417 4 12Z" />
</svg>
</button>
</li>
</ul>
</div>
</div>
</nav>

View File

@@ -0,0 +1,40 @@
<!-- Theme Toggle Functionality -->
<script>
function toggleTheme() {
const body = document.body;
const sunIcon = document.getElementById('theme-icon-sun');
const moonIcon = document.getElementById('theme-icon-moon');
body.classList.toggle('dark-mode');
if (body.classList.contains('dark-mode')) {
// In dark mode, show sun icon (to switch to light)
sunIcon.style.display = 'block';
moonIcon.style.display = 'none';
localStorage.setItem('theme', 'dark');
} else {
// In light mode, show moon icon (to switch to dark)
sunIcon.style.display = 'none';
moonIcon.style.display = 'block';
localStorage.setItem('theme', 'light');
}
}
// Load saved theme on page load
document.addEventListener('DOMContentLoaded', function () {
const savedTheme = localStorage.getItem('theme');
const sunIcon = document.getElementById('theme-icon-sun');
const moonIcon = document.getElementById('theme-icon-moon');
if (savedTheme === 'dark') {
document.body.classList.add('dark-mode');
// In dark mode, show sun icon (to switch to light)
sunIcon.style.display = 'block';
moonIcon.style.display = 'none';
} else {
// In light mode, show moon icon (to switch to dark)
sunIcon.style.display = 'none';
moonIcon.style.display = 'block';
}
});
</script>

View File

@@ -0,0 +1,443 @@
<!-- Smooth transitions for theme changes -->
<style>
body,
.card,
.alert,
.btn,
.navbar,
.dropdown-menu {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
/* Dark mode overrides */
body.dark-mode {
background-color: #1a1a1a;
color: #e9ecef;
}
body.dark-mode .navbar {
background-color: #2d2d2d !important;
border-bottom-color: #495057;
}
body.dark-mode .card {
background-color: #2d2d2d;
border-color: #495057;
color: #e9ecef;
}
body.dark-mode .bg-light {
background-color: #3a3a3a !important;
color: #e9ecef;
}
body.dark-mode pre,
body.dark-mode code {
background-color: #3a3a3a;
color: #e9ecef;
border-color: #495057;
}
body.dark-mode .text-muted {
color: #adb5bd !important;
}
body.dark-mode .border,
body.dark-mode .border-top,
body.dark-mode hr {
border-color: #495057 !important;
}
body.dark-mode .dropdown-menu {
background-color: #2d2d2d;
border-color: #495057;
}
body.dark-mode .dropdown-item {
color: #e9ecef;
}
body.dark-mode .dropdown-item:hover {
background-color: #3a3a3a;
color: #e9ecef;
}
body.dark-mode .dropdown-header {
color: #adb5bd;
}
body.dark-mode .navbar-toggler {
border-color: #495057;
}
body.dark-mode .navbar-toggler-icon {
filter: invert(1);
}
body.dark-mode .nav-link {
color: #adb5bd;
}
body.dark-mode .nav-link:hover {
color: #6ea8fe;
}
body.dark-mode .navbar-brand {
color: #e9ecef;
}
/* Tables in dark mode */
body.dark-mode .table {
color: #e9ecef;
border-color: #495057;
--bs-table-bg: transparent;
--bs-table-striped-bg: #2d2d2d;
--bs-table-hover-bg: #3a3a3a;
--bs-table-border-color: #495057;
}
body.dark-mode .table th,
body.dark-mode .table td {
border-color: #495057;
color: #e9ecef;
background-color: transparent;
}
body.dark-mode .table thead th {
background-color: #2d2d2d;
border-color: #495057;
color: #e9ecef;
}
body.dark-mode .table tbody tr {
background-color: transparent;
}
body.dark-mode .table tbody tr:hover {
background-color: #3a3a3a;
}
/* Badges in dark mode */
body.dark-mode .badge {
color: #fff;
}
body.dark-mode .badge.bg-secondary {
background-color: #6c757d !important;
}
body.dark-mode .badge.bg-danger {
background-color: #dc3545 !important;
}
body.dark-mode .badge.bg-success {
background-color: #198754 !important;
}
body.dark-mode .badge.bg-primary {
background-color: #0d6efd !important;
}
/* Alerts in dark mode */
body.dark-mode .alert-light {
background-color: #2d2d2d;
border-color: #495057;
color: #e9ecef;
}
body.dark-mode .alert-info {
background-color: #052c65;
border-color: #084298;
color: #6ea8fe;
}
/* Links in dark mode */
body.dark-mode a {
color: #6ea8fe;
}
body.dark-mode a:hover {
color: #8bb9fe;
}
/* Theme toggle button in navbar */
.theme-toggle-navbar {
background-color: transparent;
border: 1px solid #dee2e6;
border-radius: 50%;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
margin-left: 1rem;
}
body.dark-mode .theme-toggle-navbar {
border-color: #495057;
}
.theme-toggle-navbar:hover {
background-color: #f8f9fa;
transform: scale(1.05);
}
body.dark-mode .theme-toggle-navbar:hover {
background-color: #3a3a3a;
}
.theme-toggle-navbar svg {
width: 1.25rem;
height: 1.25rem;
fill: currentColor;
}
/* Sun icon should be light in dark mode */
body.dark-mode .theme-toggle-navbar svg {
fill: #f8f9fa;
}
/* Moon icon should be dark in light mode */
body:not(.dark-mode) .theme-toggle-navbar svg {
fill: #212529;
}
/* Responsive improvements */
@media (max-width: 768px) {
.theme-toggle-navbar {
margin-left: 0;
margin-top: 0.5rem;
}
}
/* Table of Contents */
.toc {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1rem;
}
body.dark-mode .toc {
background-color: #2d2d2d;
border-color: #495057;
}
.toc-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
padding: 0.5rem;
border-radius: 0.25rem;
transition: background-color 0.2s;
}
.toc-header:hover {
background-color: #e9ecef;
}
body.dark-mode .toc-header:hover {
background-color: #3a3a3a;
}
.toc-toggle-icon {
transition: transform 0.3s;
font-size: 0.875rem;
}
.toc-toggle-icon.collapsed {
transform: rotate(-90deg);
}
.toc-group {
margin-bottom: 1rem;
}
.toc-group-header {
cursor: pointer;
padding: 0.5rem;
border-radius: 0.25rem;
display: flex;
justify-content: space-between;
align-items: center;
transition: background-color 0.2s;
}
.toc-group-header:hover {
background-color: #e9ecef;
}
body.dark-mode .toc-group-header:hover {
background-color: #3a3a3a;
}
.toc-group-toggle {
display: inline-block;
transition: transform 0.3s;
margin-right: 0.5rem;
font-size: 0.75rem;
}
.toc-group-toggle.collapsed {
transform: rotate(-90deg);
}
.toc-group-methods {
list-style: none;
padding-left: 1.5rem;
margin-top: 0.5rem;
}
.toc-group-methods li {
padding: 0.25rem 0;
}
/* Method Groups */
.method-group-section {
margin-bottom: 2rem;
}
.method-group-header {
cursor: pointer;
padding: 1rem;
background-color: #f8f9fa;
border-radius: 0.375rem;
margin-bottom: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
transition: background-color 0.2s;
}
body.dark-mode .method-group-header {
background-color: #2d2d2d;
}
.method-group-header:hover {
background-color: #e9ecef;
}
body.dark-mode .method-group-header:hover {
background-color: #3a3a3a;
}
.method-group-toggle {
transition: transform 0.3s;
font-size: 1.25rem;
}
.method-group-toggle.collapsed {
transform: rotate(-90deg);
}
/* Method Cards */
.method-card .card-header {
cursor: pointer;
transition: background-color 0.2s;
}
.method-card .card-header:hover {
background-color: #f8f9fa;
}
body.dark-mode .method-card .card-header:hover {
background-color: #3a3a3a;
}
.collapsible-toggle {
display: inline-block;
transition: transform 0.3s;
margin-right: 0.5rem;
font-size: 0.875rem;
}
.collapsible-toggle.collapsed {
transform: rotate(-90deg);
}
.method-endpoint {
font-family: 'Courier New', monospace;
background-color: #e9ecef;
padding: 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
}
body.dark-mode .method-endpoint {
background-color: #3a3a3a;
color: #e9ecef;
}
/* Code blocks */
.code-block {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1rem;
overflow-x: auto;
font-family: 'Courier New', monospace;
position: relative;
}
body.dark-mode .code-block {
background-color: #2d2d2d;
border-color: #495057;
color: #e9ecef;
}
.code-block pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
/* Copy button */
.copy-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0.25rem 0.75rem;
font-size: 0.875rem;
background-color: #0d6efd;
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
transition: background-color 0.2s;
}
.copy-button:hover {
background-color: #0b5ed7;
}
.copy-button.copied {
background-color: #198754;
}
/* Curl section */
.curl-section {
margin-top: 1rem;
position: relative;
}
.curl-command {
background-color: #212529;
color: #f8f9fa;
padding: 1rem;
border-radius: 0.375rem;
overflow-x: auto;
font-family: 'Courier New', monospace;
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
body.dark-mode .curl-command {
background-color: #1a1a1a;
border: 1px solid #495057;
}
</style>

View File

@@ -86,13 +86,13 @@ pub fn (mut handler Handler) register_api_handler(groupname string, procedure_gr
pub struct Procedure[T, U] {
pub mut:
method string
function fn (T) !U
function fn (T) !U @[required]
}
pub struct ProcedureVoid[T] {
pub mut:
method string
function fn (T) !
function fn (T) ! @[required]
}
pub fn (pw Procedure[T, U]) handle(request Request) !Response {

View File

@@ -52,10 +52,10 @@ pub fn new_request(method string, params string) Request {
pub fn decode_request(data string) !Request {
mut r2 := json2.decode[json2.Any](data)!
mut r3 := r2.as_map()
a := r3['jsonrpc'].str()
b := r3['method'].str()
c := r3['params'].str()
d := r3['id'].int()
a := (r3['jsonrpc'] or { return error('jsonrpc field not found') }).str()
b := (r3['method'] or { return error('method field not found') }).str()
c := (r3['params'] or { return error('params field not found') }).str()
d := (r3['id'] or { return error('id field not found') }).int()
mut r4 := Request{
jsonrpc: a
method: b
@@ -70,7 +70,7 @@ pub fn decode_request(data string) !Request {
// Returns:
// - A JSON string representation of the Request
pub fn (req Request) encode() string {
return json2.encode_pretty(req)
return json2.encode(req, prettify: true)
}
// validate checks if the Request object contains all required fields
@@ -133,7 +133,7 @@ pub fn new_request_generic[T](method string, params T) RequestGeneric[T] {
// Returns:
// - The ID as a string, or an error if the ID field is missing
pub fn decode_request_id(data string) !int {
data_any := json2.raw_decode(data)!
data_any := json2.decode[json2.Any](data)!
data_map := data_any.as_map()
id_any := data_map['id'] or { return error('ID field not found') }
return id_any.int()
@@ -148,7 +148,7 @@ pub fn decode_request_id(data string) !int {
// Returns:
// - The method name as a string, or an error if the method field is missing
pub fn decode_request_method(data string) !string {
data_any := json2.raw_decode(data)!
data_any := json2.decode[json2.Any](data)!
data_map := data_any.as_map()
method_any := data_map['method'] or { return error('Method field not found') }
return method_any.str()

View File

@@ -105,7 +105,7 @@ pub fn new_error_response(id int, error RPCError) Response {
// Returns:
// - A Response object or an error if parsing fails or the response is invalid
pub fn decode_response(data string) !Response {
raw := json2.raw_decode(data) or {
raw := json2.decode[json2.Any](data) or {
return error('Failed to decode JSONRPC response ${data}\n${err}')
}
raw_map := raw.as_map()

View File

@@ -7,7 +7,7 @@ import json
// Handles complex fields like properties, additionalProperties, items, and examples
// that require custom parsing beyond standard JSON decoding.
pub fn decode(data string) !Schema {
schema_map := json2.raw_decode(data)!.as_map()
schema_map := json2.decode[Any](data)!.as_map()
mut schema := json.decode(Schema, data)!
// Process fields that require custom decoding

View File

@@ -19,7 +19,7 @@ pub fn decode(data string) !OpenRPC {
mut object := json.decode(OpenRPC, data) or {
return error('Failed to decode json\n=======\n${data}\n===========\n${err}')
}
data_map := json2.raw_decode(data)!.as_map()
data_map := json2.decode[Any](data)!.as_map()
if 'components' in data_map {
object.components = decode_components(data_map) or {
return error('Failed to decode components\n${err}')

View File

@@ -12,7 +12,11 @@ pub fn (s OpenRPC) inflate_method(method Method) Method {
pub fn (s OpenRPC) inflate_content_descriptor(cd_ ContentDescriptorRef) ContentDescriptor {
cd := if cd_ is Reference {
s.components.content_descriptors[cd_.ref] as ContentDescriptor
ref_key := cd_.ref
descriptor_ref := s.components.content_descriptors[ref_key] or {
panic('Content descriptor not found: ${ref_key}')
}
descriptor_ref as ContentDescriptor
} else {
cd_ as ContentDescriptor
}
@@ -35,7 +39,9 @@ pub fn (s OpenRPC) inflate_schema(schema_ref SchemaRef) Schema {
panic('not implemented')
}
schema_name := schema_ref.ref.trim_string_left('#/components/schemas/')
s.inflate_schema(s.components.schemas[schema_name])
s.inflate_schema(s.components.schemas[schema_name] or {
panic('Schema not found: ${schema_name}')
})
} else {
schema_ref as Schema
}