Add built-in storage support from v-hono-storage module: - StorageProvider interface with 4 backends - FileService with HTTP handlers - Multipart upload support - 86 property-based tests - Retry mechanism with exponential backoff Breaking: None (backward compatible)
1707 lines
48 KiB
Markdown
1707 lines
48 KiB
Markdown
# v-hono
|
|
|
|
A high-performance V language web framework inspired by [Hono.js](https://hono.dev/).
|
|
|
|
## Features
|
|
|
|
- 🚀 **High Performance** - Hybrid routing with LRU cache for optimal speed
|
|
- 🎯 **Simple API** - Clean and intuitive API design inspired by Hono.js
|
|
- 🔧 **Middleware Support** - Flexible middleware system with onion model
|
|
- 🌐 **CORS** - Built-in CORS middleware with full configuration
|
|
- 🍪 **Cookie Helper** - Easy cookie management with signed cookie support
|
|
- 🔐 **JWT Authentication** - JWT middleware with HS256/384/512 support
|
|
- 🎫 **Bearer Auth** - Simple Bearer token authentication
|
|
- 📦 **Compression** - Gzip and deflate response compression
|
|
- ⏱️ **Rate Limiting** - Request rate limiting with customizable stores
|
|
- ✅ **Validation** - Schema-based request validation
|
|
- 📁 **Static File Serving** - Built-in static file server with caching
|
|
- 🛡️ **Security** - Path validation and security utilities
|
|
- 📤 **File Upload** - Chunked file upload support
|
|
- ☁️ **Multi-Cloud Storage** - Built-in support for Local, S3, Aliyun OSS, Tencent COS
|
|
- 🗄️ **Database** - SQLite integration for data persistence
|
|
- 🔌 **WebSocket** - RFC 6455 compliant WebSocket support with event-based API
|
|
- 📡 **SSE Streaming** - Server-Sent Events and streaming response support
|
|
- 📖 **Swagger UI** - Interactive API documentation with OpenAPI 3.0/3.1 support
|
|
|
|
## Installation
|
|
|
|
### From VPM
|
|
|
|
```bash
|
|
v install meiseayoung.hono
|
|
```
|
|
|
|
### From GitHub
|
|
|
|
```bash
|
|
v install --git https://github.com/meiseayoung/v-hono
|
|
```
|
|
|
|
### Local Development
|
|
|
|
Clone the repository and the module will be available for import:
|
|
|
|
```bash
|
|
git clone https://github.com/meiseayoung/v-hono.git
|
|
cd v-hono
|
|
```
|
|
|
|
|
|
## Build & Run
|
|
|
|
### Standard Build (picoev backend)
|
|
|
|
```bash
|
|
# macOS / Linux
|
|
v -prod -o app your_app.v
|
|
./app
|
|
|
|
# Windows
|
|
v -o app.exe your_app.v
|
|
.\app.exe
|
|
```
|
|
|
|
### High-Performance Build (uSockets backend)
|
|
|
|
For maximum performance with uSockets backend:
|
|
|
|
```bash
|
|
# macOS / Linux
|
|
v -prod -o app your_app.v
|
|
./app
|
|
|
|
# Windows (must use gcc, tcc does not support .a static library format)
|
|
v -cc gcc -ldflags "-ldbghelp" -o app.exe your_app.v
|
|
.\app.exe
|
|
```
|
|
|
|
**Important Notes:**
|
|
- 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
|
|
- The `-enable-globals` flag is **only** needed if your application uses global variables (e.g., for shared state)
|
|
|
|
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.
|
|
|
|
### Using uSockets Backend
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
app.get('/', fn (mut c hono.Context) http.Response {
|
|
return c.text('Hello, World!')
|
|
})
|
|
|
|
// Use uSockets backend (high concurrency optimized)
|
|
app.listen_usockets(3000)
|
|
|
|
// Or use default picoev backend
|
|
// app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
See [benchmark/README.md](benchmark/README.md) for performance benchmarks and [usockets/README.md](usockets/README.md) for uSockets integration details.
|
|
|
|
## High Concurrency Support
|
|
|
|
v-hono with uSockets backend supports **10,000+ concurrent connections** out of the box.
|
|
|
|
### Performance Results
|
|
|
|
| Concurrent | RPS | Avg Latency | P99 Latency | Success Rate |
|
|
|------------|-----|-------------|-------------|--------------|
|
|
| 5,000 | 81,524 | 60.77ms | 295.17ms | 100% |
|
|
| 6,000 | 56,290 | 98.46ms | 557.47ms | 100% |
|
|
| 7,000 | 42,895 | 130.04ms | 824.80ms | 100% |
|
|
| 8,000 | 30,537 | 165.27ms | 1058.05ms | 100% |
|
|
| 9,000 | 28,693 | 215.86ms | 1422.67ms | 100% |
|
|
| 10,000 | 24,198 | 240.90ms | 1602.10ms | 100% |
|
|
|
|
### System Configuration (macOS)
|
|
|
|
For optimal high-concurrency performance:
|
|
|
|
```bash
|
|
# Increase socket backlog limit
|
|
sudo sysctl -w kern.ipc.somaxconn=8192
|
|
|
|
# Increase file descriptor limit
|
|
ulimit -n 65535
|
|
```
|
|
|
|
See [docs/HIGH_CONCURRENCY.md](docs/HIGH_CONCURRENCY.md) for detailed optimization guide.
|
|
|
|
## Quick Start
|
|
|
|
```v
|
|
import net.http
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
app.get('/', fn (mut c hono.Context) http.Response {
|
|
return c.text('Hello, World!')
|
|
})
|
|
|
|
app.get('/json', fn (mut c hono.Context) http.Response {
|
|
return c.json('{"message": "Hello, JSON!"}')
|
|
})
|
|
|
|
app.get('/users/:id', fn (mut c hono.Context) http.Response {
|
|
user_id := c.params['id'] or { 'unknown' }
|
|
return c.json('{"user_id": "${user_id}"}')
|
|
})
|
|
|
|
// Redirect example
|
|
app.get('/old-page', fn (mut c hono.Context) http.Response {
|
|
return c.redirect('/new-page', 301)
|
|
})
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
## Middleware
|
|
|
|
```v
|
|
import net.http
|
|
import time
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// Logger middleware
|
|
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
|
|
start := time.now()
|
|
response := next(mut c)
|
|
duration := time.since(start)
|
|
println('[${c.req.method}] ${c.path} - ${duration}')
|
|
return response
|
|
})
|
|
|
|
app.get('/', fn (mut c hono.Context) http.Response {
|
|
return c.text('Hello with middleware!')
|
|
})
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
## Built-in Middleware
|
|
|
|
v-hono provides 7 built-in middleware components inspired by Hono.js:
|
|
|
|
### 1. CORS Middleware
|
|
|
|
Cross-Origin Resource Sharing middleware for handling CORS requests.
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// Allow all origins
|
|
app.use(hono.cors())
|
|
|
|
// Custom configuration
|
|
app.use(hono.cors(hono.CorsOptions{
|
|
origin: 'https://example.com'
|
|
credentials: true
|
|
max_age: 600
|
|
allow_methods: ['GET', 'POST', 'PUT', 'DELETE']
|
|
allow_headers: ['Content-Type', 'Authorization']
|
|
}))
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
### 2. Cookie Helper
|
|
|
|
Utilities for managing HTTP cookies, including signed cookies.
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
app.get('/cookie/set', fn (mut c hono.Context) http.Response {
|
|
// Set a cookie
|
|
hono.set_cookie(mut c, 'session_id', 'abc123', hono.CookieOptions{
|
|
http_only: true
|
|
secure: true
|
|
max_age: 3600
|
|
path: '/'
|
|
})
|
|
return c.json('{"message": "Cookie set"}')
|
|
})
|
|
|
|
app.get('/cookie/get', fn (mut c hono.Context) http.Response {
|
|
// Get a cookie
|
|
if session := hono.get_cookie(c, 'session_id') {
|
|
return c.json('{"session": "${session}"}')
|
|
}
|
|
return c.json('{"error": "Cookie not found"}')
|
|
})
|
|
|
|
app.get('/cookie/delete', fn (mut c hono.Context) http.Response {
|
|
hono.delete_cookie(mut c, 'session_id')
|
|
return c.json('{"message": "Cookie deleted"}')
|
|
})
|
|
|
|
// Signed cookies (tamper-proof)
|
|
app.get('/signed/set', fn (mut c hono.Context) http.Response {
|
|
secret := 'my-secret-key'
|
|
hono.set_signed_cookie(mut c, 'user_data', 'user123', secret) or {
|
|
return c.json('{"error": "Failed to set signed cookie"}')
|
|
}
|
|
return c.json('{"message": "Signed cookie set"}')
|
|
})
|
|
|
|
app.get('/signed/get', fn (mut c hono.Context) http.Response {
|
|
secret := 'my-secret-key'
|
|
user_data := hono.get_signed_cookie(c, 'user_data', secret) or {
|
|
return c.json('{"error": "Invalid or missing signed cookie"}')
|
|
}
|
|
return c.json('{"user_data": "${user_data}"}')
|
|
})
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
### 3. JWT Middleware
|
|
|
|
JSON Web Token authentication middleware.
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
import time
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
secret := 'my-jwt-secret-key'
|
|
|
|
// Generate JWT token
|
|
app.post('/auth/login', fn [secret] (mut c hono.Context) http.Response {
|
|
payload := hono.JwtPayload{
|
|
sub: 'user123'
|
|
iss: 'my-app'
|
|
exp: time.now().unix() + 3600 // 1 hour
|
|
iat: time.now().unix()
|
|
claims: {
|
|
'role': 'admin'
|
|
'name': 'John Doe'
|
|
}
|
|
}
|
|
|
|
token := hono.sign_jwt(payload, secret, .hs256) or {
|
|
c.status(500)
|
|
return c.json('{"error": "Failed to generate token"}')
|
|
}
|
|
|
|
return c.json('{"token": "${token}"}')
|
|
})
|
|
|
|
// Protect routes with JWT middleware
|
|
app.use('/api/*', hono.jwt_middleware(hono.JwtOptions{
|
|
secret: secret
|
|
alg: .hs256
|
|
}))
|
|
|
|
app.get('/api/profile', fn (mut c hono.Context) http.Response {
|
|
// Access JWT payload from context
|
|
if payload := hono.get_jwt_payload(c) {
|
|
return c.json('{"user": "${payload.sub}"}')
|
|
}
|
|
return c.json('{"error": "No payload"}')
|
|
})
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
### 4. Bearer Auth Middleware
|
|
|
|
Simple Bearer token authentication.
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// Single token authentication
|
|
app.use('/api/*', hono.bearer_auth(hono.BearerAuthOptions{
|
|
token: 'my-api-token'
|
|
realm: 'Protected API'
|
|
}))
|
|
|
|
// Multiple tokens
|
|
app.use('/admin/*', hono.bearer_auth(hono.BearerAuthOptions{
|
|
token: hono.BearerToken(['token1', 'token2', 'token3'])
|
|
}))
|
|
|
|
// Custom verification
|
|
app.use('/custom/*', hono.bearer_auth(hono.BearerAuthOptions{
|
|
verify_token: fn (token string, c hono.Context) bool {
|
|
// Custom validation logic
|
|
return token.len > 10 && token.starts_with('valid_')
|
|
}
|
|
}))
|
|
|
|
app.get('/api/data', fn (mut c hono.Context) http.Response {
|
|
token := hono.get_bearer_token(c) or { 'unknown' }
|
|
return c.json('{"message": "Protected data", "token": "${token}"}')
|
|
})
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
### 5. Compression Middleware
|
|
|
|
Response compression with gzip and deflate support.
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// Auto-select best encoding based on Accept-Encoding header
|
|
app.use(hono.compress())
|
|
|
|
// Force gzip compression
|
|
app.use(hono.gzip())
|
|
|
|
// Custom configuration
|
|
app.use(hono.compress(hono.CompressOptions{
|
|
encoding: .gzip
|
|
threshold: 2048 // Only compress responses > 2KB
|
|
level: 6 // Compression level (1-9)
|
|
}))
|
|
|
|
app.get('/large-data', fn (mut c hono.Context) http.Response {
|
|
// Large response will be automatically compressed
|
|
return c.json('{"data": "...large content..."}')
|
|
})
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
### 6. Rate Limiting Middleware
|
|
|
|
Request rate limiting to protect against abuse.
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// Create memory store for rate limiting
|
|
store := hono.MemoryStore.new()
|
|
|
|
// Default: 100 requests per minute
|
|
app.use(hono.rate_limit(hono.RateLimitOptions{
|
|
store: store
|
|
window_ms: 60000 // 1 minute
|
|
limit: 100 // Max 100 requests
|
|
headers: true // Add X-RateLimit-* headers
|
|
}))
|
|
|
|
// Custom key generator (e.g., by user ID)
|
|
app.use('/api/*', hono.rate_limit(hono.RateLimitOptions{
|
|
store: store
|
|
window_ms: 60000
|
|
limit: 50
|
|
key_generator: fn (c hono.Context) string {
|
|
if user_id := c.get('user_id') {
|
|
return user_id
|
|
}
|
|
return c.get_client_ip()
|
|
}
|
|
}))
|
|
|
|
// Skip rate limiting for certain requests
|
|
app.use(hono.rate_limit(hono.RateLimitOptions{
|
|
store: store
|
|
limit: 100
|
|
skip: fn (c hono.Context) bool {
|
|
// Skip for health check endpoints
|
|
return c.path == '/health'
|
|
}
|
|
}))
|
|
|
|
app.get('/', fn (mut c hono.Context) http.Response {
|
|
return c.text('Hello!')
|
|
})
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
### 7. Request Validator
|
|
|
|
Schema-based request validation for JSON body, query parameters, path parameters, and headers.
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// Validate JSON body
|
|
app.post('/users',
|
|
hono.validate_json(hono.v_object({
|
|
'name': hono.v_string().required().min(2).max(50)
|
|
'email': hono.v_string().required().pattern(r'^[\w\.-]+@[\w\.-]+\.\w+$')
|
|
'age': hono.v_int().min(0).max(150)
|
|
})),
|
|
fn (mut c hono.Context) http.Response {
|
|
data := hono.get_validated_data(c)
|
|
name := data['name'] or { '' }
|
|
email := data['email'] or { '' }
|
|
return c.json('{"message": "User created", "name": "${name}", "email": "${email}"}')
|
|
}
|
|
)
|
|
|
|
// Validate query parameters
|
|
app.get('/search',
|
|
hono.validate_query(hono.v_object({
|
|
'q': hono.v_string().required().min(1)
|
|
'page': hono.v_int().min(1)
|
|
'size': hono.v_int().min(1).max(100)
|
|
})),
|
|
fn (mut c hono.Context) http.Response {
|
|
q := hono.get_validated_field(c, 'q') or { '' }
|
|
page := hono.get_validated_field(c, 'page') or { '1' }
|
|
return c.json('{"query": "${q}", "page": ${page}}')
|
|
}
|
|
)
|
|
|
|
// Validate path parameters
|
|
app.get('/users/:id',
|
|
hono.validate_params(hono.v_object({
|
|
'id': hono.v_int().required().min(1)
|
|
})),
|
|
fn (mut c hono.Context) http.Response {
|
|
id := hono.get_validated_field(c, 'id') or { '0' }
|
|
return c.json('{"user_id": ${id}}')
|
|
}
|
|
)
|
|
|
|
// Validate headers
|
|
app.use('/api/*', hono.validate_headers(hono.v_object({
|
|
'X-API-Key': hono.v_string().required()
|
|
})))
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
#### Schema Builder API
|
|
|
|
```v
|
|
// String schema
|
|
hono.v_string()
|
|
.required() // Field is required
|
|
.min(2) // Minimum length
|
|
.max(100) // Maximum length
|
|
.pattern(r'^\w+$') // Regex pattern
|
|
.enum_of(['a', 'b']) // Allowed values
|
|
|
|
// Integer schema
|
|
hono.v_int()
|
|
.required()
|
|
.min(0)
|
|
.max(100)
|
|
|
|
// Float schema
|
|
hono.v_float()
|
|
.required()
|
|
.min(0.0)
|
|
.max(100.0)
|
|
|
|
// Boolean schema
|
|
hono.v_bool()
|
|
.required()
|
|
|
|
// Array schema
|
|
hono.v_array(hono.v_string())
|
|
.min_items(1)
|
|
.max_items(10)
|
|
|
|
// Nested object schema
|
|
hono.v_object({
|
|
'address': hono.v_object({
|
|
'street': hono.v_string().required()
|
|
'city': hono.v_string().required()
|
|
})
|
|
})
|
|
```
|
|
|
|
### Utility Middleware
|
|
|
|
Additional utility middleware included:
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// Security headers (X-Content-Type-Options, X-Frame-Options, etc.)
|
|
app.use(hono.secure_headers())
|
|
|
|
// Request ID generation
|
|
app.use(hono.request_id())
|
|
|
|
// Request timing (adds X-Response-Time header)
|
|
app.use(hono.timing())
|
|
|
|
// Combine multiple middleware
|
|
combined := hono.combine_middlewares([
|
|
hono.cors(),
|
|
hono.secure_headers(),
|
|
hono.timing(),
|
|
])
|
|
app.use(combined)
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
### Context Store
|
|
|
|
Store and retrieve data within request context:
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// Auth middleware stores user info
|
|
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
|
|
c.set('user_id', '12345')
|
|
c.set('role', 'admin')
|
|
return next(mut c)
|
|
})
|
|
|
|
app.get('/profile', fn (mut c hono.Context) http.Response {
|
|
user_id := c.get('user_id') or { 'unknown' }
|
|
role := c.get('role') or { 'guest' }
|
|
client_ip := c.get_client_ip()
|
|
return c.json('{"user_id": "${user_id}", "role": "${role}", "ip": "${client_ip}"}')
|
|
})
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
### 8. WebSocket Helper
|
|
|
|
RFC 6455 compliant WebSocket support for real-time bidirectional communication.
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// Basic WebSocket endpoint
|
|
app.get('/ws', hono.upgrade_websocket(fn (c hono.Context) hono.WSEvents {
|
|
return hono.WSEvents{
|
|
on_open: fn (mut ws hono.WSContext) {
|
|
println('Client connected')
|
|
ws.send('Welcome to the WebSocket server!') or {}
|
|
}
|
|
on_message: fn (event hono.WSMessageEvent, mut ws hono.WSContext) {
|
|
println('Received: ${event.data}')
|
|
// Echo the message back
|
|
ws.send('Echo: ${event.data}') or {}
|
|
}
|
|
on_close: fn (event hono.WSCloseEvent, mut ws hono.WSContext) {
|
|
println('Client disconnected: ${event.code} - ${event.reason}')
|
|
}
|
|
on_error: fn (error string, mut ws hono.WSContext) {
|
|
println('WebSocket error: ${error}')
|
|
}
|
|
}
|
|
}))
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
#### WebSocket with Configuration Options
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// WebSocket with custom options
|
|
app.get('/ws/chat', hono.upgrade_websocket(
|
|
fn (c hono.Context) hono.WSEvents {
|
|
return hono.WSEvents{
|
|
on_message: fn (event hono.WSMessageEvent, mut ws hono.WSContext) {
|
|
ws.send('Received: ${event.data}') or {}
|
|
}
|
|
}
|
|
},
|
|
hono.WebSocketOptions{
|
|
ping_interval: 30000 // Send ping every 30 seconds
|
|
max_message_size: 1048576 // Max 1MB message size
|
|
timeout: 60000 // 60 second timeout
|
|
protocols: ['chat', 'json'] // Supported subprotocols
|
|
}
|
|
))
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
#### WebSocket with Route Parameters
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// WebSocket with route parameters
|
|
app.get('/ws/room/:room_id', hono.upgrade_websocket(fn (c hono.Context) hono.WSEvents {
|
|
// Access route parameters from HTTP context
|
|
room_id := c.params['room_id'] or { 'default' }
|
|
|
|
return hono.WSEvents{
|
|
on_open: fn [room_id] (mut ws hono.WSContext) {
|
|
ws.send('Joined room: ${room_id}') or {}
|
|
}
|
|
on_message: fn [room_id] (event hono.WSMessageEvent, mut ws hono.WSContext) {
|
|
ws.send('[${room_id}] ${event.data}') or {}
|
|
}
|
|
}
|
|
}))
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
#### Sending Different Message Types
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
app.get('/ws', hono.upgrade_websocket(fn (c hono.Context) hono.WSEvents {
|
|
return hono.WSEvents{
|
|
on_message: fn (event hono.WSMessageEvent, mut ws hono.WSContext) {
|
|
// Send text message
|
|
ws.send('Hello, World!') or {}
|
|
|
|
// Send binary data
|
|
ws.send_bytes([u8(0x01), 0x02, 0x03]) or {}
|
|
|
|
// Send JSON data
|
|
ws.send_json('{"type": "message", "content": "Hello"}') or {}
|
|
|
|
// Close connection gracefully
|
|
// ws.close(1000, 'Normal closure') or {}
|
|
}
|
|
}
|
|
}))
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
#### WebSocket Configuration Options
|
|
|
|
| Option | Type | Default | Description |
|
|
|--------|------|---------|-------------|
|
|
| `ping_interval` | `int` | `30000` | Ping interval in milliseconds (0 to disable) |
|
|
| `max_message_size` | `int` | `1048576` | Maximum message size in bytes (1MB) |
|
|
| `timeout` | `int` | `60000` | Connection timeout in milliseconds |
|
|
| `protocols` | `[]string` | `[]` | Supported WebSocket subprotocols |
|
|
|
|
#### WSContext Methods
|
|
|
|
| Method | Description |
|
|
|--------|-------------|
|
|
| `send(data string)` | Send text message |
|
|
| `send_bytes(data []u8)` | Send binary message |
|
|
| `send_json(data string)` | Send JSON data as text frame |
|
|
| `close(code int, reason string)` | Initiate graceful close |
|
|
| `get_context()` | Get original HTTP context |
|
|
|
|
#### WSContext Properties
|
|
|
|
| Property | Type | Description |
|
|
|----------|------|-------------|
|
|
| `ready_state` | `WSReadyState` | Connection state (connecting, open, closing, closed) |
|
|
| `protocol` | `string` | Negotiated subprotocol |
|
|
| `params` | `map[string]string` | Route parameters |
|
|
| `query` | `map[string]string` | Query parameters |
|
|
| `store` | `map[string]string` | Middleware store values |
|
|
|
|
#### WebSocket Close Codes
|
|
|
|
| Code | Constant | Description |
|
|
|------|----------|-------------|
|
|
| 1000 | `ws_close_normal` | Normal closure |
|
|
| 1001 | `ws_close_going_away` | Server shutting down |
|
|
| 1002 | `ws_close_protocol_error` | Protocol error |
|
|
| 1003 | `ws_close_unsupported_data` | Unsupported data type |
|
|
| 1007 | `ws_close_invalid_payload` | Invalid payload data |
|
|
| 1008 | `ws_close_policy_violation` | Policy violation |
|
|
| 1009 | `ws_close_message_too_big` | Message too big |
|
|
| 1011 | `ws_close_internal_error` | Internal server error |
|
|
|
|
### 9. SSE Streaming Helper
|
|
|
|
Server-Sent Events (SSE) and streaming response support for real-time data push.
|
|
|
|
#### Basic Binary Streaming
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// Basic stream - binary data streaming
|
|
// Sets Transfer-Encoding: chunked header
|
|
app.get('/stream', fn (mut c hono.Context) http.Response {
|
|
return hono.c_stream(mut c, fn (mut stream hono.StreamContext) ! {
|
|
// Register abort callback for client disconnection
|
|
stream.on_abort(fn () {
|
|
println('Client disconnected')
|
|
})
|
|
|
|
// Stream binary data in chunks
|
|
for i in 0 .. 5 {
|
|
data := 'Chunk ${i + 1}: Hello from stream!\n'
|
|
stream.write(data.bytes())!
|
|
stream.sleep(500) // 500ms delay between chunks
|
|
}
|
|
})
|
|
})
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
#### Text Streaming
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// Text stream - for streaming text content
|
|
// Sets Content-Type: text/plain; charset=utf-8
|
|
// Sets Transfer-Encoding: chunked
|
|
// Sets X-Content-Type-Options: nosniff
|
|
app.get('/stream-text', fn (mut c hono.Context) http.Response {
|
|
return hono.c_stream_text(mut c, fn (mut stream hono.StreamContext) ! {
|
|
stream.writeln('=== Text Streaming Demo ===')!
|
|
stream.sleep(300)
|
|
|
|
for i in 0 .. 5 {
|
|
stream.write_string('Processing item ${i + 1}... ')!
|
|
stream.sleep(200)
|
|
stream.writeln('Done!')!
|
|
}
|
|
})
|
|
})
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
#### Server-Sent Events (SSE)
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// SSE stream - for Server-Sent Events
|
|
// Sets Content-Type: text/event-stream
|
|
// Sets Cache-Control: no-cache
|
|
// Sets Connection: keep-alive
|
|
app.get('/sse', fn (mut c hono.Context) http.Response {
|
|
return hono.c_stream_sse(mut c, fn (mut stream hono.StreamContext) ! {
|
|
// Send initial connection event
|
|
stream.write_sse(hono.SSEEvent{
|
|
data: 'Connected to SSE stream'
|
|
event: 'connect'
|
|
id: '0'
|
|
})!
|
|
|
|
// Send periodic updates
|
|
for i in 1 .. 6 {
|
|
stream.sleep(1000) // 1 second delay
|
|
|
|
stream.write_sse(hono.SSEEvent{
|
|
data: 'Update ${i}'
|
|
event: 'update'
|
|
id: '${i}'
|
|
})!
|
|
}
|
|
|
|
// Send completion event
|
|
stream.write_sse(hono.SSEEvent{
|
|
data: 'Stream completed'
|
|
event: 'complete'
|
|
id: '999'
|
|
})!
|
|
})
|
|
})
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
#### SSE with Multi-line Data
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
app.get('/sse-json', fn (mut c hono.Context) http.Response {
|
|
return hono.c_stream_sse(mut c, fn (mut stream hono.StreamContext) ! {
|
|
// Send JSON data (multi-line formatted)
|
|
json_data := '{\n "name": "v-hono",\n "version": "1.0.0"\n}'
|
|
stream.write_sse(hono.SSEEvent{
|
|
data: json_data
|
|
event: 'json'
|
|
id: '1'
|
|
})!
|
|
})
|
|
})
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
#### SSE with Error Handling
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
app.get('/sse-error', fn (mut c hono.Context) http.Response {
|
|
return hono.c_stream_sse(mut c, fn (mut stream hono.StreamContext) ! {
|
|
stream.write_sse(hono.SSEEvent{
|
|
data: 'Starting...'
|
|
event: 'start'
|
|
})!
|
|
|
|
// Simulate an error
|
|
return error('Something went wrong')
|
|
}, fn (err IError, mut stream hono.StreamContext) {
|
|
// Custom error handler
|
|
stream.write_sse(hono.SSEEvent{
|
|
data: 'Error: ${err.msg()}'
|
|
event: 'error'
|
|
}) or {}
|
|
})
|
|
})
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
#### SSE with Retry Field
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
app.get('/sse-retry', fn (mut c hono.Context) http.Response {
|
|
return hono.c_stream_sse(mut c, fn (mut stream hono.StreamContext) ! {
|
|
// Send event with retry field (client reconnects after 3 seconds)
|
|
stream.write_sse(hono.SSEEvent{
|
|
data: 'This event includes a retry field'
|
|
event: 'message'
|
|
id: '1'
|
|
retry: 3000 // 3 seconds
|
|
})!
|
|
})
|
|
})
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
#### StreamContext Methods
|
|
|
|
| Method | Description |
|
|
|--------|-------------|
|
|
| `write(data []u8)` | Write raw bytes to stream |
|
|
| `write_string(data string)` | Write string to stream |
|
|
| `writeln(data string)` | Write string with newline |
|
|
| `write_sse(event SSEEvent)` | Write SSE event |
|
|
| `sleep(ms int)` | Pause execution for milliseconds |
|
|
| `pipe(data []u8)` | Pipe data to stream |
|
|
| `on_abort(callback fn())` | Register abort callback |
|
|
| `close()` | Close the stream |
|
|
| `is_open()` | Check if stream is still open |
|
|
|
|
#### SSEEvent Fields
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `data` | `string` | Yes | Event data (supports multi-line) |
|
|
| `event` | `string` | No | Event type name |
|
|
| `id` | `string` | No | Event ID for client reconnection |
|
|
| `retry` | `int` | No | Reconnection interval in milliseconds |
|
|
|
|
#### Streaming Functions
|
|
|
|
| Function | Headers Set | Use Case |
|
|
|----------|-------------|----------|
|
|
| `c_stream()` | `Transfer-Encoding: chunked` | Binary data streaming |
|
|
| `c_stream_text()` | `Content-Type: text/plain`, `Transfer-Encoding: chunked`, `X-Content-Type-Options: nosniff` | Text streaming |
|
|
| `c_stream_sse()` | `Content-Type: text/event-stream`, `Cache-Control: no-cache`, `Connection: keep-alive` | Server-Sent Events |
|
|
|
|
#### Client-Side JavaScript Example
|
|
|
|
```html
|
|
<script>
|
|
// Connect to SSE endpoint
|
|
const eventSource = new EventSource('/sse');
|
|
|
|
// Listen for specific event types
|
|
eventSource.addEventListener('connect', (e) => {
|
|
console.log('Connected:', e.data);
|
|
});
|
|
|
|
eventSource.addEventListener('update', (e) => {
|
|
console.log('Update:', e.data, 'ID:', e.lastEventId);
|
|
});
|
|
|
|
eventSource.addEventListener('complete', (e) => {
|
|
console.log('Complete:', e.data);
|
|
eventSource.close();
|
|
});
|
|
|
|
// Handle errors
|
|
eventSource.onerror = (e) => {
|
|
console.error('SSE Error:', e);
|
|
};
|
|
</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 |
|
|
|
|
### 11. Multi-Cloud Storage
|
|
|
|
Built-in support for multiple storage backends including Local filesystem, AWS S3/MinIO, Aliyun OSS, and Tencent COS.
|
|
|
|
#### Storage Configuration
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
// Local storage configuration
|
|
local_config := hono.StorageConfig{
|
|
storage_type: .local
|
|
local: hono.LocalStorageConfig{
|
|
base_path: './storage'
|
|
url_prefix: '/files'
|
|
create_dirs: true
|
|
}
|
|
}
|
|
|
|
// S3/MinIO configuration
|
|
s3_config := hono.StorageConfig{
|
|
storage_type: .s3
|
|
s3: hono.S3Config{
|
|
endpoint: 'https://s3.amazonaws.com'
|
|
access_key: 'your-access-key'
|
|
secret_key: 'your-secret-key'
|
|
region: 'us-east-1'
|
|
default_bucket: 'my-bucket'
|
|
}
|
|
}
|
|
|
|
// Aliyun OSS configuration
|
|
oss_config := hono.StorageConfig{
|
|
storage_type: .aliyun_oss
|
|
aliyun_oss: hono.AliyunOSSConfig{
|
|
endpoint: 'oss-cn-hangzhou.aliyuncs.com'
|
|
access_key_id: 'your-access-key-id'
|
|
access_key_secret: 'your-access-key-secret'
|
|
default_bucket: 'my-bucket'
|
|
}
|
|
}
|
|
|
|
// Tencent COS configuration
|
|
cos_config := hono.StorageConfig{
|
|
storage_type: .tencent_cos
|
|
tencent_cos: hono.TencentCOSConfig{
|
|
secret_id: 'your-secret-id'
|
|
secret_key: 'your-secret-key'
|
|
region: 'ap-guangzhou'
|
|
default_bucket: 'my-bucket-1234567890'
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Using FileService
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// Create FileService with local storage
|
|
mut fs := hono.new_file_service(hono.FileServiceConfig{
|
|
storage: hono.StorageConfig{
|
|
storage_type: .local
|
|
local: hono.LocalStorageConfig{
|
|
base_path: './uploads'
|
|
}
|
|
}
|
|
db_path: './storage/files.db'
|
|
default_bucket: 'default'
|
|
chunk_size: 5 * 1024 * 1024 // 5MB chunks
|
|
}) or {
|
|
eprintln('Failed to create FileService: ${err}')
|
|
return
|
|
}
|
|
|
|
// Register file routes
|
|
fs.register_routes(mut app, '/api/files')
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
#### Direct Storage Provider Usage
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
// Create local storage provider
|
|
mut storage := hono.new_local_storage(hono.LocalStorageConfig{
|
|
base_path: './storage'
|
|
}) or {
|
|
eprintln('Failed to create storage: ${err}')
|
|
return
|
|
}
|
|
|
|
// Upload a file
|
|
result := storage.upload('my-bucket', 'test.txt', 'Hello, World!'.bytes(), 'text/plain') or {
|
|
eprintln('Upload failed: ${err}')
|
|
return
|
|
}
|
|
println('Uploaded: ${result.object_key}')
|
|
|
|
// Download a file
|
|
data := storage.download('my-bucket', 'test.txt') or {
|
|
eprintln('Download failed: ${err}')
|
|
return
|
|
}
|
|
println('Downloaded: ${data.bytestr()}')
|
|
|
|
// Check if file exists
|
|
exists := storage.exists('my-bucket', 'test.txt') or { false }
|
|
println('Exists: ${exists}')
|
|
|
|
// Generate presigned URL
|
|
url := storage.presign_url('my-bucket', 'test.txt', hono.PresignOptions{
|
|
expires_in: 3600 // 1 hour
|
|
method: 'GET'
|
|
}) or {
|
|
eprintln('Presign failed: ${err}')
|
|
return
|
|
}
|
|
println('Presigned URL: ${url}')
|
|
|
|
// Delete a file
|
|
storage.delete('my-bucket', 'test.txt') or {
|
|
eprintln('Delete failed: ${err}')
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Multipart Upload for Large Files
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut storage := hono.new_s3_storage(hono.S3Config{
|
|
endpoint: 'https://s3.amazonaws.com'
|
|
access_key: 'your-access-key'
|
|
secret_key: 'your-secret-key'
|
|
region: 'us-east-1'
|
|
default_bucket: 'my-bucket'
|
|
}) or {
|
|
eprintln('Failed to create S3 storage: ${err}')
|
|
return
|
|
}
|
|
|
|
// Initialize multipart upload
|
|
upload_id := storage.init_multipart('my-bucket', 'large-file.zip', 'application/zip') or {
|
|
eprintln('Init multipart failed: ${err}')
|
|
return
|
|
}
|
|
|
|
// Upload parts (in real usage, read from file in chunks)
|
|
mut parts := []hono.PartInfo{}
|
|
chunk_data := []u8{len: 5 * 1024 * 1024, init: 0} // 5MB chunk
|
|
|
|
for i in 1 .. 4 {
|
|
etag := storage.upload_part('my-bucket', 'large-file.zip', upload_id, i, chunk_data) or {
|
|
// Abort on failure
|
|
storage.abort_multipart('my-bucket', 'large-file.zip', upload_id) or {}
|
|
eprintln('Upload part ${i} failed: ${err}')
|
|
return
|
|
}
|
|
parts << hono.PartInfo{
|
|
part_number: i
|
|
etag: etag
|
|
size: chunk_data.len
|
|
}
|
|
}
|
|
|
|
// Complete multipart upload
|
|
result := storage.complete_multipart('my-bucket', 'large-file.zip', upload_id, parts) or {
|
|
eprintln('Complete multipart failed: ${err}')
|
|
return
|
|
}
|
|
println('Upload completed: ${result.object_key}')
|
|
}
|
|
```
|
|
|
|
#### HTTP Handlers for File Operations
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
mut fs := hono.new_file_service(hono.FileServiceConfig{
|
|
storage: hono.StorageConfig{
|
|
storage_type: .local
|
|
local: hono.LocalStorageConfig{
|
|
base_path: './uploads'
|
|
}
|
|
}
|
|
}) or {
|
|
eprintln('Failed to create FileService: ${err}')
|
|
return
|
|
}
|
|
|
|
// File upload endpoint
|
|
app.post('/upload', fn [mut fs] (mut c hono.Context) http.Response {
|
|
return fs.handle_upload(mut c)
|
|
})
|
|
|
|
// Chunked upload endpoints
|
|
app.post('/upload/chunk', fn [mut fs] (mut c hono.Context) http.Response {
|
|
return fs.handle_chunk_upload(mut c)
|
|
})
|
|
|
|
app.post('/upload/complete', fn [mut fs] (mut c hono.Context) http.Response {
|
|
return fs.handle_chunk_complete(mut c)
|
|
})
|
|
|
|
// File download endpoint
|
|
app.get('/files/:uuid', fn [mut fs] (mut c hono.Context) http.Response {
|
|
return fs.handle_download(mut c)
|
|
})
|
|
|
|
// File delete endpoint
|
|
app.delete('/files/:uuid', fn [mut fs] (mut c hono.Context) http.Response {
|
|
return fs.handle_delete(mut c)
|
|
})
|
|
|
|
// List files endpoint
|
|
app.get('/files', fn [fs] (mut c hono.Context) http.Response {
|
|
return fs.handle_list(mut c)
|
|
})
|
|
|
|
// Get presigned URL endpoint
|
|
app.get('/files/:uuid/presign', fn [mut fs] (mut c hono.Context) http.Response {
|
|
return fs.handle_presign(mut c)
|
|
})
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
#### Switching Storage Providers at Runtime
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut fs := hono.new_file_service(hono.FileServiceConfig{
|
|
storage: hono.StorageConfig{
|
|
storage_type: .local
|
|
local: hono.LocalStorageConfig{
|
|
base_path: './uploads'
|
|
}
|
|
}
|
|
}) or {
|
|
eprintln('Failed to create FileService: ${err}')
|
|
return
|
|
}
|
|
|
|
// Switch to S3
|
|
fs.switch_to_s3(
|
|
'https://s3.amazonaws.com',
|
|
'access-key',
|
|
'secret-key',
|
|
'my-bucket'
|
|
) or {
|
|
eprintln('Failed to switch to S3: ${err}')
|
|
}
|
|
|
|
// Switch to Aliyun OSS
|
|
fs.switch_to_aliyun_oss(
|
|
'oss-cn-hangzhou.aliyuncs.com',
|
|
'access-key-id',
|
|
'access-key-secret',
|
|
'my-bucket'
|
|
) or {
|
|
eprintln('Failed to switch to OSS: ${err}')
|
|
}
|
|
|
|
// Switch to Tencent COS
|
|
fs.switch_to_tencent_cos(
|
|
'secret-id',
|
|
'secret-key',
|
|
'ap-guangzhou',
|
|
'my-bucket-1234567890'
|
|
) or {
|
|
eprintln('Failed to switch to COS: ${err}')
|
|
}
|
|
|
|
// Switch back to local
|
|
fs.switch_to_local('./uploads') or {
|
|
eprintln('Failed to switch to local: ${err}')
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Storage Provider Interface
|
|
|
|
| Method | Description |
|
|
|--------|-------------|
|
|
| `upload(bucket, key, data, content_type)` | Upload file data |
|
|
| `upload_stream(bucket, key, reader, size, content_type)` | Upload from stream |
|
|
| `download(bucket, key)` | Download file data |
|
|
| `download_stream(bucket, key, writer)` | Download to stream |
|
|
| `delete(bucket, key)` | Delete a file |
|
|
| `exists(bucket, key)` | Check if file exists |
|
|
| `head(bucket, key)` | Get file metadata |
|
|
| `copy(src_bucket, src_key, dst_bucket, dst_key)` | Copy a file |
|
|
| `list(bucket, options)` | List files in bucket |
|
|
| `presign_url(bucket, key, options)` | Generate presigned URL |
|
|
| `init_multipart(bucket, key, content_type)` | Initialize multipart upload |
|
|
| `upload_part(bucket, key, upload_id, part_number, data)` | Upload a part |
|
|
| `complete_multipart(bucket, key, upload_id, parts)` | Complete multipart upload |
|
|
| `abort_multipart(bucket, key, upload_id)` | Abort multipart upload |
|
|
| `create_bucket(bucket)` | Create a bucket |
|
|
| `delete_bucket(bucket)` | Delete a bucket |
|
|
| `bucket_exists(bucket)` | Check if bucket exists |
|
|
| `provider_name()` | Get provider name |
|
|
|
|
#### Error Handling and Retry
|
|
|
|
```v
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
// Configure retry behavior
|
|
config := hono.StorageConfig{
|
|
storage_type: .s3
|
|
s3: hono.S3Config{
|
|
endpoint: 'https://s3.amazonaws.com'
|
|
access_key: 'your-access-key'
|
|
secret_key: 'your-secret-key'
|
|
region: 'us-east-1'
|
|
default_bucket: 'my-bucket'
|
|
}
|
|
retry_count: 3 // Retry up to 3 times
|
|
retry_delay_ms: 1000 // Initial delay 1 second
|
|
timeout_ms: 30000 // 30 second timeout
|
|
}
|
|
|
|
// Error types are automatically classified
|
|
// Retryable: network_timeout, service_unavailable, rate_limited
|
|
// Non-retryable: invalid_credentials, access_denied, object_not_found
|
|
}
|
|
```
|
|
|
|
## Route Grouping
|
|
|
|
```v
|
|
import net.http
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// Create API sub-application
|
|
mut api := hono.Hono.new()
|
|
|
|
api.get('/users', fn (mut c hono.Context) http.Response {
|
|
return c.json('[{"id": 1, "name": "Alice"}]')
|
|
})
|
|
|
|
api.get('/users/:id', fn (mut c hono.Context) http.Response {
|
|
user_id := c.params['id'] or { 'unknown' }
|
|
return c.json('{"id": ${user_id}}')
|
|
})
|
|
|
|
// Mount API routes under /api prefix
|
|
app.route('/api', mut api)
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
## Static File Serving
|
|
|
|
```v
|
|
import net.http
|
|
import meiseayoung.hono
|
|
|
|
fn main() {
|
|
mut app := hono.Hono.new()
|
|
|
|
// Serve static files from ./public directory
|
|
app.use(hono.serve_static(hono.StaticOptions{
|
|
root: './public'
|
|
path: '/static'
|
|
}))
|
|
|
|
app.listen(':3000')
|
|
}
|
|
```
|
|
|
|
## API Reference
|
|
|
|
### Hono
|
|
|
|
- `Hono.new()` - Create a new Hono application
|
|
- `app.get(path, handler)` - Register GET route
|
|
- `app.post(path, handler)` - Register POST route
|
|
- `app.put(path, handler)` - Register PUT route
|
|
- `app.delete(path, handler)` - Register DELETE route
|
|
- `app.patch(path, handler)` - Register PATCH route
|
|
- `app.all(path, handler)` - Register route for all methods
|
|
- `app.use(middleware)` - Add middleware
|
|
- `app.route(prefix, subapp)` - Mount sub-application
|
|
- `app.listen(port)` - Start server
|
|
|
|
### Context
|
|
|
|
- `c.text(data)` - Return text response
|
|
- `c.json(data)` - Return JSON response
|
|
- `c.html(data)` - Return HTML response
|
|
- `c.file(path)` - Return file response
|
|
- `c.redirect(url, status_code...)` - Redirect to URL (default status: 302)
|
|
- `c.status(code)` - Set response status code
|
|
- `c.params` - Route parameters
|
|
- `c.query` - Query parameters
|
|
- `c.body` - Request body
|
|
- `c.headers` - Response headers
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
v-hono/
|
|
├── app.v # Main application and routing
|
|
├── router.v # Hybrid router implementation
|
|
├── fast_router.v # Fast router with precompiled regex
|
|
├── trie_router.v # Trie-based router
|
|
├── cache.v # LRU cache implementation
|
|
├── request.v # Request context
|
|
├── response.v # Response utilities
|
|
├── static.v # Static file serving
|
|
├── security.v # Security utilities
|
|
├── config.v # Configuration management
|
|
├── logger.v # Logging system
|
|
├── multipart.v # Multipart form parsing
|
|
├── upload.v # File upload handling
|
|
├── database.v # Database integration
|
|
├── auth.v # Authentication system
|
|
├── auth_routes.v # Auth route handlers
|
|
├── error_handler.v # Error handling
|
|
├── middleware.v # Middleware exports and utilities
|
|
├── cors.v # CORS middleware
|
|
├── cookie.v # Cookie helper
|
|
├── jwt.v # JWT middleware
|
|
├── bearer_auth.v # Bearer auth middleware
|
|
├── compress.v # Compression middleware
|
|
├── rate_limit.v # Rate limiting middleware
|
|
├── validator.v # Request validation
|
|
├── websocket.v # WebSocket helper (RFC 6455)
|
|
├── streaming.v # SSE streaming helper
|
|
├── swagger.v # Swagger UI middleware
|
|
├── openapi.v # OpenAPI 3.0/3.1 data structures and builder
|
|
├── storage_interface.v # Unified storage provider interface
|
|
├── storage_config.v # Storage configuration types
|
|
├── storage_errors.v # Storage error types
|
|
├── local_storage.v # Local filesystem storage provider
|
|
├── s3_storage.v # S3/MinIO storage provider
|
|
├── aliyun_oss.v # Aliyun OSS storage provider
|
|
├── tencent_cos.v # Tencent COS storage provider
|
|
├── file_service.v # High-level file service
|
|
├── chunk_manager.v # Chunk upload manager
|
|
├── http_handlers.v # Storage HTTP handlers
|
|
├── retry.v # Retry mechanism with exponential backoff
|
|
├── picoev_server.v # Picoev backend with WebSocket/SSE support
|
|
├── usockets_server.v # uSockets backend with WebSocket/SSE support
|
|
├── v.mod # Module definition
|
|
├── README.md # Documentation
|
|
├── examples/ # Example applications
|
|
├── tests/ # Test files
|
|
└── docs/ # Additional documentation
|
|
```
|
|
|
|
## Examples
|
|
|
|
See the `examples/` directory for more examples:
|
|
|
|
- `examples/basic/` - Basic usage
|
|
- `examples/middleware/` - Middleware usage
|
|
- `examples/middleware_demo.v` - Built-in middleware demonstration
|
|
- `examples/route_grouping/` - Route grouping
|
|
- `examples/redirect_demo.v` - Redirect functionality examples
|
|
- `examples/swagger_demo.v` - Swagger UI and OpenAPI documentation
|
|
|
|
## License
|
|
|
|
MIT License
|
|
|
|
|