feat: add OpenAPI/Swagger UI support with interactive API documentation
- Add OpenAPI 3.0 specification builder with fluent API - Add Swagger UI integration for interactive API documentation - Add comprehensive OpenAPI data structures (schemas, parameters, responses, etc.) - Add swagger_demo.v example with Pet Store API demonstration - Use global variables for shared mutable state in demo handlers - Add property-based tests for OpenAPI serialization and validation - Update README with Swagger UI documentation
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
// Swagger UI Demo for v-hono
|
||||
// This example demonstrates how to add interactive API documentation to your v-hono application
|
||||
// Run with: v -enable-globals run examples/swagger_demo.v (from project root)
|
||||
// Or: cd examples && v -enable-globals run swagger_demo.v
|
||||
// Then visit: http://127.0.0.1:3000/ui for Swagger UI
|
||||
// http://127.0.0.1:3000/doc for OpenAPI JSON
|
||||
module main
|
||||
|
||||
import net.http
|
||||
import x.json2
|
||||
import meiseayoung.hono
|
||||
|
||||
// Pet 结构体 - 用于演示 API 数据
|
||||
struct Pet {
|
||||
id int
|
||||
name string
|
||||
tag string
|
||||
}
|
||||
|
||||
fn (p Pet) as_map() map[string]string {
|
||||
return {
|
||||
"name": p.name,
|
||||
"tag": p.tag
|
||||
}
|
||||
}
|
||||
// ErrorResponse - 错误响应结构体
|
||||
struct ErrorResponse {
|
||||
error string
|
||||
}
|
||||
|
||||
// 使用全局变量实现共享状态
|
||||
// 注意:需要使用 -enable-globals 编译选项
|
||||
__global (
|
||||
g_pets = []Pet{}
|
||||
g_next_id = 4
|
||||
)
|
||||
|
||||
fn main() {
|
||||
mut app := hono.Hono.new()
|
||||
|
||||
// =========================================================================
|
||||
// 1. Basic Usage - Using OpenAPI Builder API
|
||||
// =========================================================================
|
||||
|
||||
// Build OpenAPI specification using the fluent builder API
|
||||
// Note: In V, we need to create a mutable builder first, then chain methods
|
||||
mut builder := hono.OpenAPIBuilder.new()
|
||||
builder.openapi('3.0.0')
|
||||
builder.title('Pet Store API')
|
||||
builder.version('1.0.0')
|
||||
builder.description('A sample Pet Store API demonstrating Swagger UI integration with v-hono')
|
||||
builder.server('http://127.0.0.1:3000', 'Development server')
|
||||
builder.tag('pets', 'Pet management operations')
|
||||
builder.tag('users', 'User management operations')
|
||||
|
||||
// Define GET /pets endpoint
|
||||
mut pets_path := builder.path('/pets')
|
||||
pets_path.get(hono.OpenAPIOperation{
|
||||
summary: 'List all pets'
|
||||
description: 'Returns a list of all pets in the store'
|
||||
operation_id: 'listPets'
|
||||
tags: ['pets']
|
||||
parameters: [
|
||||
hono.OpenAPIParameter{
|
||||
name: 'limit'
|
||||
in_location: 'query'
|
||||
description: 'Maximum number of pets to return'
|
||||
required: false
|
||||
schema: hono.OpenAPISchema{
|
||||
schema_type: 'integer'
|
||||
format: 'int32'
|
||||
}
|
||||
},
|
||||
]
|
||||
responses: {
|
||||
'200': hono.OpenAPIResponse{
|
||||
description: 'A list of pets'
|
||||
content: {
|
||||
'application/json': hono.OpenAPIMediaType{
|
||||
schema: hono.OpenAPISchema{
|
||||
schema_type: 'array'
|
||||
items: &hono.OpenAPISchema{
|
||||
ref: '#/components/schemas/Pet'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
pets_path.post(hono.OpenAPIOperation{
|
||||
summary: 'Create a pet'
|
||||
description: 'Creates a new pet in the store'
|
||||
operation_id: 'createPet'
|
||||
tags: ['pets']
|
||||
request_body: hono.OpenAPIRequestBody{
|
||||
description: 'Pet to add to the store'
|
||||
required: true
|
||||
content: {
|
||||
'application/json': hono.OpenAPIMediaType{
|
||||
schema: hono.OpenAPISchema{
|
||||
ref: '#/components/schemas/NewPet'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
'201': hono.OpenAPIResponse{
|
||||
description: 'Pet created successfully'
|
||||
content: {
|
||||
'application/json': hono.OpenAPIMediaType{
|
||||
schema: hono.OpenAPISchema{
|
||||
ref: '#/components/schemas/Pet'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
pets_path.done()
|
||||
|
||||
// Define GET/DELETE /pets/{id} endpoint
|
||||
mut pet_by_id_path := builder.path('/pets/{id}')
|
||||
pet_by_id_path.get(hono.OpenAPIOperation{
|
||||
summary: 'Get a pet by ID'
|
||||
description: 'Returns a single pet by its ID'
|
||||
operation_id: 'getPetById'
|
||||
tags: ['pets']
|
||||
parameters: [
|
||||
hono.OpenAPIParameter{
|
||||
name: 'id'
|
||||
in_location: 'path'
|
||||
description: 'ID of the pet to retrieve'
|
||||
required: true
|
||||
schema: hono.OpenAPISchema{
|
||||
schema_type: 'integer'
|
||||
format: 'int64'
|
||||
}
|
||||
},
|
||||
]
|
||||
responses: {
|
||||
'200': hono.OpenAPIResponse{
|
||||
description: 'Pet found'
|
||||
content: {
|
||||
'application/json': hono.OpenAPIMediaType{
|
||||
schema: hono.OpenAPISchema{
|
||||
ref: '#/components/schemas/Pet'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'404': hono.OpenAPIResponse{
|
||||
description: 'Pet not found'
|
||||
}
|
||||
}
|
||||
})
|
||||
pet_by_id_path.delete(hono.OpenAPIOperation{
|
||||
summary: 'Delete a pet'
|
||||
description: 'Deletes a pet by its ID'
|
||||
operation_id: 'deletePet'
|
||||
tags: ['pets']
|
||||
parameters: [
|
||||
hono.OpenAPIParameter{
|
||||
name: 'id'
|
||||
in_location: 'path'
|
||||
description: 'ID of the pet to delete'
|
||||
required: true
|
||||
schema: hono.OpenAPISchema{
|
||||
schema_type: 'integer'
|
||||
format: 'int64'
|
||||
}
|
||||
},
|
||||
]
|
||||
responses: {
|
||||
'204': hono.OpenAPIResponse{
|
||||
description: 'Pet deleted successfully'
|
||||
}
|
||||
'404': hono.OpenAPIResponse{
|
||||
description: 'Pet not found'
|
||||
}
|
||||
}
|
||||
})
|
||||
pet_by_id_path.done()
|
||||
|
||||
// Add reusable schemas to components
|
||||
builder.add_schema('Pet', hono.OpenAPISchema{
|
||||
schema_type: 'object'
|
||||
required: ['id', 'name']
|
||||
properties: {
|
||||
'id': hono.OpenAPISchema{
|
||||
schema_type: 'integer'
|
||||
format: 'int64'
|
||||
description: 'Unique identifier for the pet'
|
||||
}
|
||||
'name': hono.OpenAPISchema{
|
||||
schema_type: 'string'
|
||||
description: 'Name of the pet'
|
||||
}
|
||||
'tag': hono.OpenAPISchema{
|
||||
schema_type: 'string'
|
||||
description: 'Tag for categorizing the pet'
|
||||
}
|
||||
}
|
||||
})
|
||||
builder.add_schema('NewPet', hono.OpenAPISchema{
|
||||
schema_type: 'object'
|
||||
required: ['name']
|
||||
properties: {
|
||||
'name': hono.OpenAPISchema{
|
||||
schema_type: 'string'
|
||||
description: 'Name of the pet'
|
||||
}
|
||||
'tag': hono.OpenAPISchema{
|
||||
schema_type: 'string'
|
||||
description: 'Tag for categorizing the pet'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
spec := builder.build() or {
|
||||
eprintln('Failed to build OpenAPI spec: ${err}')
|
||||
return
|
||||
}
|
||||
|
||||
// Register the OpenAPI JSON endpoint
|
||||
app.doc('/doc', spec)
|
||||
|
||||
// =========================================================================
|
||||
// 2. Swagger UI with Default Options
|
||||
// =========================================================================
|
||||
|
||||
// Serve Swagger UI at /ui with default options
|
||||
app.get('/ui', hono.swagger_ui(hono.SwaggerUIOptions{
|
||||
url: '/doc'
|
||||
title: 'Pet Store API Documentation'
|
||||
}))
|
||||
|
||||
// =========================================================================
|
||||
// 3. Swagger UI with Custom Options
|
||||
// =========================================================================
|
||||
|
||||
// Serve Swagger UI at /docs with custom options
|
||||
app.get('/docs', hono.swagger_ui(hono.SwaggerUIOptions{
|
||||
url: '/doc'
|
||||
title: 'Pet Store API - Custom Theme'
|
||||
deep_linking: true
|
||||
display_request_duration: true
|
||||
default_models_expand_depth: 2
|
||||
doc_expansion: 'full' // 'list', 'full', or 'none'
|
||||
filter: true
|
||||
show_extensions: true
|
||||
show_common_extensions: true
|
||||
try_it_out_enabled: true
|
||||
// Custom CSS to change the header color
|
||||
custom_css: '.swagger-ui .topbar { background-color: #2c3e50; }'
|
||||
}))
|
||||
|
||||
// =========================================================================
|
||||
// 4. Actual API Endpoints (for testing with Swagger UI)
|
||||
// =========================================================================
|
||||
|
||||
// 初始化全局宠物数据
|
||||
// 使用全局变量可以在多个 handler 之间共享和修改数据
|
||||
g_pets = [
|
||||
Pet{id: 1, name: 'Fluffy', tag: 'cat'},
|
||||
Pet{id: 2, name: 'Buddy', tag: 'dog'},
|
||||
Pet{id: 3, name: 'Goldie', tag: 'fish'},
|
||||
]
|
||||
|
||||
// GET /pets - List all pets
|
||||
app.get('/pets', fn (mut c hono.Context) http.Response {
|
||||
return c.json(json2.encode(g_pets))
|
||||
})
|
||||
|
||||
// POST /pets - Create a new pet
|
||||
app.post('/pets', fn (mut c hono.Context) http.Response {
|
||||
println(c.body)
|
||||
pet_dic := json2.decode[Pet](c.body) or {
|
||||
panic(err)
|
||||
}
|
||||
pet := Pet{id: g_next_id, name: pet_dic.name, tag: pet_dic.tag}
|
||||
g_pets << pet
|
||||
g_next_id++
|
||||
c.status(201)
|
||||
return c.json(json2.encode(pet))
|
||||
})
|
||||
|
||||
// GET /pets/:id - Get a pet by ID
|
||||
app.get('/pets/:id', fn (mut c hono.Context) http.Response {
|
||||
pet_id := c.params['id'] or { '' }
|
||||
for pet in g_pets {
|
||||
if pet.id == pet_id.int() {
|
||||
return c.json(json2.encode(pet))
|
||||
}
|
||||
}
|
||||
c.status(404)
|
||||
return c.json(json2.encode(ErrorResponse{error: 'Pet not found'}))
|
||||
})
|
||||
|
||||
// DELETE /pets/:id - Delete a pet
|
||||
app.delete('/pets/:id', fn (mut c hono.Context) http.Response {
|
||||
pet_id := c.params['id'] or { '' }
|
||||
for i, pet in g_pets {
|
||||
if pet.id == pet_id.int() {
|
||||
g_pets.delete(i)
|
||||
return http.Response{
|
||||
status_code: 204
|
||||
header: http.new_header(key: .content_type, value: 'application/json')
|
||||
body: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
c.status(404)
|
||||
return c.json(json2.encode(ErrorResponse{error: 'Pet not found'}))
|
||||
})
|
||||
|
||||
// =========================================================================
|
||||
// Start Server
|
||||
// =========================================================================
|
||||
|
||||
println('=== Swagger UI Demo ===')
|
||||
println('Server starting on http://127.0.0.1:3000')
|
||||
println('')
|
||||
println('Available endpoints:')
|
||||
println(' - Swagger UI (default): http://127.0.0.1:3000/ui')
|
||||
println(' - Swagger UI (custom): http://127.0.0.1:3000/docs')
|
||||
println(' - OpenAPI JSON: http://127.0.0.1:3000/doc')
|
||||
println('')
|
||||
println('API endpoints:')
|
||||
println(' - GET /pets - List all pets')
|
||||
println(' - POST /pets - Create a new pet')
|
||||
println(' - GET /pets/:id - Get a pet by ID')
|
||||
println(' - DELETE /pets/:id - Delete a pet')
|
||||
println('')
|
||||
|
||||
app.listen_usockets(3000)
|
||||
}
|
||||
Reference in New Issue
Block a user