diff --git a/examples/hero/heromodels/heroserver_example.vsh b/examples/hero/heromodels/heroserver_example.vsh index 441a1f0e..1078ca46 100755 --- a/examples/hero/heromodels/heroserver_example.vsh +++ b/examples/hero/heromodels/heroserver_example.vsh @@ -1,7 +1,6 @@ #!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals -no-skip-unused run import incubaid.herolib.hero.heromodels -import incubaid.herolib.hero.db import time fn main() { diff --git a/lib/data/ourjson/ourjson.v b/lib/data/ourjson/ourjson.v index c3f2e2a5..1db2213d 100644 --- a/lib/data/ourjson/ourjson.v +++ b/lib/data/ourjson/ourjson.v @@ -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() diff --git a/lib/hero/heroserver/doc_handler.v b/lib/hero/heroserver/doc_handler.v index 5794015f..748a426f 100644 --- a/lib/hero/heroserver/doc_handler.v +++ b/lib/hero/heroserver/doc_handler.v @@ -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') diff --git a/lib/hero/heroserver/examples.v b/lib/hero/heroserver/examples.v index 77db5cf8..1db7081d 100644 --- a/lib/hero/heroserver/examples.v +++ b/lib/hero/heroserver/examples.v @@ -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 '""' - } + 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 '{}' } diff --git a/lib/hero/heroserver/templates/doc.html b/lib/hero/heroserver/templates/doc.html index 6de7c6de..7fdfb989 100644 --- a/lib/hero/heroserver/templates/doc.html +++ b/lib/hero/heroserver/templates/doc.html @@ -8,470 +8,11 @@ - - + @include 'partials/theme_styles' - - + @include 'partials/navbar'
@@ -531,27 +72,6 @@
- - - @if spec.methods.len > 0 || spec.objects.len > 0
@@ -691,13 +211,21 @@
Example Request:
-
${method.example_request}
+ +
${method.example_request}
Example Response:
-
${method.example_response}
+ +
${method.example_response}
@@ -792,17 +320,25 @@ @end -
+
Example Request:
-
${method.example_request}
+ +
${method.example_request}
Example Response:
-
${method.example_response}
+ +
${method.example_response}
@@ -810,10 +346,10 @@
Curl Command:
- -
${method.curl_example}
+
${method.curl_example}
@@ -824,66 +360,14 @@ @end @end - - + @include 'partials/footer' + - - + @include 'partials/theme_script' diff --git a/lib/hero/heroserver/templates/home.html b/lib/hero/heroserver/templates/home.html index 15e71b2a..c51a92ed 100644 --- a/lib/hero/heroserver/templates/home.html +++ b/lib/hero/heroserver/templates/home.html @@ -8,192 +8,11 @@ - - + @include 'partials/theme_styles' - - + @include 'partials/navbar'
@@ -328,10 +147,10 @@
-
+

Available API Endpoints

-

Explore the available API handlers and their documentation

+

Explore the available API handlers and their documentation

@@ -362,7 +181,7 @@

- - + @include 'partials/footer' - - + @include 'partials/theme_script' \ No newline at end of file diff --git a/lib/hero/heroserver/templates/partials/footer.html b/lib/hero/heroserver/templates/partials/footer.html new file mode 100644 index 00000000..26145dc3 --- /dev/null +++ b/lib/hero/heroserver/templates/partials/footer.html @@ -0,0 +1,12 @@ + + + diff --git a/lib/hero/heroserver/templates/partials/navbar.html b/lib/hero/heroserver/templates/partials/navbar.html new file mode 100644 index 00000000..e492dac2 --- /dev/null +++ b/lib/hero/heroserver/templates/partials/navbar.html @@ -0,0 +1,55 @@ + + + diff --git a/lib/hero/heroserver/templates/partials/theme_script.html b/lib/hero/heroserver/templates/partials/theme_script.html new file mode 100644 index 00000000..5dbd541d --- /dev/null +++ b/lib/hero/heroserver/templates/partials/theme_script.html @@ -0,0 +1,40 @@ + + \ No newline at end of file diff --git a/lib/hero/heroserver/templates/partials/theme_styles.html b/lib/hero/heroserver/templates/partials/theme_styles.html new file mode 100644 index 00000000..a4e93828 --- /dev/null +++ b/lib/hero/heroserver/templates/partials/theme_styles.html @@ -0,0 +1,443 @@ + + \ No newline at end of file diff --git a/lib/schemas/jsonrpc/handler.v b/lib/schemas/jsonrpc/handler.v index dbf13728..ae27ba2c 100644 --- a/lib/schemas/jsonrpc/handler.v +++ b/lib/schemas/jsonrpc/handler.v @@ -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 { diff --git a/lib/schemas/jsonrpc/model_request.v b/lib/schemas/jsonrpc/model_request.v index 06a6be10..00802ad4 100644 --- a/lib/schemas/jsonrpc/model_request.v +++ b/lib/schemas/jsonrpc/model_request.v @@ -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() diff --git a/lib/schemas/jsonrpc/model_response.v b/lib/schemas/jsonrpc/model_response.v index 489c7364..71d4c8a2 100644 --- a/lib/schemas/jsonrpc/model_response.v +++ b/lib/schemas/jsonrpc/model_response.v @@ -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() diff --git a/lib/schemas/jsonschema/decode.v b/lib/schemas/jsonschema/decode.v index 03c8ca8c..92af4b83 100644 --- a/lib/schemas/jsonschema/decode.v +++ b/lib/schemas/jsonschema/decode.v @@ -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 diff --git a/lib/schemas/openrpc/decode.v b/lib/schemas/openrpc/decode.v index 72c9b405..4256f73f 100644 --- a/lib/schemas/openrpc/decode.v +++ b/lib/schemas/openrpc/decode.v @@ -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}') diff --git a/lib/schemas/openrpc/inflate.v b/lib/schemas/openrpc/inflate.v index 5006412d..ddb57477 100644 --- a/lib/schemas/openrpc/inflate.v +++ b/lib/schemas/openrpc/inflate.v @@ -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 }