Compare commits

..
17 Commits
Author SHA1 Message Date
meiseayoung afbb6586fa chore: update v.mod version 2026-01-07 10:55:58 +08:00
meiseayoung a043b24f69 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
2026-01-07 10:52:47 +08:00
meiseayoung 4d342b9198 docs: remove duplicate Windows prerequisites section 2026-01-07 08:00:26 +08:00
meiseayoung a6a859c566 docs: move uSockets build instructions to usockets/README.md 2026-01-07 07:57:05 +08:00
meiseayoung a2afd947d0 chore: update v.mod version 2026-01-06 17:03:34 +08:00
meiseayoung cfde1b4306 Merge remote-tracking branch: keep optimized HTTP/1.0 connection handling 2026-01-06 16:58:53 +08:00
meiseayoung 0a4e5d50f3 fix: HTTP/1.0 connection not closing causes ab benchmark to hang
- Add HTTP version detection in parse_http_request_usockets
- Return Connection: close header for HTTP/1.0 requests
- Call shutdown() for graceful connection close after HTTP/1.0 response
- Fix missing is_http11 parameter in WebSocket upgrade handler
- Support HTTP/1.0 with Connection: Keep-Alive header (ab -k mode)
- Optimize: only check Connection header value for keep-alive (like Go net/http)

Root cause: ab uses HTTP/1.0 by default, but server always returned
Connection: keep-alive, causing ab to wait for server to close connection.
2026-01-06 16:36:07 +08:00
meiseayoung ed55ce758e fix: HTTP/1.0 connection not closing causes ab benchmark to hang
- Add HTTP version detection in parse_http_request_usockets
- Return Connection: close header for HTTP/1.0 requests
- Call shutdown() for graceful connection close after HTTP/1.0 response
- Fix missing is_http11 parameter in WebSocket upgrade handler

Root cause: ab uses HTTP/1.0 by default, but server always returned
Connection: keep-alive, causing ab to wait for server to close connection.
2026-01-06 15:58:24 +08:00
meiseayoung 22e8dd4548 chore: update v.mod version 2026-01-06 14:11:40 +08:00
meiseayoung a702746df7 build(usockets): add pre-compiled library for MacOS-arm64 platform 2026-01-06 14:10:02 +08:00
meiseayoung a57ce2e77a chore: update v.mod version 2026-01-06 12:01:45 +08:00
meiseayoung 7a08c3142a build(usockets): add pre-compiled library for MacOS-x86 platform 2026-01-06 12:01:20 +08:00
meiseayoung 362015c1cf docs: update build instructions for uSockets backend
- Clarify that Windows requires gcc (tcc does not support .a static library format)
- Add separate build commands for macOS/Linux and Windows
- Document pre-compiled library locations by platform
- Add w64devkit as recommended MinGW download option
2026-01-05 12:25:06 +08:00
meiseayoung 0969901481 chore: update v.mod version 2026-01-05 11:50:46 +08:00
meiseayoung 6bddfc3731 build(usockets): add pre-compiled library for Linux platform 2026-01-05 11:49:47 +08:00
meiseayoung 26ac804b1d chore: update v.mod version 2026-01-05 11:07:41 +08:00
meiseayoung 56299b5485 refactor(usockets): reorganize lib directory structure for multi-platform support
- Organize lib directory by platform: windows/, macos-arm64/, macos-x64/, linux/
- Update build.sh to support unified build process for Windows/macOS/Linux
- Compile libuv + uSockets and merge into complete static library
- Update usockets.v conditional compilation to point to new directory structure
- Optimize benchmark tests with connection pooling for high concurrency testing
- Clean up unused legacy library files

Performance test results (Windows):
- 1000 concurrent, 1M requests: 35,223 req/s
- 10000 concurrent, 1M requests: 26,884 req/s
2026-01-05 11:06:56 +08:00
34 changed files with 7251 additions and 135 deletions
+218 -35
View File
@@ -20,6 +20,7 @@ A high-performance V language web framework inspired by [Hono.js](https://hono.d
- 🗄️ **Database** - SQLite integration for data persistence - 🗄️ **Database** - SQLite integration for data persistence
- 🔌 **WebSocket** - RFC 6455 compliant WebSocket support with event-based API - 🔌 **WebSocket** - RFC 6455 compliant WebSocket support with event-based API
- 📡 **SSE Streaming** - Server-Sent Events and streaming response support - 📡 **SSE Streaming** - Server-Sent Events and streaming response support
- 📖 **Swagger UI** - Interactive API documentation with OpenAPI 3.0/3.1 support
## Installation ## Installation
@@ -50,8 +51,13 @@ cd v-hono
### Standard Build (picoev backend) ### Standard Build (picoev backend)
```bash ```bash
v your_app.v -o app.exe # macOS / Linux
./app.exe v -prod -o app your_app.v
./app
# Windows
v -o app.exe your_app.v
.\app.exe
``` ```
### High-Performance Build (uSockets backend) ### High-Performance Build (uSockets backend)
@@ -59,42 +65,21 @@ v your_app.v -o app.exe
For maximum performance with uSockets backend: For maximum performance with uSockets backend:
```bash ```bash
v -enable-globals -cc gcc -ldflags "-ldbghelp" your_app.v -o app.exe # macOS / Linux
./app.exe v -enable-globals -prod -o app your_app.v
./app
# Windows (must use gcc, tcc does not support .a static library format)
v -enable-globals -cc gcc -ldflags "-ldbghelp" -o app.exe your_app.v
.\app.exe
``` ```
**Note**: The `-enable-globals` flag is required for uSockets backend. The `-ldflags "-ldbghelp"` fixes Windows linking issues. **Important Notes:**
- The `-enable-globals` flag is required for uSockets backend
- On Windows, you **must** use `-cc gcc` because V's default compiler (tcc) does not support MinGW `.a` static library format
- The `-ldflags "-ldbghelp"` is required on Windows for libuv linking
#### Windows Prerequisites The uSockets library and libuv are pre-compiled and included in the `usockets/lib/{platform}/` directory. See [usockets/README.md](usockets/README.md) for prerequisites, building from source, and detailed configuration options.
To use uSockets backend on Windows, you need:
1. **GCC (MinGW-w64)** - C compiler
```powershell
# Install via Scoop
scoop install mingw
# Or via Chocolatey
choco install mingw
# Or download from: https://www.mingw-w64.org/downloads/
```
2. **V Compiler** - Version 0.4.x or later
```powershell
# Install via Scoop
scoop install vlang
# Or download from: https://vlang.io/
```
3. **Verify Installation**
```powershell
gcc --version # Should show MinGW-w64 GCC
v version # Should show V 0.4.x
```
The uSockets library and libuv are pre-compiled and included in the `usockets/lib/` directory.
### Using uSockets Backend ### Using uSockets Backend
@@ -1030,6 +1015,201 @@ eventSource.onerror = (e) => {
</script> </script>
``` ```
### 10. Swagger UI
Interactive API documentation with OpenAPI 3.0/3.1 specification support.
#### Basic Usage
```v
import meiseayoung.hono
fn main() {
mut app := hono.Hono.new()
// Build OpenAPI specification using the fluent builder API
spec := hono.OpenAPIBuilder.new()
.openapi('3.0.0')
.title('My API')
.version('1.0.0')
.description('API documentation')
.server('http://localhost:3000', 'Development server')
.path('/users')
.get(hono.OpenAPIOperation{
summary: 'List all users'
tags: ['users']
responses: {
'200': hono.OpenAPIResponse{
description: 'A list of users'
}
}
})
.done()
.build() or {
eprintln('Failed to build spec: ${err}')
return
}
// Register OpenAPI JSON endpoint
app.doc('/doc', spec)
// Serve Swagger UI
app.get('/ui', hono.swagger_ui(hono.SwaggerUIOptions{
url: '/doc'
title: 'My API Documentation'
}))
app.listen(':3000')
}
```
#### OpenAPI Builder API
```v
import meiseayoung.hono
// Build a complete OpenAPI specification
spec := hono.OpenAPIBuilder.new()
.openapi('3.0.0') // OpenAPI version
.title('Pet Store API') // API title
.version('1.0.0') // API version
.description('A sample API') // Description
.server('https://api.example.com', 'Production') // Server URL
.tag('pets', 'Pet operations') // Tag definition
// Define path with multiple operations
.path('/pets')
.get(hono.OpenAPIOperation{
summary: 'List pets'
operation_id: 'listPets'
tags: ['pets']
parameters: [
hono.OpenAPIParameter{
name: 'limit'
in_location: 'query'
schema: hono.OpenAPISchema{
schema_type: 'integer'
}
},
]
responses: {
'200': hono.OpenAPIResponse{
description: 'Success'
content: {
'application/json': hono.OpenAPIMediaType{
schema: hono.OpenAPISchema{
schema_type: 'array'
items: &hono.OpenAPISchema{
ref: '#/components/schemas/Pet'
}
}
}
}
}
}
})
.post(hono.OpenAPIOperation{
summary: 'Create pet'
request_body: hono.OpenAPIRequestBody{
required: true
content: {
'application/json': hono.OpenAPIMediaType{
schema: hono.OpenAPISchema{
ref: '#/components/schemas/NewPet'
}
}
}
}
responses: {
'201': hono.OpenAPIResponse{
description: 'Created'
}
}
})
.done()
// Add reusable schemas
.add_schema('Pet', hono.OpenAPISchema{
schema_type: 'object'
required: ['id', 'name']
properties: {
'id': hono.OpenAPISchema{
schema_type: 'integer'
}
'name': hono.OpenAPISchema{
schema_type: 'string'
}
}
})
.build()!
```
#### Swagger UI Options
```v
import meiseayoung.hono
// Customize Swagger UI appearance and behavior
app.get('/docs', hono.swagger_ui(hono.SwaggerUIOptions{
url: '/doc' // OpenAPI JSON URL
title: 'API Documentation' // Page title
deep_linking: true // Enable deep linking
display_request_duration: true // Show request duration
default_models_expand_depth: 2 // Model expansion depth
doc_expansion: 'list' // 'list', 'full', or 'none'
filter: true // Enable filtering
show_extensions: true // Show extensions
show_common_extensions: true // Show common extensions
try_it_out_enabled: true // Enable Try it out
custom_css: '.topbar { background: #2c3e50; }' // Custom CSS
}))
```
#### SwaggerUIOptions Reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `url` | `string` | `'/doc'` | OpenAPI document URL |
| `title` | `string` | `'API Documentation'` | Page title |
| `deep_linking` | `bool` | `true` | Enable deep linking to operations |
| `display_request_duration` | `bool` | `true` | Show request duration |
| `default_models_expand_depth` | `int` | `1` | Model expansion depth |
| `doc_expansion` | `string` | `'list'` | Document expansion: 'list', 'full', 'none' |
| `filter` | `bool` | `false` | Enable operation filtering |
| `show_extensions` | `bool` | `false` | Show vendor extensions |
| `show_common_extensions` | `bool` | `true` | Show common extensions |
| `try_it_out_enabled` | `bool` | `true` | Enable Try it out feature |
| `custom_css` | `string` | `''` | Custom CSS styles |
| `custom_js` | `string` | `''` | Custom JavaScript |
| `custom_css_url` | `string` | `''` | Custom CSS URL |
| `custom_js_url` | `string` | `''` | Custom JavaScript URL |
#### OpenAPI Builder Methods
| Method | Description |
|--------|-------------|
| `openapi(version)` | Set OpenAPI version (3.0.0, 3.1.0) |
| `title(title)` | Set API title |
| `version(version)` | Set API version |
| `description(desc)` | Set API description |
| `server(url, desc)` | Add server |
| `tag(name, desc)` | Add tag |
| `path(path)` | Start path builder |
| `add_schema(name, schema)` | Add reusable schema |
| `add_security_scheme(name, scheme)` | Add security scheme |
| `build()` | Build and validate document |
#### Path Builder Methods
| Method | Description |
|--------|-------------|
| `get(op)` | Add GET operation |
| `post(op)` | Add POST operation |
| `put(op)` | Add PUT operation |
| `delete(op)` | Add DELETE operation |
| `patch(op)` | Add PATCH operation |
| `head(op)` | Add HEAD operation |
| `options(op)` | Add OPTIONS operation |
| `done()` | Return to parent builder |
## Route Grouping ## Route Grouping
```v ```v
@@ -1136,6 +1316,8 @@ v-hono/
├── validator.v # Request validation ├── validator.v # Request validation
├── websocket.v # WebSocket helper (RFC 6455) ├── websocket.v # WebSocket helper (RFC 6455)
├── streaming.v # SSE streaming helper ├── streaming.v # SSE streaming helper
├── swagger.v # Swagger UI middleware
├── openapi.v # OpenAPI 3.0/3.1 data structures and builder
├── picoev_server.v # Picoev backend with WebSocket/SSE support ├── picoev_server.v # Picoev backend with WebSocket/SSE support
├── usockets_server.v # uSockets backend with WebSocket/SSE support ├── usockets_server.v # uSockets backend with WebSocket/SSE support
├── v.mod # Module definition ├── v.mod # Module definition
@@ -1154,6 +1336,7 @@ See the `examples/` directory for more examples:
- `examples/middleware_demo.v` - Built-in middleware demonstration - `examples/middleware_demo.v` - Built-in middleware demonstration
- `examples/route_grouping/` - Route grouping - `examples/route_grouping/` - Route grouping
- `examples/redirect_demo.v` - Redirect functionality examples - `examples/redirect_demo.v` - Redirect functionality examples
- `examples/swagger_demo.v` - Swagger UI and OpenAPI documentation
## License ## License
+52
View File
@@ -578,6 +578,58 @@ pub fn Hono.new() Hono {
} }
} }
// ============================================================================
// OpenAPI 文档便捷方法 (Task 9.1)
// ============================================================================
// doc - 注册 OpenAPI 文档路由
// 在指定路径注册一个 GET 路由,返回 OpenAPI 文档的 JSON 格式
// 设置 Content-Type: application/json
// 使用示例:
// spec := OpenAPIBuilder.new()
// .openapi('3.0.0')
// .title('My API')
// .version('1.0.0')
// .build()!
// app.doc('/doc', spec)
pub fn (mut app Hono) doc(path string, spec OpenAPIDocument) {
// 预序列化 OpenAPI 文档为 JSON 字符串
json_content := spec.to_json_str()
// 注册 GET 路由返回 JSON
app.get(path, fn [json_content] (mut c Context) http.Response {
return http.Response{
status_code: 200
header: http.new_header(key: .content_type, value: 'application/json; charset=utf-8')
body: json_content
}
})
}
// doc_fn - 使用构建器函数注册 OpenAPI 文档路由
// 允许延迟构建 OpenAPI 文档,每次请求时调用构建器函数
// 使用示例:
// app.doc_fn('/doc', fn () OpenAPIDocument {
// return OpenAPIBuilder.new()
// .openapi('3.0.0')
// .title('My API')
// .version('1.0.0')
// .build() or { panic(err) }
// })
pub fn (mut app Hono) doc_fn(path string, builder fn () OpenAPIDocument) {
app.get(path, fn [builder] (mut c Context) http.Response {
// 每次请求时调用构建器函数
spec := builder()
json_content := spec.to_json_str()
return http.Response{
status_code: 200
header: http.new_header(key: .content_type, value: 'application/json; charset=utf-8')
body: json_content
}
})
}
// 快速解析 query string(单次遍历,避免 split)- app.v 版本 // 快速解析 query string(单次遍历,避免 split)- app.v 版本
@[inline] @[inline]
fn parse_query_string_app(query_str string) map[string]string { fn parse_query_string_app(query_str string) map[string]string {
+33 -5
View File
@@ -448,21 +448,49 @@ func checkKeepAlive(client *http.Client) bool {
func checkThroughput(client *http.Client) float64 { func checkThroughput(client *http.Client) float64 {
start := time.Now() start := time.Now()
success := 0 concurrency := 1000
requests := 200 totalRequests := 1000000
requestsPerWorker := totalRequests / concurrency
for i := 0; i < requests; i++ { // 创建共享的 Transport,启用连接池复用
resp, err := client.Get(baseURL + "/health") transport := &http.Transport{
MaxIdleConns: concurrency,
MaxIdleConnsPerHost: concurrency,
IdleConnTimeout: 30 * time.Second,
DisableKeepAlives: false,
}
results := make(chan int, concurrency)
for i := 0; i < concurrency; i++ {
go func() {
localSuccess := 0
// 每个 goroutine 使用独立 Client,但共享 Transport(连接池)
localClient := &http.Client{
Timeout: 10 * time.Second,
Transport: transport,
}
for j := 0; j < requestsPerWorker; j++ {
resp, err := localClient.Get(baseURL + "/health")
if err != nil { if err != nil {
continue continue
} }
io.Copy(io.Discard, resp.Body) io.Copy(io.Discard, resp.Body)
resp.Body.Close() resp.Body.Close()
if resp.StatusCode == 200 { if resp.StatusCode == 200 {
success++ localSuccess++
} }
} }
results <- localSuccess
}()
}
success := 0
for i := 0; i < concurrency; i++ {
success += <-results
}
elapsed := time.Since(start).Seconds() elapsed := time.Since(start).Seconds()
fmt.Printf("\n [并发: %d, 总请求: %d, 成功: %d, 耗时: %.2fs]\n ", concurrency, totalRequests, success, elapsed)
return float64(success) / elapsed return float64(success) / elapsed
} }
+337
View File
@@ -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)
}
+109 -1
View File
@@ -11,7 +11,7 @@ import rand
// 中间件导出说明 // 中间件导出说明
// ============================================================================ // ============================================================================
// //
// 本框架提供以下 8 个内置中间件: // 本框架提供以下 9 个内置中间件:
// //
// 1. CORS 中间件 (cors.v) // 1. CORS 中间件 (cors.v)
// - cors(options ...CorsOptions) ContextMiddleware // - cors(options ...CorsOptions) ContextMiddleware
@@ -82,6 +82,57 @@ import rand
// - encode_ws_frame(opcode u8, payload []u8, masked bool) []u8: 编码 WebSocket 帧 // - encode_ws_frame(opcode u8, payload []u8, masked bool) []u8: 编码 WebSocket 帧
// - decode_ws_frame(data []u8) !WSFrame: 解码 WebSocket 帧 // - decode_ws_frame(data []u8) !WSFrame: 解码 WebSocket 帧
// //
// 9. Swagger UI 中间件 (swagger.v)
// - swagger_ui(options ...SwaggerUIOptions) fn (mut Context) http.Response
// - swagger_ui_handler(options ...SwaggerUIOptions) fn (mut Context) http.Response
// - 用于提供交互式 API 文档界面
//
// Types:
// - SwaggerUIOptions: 配置选项
// - url: OpenAPI 文档 URL (默认: '/doc')
// - title: 页面标题 (默认: 'API Documentation')
// - deep_linking: 启用深度链接 (默认: true)
// - display_request_duration: 显示请求耗时 (默认: true)
// - default_models_expand_depth: 模型展开深度 (默认: 1)
// - doc_expansion: 文档展开方式 ('list', 'full', 'none')
// - filter: 启用过滤
// - show_extensions: 显示扩展
// - show_common_extensions: 显示常用扩展 (默认: true)
// - try_it_out_enabled: 启用 Try it out (默认: true)
// - custom_css: 自定义 CSS
// - custom_js: 自定义 JavaScript
// - custom_css_url: 自定义 CSS URL
// - custom_js_url: 自定义 JavaScript URL
//
// OpenAPI 文档相关 (openapi.v):
// - OpenAPIDocument: OpenAPI 文档主结构
// - OpenAPIBuilder: 流式 API 构建器
// - app.doc(path, spec): 注册 OpenAPI 文档路由
// - app.doc_fn(path, builder): 使用构建器函数注册文档路由
// - app.get_routes(): 获取应用的所有路由信息
//
// 使用示例:
// // 1. 创建 OpenAPI 文档
// spec := hono.OpenAPIBuilder.new()
// .openapi('3.0.0')
// .title('My API')
// .version('1.0.0')
// .description('API description')
// .server('https://api.example.com', 'Production')
// .path('/users')
// .get(hono.OpenAPIOperation{
// summary: 'Get all users'
// responses: { '200': hono.OpenAPIResponse{ description: 'Success' } }
// })
// .done()
// .build()!
//
// // 2. 注册 OpenAPI 文档路由
// app.doc('/doc', spec)
//
// // 3. 注册 Swagger UI 路由
// app.get('/ui', hono.swagger_ui(hono.SwaggerUIOptions{ url: '/doc' }))
//
// ============================================================================ // ============================================================================
// ============================================================================ // ============================================================================
@@ -294,6 +345,63 @@ pub fn timing() ContextMiddleware {
} }
// ============================================================================
// Swagger UI 中间件导出
// ============================================================================
//
// Swagger UI 中间件提供交互式 API 文档界面,支持 OpenAPI 3.0/3.1 规范。
// 主要功能通过 swagger.v 和 openapi.v 文件导出,包括:
//
// 核心函数:
// - swagger_ui(options...) - 创建 Swagger UI 处理器
// - swagger_ui_handler(options...) - swagger_ui 的别名
// - app.doc(path, spec) - 注册 OpenAPI 文档路由
// - app.doc_fn(path, builder) - 使用构建器函数注册文档路由
//
// 类型定义:
// - SwaggerUIOptions - Swagger UI 配置选项
// - OpenAPIDocument - OpenAPI 文档主结构
// - OpenAPIBuilder - 流式 API 构建器
// - OpenAPIInfo, OpenAPIServer, OpenAPIPathItem, OpenAPIOperation 等
//
// 使用示例:
// // 创建 OpenAPI 文档
// spec := hono.OpenAPIBuilder.new()
// .openapi('3.0.0')
// .title('My API')
// .version('1.0.0')
// .build()!
//
// // 注册文档和 UI 路由
// app.doc('/doc', spec)
// app.get('/ui', hono.swagger_ui(hono.SwaggerUIOptions{ url: '/doc' }))
//
// 带自定义选项:
// app.get('/swagger', hono.swagger_ui(hono.SwaggerUIOptions{
// url: '/api/doc'
// title: 'My API Documentation'
// deep_linking: true
// display_request_duration: true
// doc_expansion: 'list'
// try_it_out_enabled: true
// }))
// ============================================================================
// swagger - Swagger UI 处理器的简短别名
// 使用示例:
// app.get('/docs', hono.swagger(hono.SwaggerUIOptions{ url: '/doc' }))
pub fn swagger(options ...SwaggerUIOptions) fn (mut Context) http.Response {
return swagger_ui(...options)
}
// openapi_ui - Swagger UI 处理器的另一个别名
// 使用示例:
// app.get('/api-docs', hono.openapi_ui(hono.SwaggerUIOptions{ url: '/openapi.json' }))
pub fn openapi_ui(options ...SwaggerUIOptions) fn (mut Context) http.Response {
return swagger_ui(...options)
}
// ============================================================================ // ============================================================================
// WebSocket Helper 导出 // WebSocket Helper 导出
// ============================================================================ // ============================================================================
+1822
View File
File diff suppressed because it is too large Load Diff
+163
View File
@@ -0,0 +1,163 @@
// swagger.v - Swagger UI 中间件
// 本模块提供 Swagger UI 文档界面功能,类似于 Hono.js 的 @hono/swagger-ui 中间件
// 支持 OpenAPI 3.0/3.1 规范
module hono
import net.http
// ============================================================================
// Swagger UI 配置选项 (Task 7.1)
// ============================================================================
// SwaggerUIOptions - Swagger UI 配置选项
// 使用示例:
// app.get('/ui', hono.swagger_ui(hono.SwaggerUIOptions{ url: '/doc' }))
pub struct SwaggerUIOptions {
pub mut:
url string = '/doc' // OpenAPI 文档 URL
title string = 'API Documentation' // 页面标题
deep_linking bool = true // 启用深度链接
display_request_duration bool = true // 显示请求耗时
default_models_expand_depth int = 1 // 模型展开深度
doc_expansion string = 'list' // 文档展开方式: 'list', 'full', 'none'
filter bool // 启用过滤
show_extensions bool // 显示扩展
show_common_extensions bool = true // 显示常用扩展
try_it_out_enabled bool = true // 启用 Try it out
custom_css string // 自定义 CSS
custom_js string // 自定义 JavaScript
custom_css_url string // 自定义 CSS URL
custom_js_url string // 自定义 JavaScript URL
}
// ============================================================================
// Swagger UI HTML 生成 (Task 7.2)
// ============================================================================
// generate_swagger_html - 生成 Swagger UI HTML 页面
// 使用 CDN 链接加载 Swagger UI 资源
// 应用配置选项到 SwaggerUIBundle 配置
fn generate_swagger_html(options SwaggerUIOptions) string {
// 构建自定义 CSS URL 标签
custom_css_url_tag := if options.custom_css_url.len > 0 {
'<link rel="stylesheet" href="${options.custom_css_url}">'
} else {
''
}
// 构建自定义 JS URL 标签
custom_js_url_tag := if options.custom_js_url.len > 0 {
'<script src="${options.custom_js_url}"></script>'
} else {
''
}
// 构建自定义 CSS 内联样式
custom_css_inline := if options.custom_css.len > 0 {
options.custom_css
} else {
''
}
// 构建自定义 JS 内联脚本
custom_js_inline := if options.custom_js.len > 0 {
options.custom_js
} else {
''
}
// 转换布尔值为 JavaScript 字符串
deep_linking_str := if options.deep_linking { 'true' } else { 'false' }
display_request_duration_str := if options.display_request_duration { 'true' } else { 'false' }
filter_str := if options.filter { 'true' } else { 'false' }
show_extensions_str := if options.show_extensions { 'true' } else { 'false' }
show_common_extensions_str := if options.show_common_extensions { 'true' } else { 'false' }
try_it_out_enabled_str := if options.try_it_out_enabled { 'true' } else { 'false' }
return '<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${options.title}</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
${custom_css_url_tag}
<style>
html { box-sizing: border-box; overflow-y: scroll; }
*, *:before, *:after { box-sizing: inherit; }
body { margin: 0; background: #fafafa; }
${custom_css_inline}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
${custom_js_url_tag}
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
url: "${options.url}",
dom_id: \'#swagger-ui\',
deepLinking: ${deep_linking_str},
displayRequestDuration: ${display_request_duration_str},
defaultModelsExpandDepth: ${options.default_models_expand_depth},
docExpansion: "${options.doc_expansion}",
filter: ${filter_str},
showExtensions: ${show_extensions_str},
showCommonExtensions: ${show_common_extensions_str},
tryItOutEnabled: ${try_it_out_enabled_str},
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
window.ui = ui;
};
${custom_js_inline}
</script>
</body>
</html>'
}
// ============================================================================
// Swagger UI 中间件函数 (Task 7.3)
// ============================================================================
// swagger_ui - 创建 Swagger UI 处理器
// 返回处理器函数,设置正确的 Content-Type,返回生成的 HTML
// 使用示例:
// app.get('/ui', hono.swagger_ui(hono.SwaggerUIOptions{ url: '/doc' }))
// app.get('/docs', hono.swagger_ui()) // 使用默认选项
pub fn swagger_ui(options ...SwaggerUIOptions) fn (mut Context) http.Response {
// 获取选项,如果没有提供则使用默认值
opts := if options.len > 0 {
options[0]
} else {
SwaggerUIOptions{}
}
// 预生成 HTML 内容
html_content := generate_swagger_html(opts)
return fn [html_content] (mut c Context) http.Response {
return http.Response{
status_code: 200
header: http.new_header(key: .content_type, value: 'text/html; charset=utf-8')
body: html_content
}
}
}
// swagger_ui_handler - swagger_ui 的别名函数
// 使用示例:
// app.get('/swagger', hono.swagger_ui_handler(hono.SwaggerUIOptions{ url: '/api/doc' }))
pub fn swagger_ui_handler(options ...SwaggerUIOptions) fn (mut Context) http.Response {
return swagger_ui(...options)
}
@@ -0,0 +1,353 @@
// test_openapi_builder_multi_ops_property.v - Property-Based Test for Builder Multiple Operations
// **Feature: swagger-ui, Property 7: OpenAPI Builder Supports Multiple Operations Per Path**
// **Validates: Requirements 6.2, 6.3, 6.4**
//
// Property: For any path, adding multiple different HTTP method operations using the builder
// SHALL result in all operations being present in the final document.
module main
import hono
import rand
import time
const property_test_iterations = 100
struct PropertyTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
failed_examples []string
}
fn (mut stats PropertyTestStats) record_pass() {
stats.total_tests++
stats.passed_tests++
}
fn (mut stats PropertyTestStats) record_fail(example string) {
stats.total_tests++
stats.failed_tests++
stats.failed_examples << example
}
fn (stats PropertyTestStats) print_summary() {
println('\n=== Property Test Summary ===')
println('Total iterations: ${stats.total_tests}')
println('Passed: ${stats.passed_tests}')
println('Failed: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 Property test PASSED!')
} else {
println('❌ Property test FAILED!')
println('\nFailing examples:')
for i, example in stats.failed_examples {
if i >= 3 {
println('... and ${stats.failed_examples.len - 3} more')
break
}
println(' ${example}')
}
}
}
// Random string generator
fn random_string(min_len int, max_len int) string {
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
len := rand.int_in_range(min_len, max_len + 1) or { min_len }
mut result := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
result += chars[idx..idx + 1]
}
return result
}
// Random path generator
fn random_path() string {
segments := rand.int_in_range(1, 4) or { 1 }
mut path := ''
for _ in 0 .. segments {
path += '/' + random_string(3, 10)
}
return path
}
// All supported HTTP methods
const http_methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options']
// Generate a random operation with a unique identifier
fn generate_operation(method string, id string) hono.OpenAPIOperation {
return hono.OpenAPIOperation{
summary: 'Test ${method} operation - ${id}'
description: 'Description for ${method} - ${id}'
operation_id: '${method}_${id}'
responses: {
'200': hono.OpenAPIResponse{
description: 'Success response for ${method}'
}
}
}
}
// Check if an operation is set (has responses)
fn is_operation_set(op hono.OpenAPIOperation) bool {
return op.responses.len > 0
}
// Get operation from path item by method name
fn get_operation_by_method(path_item hono.OpenAPIPathItem, method string) hono.OpenAPIOperation {
return match method {
'get' { path_item.get }
'post' { path_item.post }
'put' { path_item.put }
'delete' { path_item.delete }
'patch' { path_item.patch }
'head' { path_item.head }
'options' { path_item.options }
else { hono.OpenAPIOperation{} }
}
}
// Randomly select a subset of HTTP methods
fn random_method_subset() []string {
// Select between 2 and 7 methods
num_methods := rand.int_in_range(2, http_methods.len + 1) or { 2 }
mut selected := []string{}
mut available := http_methods.clone()
for _ in 0 .. num_methods {
if available.len == 0 {
break
}
idx := rand.int_in_range(0, available.len) or { 0 }
selected << available[idx]
available.delete(idx)
}
return selected
}
// Test that multiple operations can be added to the same path
fn test_multiple_operations_on_path() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Running Property 7: OpenAPI Builder Supports Multiple Operations Per Path')
println('Iterations: ${property_test_iterations}')
println('')
for i in 0 .. property_test_iterations {
path := random_path()
methods := random_method_subset()
unique_id := random_string(5, 10)
// Create operations for selected methods
mut operations := map[string]hono.OpenAPIOperation{}
for method in methods {
operations[method] = generate_operation(method, unique_id)
}
// Build document using the builder
mut builder := hono.OpenAPIBuilder.new()
builder.openapi('3.0.0')
builder.title('Test API')
builder.version('1.0.0')
mut path_builder := builder.path(path)
// Add operations for each selected method
for method in methods {
op := operations[method] or { continue }
match method {
'get' { path_builder.get(op) }
'post' { path_builder.post(op) }
'put' { path_builder.put(op) }
'delete' { path_builder.delete(op) }
'patch' { path_builder.patch(op) }
'head' { path_builder.head(op) }
'options' { path_builder.options(op) }
else {}
}
}
path_builder.done()
// Build the document
doc := builder.build() or {
stats.record_fail('Iteration ${i}: Build failed - ${err}')
continue
}
// Verify the path exists
if path !in doc.paths {
stats.record_fail('Iteration ${i}: Path ${path} not found in document')
continue
}
path_item := doc.paths[path] or {
stats.record_fail('Iteration ${i}: Could not get path item')
continue
}
// Verify all selected methods are present
mut all_preserved := true
mut missing_methods := []string{}
for method in methods {
stored_op := get_operation_by_method(path_item, method)
expected_op := operations[method] or { continue }
if !is_operation_set(stored_op) {
all_preserved = false
missing_methods << method
continue
}
if stored_op.summary != expected_op.summary {
all_preserved = false
missing_methods << '${method}(summary mismatch)'
}
}
if all_preserved {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Methods not preserved: ${missing_methods.join(", ")}. Selected: ${methods.join(", ")}')
}
// Progress indicator
if (i + 1) % 20 == 0 {
print('.')
}
}
println('')
return stats
}
// Test that operations on different paths don't interfere
fn test_multiple_paths_with_operations() PropertyTestStats {
mut stats := PropertyTestStats{}
println('\nRunning additional test: Multiple paths with different operations')
println('Iterations: ${property_test_iterations}')
println('')
for i in 0 .. property_test_iterations {
// Generate 2-3 different paths with fixed operations
path1 := random_path()
path2 := random_path()
unique_id := random_string(5, 10)
// Create operations for each path
get_op1 := generate_operation('get', '${path1}_${unique_id}')
post_op1 := generate_operation('post', '${path1}_${unique_id}')
get_op2 := generate_operation('get', '${path2}_${unique_id}')
put_op2 := generate_operation('put', '${path2}_${unique_id}')
// Build document
mut builder := hono.OpenAPIBuilder.new()
builder.openapi('3.0.0')
builder.title('Test API')
builder.version('1.0.0')
// Add path1 with GET and POST
mut path_builder1 := builder.path(path1)
path_builder1.get(get_op1)
path_builder1.post(post_op1)
path_builder1.done()
// Add path2 with GET and PUT
mut path_builder2 := builder.path(path2)
path_builder2.get(get_op2)
path_builder2.put(put_op2)
path_builder2.done()
doc := builder.build() or {
stats.record_fail('Iteration ${i}: Build failed - ${err}')
continue
}
// Verify all paths and their operations
mut all_correct := true
mut errors := []string{}
// Check path1
if path1 !in doc.paths {
all_correct = false
errors << 'Path ${path1} not found'
} else {
path_item1 := doc.paths[path1] or {
all_correct = false
errors << 'Could not get path item for ${path1}'
hono.OpenAPIPathItem{}
}
if !is_operation_set(path_item1.get) || path_item1.get.summary != get_op1.summary {
all_correct = false
errors << '${path1}:get'
}
if !is_operation_set(path_item1.post) || path_item1.post.summary != post_op1.summary {
all_correct = false
errors << '${path1}:post'
}
}
// Check path2
if path2 !in doc.paths {
all_correct = false
errors << 'Path ${path2} not found'
} else {
path_item2 := doc.paths[path2] or {
all_correct = false
errors << 'Could not get path item for ${path2}'
hono.OpenAPIPathItem{}
}
if !is_operation_set(path_item2.get) || path_item2.get.summary != get_op2.summary {
all_correct = false
errors << '${path2}:get'
}
if !is_operation_set(path_item2.put) || path_item2.put.summary != put_op2.summary {
all_correct = false
errors << '${path2}:put'
}
}
if all_correct {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Errors: ${errors.join(", ")}')
}
if (i + 1) % 20 == 0 {
print('.')
}
}
println('')
return stats
}
fn main() {
println('🧪 Property-Based Test: OpenAPI Builder Multiple Operations Per Path')
println('=====================================================================')
println('')
// Seed random number generator
rand.seed([u32(time.now().unix()), u32(time.now().unix() >> 32)])
// Run main property test
stats1 := test_multiple_operations_on_path()
stats1.print_summary()
// Run additional test
stats2 := test_multiple_paths_with_operations()
stats2.print_summary()
// Exit with appropriate code
if stats1.failed_tests > 0 || stats2.failed_tests > 0 {
exit(1)
}
}
+305
View File
@@ -0,0 +1,305 @@
// test_openapi_http_methods_property.v - Property-Based Test for HTTP Methods Support
// **Feature: swagger-ui, Property 4: HTTP Methods Support in Path Items**
// **Validates: Requirements 2.3**
//
// Property: For any HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS),
// adding an operation with that method to a path item SHALL succeed and be preserved in the document.
module main
import hono
import rand
import time
const property_test_iterations = 100
struct PropertyTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
failed_examples []string
}
fn (mut stats PropertyTestStats) record_pass() {
stats.total_tests++
stats.passed_tests++
}
fn (mut stats PropertyTestStats) record_fail(example string) {
stats.total_tests++
stats.failed_tests++
stats.failed_examples << example
}
fn (stats PropertyTestStats) print_summary() {
println('\n=== Property Test Summary ===')
println('Total iterations: ${stats.total_tests}')
println('Passed: ${stats.passed_tests}')
println('Failed: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 Property test PASSED!')
} else {
println('❌ Property test FAILED!')
println('\nFailing examples:')
for i, example in stats.failed_examples {
if i >= 3 {
println('... and ${stats.failed_examples.len - 3} more')
break
}
println(' ${example}')
}
}
}
// Random string generator
fn random_string(min_len int, max_len int) string {
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
len := rand.int_in_range(min_len, max_len + 1) or { min_len }
mut result := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
result += chars[idx..idx + 1]
}
return result
}
// Random path generator
fn random_path() string {
segments := rand.int_in_range(1, 4) or { 1 }
mut path := ''
for _ in 0 .. segments {
path += '/' + random_string(3, 10)
}
return path
}
// All supported HTTP methods
const http_methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options']
// Generate a random operation with a unique summary for identification
fn generate_operation(method string) hono.OpenAPIOperation {
return hono.OpenAPIOperation{
summary: 'Test ${method} operation - ${random_string(5, 10)}'
description: 'Description for ${method}'
operation_id: '${method}_${random_string(5, 10)}'
responses: {
'200': hono.OpenAPIResponse{
description: 'Success response for ${method}'
}
}
}
}
// Check if an operation is set (has responses)
fn is_operation_set(op hono.OpenAPIOperation) bool {
return op.responses.len > 0
}
// Get operation from path item by method name
fn get_operation_by_method(path_item hono.OpenAPIPathItem, method string) hono.OpenAPIOperation {
return match method {
'get' { path_item.get }
'post' { path_item.post }
'put' { path_item.put }
'delete' { path_item.delete }
'patch' { path_item.patch }
'head' { path_item.head }
'options' { path_item.options }
else { hono.OpenAPIOperation{} }
}
}
// Test that each HTTP method can be added via the builder and is preserved
fn test_http_method_via_builder(method string) bool {
path := random_path()
op := generate_operation(method)
// Build document using the builder - step by step
mut builder := hono.OpenAPIBuilder.new()
builder.openapi('3.0.0')
builder.title('Test API')
builder.version('1.0.0')
// Add operation using the path builder
mut path_builder := builder.path(path)
match method {
'get' { path_builder.get(op) }
'post' { path_builder.post(op) }
'put' { path_builder.put(op) }
'delete' { path_builder.delete(op) }
'patch' { path_builder.patch(op) }
'head' { path_builder.head(op) }
'options' { path_builder.options(op) }
else {}
}
path_builder.done()
// Build the document
doc := builder.build() or { return false }
// Verify the path exists
if path !in doc.paths {
return false
}
// Verify the operation is preserved
path_item := doc.paths[path] or { return false }
stored_op := get_operation_by_method(path_item, method)
// Check operation is set and has correct summary
if !is_operation_set(stored_op) {
return false
}
if stored_op.summary != op.summary {
return false
}
if stored_op.operation_id != op.operation_id {
return false
}
return true
}
// Property test: All HTTP methods are supported
fn test_http_methods_property() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Running Property 4: HTTP Methods Support in Path Items')
println('Iterations: ${property_test_iterations}')
println('')
for i in 0 .. property_test_iterations {
// Pick a random HTTP method
method_idx := rand.int_in_range(0, http_methods.len) or { 0 }
method := http_methods[method_idx]
if test_http_method_via_builder(method) {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Method ${method} was not preserved in document')
}
// Progress indicator
if (i + 1) % 20 == 0 {
print('.')
}
}
println('')
return stats
}
// Additional test: All methods can be added to the same path
fn test_all_methods_on_same_path() PropertyTestStats {
mut stats := PropertyTestStats{}
println('\nRunning additional test: All HTTP methods on same path')
println('Iterations: ${property_test_iterations}')
println('')
for i in 0 .. property_test_iterations {
path := random_path()
// Create operations for all methods
get_op := generate_operation('get')
post_op := generate_operation('post')
put_op := generate_operation('put')
delete_op := generate_operation('delete')
patch_op := generate_operation('patch')
head_op := generate_operation('head')
options_op := generate_operation('options')
// Build document with all methods on the same path
mut builder := hono.OpenAPIBuilder.new()
builder.openapi('3.0.0')
builder.title('Test API')
builder.version('1.0.0')
mut path_builder := builder.path(path)
path_builder.get(get_op)
path_builder.post(post_op)
path_builder.put(put_op)
path_builder.delete(delete_op)
path_builder.patch(patch_op)
path_builder.head(head_op)
path_builder.options(options_op)
path_builder.done()
doc := builder.build() or {
stats.record_fail('Iteration ${i}: Build failed - ${err}')
continue
}
// Verify all methods are preserved
path_item := doc.paths[path] or {
stats.record_fail('Iteration ${i}: Path not found')
continue
}
mut all_preserved := true
// Check each method
if !is_operation_set(path_item.get) || path_item.get.summary != get_op.summary {
all_preserved = false
}
if !is_operation_set(path_item.post) || path_item.post.summary != post_op.summary {
all_preserved = false
}
if !is_operation_set(path_item.put) || path_item.put.summary != put_op.summary {
all_preserved = false
}
if !is_operation_set(path_item.delete) || path_item.delete.summary != delete_op.summary {
all_preserved = false
}
if !is_operation_set(path_item.patch) || path_item.patch.summary != patch_op.summary {
all_preserved = false
}
if !is_operation_set(path_item.head) || path_item.head.summary != head_op.summary {
all_preserved = false
}
if !is_operation_set(path_item.options) || path_item.options.summary != options_op.summary {
all_preserved = false
}
if all_preserved {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Not all methods preserved on path ${path}')
}
if (i + 1) % 20 == 0 {
print('.')
}
}
println('')
return stats
}
fn main() {
println('🧪 Property-Based Test: HTTP Methods Support in Path Items')
println('============================================================')
println('')
// Seed random number generator
rand.seed([u32(time.now().unix()), u32(time.now().unix() >> 32)])
// Run main property test
stats1 := test_http_methods_property()
stats1.print_summary()
// Run additional test
stats2 := test_all_methods_on_same_path()
stats2.print_summary()
// Exit with appropriate code
if stats1.failed_tests > 0 || stats2.failed_tests > 0 {
exit(1)
}
}
+347
View File
@@ -0,0 +1,347 @@
// test_openapi_invalid_path_property.v - Property-Based Test for Invalid Path Error Identification
// **Feature: swagger-ui, Property 9: Invalid Path Error Identification**
// **Validates: Requirements 8.3**
//
// Property: For any OpenAPI document with an invalid path definition,
// the validation error message SHALL identify the problematic path.
module main
import hono
import rand
import time
const property_test_iterations = 100
struct PropertyTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
failed_examples []string
}
fn (mut stats PropertyTestStats) record_pass() {
stats.total_tests++
stats.passed_tests++
}
fn (mut stats PropertyTestStats) record_fail(example string) {
stats.total_tests++
stats.failed_tests++
stats.failed_examples << example
}
fn (stats PropertyTestStats) print_summary() {
println('\n=== Property Test Summary ===')
println('Total iterations: ${stats.total_tests}')
println('Passed: ${stats.passed_tests}')
println('Failed: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 Property test PASSED!')
} else {
println('❌ Property test FAILED!')
println('\nFailing examples:')
for i, example in stats.failed_examples {
if i >= 5 {
println('... and ${stats.failed_examples.len - 5} more')
break
}
println(' ${example}')
}
}
}
// Random string generator
fn random_string(min_len int, max_len int) string {
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-'
len := rand.int_in_range(min_len, max_len + 1) or { min_len }
mut result := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
result += chars[idx..idx + 1]
}
return result
}
// Random OpenAPI version
fn random_openapi_version() string {
versions := ['3.0.0', '3.0.1', '3.0.2', '3.0.3', '3.1.0']
return versions[rand.int_in_range(0, versions.len) or { 0 }]
}
// Generate a valid path item
fn generate_valid_path_item() hono.OpenAPIPathItem {
return hono.OpenAPIPathItem{
get: hono.OpenAPIOperation{
summary: random_string(5, 20)
responses: {
'200': hono.OpenAPIResponse{description: 'Success'}
}
}
}
}
// Generate a document with a specific invalid path
fn generate_doc_with_invalid_path(invalid_path string) hono.OpenAPIDocument {
return hono.OpenAPIDocument{
openapi: random_openapi_version()
info: hono.OpenAPIInfo{
title: random_string(5, 20)
version: '1.0.0'
}
paths: {
invalid_path: generate_valid_path_item()
}
}
}
// Test 1: Path not starting with '/' should return error mentioning the path
fn test_path_not_starting_with_slash() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Test 1: Path not starting with slash validation')
for _ in 0 .. property_test_iterations {
// Generate invalid path (not starting with /)
invalid_path := random_string(3, 15) // e.g., "users" instead of "/users"
doc := generate_doc_with_invalid_path(invalid_path)
// Validate should fail
doc.validate() or {
// Check that error message contains the invalid path
if err.msg().contains(invalid_path) {
stats.record_pass()
continue
}
stats.record_fail("Error message doesn't contain invalid path '${invalid_path}': ${err.msg()}")
continue
}
// If validation passed, that's a failure
stats.record_fail("Validation passed for invalid path '${invalid_path}'")
}
return stats
}
// Test 2: Path with unmatched '{' should return error mentioning the path
fn test_path_with_unmatched_open_brace() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Test 2: Path with unmatched open brace validation')
for _ in 0 .. property_test_iterations {
// Generate invalid path with unmatched '{'
param_name := random_string(3, 10)
invalid_path := '/${random_string(3, 10)}/{${param_name}' // e.g., "/users/{id" (missing closing brace)
doc := generate_doc_with_invalid_path(invalid_path)
// Validate should fail
doc.validate() or {
// Check that error message contains the invalid path
if err.msg().contains(invalid_path) {
stats.record_pass()
continue
}
stats.record_fail("Error message doesn't contain invalid path '${invalid_path}': ${err.msg()}")
continue
}
// If validation passed, that's a failure
stats.record_fail("Validation passed for invalid path '${invalid_path}'")
}
return stats
}
// Test 3: Path with unmatched '}' should return error mentioning the path
fn test_path_with_unmatched_close_brace() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Test 3: Path with unmatched close brace validation')
for _ in 0 .. property_test_iterations {
// Generate invalid path with unmatched '}'
param_name := random_string(3, 10)
invalid_path := '/${random_string(3, 10)}/${param_name}}' // e.g., "/users/id}" (extra closing brace)
doc := generate_doc_with_invalid_path(invalid_path)
// Validate should fail
doc.validate() or {
// Check that error message contains the invalid path
if err.msg().contains(invalid_path) {
stats.record_pass()
continue
}
stats.record_fail("Error message doesn't contain invalid path '${invalid_path}': ${err.msg()}")
continue
}
// If validation passed, that's a failure
stats.record_fail("Validation passed for invalid path '${invalid_path}'")
}
return stats
}
// Test 4: Path with empty parameter '{}' should return error mentioning the path
fn test_path_with_empty_parameter() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Test 4: Path with empty parameter validation')
for _ in 0 .. property_test_iterations {
// Generate invalid path with empty parameter
invalid_path := '/${random_string(3, 10)}/{}' // e.g., "/users/{}" (empty parameter)
doc := generate_doc_with_invalid_path(invalid_path)
// Validate should fail
doc.validate() or {
// Check that error message contains the invalid path
if err.msg().contains(invalid_path) {
stats.record_pass()
continue
}
stats.record_fail("Error message doesn't contain invalid path '${invalid_path}': ${err.msg()}")
continue
}
// If validation passed, that's a failure
stats.record_fail("Validation passed for invalid path '${invalid_path}'")
}
return stats
}
// Test 5: Path with consecutive slashes should return error mentioning the path
fn test_path_with_consecutive_slashes() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Test 5: Path with consecutive slashes validation')
for _ in 0 .. property_test_iterations {
// Generate invalid path with consecutive slashes
invalid_path := '/${random_string(3, 10)}//${random_string(3, 10)}' // e.g., "/users//profile"
doc := generate_doc_with_invalid_path(invalid_path)
// Validate should fail
doc.validate() or {
// Check that error message contains the invalid path
if err.msg().contains(invalid_path) {
stats.record_pass()
continue
}
stats.record_fail("Error message doesn't contain invalid path '${invalid_path}': ${err.msg()}")
continue
}
// If validation passed, that's a failure
stats.record_fail("Validation passed for invalid path '${invalid_path}'")
}
return stats
}
// Test 6: Valid paths should pass validation
fn test_valid_paths_pass() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Test 6: Valid paths pass validation')
valid_path_patterns := [
'/users',
'/users/{id}',
'/api/v1/users',
'/api/v1/users/{userId}/posts/{postId}',
'/{resource}',
'/health',
'/api/v2/items/{itemId}/details',
]
for _ in 0 .. property_test_iterations {
// Pick a random valid path pattern
valid_path := valid_path_patterns[rand.int_in_range(0, valid_path_patterns.len) or { 0 }]
doc := hono.OpenAPIDocument{
openapi: random_openapi_version()
info: hono.OpenAPIInfo{
title: random_string(5, 20)
version: '1.0.0'
}
paths: {
valid_path: generate_valid_path_item()
}
}
// Validate should pass
doc.validate() or {
stats.record_fail("Validation failed for valid path '${valid_path}': ${err.msg()}")
continue
}
stats.record_pass()
}
return stats
}
fn main() {
println('🧪 Property-Based Test: Invalid Path Error Identification')
println('==========================================================')
println('')
// Seed random number generator
rand.seed([u32(time.now().unix()), u32(time.now().unix() >> 32)])
mut total_stats := PropertyTestStats{}
// Run all tests
stats1 := test_path_not_starting_with_slash()
total_stats.total_tests += stats1.total_tests
total_stats.passed_tests += stats1.passed_tests
total_stats.failed_tests += stats1.failed_tests
total_stats.failed_examples << stats1.failed_examples
stats2 := test_path_with_unmatched_open_brace()
total_stats.total_tests += stats2.total_tests
total_stats.passed_tests += stats2.passed_tests
total_stats.failed_tests += stats2.failed_tests
total_stats.failed_examples << stats2.failed_examples
stats3 := test_path_with_unmatched_close_brace()
total_stats.total_tests += stats3.total_tests
total_stats.passed_tests += stats3.passed_tests
total_stats.failed_tests += stats3.failed_tests
total_stats.failed_examples << stats3.failed_examples
stats4 := test_path_with_empty_parameter()
total_stats.total_tests += stats4.total_tests
total_stats.passed_tests += stats4.passed_tests
total_stats.failed_tests += stats4.failed_tests
total_stats.failed_examples << stats4.failed_examples
stats5 := test_path_with_consecutive_slashes()
total_stats.total_tests += stats5.total_tests
total_stats.passed_tests += stats5.passed_tests
total_stats.failed_tests += stats5.failed_tests
total_stats.failed_examples << stats5.failed_examples
stats6 := test_valid_paths_pass()
total_stats.total_tests += stats6.total_tests
total_stats.passed_tests += stats6.passed_tests
total_stats.failed_tests += stats6.failed_tests
total_stats.failed_examples << stats6.failed_examples
total_stats.print_summary()
// Exit with appropriate code
if total_stats.failed_tests > 0 {
exit(1)
}
}
@@ -0,0 +1,388 @@
// test_openapi_optional_fields_property.v - Property-Based Test for Optional Fields Omission
// **Feature: swagger-ui, Property 8: Optional Fields Omission in JSON**
// **Validates: Requirements 3.4**
//
// Property: For any OpenAPI document with optional fields not set,
// the serialized JSON SHALL NOT contain those fields.
module main
import hono
import rand
import time
const property_test_iterations = 100
struct PropertyTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
failed_examples []string
}
fn (mut stats PropertyTestStats) record_pass() {
stats.total_tests++
stats.passed_tests++
}
fn (mut stats PropertyTestStats) record_fail(example string) {
stats.total_tests++
stats.failed_tests++
stats.failed_examples << example
}
fn (stats PropertyTestStats) print_summary() {
println('\n=== Property Test Summary ===')
println('Total iterations: ${stats.total_tests}')
println('Passed: ${stats.passed_tests}')
println('Failed: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 Property test PASSED!')
} else {
println('❌ Property test FAILED!')
println('\nFailing examples:')
for i, example in stats.failed_examples {
if i >= 3 {
println('... and ${stats.failed_examples.len - 3} more')
break
}
println(' ${example}')
}
}
}
// Random string generator
fn random_string(min_len int, max_len int) string {
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
len := rand.int_in_range(min_len, max_len + 1) or { min_len }
mut result := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
result += chars[idx..idx + 1]
}
return result
}
// Test 1: Empty description should not appear in JSON
fn test_empty_description_omitted() bool {
info := hono.OpenAPIInfo{
title: random_string(5, 15)
version: '1.0.0'
// description is empty - should be omitted
}
json_str := info.to_json().str()
return !json_str.contains('"description"')
}
// Test 2: Empty contact should not appear in JSON
fn test_empty_contact_omitted() bool {
info := hono.OpenAPIInfo{
title: random_string(5, 15)
version: '1.0.0'
// contact is empty - should be omitted
}
json_str := info.to_json().str()
return !json_str.contains('"contact"')
}
// Test 3: Empty license should not appear in JSON
fn test_empty_license_omitted() bool {
info := hono.OpenAPIInfo{
title: random_string(5, 15)
version: '1.0.0'
// license is empty - should be omitted
}
json_str := info.to_json().str()
return !json_str.contains('"license"')
}
// Test 4: Empty servers array should not appear in JSON
fn test_empty_servers_omitted() bool {
doc := hono.OpenAPIDocument{
openapi: '3.0.0'
info: hono.OpenAPIInfo{
title: random_string(5, 15)
version: '1.0.0'
}
paths: {}
// servers is empty - should be omitted
}
json_str := doc.to_json_str()
return !json_str.contains('"servers"')
}
// Test 5: Empty tags array should not appear in JSON
fn test_empty_tags_omitted() bool {
doc := hono.OpenAPIDocument{
openapi: '3.0.0'
info: hono.OpenAPIInfo{
title: random_string(5, 15)
version: '1.0.0'
}
paths: {}
// tags is empty - should be omitted
}
json_str := doc.to_json_str()
return !json_str.contains('"tags"')
}
// Test 6: Empty components should not appear in JSON
fn test_empty_components_omitted() bool {
doc := hono.OpenAPIDocument{
openapi: '3.0.0'
info: hono.OpenAPIInfo{
title: random_string(5, 15)
version: '1.0.0'
}
paths: {}
// components is empty - should be omitted
}
json_str := doc.to_json_str()
return !json_str.contains('"components"')
}
// Test 7: False boolean values should not appear in JSON (for optional booleans)
fn test_false_boolean_omitted() bool {
param := hono.OpenAPIParameter{
name: random_string(3, 10)
in_location: 'query'
required: false // should be omitted
deprecated: false // should be omitted
}
json_str := param.to_json().str()
no_required := !json_str.contains('"required"')
no_deprecated := !json_str.contains('"deprecated"')
return no_required && no_deprecated
}
// Test 8: True boolean values SHOULD appear in JSON
fn test_true_boolean_included() bool {
param := hono.OpenAPIParameter{
name: random_string(3, 10)
in_location: 'query'
required: true // should be included
deprecated: true // should be included
}
json_str := param.to_json().str()
has_required := json_str.contains('"required":true')
has_deprecated := json_str.contains('"deprecated":true')
return has_required && has_deprecated
}
// Test 9: Empty operation should not appear in path item
fn test_empty_operation_omitted() bool {
path_item := hono.OpenAPIPathItem{
summary: random_string(5, 15)
get: hono.OpenAPIOperation{
summary: 'Get'
responses: {
'200': hono.OpenAPIResponse{description: 'OK'}
}
}
// post, put, delete, etc. are empty - should be omitted
}
json_str := path_item.to_json().str()
has_get := json_str.contains('"get"')
no_post := !json_str.contains('"post"')
no_put := !json_str.contains('"put"')
no_delete := !json_str.contains('"delete"')
return has_get && no_post && no_put && no_delete
}
// Test 10: Empty parameters array should not appear in operation
fn test_empty_parameters_omitted() bool {
op := hono.OpenAPIOperation{
summary: random_string(5, 15)
responses: {
'200': hono.OpenAPIResponse{description: 'OK'}
}
// parameters is empty - should be omitted
}
json_str := op.to_json().str()
return !json_str.contains('"parameters"')
}
// Test 11: Empty request body should not appear in operation
fn test_empty_request_body_omitted() bool {
op := hono.OpenAPIOperation{
summary: random_string(5, 15)
responses: {
'200': hono.OpenAPIResponse{description: 'OK'}
}
// request_body is empty - should be omitted
}
json_str := op.to_json().str()
return !json_str.contains('"requestBody"')
}
// Test 12: Zero numeric values should not appear (for optional numerics)
fn test_zero_numeric_omitted() bool {
schema := hono.OpenAPISchema{
schema_type: 'integer'
// minimum, maximum, min_length, max_length are 0 - should be omitted
}
json_str := schema.to_json().str()
no_minimum := !json_str.contains('"minimum"')
no_maximum := !json_str.contains('"maximum"')
no_min_length := !json_str.contains('"minLength"')
no_max_length := !json_str.contains('"maxLength"')
return no_minimum && no_maximum && no_min_length && no_max_length
}
// Test 13: Non-zero numeric values SHOULD appear
fn test_nonzero_numeric_included() bool {
schema := hono.OpenAPISchema{
schema_type: 'integer'
minimum: 1
maximum: 100
min_length: 5
max_length: 50
}
json_str := schema.to_json().str()
has_minimum := json_str.contains('"minimum"')
has_maximum := json_str.contains('"maximum"')
has_min_length := json_str.contains('"minLength"')
has_max_length := json_str.contains('"maxLength"')
return has_minimum && has_maximum && has_min_length && has_max_length
}
// Property test: Optional fields omission
fn test_optional_fields_property() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Running Property 8: Optional Fields Omission in JSON')
println('Iterations: ${property_test_iterations}')
println('')
for i in 0 .. property_test_iterations {
// Test 1: Empty description
if test_empty_description_omitted() {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Empty description was not omitted')
}
// Test 2: Empty contact
if test_empty_contact_omitted() {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Empty contact was not omitted')
}
// Test 3: Empty license
if test_empty_license_omitted() {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Empty license was not omitted')
}
// Test 4: Empty servers
if test_empty_servers_omitted() {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Empty servers was not omitted')
}
// Test 5: Empty tags
if test_empty_tags_omitted() {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Empty tags was not omitted')
}
// Test 6: Empty components
if test_empty_components_omitted() {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Empty components was not omitted')
}
// Test 7: False boolean omitted
if test_false_boolean_omitted() {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: False boolean was not omitted')
}
// Test 8: True boolean included
if test_true_boolean_included() {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: True boolean was not included')
}
// Test 9: Empty operation omitted
if test_empty_operation_omitted() {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Empty operation was not omitted')
}
// Test 10: Empty parameters omitted
if test_empty_parameters_omitted() {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Empty parameters was not omitted')
}
// Test 11: Empty request body omitted
if test_empty_request_body_omitted() {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Empty request body was not omitted')
}
// Test 12: Zero numeric omitted
if test_zero_numeric_omitted() {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Zero numeric was not omitted')
}
// Test 13: Non-zero numeric included
if test_nonzero_numeric_included() {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Non-zero numeric was not included')
}
// Progress indicator
if (i + 1) % 20 == 0 {
print('.')
}
}
println('')
return stats
}
fn main() {
println('🧪 Property-Based Test: Optional Fields Omission')
println('=================================================')
println('')
// Seed random number generator
rand.seed([u32(time.now().unix()), u32(time.now().unix() >> 32)])
stats := test_optional_fields_property()
stats.print_summary()
// Exit with appropriate code
if stats.failed_tests > 0 {
exit(1)
}
}
@@ -0,0 +1,282 @@
// test_openapi_param_locations_property.v - Property-Based Test for Parameter Locations Support
// **Feature: swagger-ui, Property 5: Parameter Locations Support**
// **Validates: Requirements 2.5**
//
// Property: For any parameter location (path, query, header, cookie),
// adding a parameter with that location SHALL succeed and be preserved in the document.
module main
import hono
import rand
import time
const property_test_iterations = 100
struct PropertyTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
failed_examples []string
}
fn (mut stats PropertyTestStats) record_pass() {
stats.total_tests++
stats.passed_tests++
}
fn (mut stats PropertyTestStats) record_fail(example string) {
stats.total_tests++
stats.failed_tests++
stats.failed_examples << example
}
fn (stats PropertyTestStats) print_summary() {
println('\n=== Property Test Summary ===')
println('Total iterations: ${stats.total_tests}')
println('Passed: ${stats.passed_tests}')
println('Failed: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 Property test PASSED!')
} else {
println('❌ Property test FAILED!')
println('\nFailing examples:')
for i, example in stats.failed_examples {
if i >= 3 {
println('... and ${stats.failed_examples.len - 3} more')
break
}
println(' ${example}')
}
}
}
// Random string generator
fn random_string(min_len int, max_len int) string {
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
len := rand.int_in_range(min_len, max_len + 1) or { min_len }
mut result := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
result += chars[idx..idx + 1]
}
return result
}
// Random path generator
fn random_path() string {
segments := rand.int_in_range(1, 4) or { 1 }
mut path := ''
for _ in 0 .. segments {
path += '/' + random_string(3, 10)
}
return path
}
// All supported parameter locations
const param_locations = ['path', 'query', 'header', 'cookie']
// Generate a random parameter with a specific location
fn generate_parameter(location string) hono.OpenAPIParameter {
return hono.OpenAPIParameter{
name: random_string(3, 10)
in_location: location
description: 'Test ${location} parameter - ${random_string(5, 10)}'
required: location == 'path' || rand.int_in_range(0, 2) or { 0 } == 1
schema: hono.OpenAPISchema{
schema_type: 'string'
}
}
}
// Test that each parameter location can be added and is preserved
fn test_param_location_via_builder(location string) bool {
path := random_path()
param := generate_parameter(location)
// Create an operation with the parameter
op := hono.OpenAPIOperation{
summary: 'Test operation with ${location} parameter'
parameters: [param]
responses: {
'200': hono.OpenAPIResponse{
description: 'Success'
}
}
}
// Build document using the builder
mut builder := hono.OpenAPIBuilder.new()
builder.openapi('3.0.0')
builder.title('Test API')
builder.version('1.0.0')
mut path_builder := builder.path(path)
path_builder.get(op)
path_builder.done()
// Build the document
doc := builder.build() or { return false }
// Verify the path exists
if path !in doc.paths {
return false
}
// Verify the parameter is preserved
path_item := doc.paths[path] or { return false }
if path_item.get.parameters.len == 0 {
return false
}
stored_param := path_item.get.parameters[0]
// Check parameter location is preserved
if stored_param.in_location != location {
return false
}
// Check parameter name is preserved
if stored_param.name != param.name {
return false
}
return true
}
// Property test: All parameter locations are supported
fn test_param_locations_property() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Running Property 5: Parameter Locations Support')
println('Iterations: ${property_test_iterations}')
println('')
for i in 0 .. property_test_iterations {
// Pick a random parameter location
location_idx := rand.int_in_range(0, param_locations.len) or { 0 }
location := param_locations[location_idx]
if test_param_location_via_builder(location) {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Location ${location} was not preserved in document')
}
// Progress indicator
if (i + 1) % 20 == 0 {
print('.')
}
}
println('')
return stats
}
// Additional test: All locations can be used in the same operation
fn test_all_locations_in_same_operation() PropertyTestStats {
mut stats := PropertyTestStats{}
println('\nRunning additional test: All parameter locations in same operation')
println('Iterations: ${property_test_iterations}')
println('')
for i in 0 .. property_test_iterations {
path := random_path()
// Create parameters for all locations
path_param := generate_parameter('path')
query_param := generate_parameter('query')
header_param := generate_parameter('header')
cookie_param := generate_parameter('cookie')
// Create an operation with all parameters
op := hono.OpenAPIOperation{
summary: 'Test operation with all parameter locations'
parameters: [path_param, query_param, header_param, cookie_param]
responses: {
'200': hono.OpenAPIResponse{
description: 'Success'
}
}
}
// Build document
mut builder := hono.OpenAPIBuilder.new()
builder.openapi('3.0.0')
builder.title('Test API')
builder.version('1.0.0')
mut path_builder := builder.path(path)
path_builder.get(op)
path_builder.done()
doc := builder.build() or {
stats.record_fail('Iteration ${i}: Build failed - ${err}')
continue
}
// Verify all parameters are preserved
path_item := doc.paths[path] or {
stats.record_fail('Iteration ${i}: Path not found')
continue
}
if path_item.get.parameters.len != 4 {
stats.record_fail('Iteration ${i}: Expected 4 parameters, got ${path_item.get.parameters.len}')
continue
}
// Check each location is present
mut found_locations := map[string]bool{}
for param in path_item.get.parameters {
found_locations[param.in_location] = true
}
mut all_found := true
for loc in param_locations {
if loc !in found_locations {
all_found = false
break
}
}
if all_found {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Not all parameter locations preserved')
}
if (i + 1) % 20 == 0 {
print('.')
}
}
println('')
return stats
}
fn main() {
println('🧪 Property-Based Test: Parameter Locations Support')
println('====================================================')
println('')
// Seed random number generator
rand.seed([u32(time.now().unix()), u32(time.now().unix() >> 32)])
// Run main property test
stats1 := test_param_locations_property()
stats1.print_summary()
// Run additional test
stats2 := test_all_locations_in_same_operation()
stats2.print_summary()
// Exit with appropriate code
if stats1.failed_tests > 0 || stats2.failed_tests > 0 {
exit(1)
}
}
@@ -0,0 +1,341 @@
// test_openapi_required_fields_property.v - Property-Based Test for OpenAPI Required Fields Validation
// **Feature: swagger-ui, Property 3: OpenAPI Document Required Fields Validation**
// **Validates: Requirements 2.2, 8.1**
//
// Property: For any OpenAPI document missing a required field (openapi, info, or paths),
// the validate() method SHALL return an error identifying the missing field.
module main
import hono
import rand
import time
const property_test_iterations = 100
struct PropertyTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
failed_examples []string
}
fn (mut stats PropertyTestStats) record_pass() {
stats.total_tests++
stats.passed_tests++
}
fn (mut stats PropertyTestStats) record_fail(example string) {
stats.total_tests++
stats.failed_tests++
stats.failed_examples << example
}
fn (stats PropertyTestStats) print_summary() {
println('\n=== Property Test Summary ===')
println('Total iterations: ${stats.total_tests}')
println('Passed: ${stats.passed_tests}')
println('Failed: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 Property test PASSED!')
} else {
println('❌ Property test FAILED!')
println('\nFailing examples:')
for i, example in stats.failed_examples {
if i >= 5 {
println('... and ${stats.failed_examples.len - 5} more')
break
}
println(' ${example}')
}
}
}
// Random string generator
fn random_string(min_len int, max_len int) string {
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-'
len := rand.int_in_range(min_len, max_len + 1) or { min_len }
mut result := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
result += chars[idx..idx + 1]
}
return result
}
// Random path generator
fn random_path() string {
segments := rand.int_in_range(1, 4) or { 1 }
mut path := ''
for _ in 0 .. segments {
path += '/' + random_string(3, 10)
}
return path
}
// Random OpenAPI version
fn random_openapi_version() string {
versions := ['3.0.0', '3.0.1', '3.0.2', '3.0.3', '3.1.0']
return versions[rand.int_in_range(0, versions.len) or { 0 }]
}
// Generate a valid OpenAPIDocument
fn generate_valid_document() hono.OpenAPIDocument {
return hono.OpenAPIDocument{
openapi: random_openapi_version()
info: hono.OpenAPIInfo{
title: random_string(5, 20)
version: '${rand.int_in_range(1, 10) or { 1 }}.${rand.int_in_range(0, 10) or { 0 }}.${rand.int_in_range(0, 10) or { 0 }}'
}
paths: {
random_path(): hono.OpenAPIPathItem{
get: hono.OpenAPIOperation{
summary: random_string(5, 20)
responses: {
'200': hono.OpenAPIResponse{description: 'Success'}
}
}
}
}
}
}
// Test 1: Missing 'openapi' field should return error
fn test_missing_openapi_field() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Test 1: Missing openapi field validation')
for _ in 0 .. property_test_iterations {
// Generate document with missing openapi field
doc := hono.OpenAPIDocument{
openapi: '' // Missing required field
info: hono.OpenAPIInfo{
title: random_string(5, 20)
version: '1.0.0'
}
paths: {
random_path(): hono.OpenAPIPathItem{
get: hono.OpenAPIOperation{
responses: {'200': hono.OpenAPIResponse{description: 'OK'}}
}
}
}
}
// Validate should fail
doc.validate() or {
// Check that error message mentions 'openapi'
if err.msg().contains('openapi') {
stats.record_pass()
continue
}
stats.record_fail("Error message doesn't mention 'openapi': ${err.msg()}")
continue
}
// If validation passed, that's a failure
stats.record_fail('Validation passed for document with missing openapi field')
}
return stats
}
// Test 2: Missing 'info.title' field should return error
fn test_missing_info_title_field() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Test 2: Missing info.title field validation')
for _ in 0 .. property_test_iterations {
// Generate document with missing info.title field
doc := hono.OpenAPIDocument{
openapi: random_openapi_version()
info: hono.OpenAPIInfo{
title: '' // Missing required field
version: '1.0.0'
}
paths: {
random_path(): hono.OpenAPIPathItem{
get: hono.OpenAPIOperation{
responses: {'200': hono.OpenAPIResponse{description: 'OK'}}
}
}
}
}
// Validate should fail
doc.validate() or {
// Check that error message mentions 'info.title'
if err.msg().contains('info.title') {
stats.record_pass()
continue
}
stats.record_fail("Error message doesn't mention 'info.title': ${err.msg()}")
continue
}
// If validation passed, that's a failure
stats.record_fail('Validation passed for document with missing info.title field')
}
return stats
}
// Test 3: Missing 'info.version' field should return error
fn test_missing_info_version_field() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Test 3: Missing info.version field validation')
for _ in 0 .. property_test_iterations {
// Generate document with missing info.version field
doc := hono.OpenAPIDocument{
openapi: random_openapi_version()
info: hono.OpenAPIInfo{
title: random_string(5, 20)
version: '' // Missing required field
}
paths: {
random_path(): hono.OpenAPIPathItem{
get: hono.OpenAPIOperation{
responses: {'200': hono.OpenAPIResponse{description: 'OK'}}
}
}
}
}
// Validate should fail
doc.validate() or {
// Check that error message mentions 'info.version'
if err.msg().contains('info.version') {
stats.record_pass()
continue
}
stats.record_fail("Error message doesn't mention 'info.version': ${err.msg()}")
continue
}
// If validation passed, that's a failure
stats.record_fail('Validation passed for document with missing info.version field')
}
return stats
}
// Test 4: Valid document should pass validation
fn test_valid_document_passes() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Test 4: Valid document passes validation')
for _ in 0 .. property_test_iterations {
// Generate valid document
doc := generate_valid_document()
// Validate should pass
doc.validate() or {
stats.record_fail('Validation failed for valid document: ${err.msg()}')
continue
}
stats.record_pass()
}
return stats
}
// Test 5: Unsupported OpenAPI version should return error
fn test_unsupported_openapi_version() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Test 5: Unsupported OpenAPI version validation')
invalid_versions := ['2.0.0', '1.0.0', '4.0.0', '3.2.0', 'invalid', '']
for _ in 0 .. property_test_iterations {
// Pick a random invalid version
invalid_version := invalid_versions[rand.int_in_range(0, invalid_versions.len - 1) or { 0 }]
doc := hono.OpenAPIDocument{
openapi: invalid_version
info: hono.OpenAPIInfo{
title: random_string(5, 20)
version: '1.0.0'
}
paths: {
random_path(): hono.OpenAPIPathItem{
get: hono.OpenAPIOperation{
responses: {'200': hono.OpenAPIResponse{description: 'OK'}}
}
}
}
}
// Validate should fail
doc.validate() or {
// Check that error message mentions version issue
if err.msg().contains('openapi') || err.msg().contains('version') || err.msg().contains('Unsupported') {
stats.record_pass()
continue
}
stats.record_fail("Error message doesn't mention version issue: ${err.msg()}")
continue
}
// If validation passed, that's a failure
stats.record_fail('Validation passed for document with unsupported version: ${invalid_version}')
}
return stats
}
fn main() {
println('🧪 Property-Based Test: OpenAPI Required Fields Validation')
println('============================================================')
println('')
// Seed random number generator
rand.seed([u32(time.now().unix()), u32(time.now().unix() >> 32)])
mut total_stats := PropertyTestStats{}
// Run all tests
stats1 := test_missing_openapi_field()
total_stats.total_tests += stats1.total_tests
total_stats.passed_tests += stats1.passed_tests
total_stats.failed_tests += stats1.failed_tests
total_stats.failed_examples << stats1.failed_examples
stats2 := test_missing_info_title_field()
total_stats.total_tests += stats2.total_tests
total_stats.passed_tests += stats2.passed_tests
total_stats.failed_tests += stats2.failed_tests
total_stats.failed_examples << stats2.failed_examples
stats3 := test_missing_info_version_field()
total_stats.total_tests += stats3.total_tests
total_stats.passed_tests += stats3.passed_tests
total_stats.failed_tests += stats3.failed_tests
total_stats.failed_examples << stats3.failed_examples
stats4 := test_valid_document_passes()
total_stats.total_tests += stats4.total_tests
total_stats.passed_tests += stats4.passed_tests
total_stats.failed_tests += stats4.failed_tests
total_stats.failed_examples << stats4.failed_examples
stats5 := test_unsupported_openapi_version()
total_stats.total_tests += stats5.total_tests
total_stats.passed_tests += stats5.passed_tests
total_stats.failed_tests += stats5.failed_tests
total_stats.failed_examples << stats5.failed_examples
total_stats.print_summary()
// Exit with appropriate code
if total_stats.failed_tests > 0 {
exit(1)
}
}
+486
View File
@@ -0,0 +1,486 @@
// test_openapi_roundtrip_property.v - Property-Based Test for OpenAPI Round-Trip Serialization
// **Feature: swagger-ui, Property 1: OpenAPI Document Round-Trip Serialization**
// **Validates: Requirements 3.2**
//
// Property: For any valid OpenAPI document, serializing to JSON and then
// deserializing back SHALL produce an equivalent document.
module main
import hono
import rand
import time
const property_test_iterations = 100
struct PropertyTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
failed_examples []string
}
fn (mut stats PropertyTestStats) record_pass() {
stats.total_tests++
stats.passed_tests++
}
fn (mut stats PropertyTestStats) record_fail(example string) {
stats.total_tests++
stats.failed_tests++
stats.failed_examples << example
}
fn (stats PropertyTestStats) print_summary() {
println('\n=== Property Test Summary ===')
println('Total iterations: ${stats.total_tests}')
println('Passed: ${stats.passed_tests}')
println('Failed: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 Property test PASSED!')
} else {
println('❌ Property test FAILED!')
println('\nFailing examples:')
for i, example in stats.failed_examples {
if i >= 3 {
println('... and ${stats.failed_examples.len - 3} more')
break
}
println(' ${example}')
}
}
}
// Random string generator
fn random_string(min_len int, max_len int) string {
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-'
len := rand.int_in_range(min_len, max_len + 1) or { min_len }
mut result := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
result += chars[idx..idx + 1]
}
return result
}
// Random URL generator
fn random_url() string {
protocols := ['http', 'https']
protocol := protocols[rand.int_in_range(0, 2) or { 0 }]
domain := random_string(3, 10)
tld := ['com', 'org', 'net', 'io'][rand.int_in_range(0, 4) or { 0 }]
return '${protocol}://${domain}.${tld}'
}
// Random email generator
fn random_email() string {
return '${random_string(3, 8)}@${random_string(3, 6)}.com'
}
// Random path generator
fn random_path() string {
segments := rand.int_in_range(1, 4) or { 1 }
mut path := ''
for _ in 0 .. segments {
path += '/' + random_string(3, 10)
}
return path
}
// Random OpenAPI version
fn random_openapi_version() string {
versions := ['3.0.0', '3.0.1', '3.0.2', '3.0.3', '3.1.0']
return versions[rand.int_in_range(0, versions.len) or { 0 }]
}
// Random HTTP method
fn random_http_method() string {
methods := ['get', 'post', 'put', 'delete', 'patch']
return methods[rand.int_in_range(0, methods.len) or { 0 }]
}
// Random parameter location
fn random_param_location() string {
locations := ['path', 'query', 'header', 'cookie']
return locations[rand.int_in_range(0, locations.len) or { 0 }]
}
// Random schema type
fn random_schema_type() string {
types := ['string', 'integer', 'number', 'boolean', 'array', 'object']
return types[rand.int_in_range(0, types.len) or { 0 }]
}
// Generate random OpenAPIContact
fn generate_random_contact() hono.OpenAPIContact {
return hono.OpenAPIContact{
name: random_string(3, 15)
url: random_url()
email: random_email()
}
}
// Generate random OpenAPILicense
fn generate_random_license() hono.OpenAPILicense {
licenses := ['MIT', 'Apache-2.0', 'GPL-3.0', 'BSD-3-Clause']
return hono.OpenAPILicense{
name: licenses[rand.int_in_range(0, licenses.len) or { 0 }]
url: random_url()
}
}
// Generate random OpenAPIInfo
fn generate_random_info() hono.OpenAPIInfo {
return hono.OpenAPIInfo{
title: random_string(5, 20)
version: '${rand.int_in_range(1, 10) or { 1 }}.${rand.int_in_range(0, 10) or { 0 }}.${rand.int_in_range(0, 10) or { 0 }}'
description: random_string(10, 50)
terms_of_service: random_url()
contact: generate_random_contact()
license: generate_random_license()
}
}
// Generate random OpenAPIServer
fn generate_random_server() hono.OpenAPIServer {
return hono.OpenAPIServer{
url: random_url()
description: random_string(5, 20)
}
}
// Generate random OpenAPISchema (simple, non-recursive)
fn generate_random_schema() hono.OpenAPISchema {
schema_type := random_schema_type()
return hono.OpenAPISchema{
schema_type: schema_type
format: if schema_type == 'integer' { 'int64' } else if schema_type == 'number' { 'double' } else { '' }
description: random_string(5, 20)
example: random_string(3, 10)
}
}
// Generate random OpenAPIParameter
fn generate_random_parameter() hono.OpenAPIParameter {
location := random_param_location()
return hono.OpenAPIParameter{
name: random_string(3, 10)
in_location: location
description: random_string(5, 20)
required: location == 'path' || rand.int_in_range(0, 2) or { 0 } == 1
deprecated: rand.int_in_range(0, 10) or { 0 } == 0 // 10% chance
schema: generate_random_schema()
}
}
// Generate random OpenAPIResponse
fn generate_random_response() hono.OpenAPIResponse {
return hono.OpenAPIResponse{
description: random_string(5, 30)
content: {
'application/json': hono.OpenAPIMediaType{
schema: generate_random_schema()
}
}
}
}
// Generate random OpenAPIOperation
fn generate_random_operation() hono.OpenAPIOperation {
num_params := rand.int_in_range(0, 3) or { 0 }
mut params := []hono.OpenAPIParameter{}
for _ in 0 .. num_params {
params << generate_random_parameter()
}
num_tags := rand.int_in_range(0, 3) or { 0 }
mut tags := []string{}
for _ in 0 .. num_tags {
tags << random_string(3, 10)
}
return hono.OpenAPIOperation{
summary: random_string(5, 30)
description: random_string(10, 50)
operation_id: random_string(5, 15)
tags: tags
parameters: params
responses: {
'200': generate_random_response()
'400': hono.OpenAPIResponse{description: 'Bad Request'}
'500': hono.OpenAPIResponse{description: 'Internal Server Error'}
}
deprecated: rand.int_in_range(0, 10) or { 0 } == 0 // 10% chance
}
}
// Generate random OpenAPIPathItem
fn generate_random_path_item() hono.OpenAPIPathItem {
mut path_item := hono.OpenAPIPathItem{
summary: random_string(5, 20)
description: random_string(10, 30)
}
// Randomly add HTTP methods
if rand.int_in_range(0, 2) or { 0 } == 1 {
path_item.get = generate_random_operation()
}
if rand.int_in_range(0, 2) or { 0 } == 1 {
path_item.post = generate_random_operation()
}
if rand.int_in_range(0, 3) or { 0 } == 0 {
path_item.put = generate_random_operation()
}
if rand.int_in_range(0, 3) or { 0 } == 0 {
path_item.delete = generate_random_operation()
}
// Ensure at least one method is set
if path_item.get.responses.len == 0 && path_item.post.responses.len == 0 &&
path_item.put.responses.len == 0 && path_item.delete.responses.len == 0 {
path_item.get = generate_random_operation()
}
return path_item
}
// Generate random OpenAPITag
fn generate_random_tag() hono.OpenAPITag {
return hono.OpenAPITag{
name: random_string(3, 10)
description: random_string(10, 30)
}
}
// Generate random OpenAPIDocument
fn generate_random_document() hono.OpenAPIDocument {
// Generate random number of paths (1-5)
num_paths := rand.int_in_range(1, 6) or { 1 }
mut paths := map[string]hono.OpenAPIPathItem{}
for _ in 0 .. num_paths {
paths[random_path()] = generate_random_path_item()
}
// Generate random number of servers (0-3)
num_servers := rand.int_in_range(0, 4) or { 0 }
mut servers := []hono.OpenAPIServer{}
for _ in 0 .. num_servers {
servers << generate_random_server()
}
// Generate random number of tags (0-5)
num_tags := rand.int_in_range(0, 6) or { 0 }
mut tags := []hono.OpenAPITag{}
for _ in 0 .. num_tags {
tags << generate_random_tag()
}
return hono.OpenAPIDocument{
openapi: random_openapi_version()
info: generate_random_info()
servers: servers
paths: paths
tags: tags
}
}
// Compare two OpenAPIInfo objects
fn compare_info(a hono.OpenAPIInfo, b hono.OpenAPIInfo) bool {
return a.title == b.title &&
a.version == b.version &&
a.description == b.description &&
a.terms_of_service == b.terms_of_service &&
a.contact.name == b.contact.name &&
a.contact.url == b.contact.url &&
a.contact.email == b.contact.email &&
a.license.name == b.license.name &&
a.license.url == b.license.url
}
// Compare two OpenAPISchema objects
fn compare_schema(a hono.OpenAPISchema, b hono.OpenAPISchema) bool {
return a.schema_type == b.schema_type &&
a.format == b.format &&
a.description == b.description &&
a.example == b.example
}
// Compare two OpenAPIParameter objects
fn compare_parameter(a hono.OpenAPIParameter, b hono.OpenAPIParameter) bool {
return a.name == b.name &&
a.in_location == b.in_location &&
a.description == b.description &&
a.required == b.required &&
a.deprecated == b.deprecated &&
compare_schema(a.schema, b.schema)
}
// Compare two OpenAPIOperation objects
fn compare_operation(a hono.OpenAPIOperation, b hono.OpenAPIOperation) bool {
if a.summary != b.summary || a.description != b.description ||
a.operation_id != b.operation_id || a.deprecated != b.deprecated {
return false
}
if a.tags.len != b.tags.len {
return false
}
for i, tag in a.tags {
if tag != b.tags[i] {
return false
}
}
if a.parameters.len != b.parameters.len {
return false
}
for i, param in a.parameters {
if !compare_parameter(param, b.parameters[i]) {
return false
}
}
if a.responses.len != b.responses.len {
return false
}
return true
}
// Compare two OpenAPIPathItem objects
fn compare_path_item(a hono.OpenAPIPathItem, b hono.OpenAPIPathItem) bool {
if a.summary != b.summary || a.description != b.description {
return false
}
// Compare operations (only if they have responses, meaning they're set)
if a.get.responses.len > 0 || b.get.responses.len > 0 {
if !compare_operation(a.get, b.get) {
return false
}
}
if a.post.responses.len > 0 || b.post.responses.len > 0 {
if !compare_operation(a.post, b.post) {
return false
}
}
if a.put.responses.len > 0 || b.put.responses.len > 0 {
if !compare_operation(a.put, b.put) {
return false
}
}
if a.delete.responses.len > 0 || b.delete.responses.len > 0 {
if !compare_operation(a.delete, b.delete) {
return false
}
}
return true
}
// Compare two OpenAPIDocument objects
fn compare_documents(original hono.OpenAPIDocument, restored hono.OpenAPIDocument) bool {
// Compare basic fields
if original.openapi != restored.openapi {
return false
}
// Compare info
if !compare_info(original.info, restored.info) {
return false
}
// Compare servers
if original.servers.len != restored.servers.len {
return false
}
for i, server in original.servers {
if server.url != restored.servers[i].url ||
server.description != restored.servers[i].description {
return false
}
}
// Compare paths
if original.paths.len != restored.paths.len {
return false
}
for path, path_item in original.paths {
if path !in restored.paths {
return false
}
restored_item := restored.paths[path] or { return false }
if !compare_path_item(path_item, restored_item) {
return false
}
}
// Compare tags
if original.tags.len != restored.tags.len {
return false
}
for i, tag in original.tags {
if tag.name != restored.tags[i].name ||
tag.description != restored.tags[i].description {
return false
}
}
return true
}
// Property test: Round-trip serialization
fn test_round_trip_property() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Running Property 1: OpenAPI Document Round-Trip Serialization')
println('Iterations: ${property_test_iterations}')
println('')
for i in 0 .. property_test_iterations {
// Generate random document
original := generate_random_document()
// Serialize to JSON
json_str := original.to_json_str()
// Deserialize back
restored := hono.OpenAPIDocument.from_json_str(json_str) or {
stats.record_fail('Iteration ${i}: Deserialization failed - ${err}')
continue
}
// Compare
if compare_documents(original, restored) {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Documents not equal after round-trip. OpenAPI version: ${original.openapi}, Paths: ${original.paths.len}')
}
// Progress indicator
if (i + 1) % 20 == 0 {
print('.')
}
}
println('')
return stats
}
fn main() {
println('🧪 Property-Based Test: OpenAPI Round-Trip Serialization')
println('=========================================================')
println('')
// Seed random number generator
rand.seed([u32(time.now().unix()), u32(time.now().unix() >> 32)])
stats := test_round_trip_property()
stats.print_summary()
// Exit with appropriate code
if stats.failed_tests > 0 {
exit(1)
}
}
+504
View File
@@ -0,0 +1,504 @@
// test_openapi_serialization.v - 测试 OpenAPI 序列化和反序列化
// **Feature: swagger-ui, Property 1: OpenAPI Document Round-Trip Serialization**
// **Validates: Requirements 3.1, 3.2, 3.4**
module main
import hono
import x.json2
struct TestStats {
mut:
total_tests int
passed_tests int
failed_tests int
}
fn (mut stats TestStats) run_test(test_name string, test_func fn () bool) {
stats.total_tests++
print('🧪 ${test_name}... ')
if test_func() {
stats.passed_tests++
println('✅')
} else {
stats.failed_tests++
println('❌')
}
}
fn (stats TestStats) print_summary() {
println('\n=== OpenAPI 序列化测试总结 ===')
println('总测试数: ${stats.total_tests}')
println('通过: ${stats.passed_tests}')
println('失败: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 所有测试通过!')
} else {
println('⚠️ 有 ${stats.failed_tests} 个测试失败')
}
}
// 测试 1: OpenAPIDocument to_json_str 基本功能
fn test_document_to_json_str() bool {
doc := hono.OpenAPIDocument{
openapi: '3.0.0'
info: hono.OpenAPIInfo{
title: 'Test API'
version: '1.0.0'
}
paths: {
'/users': hono.OpenAPIPathItem{
get: hono.OpenAPIOperation{
summary: 'Get users'
responses: {
'200': hono.OpenAPIResponse{
description: 'Success'
}
}
}
}
}
}
json_str := doc.to_json_str()
has_openapi := json_str.contains('"openapi":"3.0.0"')
has_title := json_str.contains('"title":"Test API"')
has_users := json_str.contains('"/users"')
return has_openapi && has_title && has_users
}
// 测试 2: 字段名映射 (schema_type -> type, in_location -> in)
fn test_field_name_mapping() bool {
param := hono.OpenAPIParameter{
name: 'id'
in_location: 'path'
schema: hono.OpenAPISchema{
schema_type: 'integer'
}
}
json_any := param.to_json()
json_str := json_any.str()
// Should use 'in' not 'in_location', 'type' not 'schema_type'
has_in := json_str.contains('"in":"path"')
has_type := json_str.contains('"type":"integer"')
no_in_location := !json_str.contains('in_location')
no_schema_type := !json_str.contains('schema_type')
return has_in && has_type && no_in_location && no_schema_type
}
// 测试 3: ref 字段映射
fn test_ref_field_mapping() bool {
schema := hono.OpenAPISchema{
ref: '#/components/schemas/User'
}
json_any := schema.to_json()
json_str := json_any.str()
return json_str.contains(r'"$ref":"#/components/schemas/User"')
}
// 测试 4: 可选字段省略 - 空字符串不应出现
fn test_optional_fields_omission_strings() bool {
info := hono.OpenAPIInfo{
title: 'Test'
version: '1.0.0'
// description is empty, should be omitted
}
json_any := info.to_json()
json_str := json_any.str()
no_desc := !json_str.contains('"description"')
has_title := json_str.contains('"title":"Test"')
has_version := json_str.contains('"version":"1.0.0"')
return no_desc && has_title && has_version
}
// 测试 5: 可选字段省略 - 空数组不应出现
fn test_optional_fields_omission_arrays() bool {
doc := hono.OpenAPIDocument{
openapi: '3.0.0'
info: hono.OpenAPIInfo{
title: 'Test'
version: '1.0.0'
}
paths: {}
// servers is empty, should be omitted
// tags is empty, should be omitted
}
json_str := doc.to_json_str()
no_servers := !json_str.contains('"servers"')
no_tags := !json_str.contains('"tags"')
return no_servers && no_tags
}
// 测试 6: 可选字段省略 - 空 map 不应出现
fn test_optional_fields_omission_maps() bool {
doc := hono.OpenAPIDocument{
openapi: '3.0.0'
info: hono.OpenAPIInfo{
title: 'Test'
version: '1.0.0'
}
paths: {}
// components is empty, should be omitted
}
json_str := doc.to_json_str()
return !json_str.contains('"components"')
}
// 测试 7: 布尔值 false 不应出现 (除非必需)
fn test_optional_boolean_omission() bool {
param := hono.OpenAPIParameter{
name: 'test'
in_location: 'query'
required: false // should be omitted
deprecated: false // should be omitted
}
json_any := param.to_json()
json_str := json_any.str()
no_required := !json_str.contains('"required"')
no_deprecated := !json_str.contains('"deprecated"')
return no_required && no_deprecated
}
// 测试 8: 布尔值 true 应该出现
fn test_boolean_true_included() bool {
param := hono.OpenAPIParameter{
name: 'test'
in_location: 'query'
required: true
deprecated: true
}
json_any := param.to_json()
json_str := json_any.str()
has_required := json_str.contains('"required":true')
has_deprecated := json_str.contains('"deprecated":true')
return has_required && has_deprecated
}
// 测试 9: 嵌套结构序列化
fn test_nested_structure_serialization() bool {
doc := hono.OpenAPIDocument{
openapi: '3.0.0'
info: hono.OpenAPIInfo{
title: 'Test API'
version: '1.0.0'
contact: hono.OpenAPIContact{
name: 'Support'
email: 'support@example.com'
}
}
paths: {
'/users': hono.OpenAPIPathItem{
get: hono.OpenAPIOperation{
summary: 'Get users'
parameters: [
hono.OpenAPIParameter{
name: 'limit'
in_location: 'query'
schema: hono.OpenAPISchema{
schema_type: 'integer'
}
}
]
responses: {
'200': hono.OpenAPIResponse{
description: 'Success'
content: {
'application/json': hono.OpenAPIMediaType{
schema: hono.OpenAPISchema{
schema_type: 'array'
}
}
}
}
}
}
}
}
}
json_str := doc.to_json_str()
has_contact := json_str.contains('"contact"')
has_email := json_str.contains('"email":"support@example.com"')
has_params := json_str.contains('"parameters"')
has_content := json_str.contains('"content"')
has_json := json_str.contains('"application/json"')
return has_contact && has_email && has_params && has_content && has_json
}
// 测试 10: to_json_pretty 格式化输出
fn test_to_json_pretty() bool {
doc := hono.OpenAPIDocument{
openapi: '3.0.0'
info: hono.OpenAPIInfo{
title: 'Test'
version: '1.0.0'
}
paths: {}
}
pretty_json := doc.to_json_pretty()
// Pretty JSON should contain newlines and indentation
has_newline := pretty_json.contains('\n')
has_indent := pretty_json.contains(' ')
return has_newline && has_indent
}
// 测试 11: 反序列化基本功能
fn test_from_json_basic() bool {
json_str := '{"openapi":"3.0.0","info":{"title":"Test API","version":"1.0.0"},"paths":{}}'
doc := hono.OpenAPIDocument.from_json_str(json_str) or {
println('Error: ${err}')
return false
}
ok_openapi := doc.openapi == '3.0.0'
ok_title := doc.info.title == 'Test API'
ok_version := doc.info.version == '1.0.0'
return ok_openapi && ok_title && ok_version
}
// 测试 12: 反序列化嵌套结构
fn test_from_json_nested() bool {
json_str := '{"openapi":"3.0.0","info":{"title":"Test","version":"1.0.0","contact":{"name":"Support","email":"test@example.com"}},"paths":{"/users":{"get":{"summary":"Get users","responses":{"200":{"description":"OK"}}}}}}'
doc := hono.OpenAPIDocument.from_json_str(json_str) or {
println('Error: ${err}')
return false
}
ok_contact_name := doc.info.contact.name == 'Support'
ok_contact_email := doc.info.contact.email == 'test@example.com'
ok_has_users := '/users' in doc.paths
users_path := doc.paths['/users'] or { return false }
ok_summary := users_path.get.summary == 'Get users'
return ok_contact_name && ok_contact_email && ok_has_users && ok_summary
}
// 测试 13: 反序列化字段名映射 (in -> in_location, type -> schema_type)
fn test_from_json_field_mapping() bool {
json_str := '{"name":"id","in":"path","schema":{"type":"integer"}}'
parsed := json2.decode[json2.Any](json_str) or {
return false
}
param := hono.OpenAPIParameter.from_json(parsed)
ok_name := param.name == 'id'
ok_in := param.in_location == 'path'
ok_type := param.schema.schema_type == 'integer'
return ok_name && ok_in && ok_type
}
// 测试 14: 往返序列化 - 简单文档
fn test_round_trip_simple() bool {
original := hono.OpenAPIDocument{
openapi: '3.0.0'
info: hono.OpenAPIInfo{
title: 'Test API'
version: '1.0.0'
description: 'A test API'
}
paths: {
'/health': hono.OpenAPIPathItem{
get: hono.OpenAPIOperation{
summary: 'Health check'
responses: {
'200': hono.OpenAPIResponse{
description: 'OK'
}
}
}
}
}
}
// Serialize
json_str := original.to_json_str()
// Deserialize
restored := hono.OpenAPIDocument.from_json_str(json_str) or {
println('Error: ${err}')
return false
}
// Compare
ok_openapi := restored.openapi == original.openapi
ok_title := restored.info.title == original.info.title
ok_version := restored.info.version == original.info.version
ok_desc := restored.info.description == original.info.description
ok_has_health := '/health' in restored.paths
restored_health := restored.paths['/health'] or { return false }
original_health := original.paths['/health'] or { return false }
ok_summary := restored_health.get.summary == original_health.get.summary
return ok_openapi && ok_title && ok_version && ok_desc && ok_has_health && ok_summary
}
// 测试 15: 往返序列化 - 复杂文档
fn test_round_trip_complex() bool {
original := hono.OpenAPIDocument{
openapi: '3.1.0'
info: hono.OpenAPIInfo{
title: 'Complex API'
version: '2.0.0'
description: 'A complex test API'
contact: hono.OpenAPIContact{
name: 'API Support'
email: 'support@example.com'
url: 'https://example.com/support'
}
license: hono.OpenAPILicense{
name: 'MIT'
url: 'https://opensource.org/licenses/MIT'
}
}
servers: [
hono.OpenAPIServer{
url: 'https://api.example.com'
description: 'Production'
}
]
paths: {
'/users': hono.OpenAPIPathItem{
get: hono.OpenAPIOperation{
summary: 'List users'
operation_id: 'listUsers'
tags: ['users']
parameters: [
hono.OpenAPIParameter{
name: 'limit'
in_location: 'query'
required: false
schema: hono.OpenAPISchema{
schema_type: 'integer'
minimum: 1
maximum: 100
}
}
]
responses: {
'200': hono.OpenAPIResponse{
description: 'Success'
content: {
'application/json': hono.OpenAPIMediaType{
schema: hono.OpenAPISchema{
schema_type: 'array'
}
}
}
}
}
}
post: hono.OpenAPIOperation{
summary: 'Create user'
operation_id: 'createUser'
tags: ['users']
request_body: hono.OpenAPIRequestBody{
required: true
content: {
'application/json': hono.OpenAPIMediaType{
schema: hono.OpenAPISchema{
schema_type: 'object'
required: ['name', 'email']
properties: {
'name': hono.OpenAPISchema{
schema_type: 'string'
}
'email': hono.OpenAPISchema{
schema_type: 'string'
format: 'email'
}
}
}
}
}
}
responses: {
'201': hono.OpenAPIResponse{
description: 'Created'
}
}
}
}
}
tags: [
hono.OpenAPITag{
name: 'users'
description: 'User operations'
}
]
}
// Serialize
json_str := original.to_json_str()
// Deserialize
restored := hono.OpenAPIDocument.from_json_str(json_str) or {
println('Error: ${err}')
return false
}
// Compare key fields
ok_openapi := restored.openapi == original.openapi
ok_title := restored.info.title == original.info.title
ok_contact := restored.info.contact.name == original.info.contact.name
ok_license := restored.info.license.name == original.info.license.name
ok_servers_len := restored.servers.len == original.servers.len
ok_server_url := restored.servers[0].url == original.servers[0].url
ok_has_users := '/users' in restored.paths
restored_users := restored.paths['/users'] or { return false }
ok_get_op := restored_users.get.operation_id == 'listUsers'
ok_post_op := restored_users.post.operation_id == 'createUser'
ok_tags_len := restored.tags.len == original.tags.len
ok_tag_name := restored.tags[0].name == 'users'
return ok_openapi && ok_title && ok_contact && ok_license && ok_servers_len && ok_server_url && ok_has_users && ok_get_op && ok_post_op && ok_tags_len && ok_tag_name
}
fn main() {
println('🚀 开始 OpenAPI 序列化测试...\n')
mut stats := TestStats{}
// 序列化测试
stats.run_test('to_json_str 基本功能', test_document_to_json_str)
stats.run_test('字段名映射 (schema_type->type, in_location->in)', test_field_name_mapping)
stats.run_test('ref 字段映射', test_ref_field_mapping)
stats.run_test('可选字段省略 - 空字符串', test_optional_fields_omission_strings)
stats.run_test('可选字段省略 - 空数组', test_optional_fields_omission_arrays)
stats.run_test('可选字段省略 - 空 map', test_optional_fields_omission_maps)
stats.run_test('布尔值 false 省略', test_optional_boolean_omission)
stats.run_test('布尔值 true 包含', test_boolean_true_included)
stats.run_test('嵌套结构序列化', test_nested_structure_serialization)
stats.run_test('to_json_pretty 格式化', test_to_json_pretty)
// 反序列化测试
stats.run_test('from_json 基本功能', test_from_json_basic)
stats.run_test('from_json 嵌套结构', test_from_json_nested)
stats.run_test('from_json 字段名映射', test_from_json_field_mapping)
// 往返测试
stats.run_test('往返序列化 - 简单文档', test_round_trip_simple)
stats.run_test('往返序列化 - 复杂文档', test_round_trip_complex)
// 打印测试总结
stats.print_summary()
}
+354
View File
@@ -0,0 +1,354 @@
// test_openapi_structures.v - 测试 OpenAPI 数据结构
import meiseayoung.hono
// OpenAPI 数据结构测试
// 测试 OpenAPI 3.0/3.1 规范的数据结构定义
struct TestStats {
mut:
total_tests int
passed_tests int
failed_tests int
}
fn (mut stats TestStats) run_test(test_name string, test_func fn () bool) {
stats.total_tests++
print('🧪 ${test_name}... ')
if test_func() {
stats.passed_tests++
println('✅')
} else {
stats.failed_tests++
println('❌')
}
}
fn (stats TestStats) print_summary() {
println('\n=== OpenAPI 数据结构测试总结 ===')
println('总测试数: ${stats.total_tests}')
println('通过: ${stats.passed_tests}')
println('失败: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 所有测试通过!')
} else {
println('⚠️ 有 ${stats.failed_tests} 个测试失败')
}
}
// 测试 1: OpenAPIContact 结构体
fn test_openapi_contact() bool {
contact := hono.OpenAPIContact{
name: 'API Support'
url: 'https://example.com/support'
email: 'support@example.com'
}
return contact.name == 'API Support' &&
contact.url == 'https://example.com/support' &&
contact.email == 'support@example.com'
}
// 测试 2: OpenAPILicense 结构体
fn test_openapi_license() bool {
license := hono.OpenAPILicense{
name: 'MIT'
url: 'https://opensource.org/licenses/MIT'
}
return license.name == 'MIT' &&
license.url == 'https://opensource.org/licenses/MIT'
}
// 测试 3: OpenAPIInfo 结构体
fn test_openapi_info() bool {
info := hono.OpenAPIInfo{
title: 'My API'
version: '1.0.0'
description: 'A sample API'
terms_of_service: 'https://example.com/tos'
contact: hono.OpenAPIContact{
name: 'Support'
}
license: hono.OpenAPILicense{
name: 'MIT'
}
}
return info.title == 'My API' &&
info.version == '1.0.0' &&
info.description == 'A sample API'
}
// 测试 4: OpenAPIServer 结构体
fn test_openapi_server() bool {
server := hono.OpenAPIServer{
url: 'https://api.example.com'
description: 'Production server'
}
return server.url == 'https://api.example.com' &&
server.description == 'Production server'
}
// 测试 5: OpenAPITag 结构体
fn test_openapi_tag() bool {
tag := hono.OpenAPITag{
name: 'users'
description: 'User operations'
external_docs: hono.OpenAPIExternalDocs{
url: 'https://docs.example.com/users'
description: 'User documentation'
}
}
return tag.name == 'users' &&
tag.description == 'User operations'
}
// 测试 6: OpenAPIParameter 结构体
fn test_openapi_parameter() bool {
param := hono.OpenAPIParameter{
name: 'id'
in_location: 'path'
description: 'User ID'
required: true
deprecated: false
schema: hono.OpenAPISchema{
schema_type: 'integer'
format: 'int64'
}
}
return param.name == 'id' &&
param.in_location == 'path' &&
param.required == true
}
// 测试 7: OpenAPIResponse 结构体
fn test_openapi_response() bool {
response := hono.OpenAPIResponse{
description: 'Successful response'
content: {
'application/json': hono.OpenAPIMediaType{
schema: hono.OpenAPISchema{
schema_type: 'object'
}
}
}
}
return response.description == 'Successful response' &&
'application/json' in response.content
}
// 测试 8: OpenAPIOperation 结构体
fn test_openapi_operation() bool {
op := hono.OpenAPIOperation{
summary: 'Get user'
description: 'Get user by ID'
operation_id: 'getUser'
tags: ['users']
responses: {
'200': hono.OpenAPIResponse{
description: 'Success'
}
}
}
return op.summary == 'Get user' &&
op.operation_id == 'getUser' &&
'users' in op.tags
}
// 测试 9: OpenAPIPathItem 结构体
fn test_openapi_path_item() bool {
path_item := hono.OpenAPIPathItem{
summary: 'User operations'
get: hono.OpenAPIOperation{
summary: 'Get user'
responses: {
'200': hono.OpenAPIResponse{
description: 'Success'
}
}
}
post: hono.OpenAPIOperation{
summary: 'Create user'
responses: {
'201': hono.OpenAPIResponse{
description: 'Created'
}
}
}
}
return path_item.summary == 'User operations' &&
path_item.get.summary == 'Get user' &&
path_item.post.summary == 'Create user'
}
// 测试 10: OpenAPISchema 结构体
fn test_openapi_schema() bool {
schema := hono.OpenAPISchema{
schema_type: 'object'
title: 'User'
description: 'User object'
required: ['id', 'name']
properties: {
'id': hono.OpenAPISchema{
schema_type: 'integer'
format: 'int64'
}
'name': hono.OpenAPISchema{
schema_type: 'string'
}
}
}
return schema.schema_type == 'object' &&
schema.title == 'User' &&
'id' in schema.required &&
'name' in schema.properties
}
// 测试 11: OpenAPISecurityScheme 结构体
fn test_openapi_security_scheme() bool {
scheme := hono.OpenAPISecurityScheme{
scheme_type: 'http'
description: 'Bearer token authentication'
scheme: 'bearer'
bearer_format: 'JWT'
}
return scheme.scheme_type == 'http' &&
scheme.scheme == 'bearer' &&
scheme.bearer_format == 'JWT'
}
// 测试 12: OpenAPIComponents 结构体
fn test_openapi_components() bool {
components := hono.OpenAPIComponents{
schemas: {
'User': hono.OpenAPISchema{
schema_type: 'object'
}
}
security_schemes: {
'bearerAuth': hono.OpenAPISecurityScheme{
scheme_type: 'http'
scheme: 'bearer'
}
}
}
return 'User' in components.schemas &&
'bearerAuth' in components.security_schemes
}
// 测试 13: OpenAPIDocument 结构体
fn test_openapi_document() bool {
doc := hono.OpenAPIDocument{
openapi: '3.0.0'
info: hono.OpenAPIInfo{
title: 'My API'
version: '1.0.0'
}
servers: [
hono.OpenAPIServer{
url: 'https://api.example.com'
}
]
paths: {
'/users': hono.OpenAPIPathItem{
get: hono.OpenAPIOperation{
summary: 'List users'
responses: {
'200': hono.OpenAPIResponse{
description: 'Success'
}
}
}
}
}
tags: [
hono.OpenAPITag{
name: 'users'
}
]
}
return doc.openapi == '3.0.0' &&
doc.info.title == 'My API' &&
'/users' in doc.paths
}
// 测试 14: 参数位置支持 (path, query, header, cookie)
fn test_parameter_locations() bool {
locations := ['path', 'query', 'header', 'cookie']
for loc in locations {
param := hono.OpenAPIParameter{
name: 'test'
in_location: loc
}
if param.in_location != loc {
return false
}
}
return true
}
// 测试 15: HTTP 方法支持
fn test_http_methods() bool {
path_item := hono.OpenAPIPathItem{
get: hono.OpenAPIOperation{
summary: 'GET'
responses: {'200': hono.OpenAPIResponse{description: 'OK'}}
}
post: hono.OpenAPIOperation{
summary: 'POST'
responses: {'201': hono.OpenAPIResponse{description: 'Created'}}
}
put: hono.OpenAPIOperation{
summary: 'PUT'
responses: {'200': hono.OpenAPIResponse{description: 'OK'}}
}
delete: hono.OpenAPIOperation{
summary: 'DELETE'
responses: {'204': hono.OpenAPIResponse{description: 'No Content'}}
}
patch: hono.OpenAPIOperation{
summary: 'PATCH'
responses: {'200': hono.OpenAPIResponse{description: 'OK'}}
}
head: hono.OpenAPIOperation{
summary: 'HEAD'
responses: {'200': hono.OpenAPIResponse{description: 'OK'}}
}
options: hono.OpenAPIOperation{
summary: 'OPTIONS'
responses: {'200': hono.OpenAPIResponse{description: 'OK'}}
}
}
return path_item.get.summary == 'GET' &&
path_item.post.summary == 'POST' &&
path_item.put.summary == 'PUT' &&
path_item.delete.summary == 'DELETE' &&
path_item.patch.summary == 'PATCH' &&
path_item.head.summary == 'HEAD' &&
path_item.options.summary == 'OPTIONS'
}
fn main() {
println('🚀 开始 OpenAPI 数据结构测试...\n')
mut stats := TestStats{}
// 运行所有测试
stats.run_test('OpenAPIContact 结构体', test_openapi_contact)
stats.run_test('OpenAPILicense 结构体', test_openapi_license)
stats.run_test('OpenAPIInfo 结构体', test_openapi_info)
stats.run_test('OpenAPIServer 结构体', test_openapi_server)
stats.run_test('OpenAPITag 结构体', test_openapi_tag)
stats.run_test('OpenAPIParameter 结构体', test_openapi_parameter)
stats.run_test('OpenAPIResponse 结构体', test_openapi_response)
stats.run_test('OpenAPIOperation 结构体', test_openapi_operation)
stats.run_test('OpenAPIPathItem 结构体', test_openapi_path_item)
stats.run_test('OpenAPISchema 结构体', test_openapi_schema)
stats.run_test('OpenAPISecurityScheme 结构体', test_openapi_security_scheme)
stats.run_test('OpenAPIComponents 结构体', test_openapi_components)
stats.run_test('OpenAPIDocument 结构体', test_openapi_document)
stats.run_test('参数位置支持', test_parameter_locations)
stats.run_test('HTTP 方法支持', test_http_methods)
// 打印测试总结
stats.print_summary()
}
+338
View File
@@ -0,0 +1,338 @@
// test_route_extraction_property.v - Property test for route extraction completeness
// **Feature: swagger-ui, Property 6: Route Extraction Completeness**
// **Validates: Requirements 5.1, 5.2, 5.3**
module main
import hono
import net.http
import rand
// Test data generators
fn generate_random_path() string {
paths := [
'/',
'/users',
'/users/:id',
'/api/v1/items',
'/api/v1/items/:itemId',
'/posts/:postId/comments/:commentId',
'/products/:productId/reviews',
'/admin/settings',
'/health',
'/api/users/:userId/orders/:orderId/items',
]
return paths[rand.intn(paths.len) or { 0 }]
}
fn generate_random_method() string {
methods := ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']
return methods[rand.intn(methods.len) or { 0 }]
}
// Simple handler for testing
fn test_handler(mut c hono.Context) http.Response {
return c.text('OK')
}
// Property 6: Route Extraction Completeness
// *For any* Hono application with registered routes (including sub-applications),
// calling get_routes() SHALL return all routes with correct paths, methods, and path parameters.
fn test_property_route_extraction_completeness() {
println('Property 6: Route Extraction Completeness')
println('Testing that get_routes() returns all registered routes with correct information...')
// Test 1: Basic routes extraction
{
mut app := hono.Hono.new()
// Register routes with different methods
app.get('/users', test_handler)
app.post('/users', test_handler)
app.get('/users/:id', test_handler)
app.put('/users/:id', test_handler)
app.delete('/users/:id', test_handler)
routes := app.get_routes()
// Verify all routes are extracted
assert routes.len == 5, 'Expected 5 routes, got ${routes.len}'
// Verify methods are correct
mut methods := map[string]int{}
for route in routes {
methods[route.method]++
}
assert methods['GET'] == 2, 'Expected 2 GET routes'
assert methods['POST'] == 1, 'Expected 1 POST route'
assert methods['PUT'] == 1, 'Expected 1 PUT route'
assert methods['DELETE'] == 1, 'Expected 1 DELETE route'
println(' ✅ Basic routes extraction: PASSED')
}
// Test 2: Path parameters extraction
{
mut app := hono.Hono.new()
app.get('/users/:userId', test_handler)
app.get('/posts/:postId/comments/:commentId', test_handler)
app.get('/api/v1/items/:itemId/reviews/:reviewId', test_handler)
routes := app.get_routes()
// Find routes and verify path parameters
for route in routes {
if route.path == '/users/:userId' {
assert route.path_params.len == 1, 'Expected 1 path param for /users/:userId'
assert 'userId' in route.path_params, 'Expected userId in path params'
} else if route.path == '/posts/:postId/comments/:commentId' {
assert route.path_params.len == 2, 'Expected 2 path params for /posts/:postId/comments/:commentId'
assert 'postId' in route.path_params, 'Expected postId in path params'
assert 'commentId' in route.path_params, 'Expected commentId in path params'
} else if route.path == '/api/v1/items/:itemId/reviews/:reviewId' {
assert route.path_params.len == 2, 'Expected 2 path params'
assert 'itemId' in route.path_params, 'Expected itemId in path params'
assert 'reviewId' in route.path_params, 'Expected reviewId in path params'
}
}
println(' ✅ Path parameters extraction: PASSED')
}
// Test 3: All HTTP methods support
{
mut app := hono.Hono.new()
app.get('/test', test_handler)
app.post('/test', test_handler)
app.put('/test', test_handler)
app.delete('/test', test_handler)
app.patch('/test', test_handler)
app.head('/test', test_handler)
app.options('/test', test_handler)
routes := app.get_routes()
// Verify all 7 HTTP methods are present
mut found_methods := map[string]bool{}
for route in routes {
found_methods[route.method] = true
}
assert found_methods['GET'], 'GET method not found'
assert found_methods['POST'], 'POST method not found'
assert found_methods['PUT'], 'PUT method not found'
assert found_methods['DELETE'], 'DELETE method not found'
assert found_methods['PATCH'], 'PATCH method not found'
assert found_methods['HEAD'], 'HEAD method not found'
assert found_methods['OPTIONS'], 'OPTIONS method not found'
println(' ✅ All HTTP methods support: PASSED')
}
// Test 4: Sub-application routes extraction
{
mut app := hono.Hono.new()
mut subapp := hono.Hono.new()
// Register routes in main app
app.get('/main', test_handler)
// Register routes in sub-app
subapp.get('/items', test_handler)
subapp.get('/items/:id', test_handler)
subapp.post('/items', test_handler)
// Mount sub-app
app.route('/api', mut subapp)
routes := app.get_routes()
// Verify main app route
mut found_main := false
for route in routes {
if route.path == '/main' && route.method == 'GET' {
found_main = true
break
}
}
assert found_main, 'Main app route /main not found'
// Verify sub-app routes are included with prefix
mut found_api_items := false
mut found_api_items_id := false
mut found_api_items_post := false
for route in routes {
if route.path == '/api/items' && route.method == 'GET' {
found_api_items = true
}
if route.path == '/api/items/:id' && route.method == 'GET' {
found_api_items_id = true
// Verify path params are extracted correctly
assert 'id' in route.path_params, 'Expected id in path params for /api/items/:id'
}
if route.path == '/api/items' && route.method == 'POST' {
found_api_items_post = true
}
}
assert found_api_items, 'Sub-app route /api/items GET not found'
assert found_api_items_id, 'Sub-app route /api/items/:id GET not found'
assert found_api_items_post, 'Sub-app route /api/items POST not found'
println(' ✅ Sub-application routes extraction: PASSED')
}
// Test 5: Empty path parameters for static routes
{
mut app := hono.Hono.new()
app.get('/static/path', test_handler)
app.get('/another/static', test_handler)
routes := app.get_routes()
for route in routes {
assert route.path_params.len == 0, 'Static route should have no path params'
}
println(' ✅ Static routes have no path params: PASSED')
}
// Test 6: Route handler is preserved
{
mut app := hono.Hono.new()
app.get('/test', test_handler)
routes := app.get_routes()
assert routes.len == 1, 'Expected 1 route'
// Handler should not be nil (we can't directly compare functions, but we can check it exists)
// The handler field is of type IHandler which is an interface
println(' ✅ Route handler preservation: PASSED')
}
println('\n✅ Property 6: Route Extraction Completeness - ALL TESTS PASSED')
}
// Expected route structure for testing
struct ExpectedRoute {
path string
method string
}
// Property test with randomized inputs
fn test_property_route_extraction_randomized() {
println('\nProperty 6 (Randomized): Route Extraction with Random Inputs')
iterations := 100
mut passed := 0
for i in 0 .. iterations {
mut app := hono.Hono.new()
// Generate random number of routes (1-10)
num_routes := rand.intn(10) or { 1 } + 1
mut expected_routes := []ExpectedRoute{}
for _ in 0 .. num_routes {
path := generate_random_path()
method := generate_random_method()
// Register route based on method
match method {
'GET' { app.get(path, test_handler) }
'POST' { app.post(path, test_handler) }
'PUT' { app.put(path, test_handler) }
'DELETE' { app.delete(path, test_handler) }
'PATCH' { app.patch(path, test_handler) }
'HEAD' { app.head(path, test_handler) }
'OPTIONS' { app.options(path, test_handler) }
else {}
}
expected_routes << ExpectedRoute{
path: path
method: method
}
}
// Get routes and verify
routes := app.get_routes()
// Verify count matches
if routes.len != expected_routes.len {
println(' ❌ Iteration ${i}: Expected ${expected_routes.len} routes, got ${routes.len}')
continue
}
// Verify all expected routes are present
mut all_found := true
for expected in expected_routes {
mut found := false
for route in routes {
if route.path == expected.path && route.method == expected.method {
found = true
// Verify path params are correctly extracted
expected_params := extract_expected_params(expected.path)
if route.path_params.len != expected_params.len {
all_found = false
break
}
for param in expected_params {
if param !in route.path_params {
all_found = false
break
}
}
break
}
}
if !found {
all_found = false
break
}
}
if all_found {
passed++
}
}
success_rate := f64(passed) / f64(iterations) * 100.0
println(' Passed: ${passed}/${iterations} (${success_rate:.1}%)')
// Allow some tolerance for edge cases
assert passed >= iterations * 95 / 100, 'Property test failed: less than 95% success rate'
println('✅ Property 6 (Randomized): PASSED')
}
// Helper function to extract expected path parameters from a path
fn extract_expected_params(path string) []string {
mut params := []string{}
segments := path.split('/')
for segment in segments {
if segment.starts_with(':') && segment.len > 1 {
params << segment[1..]
}
}
return params
}
fn main() {
println('=' .repeat(70))
println('Property-Based Test: Route Extraction Completeness')
println('=' .repeat(70))
test_property_route_extraction_completeness()
test_property_route_extraction_randomized()
println('\n' + '=' .repeat(70))
println('All Property Tests Completed Successfully!')
println('=' .repeat(70))
}
+454
View File
@@ -0,0 +1,454 @@
// test_swagger_ui_options_property.v - Property-Based Test for Swagger UI Options
// **Feature: swagger-ui, Property 2: Swagger UI Options Appear in Generated HTML**
// **Validates: Requirements 1.5, 4.2, 4.3, 4.5**
//
// Property: For any SwaggerUIOptions configuration, all non-default option values
// SHALL appear in the generated HTML output in the appropriate locations
// (title in <title> tag, url in SwaggerUIBundle config, etc.).
module main
import hono
import rand
import time
const property_test_iterations = 100
struct PropertyTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
failed_examples []string
}
fn (mut stats PropertyTestStats) record_pass() {
stats.total_tests++
stats.passed_tests++
}
fn (mut stats PropertyTestStats) record_fail(example string) {
stats.total_tests++
stats.failed_tests++
stats.failed_examples << example
}
fn (stats PropertyTestStats) print_summary() {
println('\n=== Property Test Summary ===')
println('Total iterations: ${stats.total_tests}')
println('Passed: ${stats.passed_tests}')
println('Failed: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 Property test PASSED!')
} else {
println('❌ Property test FAILED!')
println('\nFailing examples:')
for i, example in stats.failed_examples {
if i >= 3 {
println('... and ${stats.failed_examples.len - 3} more')
break
}
println(' ${example}')
}
}
}
// Random string generator for safe HTML content
fn random_string(min_len int, max_len int) string {
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
len := rand.int_in_range(min_len, max_len + 1) or { min_len }
mut result := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
result += chars[idx..idx + 1]
}
return result
}
// Random URL path generator
fn random_url_path() string {
return '/' + random_string(3, 10)
}
// Random doc expansion value
fn random_doc_expansion() string {
values := ['list', 'full', 'none']
idx := rand.int_in_range(0, values.len) or { 0 }
return values[idx]
}
// Random integer in range
fn random_int(min int, max int) int {
return rand.int_in_range(min, max + 1) or { min }
}
// Generate random SwaggerUIOptions
fn generate_random_options() hono.SwaggerUIOptions {
return hono.SwaggerUIOptions{
url: random_url_path()
title: random_string(5, 20)
deep_linking: rand.int_in_range(0, 2) or { 0 } == 1
display_request_duration: rand.int_in_range(0, 2) or { 0 } == 1
default_models_expand_depth: random_int(-1, 5)
doc_expansion: random_doc_expansion()
filter: rand.int_in_range(0, 2) or { 0 } == 1
show_extensions: rand.int_in_range(0, 2) or { 0 } == 1
show_common_extensions: rand.int_in_range(0, 2) or { 0 } == 1
try_it_out_enabled: rand.int_in_range(0, 2) or { 0 } == 1
}
}
// Test: Title appears in HTML <title> tag
fn test_title_in_html(options hono.SwaggerUIOptions, html string) bool {
expected := '<title>${options.title}</title>'
return html.contains(expected)
}
// Test: URL appears in SwaggerUIBundle config
fn test_url_in_config(options hono.SwaggerUIOptions, html string) bool {
expected := 'url: "${options.url}"'
return html.contains(expected)
}
// Test: deepLinking option appears correctly
fn test_deep_linking_in_config(options hono.SwaggerUIOptions, html string) bool {
expected_value := if options.deep_linking { 'true' } else { 'false' }
expected := 'deepLinking: ${expected_value}'
return html.contains(expected)
}
// Test: displayRequestDuration option appears correctly
fn test_display_request_duration_in_config(options hono.SwaggerUIOptions, html string) bool {
expected_value := if options.display_request_duration { 'true' } else { 'false' }
expected := 'displayRequestDuration: ${expected_value}'
return html.contains(expected)
}
// Test: defaultModelsExpandDepth option appears correctly
fn test_default_models_expand_depth_in_config(options hono.SwaggerUIOptions, html string) bool {
expected := 'defaultModelsExpandDepth: ${options.default_models_expand_depth}'
return html.contains(expected)
}
// Test: docExpansion option appears correctly
fn test_doc_expansion_in_config(options hono.SwaggerUIOptions, html string) bool {
expected := 'docExpansion: "${options.doc_expansion}"'
return html.contains(expected)
}
// Test: filter option appears correctly
fn test_filter_in_config(options hono.SwaggerUIOptions, html string) bool {
expected_value := if options.filter { 'true' } else { 'false' }
expected := 'filter: ${expected_value}'
return html.contains(expected)
}
// Test: showExtensions option appears correctly
fn test_show_extensions_in_config(options hono.SwaggerUIOptions, html string) bool {
expected_value := if options.show_extensions { 'true' } else { 'false' }
expected := 'showExtensions: ${expected_value}'
return html.contains(expected)
}
// Test: showCommonExtensions option appears correctly
fn test_show_common_extensions_in_config(options hono.SwaggerUIOptions, html string) bool {
expected_value := if options.show_common_extensions { 'true' } else { 'false' }
expected := 'showCommonExtensions: ${expected_value}'
return html.contains(expected)
}
// Test: tryItOutEnabled option appears correctly
fn test_try_it_out_enabled_in_config(options hono.SwaggerUIOptions, html string) bool {
expected_value := if options.try_it_out_enabled { 'true' } else { 'false' }
expected := 'tryItOutEnabled: ${expected_value}'
return html.contains(expected)
}
// Test: HTML is valid HTML5 document
fn test_valid_html5_document(html string) bool {
has_doctype := html.contains('<!DOCTYPE html>')
has_html_tag := html.contains('<html') && html.contains('</html>')
has_head := html.contains('<head>') && html.contains('</head>')
has_body := html.contains('<body>') && html.contains('</body>')
has_charset := html.contains('charset="UTF-8"') || html.contains("charset='UTF-8'")
return has_doctype && has_html_tag && has_head && has_body && has_charset
}
// Test: Swagger UI CDN resources are included
fn test_cdn_resources_included(html string) bool {
has_css := html.contains('https://unpkg.com/swagger-ui-dist@5/swagger-ui.css')
has_bundle_js := html.contains('https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js')
has_standalone_js := html.contains('https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js')
return has_css && has_bundle_js && has_standalone_js
}
// Test: Custom CSS URL appears when provided
fn test_custom_css_url_included() bool {
custom_url := 'https://example.com/custom.css'
options := hono.SwaggerUIOptions{
url: '/doc'
title: 'Test API'
custom_css_url: custom_url
}
handler := hono.swagger_ui(options)
mut ctx := hono.Context{}
response := handler(mut ctx)
html := response.body
return html.contains('href="${custom_url}"')
}
// Test: Custom JS URL appears when provided
fn test_custom_js_url_included() bool {
custom_url := 'https://example.com/custom.js'
options := hono.SwaggerUIOptions{
url: '/doc'
title: 'Test API'
custom_js_url: custom_url
}
handler := hono.swagger_ui(options)
mut ctx := hono.Context{}
response := handler(mut ctx)
html := response.body
return html.contains('src="${custom_url}"')
}
// Test: Custom CSS inline appears when provided
fn test_custom_css_inline_included() bool {
custom_css := '.swagger-ui { background: red; }'
options := hono.SwaggerUIOptions{
url: '/doc'
title: 'Test API'
custom_css: custom_css
}
handler := hono.swagger_ui(options)
mut ctx := hono.Context{}
response := handler(mut ctx)
html := response.body
return html.contains(custom_css)
}
// Test: Custom JS inline appears when provided
fn test_custom_js_inline_included() bool {
custom_js := 'console.log("Custom JS loaded");'
options := hono.SwaggerUIOptions{
url: '/doc'
title: 'Test API'
custom_js: custom_js
}
handler := hono.swagger_ui(options)
mut ctx := hono.Context{}
response := handler(mut ctx)
html := response.body
return html.contains(custom_js)
}
// Test: Response has correct Content-Type
fn test_response_content_type() bool {
options := hono.SwaggerUIOptions{
url: '/doc'
title: 'Test API'
}
handler := hono.swagger_ui(options)
mut ctx := hono.Context{}
response := handler(mut ctx)
content_type := response.header.get(.content_type) or { '' }
return content_type.contains('text/html')
}
// Test: Response status code is 200
fn test_response_status_code() bool {
options := hono.SwaggerUIOptions{
url: '/doc'
title: 'Test API'
}
handler := hono.swagger_ui(options)
mut ctx := hono.Context{}
response := handler(mut ctx)
return response.status_code == 200
}
// Property test: Swagger UI Options appear in generated HTML
fn test_swagger_ui_options_property() PropertyTestStats {
mut stats := PropertyTestStats{}
println('Running Property 2: Swagger UI Options Appear in Generated HTML')
println('Iterations: ${property_test_iterations}')
println('')
for i in 0 .. property_test_iterations {
// Generate random options
options := generate_random_options()
// Generate HTML using the swagger_ui handler
handler := hono.swagger_ui(options)
mut ctx := hono.Context{}
response := handler(mut ctx)
html := response.body
// Test 1: Title in HTML
if test_title_in_html(options, html) {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Title "${options.title}" not found in HTML')
}
// Test 2: URL in config
if test_url_in_config(options, html) {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: URL "${options.url}" not found in config')
}
// Test 3: deepLinking
if test_deep_linking_in_config(options, html) {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: deepLinking=${options.deep_linking} not found')
}
// Test 4: displayRequestDuration
if test_display_request_duration_in_config(options, html) {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: displayRequestDuration=${options.display_request_duration} not found')
}
// Test 5: defaultModelsExpandDepth
if test_default_models_expand_depth_in_config(options, html) {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: defaultModelsExpandDepth=${options.default_models_expand_depth} not found')
}
// Test 6: docExpansion
if test_doc_expansion_in_config(options, html) {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: docExpansion="${options.doc_expansion}" not found')
}
// Test 7: filter
if test_filter_in_config(options, html) {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: filter=${options.filter} not found')
}
// Test 8: showExtensions
if test_show_extensions_in_config(options, html) {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: showExtensions=${options.show_extensions} not found')
}
// Test 9: showCommonExtensions
if test_show_common_extensions_in_config(options, html) {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: showCommonExtensions=${options.show_common_extensions} not found')
}
// Test 10: tryItOutEnabled
if test_try_it_out_enabled_in_config(options, html) {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: tryItOutEnabled=${options.try_it_out_enabled} not found')
}
// Test 11: Valid HTML5 document
if test_valid_html5_document(html) {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: Generated HTML is not a valid HTML5 document')
}
// Test 12: CDN resources included
if test_cdn_resources_included(html) {
stats.record_pass()
} else {
stats.record_fail('Iteration ${i}: CDN resources not included')
}
// Progress indicator
if (i + 1) % 20 == 0 {
print('.')
}
}
println('')
// Additional specific tests (run once)
println('\nRunning additional specific tests...')
// Test custom CSS URL
if test_custom_css_url_included() {
stats.record_pass()
} else {
stats.record_fail('Custom CSS URL not included when provided')
}
// Test custom JS URL
if test_custom_js_url_included() {
stats.record_pass()
} else {
stats.record_fail('Custom JS URL not included when provided')
}
// Test custom CSS inline
if test_custom_css_inline_included() {
stats.record_pass()
} else {
stats.record_fail('Custom CSS inline not included when provided')
}
// Test custom JS inline
if test_custom_js_inline_included() {
stats.record_pass()
} else {
stats.record_fail('Custom JS inline not included when provided')
}
// Test response Content-Type
if test_response_content_type() {
stats.record_pass()
} else {
stats.record_fail('Response Content-Type is not text/html')
}
// Test response status code
if test_response_status_code() {
stats.record_pass()
} else {
stats.record_fail('Response status code is not 200')
}
return stats
}
fn main() {
println('🧪 Property-Based Test: Swagger UI Options Appear in Generated HTML')
println('====================================================================')
println('')
// Seed random number generator
rand.seed([u32(time.now().unix()), u32(time.now().unix() >> 32)])
stats := test_swagger_ui_options_property()
stats.print_summary()
// Exit with appropriate code
if stats.failed_tests > 0 {
exit(1)
}
}
+122 -18
View File
@@ -58,27 +58,28 @@ fn main() {
## Build Command ## Build Command
```bash ```bash
v -enable-globals -cc gcc -ldflags "-ldbghelp" your_app.v -o app.exe # macOS / Linux
v -enable-globals -prod -o app your_app.v
./app
# Windows (must use gcc, tcc does not support .a static library format)
v -enable-globals -cc gcc -ldflags "-ldbghelp" -o app.exe your_app.v
.\app.exe
``` ```
**Required flags:** **Required flags:**
- `-enable-globals` - Required for uSockets global state - `-enable-globals` - Required for uSockets global state
- `-cc gcc` - Use GCC compiler (MinGW-w64 on Windows) - `-cc gcc` - Use GCC compiler (MinGW-w64 on Windows, required because V's default compiler tcc does not support MinGW `.a` static library format)
- `-ldflags "-ldbghelp"` - Fix Windows linking (Windows only) - `-ldflags "-ldbghelp"` - Required on Windows for libuv linking
## Prerequisites (Windows) ## Pre-compiled Libraries
1. **GCC (MinGW-w64)** The uSockets library and libuv are pre-compiled and included in the `lib/{platform}/` directory:
```powershell
scoop install mingw
# or
choco install mingw
```
2. **V Compiler** (0.4.x or later) - `lib/windows/` - Windows x64
```powershell - `lib/linux/` - Linux x64
scoop install vlang - `lib/macos-arm64/` - macOS Apple Silicon
``` - `lib/macos-x64/` - macOS Intel
## Directory Structure ## Directory Structure
@@ -89,10 +90,15 @@ usockets/
│ ├── uv.h # libuv API │ ├── uv.h # libuv API
│ └── uv/ # libuv headers │ └── uv/ # libuv headers
├── lib/ # Pre-compiled libraries ├── lib/ # Pre-compiled libraries
│ ├── libusockets_full.a # uSockets + libuv (main) │ ├── windows/ # Windows x64
│ ├── libuv.a # libuv static lib │ ├── linux/ # Linux x64
│ └── ... │ ├── macos-arm64/ # macOS Apple Silicon
└── usockets.v # V bindings │ └── macos-x64/ # macOS Intel
├── src/ # Modified source files
│ └── bsd.c # Modified with backlog=16384
├── build.sh # Build script
├── usockets.v # V bindings
└── README.md # This file
``` ```
## Configuration ## Configuration
@@ -107,6 +113,104 @@ app.listen_usockets_with_config(meiseayoung.hono.UsocketsConfig{
}) })
``` ```
## Building uSockets Library from Source
If you need to rebuild the uSockets library (e.g., for a different platform or custom modifications), use the provided build script.
### Build Prerequisites
- **Git** - For cloning source repositories
- **GCC** - C compiler
- **CMake** - For building libuv
- **ar** - Archive tool (included with GCC)
#### Installing Prerequisites
**macOS:**
```bash
xcode-select --install
brew install cmake
```
**Linux (Ubuntu/Debian):**
```bash
sudo apt update
sudo apt install build-essential cmake git
```
**Linux (CentOS/RHEL):**
```bash
sudo yum groupinstall "Development Tools"
sudo yum install cmake git
```
**Windows (Git Bash/MSYS2):**
```powershell
# Install via Scoop
scoop install mingw cmake git
# Or via Chocolatey
choco install mingw cmake git
```
### Running the Build Script
```bash
cd usockets
chmod +x build.sh
./build.sh
```
The script will:
1. Clone uSockets and libuv repositories to `/tmp/`
2. Apply the backlog=16384 modification for high concurrency
3. Compile libuv as a static library
4. Compile uSockets with libuv support
5. Merge both libraries into `libusockets_full.a`
6. Output to the appropriate `lib/{platform}/` directory
### Build Script Details
The build script (`build.sh`) automatically:
- Detects your operating system (Windows/macOS/Linux)
- Detects CPU architecture (x64/arm64 on macOS)
- Applies the backlog modification from `src/bsd.c`
- Compiles with optimizations (`-O3`)
- Creates a merged static library containing both uSockets and libuv
### Custom Modifications
To customize the build:
1. **Change backlog size**: Edit `src/bsd.c` and modify the `listen()` call
2. **Enable SSL**: Remove `-DLIBUS_NO_SSL` from the build script and link OpenSSL
3. **Use different event loop**: Modify `-DLIBUS_USE_LIBUV` flag
### Troubleshooting Build Issues
**CMake not found:**
```bash
# macOS
brew install cmake
# Linux
sudo apt install cmake # or sudo yum install cmake
# Windows
scoop install cmake
```
**GCC not found (Windows):**
```powershell
scoop install mingw
# Make sure MinGW bin directory is in PATH
```
**Permission denied:**
```bash
chmod +x build.sh
```
## Notes ## Notes
- On Windows, uSockets and picoev have similar performance - On Windows, uSockets and picoev have similar performance
+92 -25
View File
@@ -1,13 +1,36 @@
#!/bin/bash #!/bin/bash
# uSockets 编译脚本 # uSockets 编译脚本
# 用于重新编译 libusockets_full.a # 用于重新编译 libusockets_full.a (包含 uSockets + libuv)
# 支持: Windows (Git Bash/MSYS2), macOS (Intel/Apple Silicon), Linux
# 输出到: usockets/lib/{platform}/libusockets_full.a
set -e set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
USOCKETS_DIR="/tmp/uSockets" USOCKETS_DIR="/tmp/uSockets"
LIBUV_DIR="/tmp/libuv"
MERGE_DIR="/tmp/merge_libs"
echo "=== uSockets 编译脚本 ===" echo "=== uSockets 编译脚本 ==="
echo "检测到系统: $OSTYPE"
# 确定输出目录
if [[ "$OSTYPE" == "darwin"* ]]; then
if [[ $(uname -m) == "arm64" ]]; then
OUTPUT_DIR="$SCRIPT_DIR/lib/macos-arm64"
else
OUTPUT_DIR="$SCRIPT_DIR/lib/macos-x64"
fi
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "mingw"* ]]; then
OUTPUT_DIR="$SCRIPT_DIR/lib/windows"
elif [[ "$OSTYPE" == "linux"* ]]; then
OUTPUT_DIR="$SCRIPT_DIR/lib/linux"
else
OUTPUT_DIR="$SCRIPT_DIR/lib/linux"
fi
echo "输出目录: $OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"
# 检查是否有 uSockets 源码 # 检查是否有 uSockets 源码
if [ ! -d "$USOCKETS_DIR" ]; then if [ ! -d "$USOCKETS_DIR" ]; then
@@ -15,41 +38,85 @@ if [ ! -d "$USOCKETS_DIR" ]; then
git clone --depth 1 https://github.com/uNetworking/uSockets.git "$USOCKETS_DIR" git clone --depth 1 https://github.com/uNetworking/uSockets.git "$USOCKETS_DIR"
fi fi
# 检查是否有 libuv 源码
if [ ! -d "$LIBUV_DIR" ]; then
echo "克隆 libuv..."
git clone --depth 1 --branch v1.48.0 https://github.com/libuv/libuv.git "$LIBUV_DIR"
fi
# 应用 backlog 修改 # 应用 backlog 修改
echo "应用 backlog=16384 修改..." echo "应用 backlog=16384 修改..."
if [ -f "$SCRIPT_DIR/src/bsd.c" ]; then if [ -f "$SCRIPT_DIR/src/bsd.c" ]; then
cp "$SCRIPT_DIR/src/bsd.c" "$USOCKETS_DIR/src/bsd.c" cp "$SCRIPT_DIR/src/bsd.c" "$USOCKETS_DIR/src/bsd.c"
else else
# 如果没有本地修改的源码,直接修改
sed -i.bak 's/listen(listenFd, 512)/listen(listenFd, 16384)/g' "$USOCKETS_DIR/src/bsd.c" sed -i.bak 's/listen(listenFd, 512)/listen(listenFd, 16384)/g' "$USOCKETS_DIR/src/bsd.c"
fi fi
# 编译 # 编译 libuv
compile_libuv() {
echo "编译 libuv..."
cd "$LIBUV_DIR"
rm -rf build 2>/dev/null || true
mkdir -p build
cd build
if ! command -v cmake &> /dev/null; then
echo "错误: 需要安装 cmake"
echo " macOS: brew install cmake"
echo " Linux: sudo apt install cmake 或 sudo yum install cmake"
echo " Windows: scoop install cmake 或 choco install cmake"
exit 1
fi
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "mingw"* ]]; then
cmake .. -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF -DLIBUV_BUILD_SHARED=OFF
else
cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF -DLIBUV_BUILD_SHARED=OFF
fi
cmake --build . --config Release
}
# 编译 uSockets
compile_usockets() {
echo "编译 uSockets..." echo "编译 uSockets..."
cd "$USOCKETS_DIR" cd "$USOCKETS_DIR"
make clean rm -f *.o *.a 2>/dev/null || true
# 根据系统选择编译参数 gcc -DLIBUS_NO_SSL -DLIBUS_USE_LIBUV -std=c11 -Isrc -I"$LIBUV_DIR/include" -O3 -c src/*.c src/eventing/*.c src/crypto/*.c
if [[ "$OSTYPE" == "darwin"* ]]; then ar rcs uSockets.a *.o
# macOS rm -f *.o
if [[ $(uname -m) == "arm64" ]]; then }
# Apple Silicon
WITH_LIBUV=1 CFLAGS="-I/opt/homebrew/include" make
else
# Intel Mac
WITH_LIBUV=1 CFLAGS="-I/usr/local/include" make
fi
else
# Linux
WITH_LIBUV=1 make
fi
# 复制到 v-hono # 合并库文件
echo "复制库文件..." merge_libraries() {
cp "$USOCKETS_DIR/uSockets.a" "$SCRIPT_DIR/../lib/libusockets_full.a" echo "合并库文件..."
rm -rf "$MERGE_DIR"
mkdir -p "$MERGE_DIR"
echo "=== 编译完成 ===" cd "$MERGE_DIR"
echo "库文件: $SCRIPT_DIR/../lib/libusockets_full.a" ar x "$USOCKETS_DIR/uSockets.a"
ar x "$LIBUV_DIR/build/libuv.a"
ar rcs "$OUTPUT_DIR/libusockets_full.a" *.o
rm -rf "$MERGE_DIR"
}
# 执行编译
compile_libuv
compile_usockets
merge_libraries
# 显示结果
LIB_SIZE=$(ls -lh "$OUTPUT_DIR/libusockets_full.a" | awk '{print $5}')
echo "" echo ""
echo "重新编译 v-hono 应用:" echo "=== 编译完成 ==="
echo " v -enable-globals -prod -o your_app your_app.v" echo "库文件: $OUTPUT_DIR/libusockets_full.a ($LIB_SIZE)"
echo ""
echo "编译 v-hono 应用:"
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "mingw"* ]]; then
echo " v -enable-globals -cc gcc -ldflags \"-ldbghelp\" -o app.exe app.v"
else
echo " v -enable-globals -prod -o app app.v"
fi
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+16 -23
View File
@@ -2,41 +2,36 @@ module usockets
// 条件编译:根据操作系统选择正确的路径和库 // 条件编译:根据操作系统选择正确的路径和库
// @VMODROOT 指向包含 v.mod 的模块根目录 (v-hono/) // @VMODROOT 指向包含 v.mod 的模块根目录 (v-hono/)
// 预编译库按平台存放在 usockets/lib/{platform}/ 目录
$if windows { $if windows {
#flag -DLIBUS_USE_LIBUV #flag -DLIBUS_USE_LIBUV
#flag -DLIBUS_NO_SSL #flag -DLIBUS_NO_SSL
#flag -I@VMODROOT/usockets/include #flag -I@VMODROOT/usockets/include
#flag -L@VMODROOT/usockets/lib #flag -L@VMODROOT/usockets/lib/windows
#flag @VMODROOT/usockets/lib/libusockets_full.a #flag @VMODROOT/usockets/lib/windows/libusockets_full.a
#flag -lws2_32 -liphlpapi -lpsapi -luserenv -lole32 #flag -lws2_32 -liphlpapi -lpsapi -luserenv -lole32 -ldbghelp
} $else $if macos { } $else $if macos {
// macOS: 支持 Apple Silicon (M1/M2/M3) 和 Intel Mac
#flag -DLIBUS_USE_LIBUV #flag -DLIBUS_USE_LIBUV
#flag -DLIBUS_NO_SSL #flag -DLIBUS_NO_SSL
// libuv 路径 - 根据架构选择 Homebrew 路径
$if arm64 {
#flag -I/opt/homebrew/include
#flag -L/opt/homebrew/lib
} $else {
#flag -I/usr/local/include
#flag -L/usr/local/lib
}
// uSockets 库路径
#flag -I@VMODROOT/usockets/include #flag -I@VMODROOT/usockets/include
#flag -L@VMODROOT/usockets/lib
#flag @VMODROOT/usockets/lib/libusockets_full.a $if arm64 {
#flag -luv // Apple Silicon (M1/M2/M3)
#flag -L@VMODROOT/usockets/lib/macos-arm64
#flag @VMODROOT/usockets/lib/macos-arm64/libusockets_full.a
} $else {
// Intel Mac
#flag -L@VMODROOT/usockets/lib/macos-x64
#flag @VMODROOT/usockets/lib/macos-x64/libusockets_full.a
}
} $else { } $else {
// Linux 和其他平台 // Linux 和其他平台
#flag -DLIBUS_USE_LIBUV #flag -DLIBUS_USE_LIBUV
#flag -DLIBUS_NO_SSL #flag -DLIBUS_NO_SSL
#flag -I@VMODROOT/usockets/include #flag -I@VMODROOT/usockets/include
#flag -L@VMODROOT/usockets/lib #flag -L@VMODROOT/usockets/lib/linux
#flag @VMODROOT/usockets/lib/libusockets_full.a #flag @VMODROOT/usockets/lib/linux/libusockets_full.a
#flag -luv
} }
#include "libusockets.h" #include "libusockets.h"
@@ -105,8 +100,6 @@ pub fn (ctx SocketContext) on_writable(h voidptr) { C.us_socket_context_on_writa
pub fn (ctx SocketContext) on_timeout(h voidptr) { C.us_socket_context_on_timeout(0, ctx, h) } pub fn (ctx SocketContext) on_timeout(h voidptr) { C.us_socket_context_on_timeout(0, ctx, h) }
pub fn (ctx SocketContext) on_end(h voidptr) { C.us_socket_context_on_end(0, ctx, h) } pub fn (ctx SocketContext) on_end(h voidptr) { C.us_socket_context_on_end(0, ctx, h) }
pub fn (ctx SocketContext) listen(port int) ListenSocket { pub fn (ctx SocketContext) listen(port int) ListenSocket {
// 参数: ssl=0, context, host=nil, port, options=LIBUS_LISTEN_DEFAULT(0), socket_ext_size=0
// 注意: backlog 是在 uSockets 库内部硬编码的,不通过此函数传递
return C.us_socket_context_listen(0, ctx, unsafe { nil }, port, 0, 0) return C.us_socket_context_listen(0, ctx, unsafe { nil }, port, 0, 0)
} }
+124 -17
View File
@@ -183,7 +183,7 @@ fn handle_usockets_ws_upgrade(s usockets.Socket, raw_data string, route_match Co
// Check if this is a WebSocket upgrade response // Check if this is a WebSocket upgrade response
if response.status_code != 101 { if response.status_code != 101 {
// Not a WebSocket upgrade, send the response as-is // Not a WebSocket upgrade, send the response as-is
send_usockets_response(s, mutable_ctx, response) send_usockets_response(s, mutable_ctx, response, true)
return false return false
} }
@@ -227,7 +227,7 @@ fn usockets_on_data(s usockets.Socket, data &char, length int) usockets.Socket {
// 解析 HTTP 请求 // 解析 HTTP 请求
raw_data := unsafe { tos(&u8(data), length) } raw_data := unsafe { tos(&u8(data), length) }
method, path, query_map, body := parse_http_request_usockets(raw_data) method, path, query_map, body, is_http11 := parse_http_request_usockets(raw_data)
if method.len == 0 { if method.len == 0 {
s.write_bytes('HTTP/1.1 400 Bad Request\r\nContent-Length: 11\r\n\r\nBad Request') s.write_bytes('HTTP/1.1 400 Bad Request\r\nContent-Length: 11\r\n\r\nBad Request')
@@ -259,11 +259,11 @@ fn usockets_on_data(s usockets.Socket, data &char, length int) usockets.Socket {
// 优化1:零中间件快速路径 // 优化1:零中间件快速路径
if !g_usockets_app.has_middlewares { if !g_usockets_app.has_middlewares {
response := route_match.handler.handle(mut hono_ctx) response := route_match.handler.handle(mut hono_ctx)
send_usockets_response(s, hono_ctx, response) send_usockets_response(s, hono_ctx, response, is_http11)
} else { } else {
middlewares := get_middlewares_for_path_usockets_optimized(g_usockets_app, path) middlewares := get_middlewares_for_path_usockets_optimized(g_usockets_app, path)
response := exec_middlewares_usockets(0, middlewares, mut hono_ctx, route_match.handler) response := exec_middlewares_usockets(0, middlewares, mut hono_ctx, route_match.handler)
send_usockets_response(s, hono_ctx, response) send_usockets_response(s, hono_ctx, response, is_http11)
} }
response_sent = true response_sent = true
} }
@@ -289,11 +289,11 @@ fn usockets_on_data(s usockets.Socket, data &char, length int) usockets.Socket {
// 优化1:零中间件快速路径 // 优化1:零中间件快速路径
if !g_usockets_app.has_middlewares { if !g_usockets_app.has_middlewares {
response := route_match.handler.handle(mut hono_ctx) response := route_match.handler.handle(mut hono_ctx)
send_usockets_response(s, hono_ctx, response) send_usockets_response(s, hono_ctx, response, is_http11)
} else { } else {
middlewares := get_middlewares_for_path_usockets_optimized(g_usockets_app, path) middlewares := get_middlewares_for_path_usockets_optimized(g_usockets_app, path)
response := exec_middlewares_usockets(0, middlewares, mut hono_ctx, route_match.handler) response := exec_middlewares_usockets(0, middlewares, mut hono_ctx, route_match.handler)
send_usockets_response(s, hono_ctx, response) send_usockets_response(s, hono_ctx, response, is_http11)
} }
response_sent = true response_sent = true
} }
@@ -305,9 +305,13 @@ fn usockets_on_data(s usockets.Socket, data &char, length int) usockets.Socket {
if handler := g_usockets_app.not_found_handler { if handler := g_usockets_app.not_found_handler {
response := handler(mut hono_ctx) response := handler(mut hono_ctx)
send_usockets_response(s, hono_ctx, response) send_usockets_response(s, hono_ctx, response, is_http11)
} else { } else {
s.write_bytes('HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 9\r\nConnection: keep-alive\r\n\r\nNot Found') conn_header := if is_http11 { 'keep-alive' } else { 'close' }
s.write_bytes('HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 9\r\nConnection: ${conn_header}\r\n\r\nNot Found')
if !is_http11 {
s.shutdown()
}
} }
} }
@@ -334,15 +338,17 @@ fn usockets_on_end(s usockets.Socket) usockets.Socket {
// 解析 HTTP 请求 - 零分配优化版 // 解析 HTTP 请求 - 零分配优化版
// 使用指针遍历避免创建临时数组 // 使用指针遍历避免创建临时数组
fn parse_http_request_usockets(raw string) (string, string, map[string]string, string) { // 返回: (method, path, query_map, body, is_http11)
fn parse_http_request_usockets(raw string) (string, string, map[string]string, string, bool) {
mut method := '' mut method := ''
mut path := '' mut path := ''
mut query_map := map[string]string{} mut query_map := map[string]string{}
mut body := '' mut body := ''
mut is_http11 := true
len := raw.len len := raw.len
if len == 0 { if len == 0 {
return method, path, query_map, body return method, path, query_map, body, is_http11
} }
// 1. 查找第一行结束位置(\r\n) // 1. 查找第一行结束位置(\r\n)
@@ -354,7 +360,7 @@ fn parse_http_request_usockets(raw string) (string, string, map[string]string, s
} }
} }
if line_end == -1 { if line_end == -1 {
return method, path, query_map, body return method, path, query_map, body, is_http11
} }
// 2. 解析 method(到第一个空格) // 2. 解析 method(到第一个空格)
@@ -363,7 +369,7 @@ fn parse_http_request_usockets(raw string) (string, string, map[string]string, s
method_end++ method_end++
} }
if method_end == 0 || method_end >= line_end { if method_end == 0 || method_end >= line_end {
return method, path, query_map, body return method, path, query_map, body, is_http11
} }
method = raw[..method_end] method = raw[..method_end]
@@ -391,7 +397,25 @@ fn parse_http_request_usockets(raw string) (string, string, map[string]string, s
path = raw[path_start..path_end] path = raw[path_start..path_end]
} }
// 4. 查找 body(\r\n\r\n 之后) // 4. 检测 HTTP 版本 - "HTTP/1.0" vs "HTTP/1.1"
// 请求行格式: "GET /path HTTP/1.1\r\n"
// HTTP/1.x 版本字符串固定为 8 字符,直接检查第 8 个字符
mut version_start := path_end + 1
for version_start < line_end && raw[version_start] == ` ` {
version_start++
}
// "HTTP/1.0" 或 "HTTP/1.1",检查索引 7 的字符(0-based)
if version_start + 7 < line_end {
is_http11 = raw[version_start + 7] != `0`
}
// 对于 HTTP/1.0,检查 Connection 头是否包含 keep-alive
// 参考 Go net/http: wantsHttp10KeepAlive() 只检查 Connection 头的值
if !is_http11 {
is_http11 = has_connection_keep_alive(raw, line_end, len)
}
// 5. 查找 body(\r\n\r\n 之后)
for i in line_end .. len - 3 { for i in line_end .. len - 3 {
if raw[i] == `\r` && raw[i + 1] == `\n` && raw[i + 2] == `\r` && raw[i + 3] == `\n` { if raw[i] == `\r` && raw[i + 1] == `\n` && raw[i + 2] == `\r` && raw[i + 3] == `\n` {
if i + 4 < len { if i + 4 < len {
@@ -401,10 +425,82 @@ fn parse_http_request_usockets(raw string) (string, string, map[string]string, s
} }
} }
return method, path, query_map, body return method, path, query_map, body, is_http11
} }
// 快速解析 query string(单次遍历,避免 split) // 检查 Connection 头是否包含 keep-alive(参考 Go net/http 实现)
// 只检查 "Connection:" 头的值,避免误判请求体中的内容
@[inline]
fn has_connection_keep_alive(raw string, headers_start int, len int) bool {
mut i := headers_start
// 遍历每一行头部
for i < len - 12 { // 至少需要 "Connection:" + 一些值
// 跳过 \r\n
if raw[i] == `\r` && i + 1 < len && raw[i + 1] == `\n` {
i += 2
// 检查是否到达头部结束(空行)
if i < len - 1 && raw[i] == `\r` && raw[i + 1] == `\n` {
break
}
}
// 检查是否是 "Connection:" 头(不区分大小写)
if (raw[i] == `C` || raw[i] == `c`) && i + 11 < len {
// 检查 "onnection:"
if (raw[i+1] == `o` || raw[i+1] == `O`) &&
(raw[i+2] == `n` || raw[i+2] == `N`) &&
(raw[i+3] == `n` || raw[i+3] == `N`) &&
(raw[i+4] == `e` || raw[i+4] == `E`) &&
(raw[i+5] == `c` || raw[i+5] == `C`) &&
(raw[i+6] == `t` || raw[i+6] == `T`) &&
(raw[i+7] == `i` || raw[i+7] == `I`) &&
(raw[i+8] == `o` || raw[i+8] == `O`) &&
(raw[i+9] == `n` || raw[i+9] == `N`) &&
raw[i+10] == `:` {
// 找到 Connection 头,检查值是否包含 keep-alive
mut value_start := i + 11
// 跳过空格
for value_start < len && raw[value_start] == ` ` {
value_start++
}
// 查找行尾
mut value_end := value_start
for value_end < len && raw[value_end] != `\r` && raw[value_end] != `\n` {
value_end++
}
// 在值中查找 "keep-alive"(不区分大小写)
return contains_keep_alive_token(raw, value_start, value_end)
}
}
i++
}
return false
}
// 检查字符串片段是否包含 keep-alive token
@[inline]
fn contains_keep_alive_token(raw string, start int, end int) bool {
// 查找 "keep-alive" 或 "Keep-Alive"(支持多值如 "keep-alive, upgrade")
mut i := start
for i <= end - 10 {
if raw[i] == `k` || raw[i] == `K` {
if (raw[i+1] == `e` || raw[i+1] == `E`) &&
(raw[i+2] == `e` || raw[i+2] == `E`) &&
(raw[i+3] == `p` || raw[i+3] == `P`) &&
raw[i+4] == `-` &&
(raw[i+5] == `a` || raw[i+5] == `A`) &&
(raw[i+6] == `l` || raw[i+6] == `L`) &&
(raw[i+7] == `i` || raw[i+7] == `I`) &&
(raw[i+8] == `v` || raw[i+8] == `V`) &&
(raw[i+9] == `e` || raw[i+9] == `E`) {
return true
}
}
i++
}
return false
}// 快速解析 query string(单次遍历,避免 split)
@[inline] @[inline]
fn parse_query_string_fast(raw string, start int, end int) map[string]string { fn parse_query_string_fast(raw string, start int, end int) map[string]string {
mut query_map := map[string]string{} mut query_map := map[string]string{}
@@ -576,7 +672,7 @@ fn exec_middlewares_usockets(idx int, middlewares []ContextMiddleware, mut ctx C
} }
// 发送 uSockets 响应 - 优化版 // 发送 uSockets 响应 - 优化版
fn send_usockets_response(s usockets.Socket, ctx Context, response http.Response) { fn send_usockets_response(s usockets.Socket, ctx Context, response http.Response, is_http11 bool) {
// Check if this is a streaming response // Check if this is a streaming response
if is_streaming_response(ctx) { if is_streaming_response(ctx) {
// Handle streaming response // Handle streaming response
@@ -630,14 +726,25 @@ fn send_usockets_response(s usockets.Socket, ctx Context, response http.Response
resp.write_string(response.body.len.str()) resp.write_string(response.body.len.str())
resp.write_string('\r\n') resp.write_string('\r\n')
// Connection // Connection - HTTP/1.0 用 close,HTTP/1.1 用 keep-alive
if is_http11 {
resp.write_string('Connection: keep-alive\r\n') resp.write_string('Connection: keep-alive\r\n')
} else {
resp.write_string('Connection: close\r\n')
}
// 空行 + body // 空行 + body
resp.write_string('\r\n') resp.write_string('\r\n')
resp.write_string(response.body) resp.write_string(response.body)
s.write_bytes(resp.str()) s.write_bytes(resp.str())
// HTTP/1.0 请求完成后关闭连接
// 只调用 shutdown() 发送 FIN,让 on_end 回调处理 close()
// 这样可以确保发送缓冲区的数据完全发送
if !is_http11 {
s.shutdown()
}
} }
// ============================================================================ // ============================================================================
+1 -1
View File
@@ -1,7 +1,7 @@
Module { Module {
name: 'hono' name: 'hono'
description: 'A high-performance V language web framework inspired by Hono.js, featuring hybrid routing, LRU cache, middleware support, and more.' description: 'A high-performance V language web framework inspired by Hono.js, featuring hybrid routing, LRU cache, middleware support, and more.'
version: '0.7.2' version: '0.8.0'
license: 'MIT' license: 'MIT'
dependencies: [] dependencies: []
} }