feat: Improve example generation for API specs

- Enhance `extract_type_from_schema` to detail array and object types.
- Introduce `generate_example_value` for dynamic example generation.
- Add `generate_array_example` and `generate_map_example` helper functions.
- Refactor `Method.example` to build JSON manually and use `json_str()`.
This commit is contained in:
Mahmoud-Emad
2025-09-22 16:04:26 +03:00
parent ba48ae255b
commit 44ec137db5
4 changed files with 190 additions and 31 deletions

View File

@@ -40,6 +40,50 @@
"schema": {
"$ref": "#/components/schemas/Calendar"
}
},
{
"name": "events",
"description": "Array of event IDs to add to the calendar",
"required": false,
"schema": {
"type": "array",
"example": [
1,
2,
3
],
"items": {
"type": "integer",
"minimum": 0
}
}
},
{
"name": "color",
"description": "Hex color code for the calendar",
"required": false,
"schema": {
"type": "string",
"example": "#FF0000"
}
},
{
"name": "timezone",
"description": "Timezone for the calendar",
"required": false,
"schema": {
"type": "string",
"example": "UTC"
}
},
{
"name": "is_public",
"description": "Whether the calendar is public",
"required": false,
"schema": {
"type": "boolean",
"example": true
}
}
],
"result": {

View File

@@ -190,7 +190,7 @@ fn create_auth_info_with_config(enabled bool) AuthDocInfo {
}
// extract_type_from_schema extracts the JSON Schema type from a SchemaRef.
// Returns the type string (e.g., 'string', 'object', 'array') or 'reference'/'unknown' for edge cases.
// Returns detailed type string (e.g., 'string', 'array[integer]', 'object') for better example generation.
fn extract_type_from_schema(schema_ref jsonschema.SchemaRef) string {
schema := match schema_ref {
jsonschema.Schema {
@@ -202,6 +202,30 @@ fn extract_type_from_schema(schema_ref jsonschema.SchemaRef) string {
}
if schema.typ.len > 0 {
// For arrays, include the item type if available
if schema.typ == 'array' {
if items := schema.items {
// Handle single schema reference (most common case)
if items is jsonschema.SchemaRef {
item_type := extract_type_from_schema(items)
return 'array[${item_type}]'
}
// Handle array of schema references (tuple validation)
if items is []jsonschema.SchemaRef {
if items.len > 0 {
item_type := extract_type_from_schema(items[0])
return 'array[${item_type}]'
}
}
}
}
// For objects with additionalProperties, include the value type
if schema.typ == 'object' {
if additional := schema.additional_properties {
value_type := extract_type_from_schema(additional)
return 'object[${value_type}]'
}
}
return schema.typ
}
return 'unknown'

View File

@@ -6,32 +6,13 @@ fn generate_request_example[T](model T) !string {
mut field_parts := []string{} // Build JSON manually to avoid type conflicts
for param in model {
value := match param.type_info.to_lower() {
'string', 'str', 'text' {
'"${rand.string(10)}"'
}
'integer', 'int', 'number' {
'${rand.intn(1000)!}'
}
'boolean', 'bool' {
if rand.intn(2)! == 0 { 'false' } else { 'true' }
}
'array', '[]' {
'[]'
}
'object' {
'{}'
}
else {
// handle generic cases like `[int]`, `[string]`, `map[string]int`, etc.
if param.type_info.starts_with('[') {
'[]'
} else if param.type_info.starts_with('map') {
'{}'
} else {
'"example_value"'
}
}
// 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)!
}
field_parts << '"${param.name}": ${value}'
}
@@ -39,6 +20,109 @@ fn generate_request_example[T](model T) !string {
return '{${field_parts.join(', ')}}'
}
// Generate dynamic example values based on type
fn generate_example_value(type_info string) !string {
type_lower := type_info.to_lower()
// Handle array types first (including array[type] format)
if type_lower.starts_with('array') || type_lower.starts_with('[') {
return generate_array_example(type_info)!
}
// 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()!
}
}
// Handle basic types
return match type_lower {
'string', 'str', 'text' {
'"${rand.string(8)}"'
}
'integer', 'int', 'number' {
'${rand.intn(1000)!}'
}
'boolean', 'bool' {
if rand.intn(2)! == 0 {
'false'
} else {
'true'
}
}
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"'
}
}
}
}
// 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'

View File

@@ -7,28 +7,35 @@ import freeflowuniverse.herolib.schemas.jsonschema
// In Method struct
pub fn (method Method) example() (string, string) {
// Extract user-provided examples from the OpenRPC specification
// Build JSON manually to avoid json2.Any sumtype conflicts
mut example_params := map[string]json2.Any{}
mut example_parts := []string{}
for param in method.params {
if param is ContentDescriptor {
param_desc := param as ContentDescriptor
if param_desc.schema is jsonschema.Schema {
schema := param_desc.schema as jsonschema.Schema
if schema.example.str() != '' {
example_params[param_desc.name] = schema.example
// Build JSON field manually to avoid sumtype conflicts
example_parts << '"${param_desc.name}": ${schema.example.json_str()}'
}
}
}
}
example_call := json.encode(example_params)
example_call := if example_parts.len > 0 {
'{${example_parts.join(', ')}}'
} else {
'{}'
}
example_response := if method.result is ContentDescriptor {
result_desc := method.result as ContentDescriptor
if result_desc.schema is jsonschema.Schema {
schema := result_desc.schema as jsonschema.Schema
if schema.example.str() != '' {
json.encode(schema.example)
// Use json_str() to avoid json2.Any sumtype conflicts
schema.example.json_str()
} else {
'{"result": "success"}'
}