diff --git a/examples/hero/heromodels/heroserver_example.vsh b/examples/hero/heromodels/heroserver_example.vsh index 441a1f0e..5311ee92 100755 --- a/examples/hero/heromodels/heroserver_example.vsh +++ b/examples/hero/heromodels/heroserver_example.vsh @@ -1,4 +1,4 @@ -#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals -no-skip-unused run +#!/usr/bin/env -S v -n -w -gc none -cc gcc -d use_openssl -enable-globals -no-skip-unused run import incubaid.herolib.hero.heromodels import incubaid.herolib.hero.db diff --git a/lib/hero/heroserver/doc_model.v b/lib/hero/heroserver/doc_model.v index e46c5a16..82828cae 100644 --- a/lib/hero/heroserver/doc_model.v +++ b/lib/hero/heroserver/doc_model.v @@ -92,9 +92,11 @@ pub fn doc_spec_from_openrpc_with_config(openrpc_spec openrpc.OpenRPC, config Do auth_info: create_auth_info_with_config(config.auth_enabled) } - // Process all methods + // Process all methods - inflate each method to resolve $ref references for method in openrpc_spec.methods { - doc_method := process_method(method, config)! + // 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 } @@ -109,11 +111,8 @@ fn process_method(method openrpc.Method, config DocConfig) !DocMethod { // Convert result doc_result := process_result(method.result)! - example_response := if doc_result.example.len > 0 { - doc_result.example - } else { - generate_response_example(doc_result)! - } + // Example is always generated by extract_example_from_schema + example_response := doc_result.example // example_call := generate_example_call(doc_params) @@ -231,35 +230,6 @@ fn extract_type_from_schema(schema_ref jsonschema.SchemaRef) string { return 'unknown' } -// extract_example_from_schema extracts the example value from a SchemaRef -fn extract_example_from_schema(schema_ref jsonschema.SchemaRef) string { - schema := match schema_ref { - jsonschema.Schema { - schema_ref - } - jsonschema.Reference { - return '"reference_value"' - } - } - - if schema.example.str() != '' { - return schema.example.str() - } - return '' -} - -// generate_example_from_schema creates an example value for a parameter or result -fn generate_example_from_schema(schema_ref jsonschema.SchemaRef, param_name string) string { - match schema_ref { - jsonschema.Schema { - return '"example_value"' - } - jsonschema.Reference { - return '"reference_value"' - } - } -} - // Create authentication documentation info fn create_auth_info() AuthDocInfo { return AuthDocInfo{ diff --git a/lib/hero/heroserver/examples.v b/lib/hero/heroserver/examples.v index 16c16901..823b1aa5 100644 --- a/lib/hero/heroserver/examples.v +++ b/lib/hero/heroserver/examples.v @@ -1,129 +1,253 @@ module heroserver -import rand +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 +// ============================================================================ fn generate_request_example[T](model T) !string { mut field_parts := []string{} // Build JSON manually to avoid type conflicts for param in model { - // Prioritize user-provided examples over generated ones - // Check for meaningful examples (not empty, not just [], not just {}) - value := if param.example.len > 0 && param.example != '[]' && param.example != '{}' - && param.example.trim_space() != '' { - param.example - } else { - generate_example_value(param.type_info)! + // 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}": ${value}' + field_parts << '"${param.name}":${param.example}' } - return '{${field_parts.join(', ')}}' + // Build compact JSON string + if field_parts.len == 0 { + return '{}' + } + + compact := '{${field_parts.join(',')}}' + + // Prettify using V's built-in json2 formatter + return prettify_json(compact) } -// Generate dynamic example values based on type -fn generate_example_value(type_info string) !string { - type_lower := type_info.to_lower() +// ============================================================================ +// Schema-based Example Generation +// ============================================================================ +// These functions generate examples from JSON Schema objects (used for response examples) - // Handle array types first (including array[type] format) - if type_lower.starts_with('array') || type_lower.starts_with('[') { - return generate_array_example(type_info)! +// 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. +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' } - // Handle object types (including object[type] format) - if type_lower.starts_with('object') { - return if type_info.contains('[') { - generate_map_example(type_info)! - } else { - generate_object_example()! + 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 '""' } } - // Handle basic types - return match type_lower { - 'string', 'str', 'text' { - '"${rand.string(8)}"' + // 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' } - 'integer', 'int', 'number' { - '${rand.intn(1000)!}' - } - 'boolean', 'bool' { - if rand.intn(2)! == 0 { - 'false' - } else { - 'true' + } + + // 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 other complex types like map[string]int, etc. - if type_info.contains('map') || type_info.starts_with('map[') { - generate_map_example(type_info)! - } else { - '"example_value"' + // 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' } } } - -// Generate example array based on type -fn generate_array_example(type_info string) !string { - // Extract item type from array notation: array[integer] -> integer - item_type := if type_info.contains('[') && type_info.contains(']') { - start := type_info.index('[') or { 0 } + 1 - end := type_info.last_index(']') or { type_info.len } - if start < end { - type_info[start..end] - } else { - 'string' - } - } else { - 'string' // default for plain "array" type - } - - // Generate 2-3 sample items based on the item type - count := rand.intn(2)! + 2 // 2 or 3 items - mut items := []string{} - for _ in 0 .. count { - items << generate_example_value(item_type)! - } - return '[${items.join(', ')}]' -} - -// Generate example map/object -fn generate_map_example(type_info string) !string { - // Extract value type from map notation: map[string]int -> int - value_type := if type_info.contains(']') { - parts := type_info.split(']') - if parts.len > 1 { - parts[1] - } else { - 'string' - } - } else { - 'string' // default - } - - // Generate 2-3 sample key-value pairs - count := rand.intn(2)! + 2 // 2 or 3 pairs - mut pairs := []string{} - for i in 0 .. count { - key := '"key${i + 1}"' - value := generate_example_value(value_type)! - pairs << '${key}: ${value}' - } - return '{${pairs.join(', ')}}' -} - -// Generate generic object example -fn generate_object_example() !string { - sample_props := [ - '"id": ${rand.intn(1000)!}', - '"name": "${rand.string(6)}"', - '"active": ${if rand.intn(2)! == 0 { 'false' } else { 'true' }}', - ] - return '{${sample_props.join(', ')}}' -} - -fn generate_response_example[T](model T) !string { - println('response model: ${model}') - return 'xxxx' -} diff --git a/lib/schemas/openrpc/inflate.v b/lib/schemas/openrpc/inflate.v index 777d7878..5006412d 100644 --- a/lib/schemas/openrpc/inflate.v +++ b/lib/schemas/openrpc/inflate.v @@ -40,15 +40,28 @@ pub fn (s OpenRPC) inflate_schema(schema_ref SchemaRef) Schema { schema_ref as Schema } - if items := schema.items { - return Schema{ - ...schema - items: s.inflate_items(items) - } + // Inflate properties recursively + mut inflated_properties := map[string]SchemaRef{} + for prop_name, prop_schema in schema.properties { + inflated_properties[prop_name] = SchemaRef(s.inflate_schema(prop_schema)) } - return Schema{ + + // Inflate items if present + mut result_schema := Schema{ ...schema + properties: inflated_properties } + + if items := schema.items { + result_schema.items = s.inflate_items(items) + } + + // Inflate additional_properties if present + if additional := schema.additional_properties { + result_schema.additional_properties = s.inflate_schema(additional) + } + + return result_schema } pub fn (s OpenRPC) inflate_items(items Items) Items {