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:
@@ -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": {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"}'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user