refactor: Improve schema example generation and inflation

- Inflate methods to resolve $ref references
- Use schema-generated examples for requests
- Implement robust recursive schema example generation
- Add constants for example generation depth and property limits
- Utilize V's json2 module for JSON pretty-printing
This commit is contained in:
Mahmoud-Emad
2025-10-21 15:02:18 +03:00
parent 27c8273eec
commit a120ef2676
4 changed files with 253 additions and 146 deletions

View File

@@ -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{

View File

@@ -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 '"<unresolved_reference>"'
}
}
// 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'
}