diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..278c149 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,245 @@ +# 贡献指南 + +感谢你对 Zoox 框架的兴趣!我们欢迎各种形式的贡献,包括代码、文档、示例、问题报告和功能建议。 + +## 🤝 贡献方式 + +### 1. 文档改进 +- 修正错误和拼写 +- 完善示例代码 +- 添加新的使用场景 +- 翻译文档 + +### 2. 示例代码 +- 创建新的示例应用 +- 改进现有示例 +- 添加最佳实践示例 +- 性能优化示例 + +### 3. 问题报告 +- 报告文档中的错误 +- 提出改进建议 +- 请求新功能文档 + +### 4. 代码贡献 +- 修复 bug +- 添加新功能 +- 性能优化 +- 测试覆盖率提升 + +## 📋 贡献流程 + +### 1. 准备工作 + +```bash +# 1. Fork 仓库到你的 GitHub 账户 + +# 2. 克隆你的 fork +git clone https://github.com/your-username/zoox.git +cd zoox + +# 3. 添加上游仓库 +git remote add upstream https://github.com/go-zoox/zoox.git + +# 4. 创建开发分支 +git checkout -b feature/your-feature-name +``` + +### 2. 开发规范 + +#### 代码风格 +```bash +# 使用 gofmt 格式化代码 +go fmt ./... + +# 使用 golint 检查代码 +golint ./... + +# 使用 go vet 检查代码 +go vet ./... +``` + +#### 提交规范 +使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范: + +``` +[optional scope]: + +[optional body] + +[optional footer(s)] +``` + +**类型 (type):** +- `feat`: 新功能 +- `fix`: 修复 bug +- `docs`: 文档更改 +- `style`: 代码格式(不影响代码运行的变动) +- `refactor`: 重构(既不是新增功能,也不是修复 bug 的代码变动) +- `test`: 增加测试 +- `chore`: 构建过程或辅助工具的变动 + +**示例:** +``` +feat(middleware): add rate limiting middleware +fix(router): fix path parameter parsing bug +docs(readme): update installation instructions +test(context): add tests for context binding methods +``` + +### 3. 测试要求 + +```bash +# 运行所有测试 +go test ./... + +# 运行测试并生成覆盖率报告 +go test -cover ./... + +# 运行特定包的测试 +go test ./middleware +``` + +#### 测试指南 +- 所有新功能必须包含测试 +- 测试覆盖率应保持在 80% 以上 +- 测试应该清晰、可读且可维护 +- 使用表驱动测试处理多种情况 + +### 4. 文档要求 + +#### 代码文档 +```go +// ExampleFunction 演示如何使用某个功能 +// 参数 param1 用于... +// 返回值表示... +func ExampleFunction(param1 string) error { + // 实现... +} +``` + +#### README 和文档 +- 所有新功能必须在文档中说明 +- 提供清晰的使用示例 +- 更新相关的 API 参考 +- 确保文档与代码同步 + +### 5. 提交和审查 + +```bash +# 1. 提交更改 +git add . +git commit -m "feat(middleware): add rate limiting middleware" + +# 2. 推送到你的 fork +git push origin feature/your-feature-name + +# 3. 创建 Pull Request +# 在 GitHub 上创建 PR,详细描述你的更改 +``` + +#### PR 要求 +- 清晰的标题和描述 +- 关联相关的 issue +- 包含测试和文档 +- 通过所有 CI 检查 + +## 🎯 贡献重点领域 + +### 高优先级 +1. **测试覆盖率提升** + - 为现有功能添加测试 + - 边界情况测试 + - 性能测试 + +2. **文档完善** + - API 文档补充 + - 使用示例增加 + - 最佳实践指南 + +3. **性能优化** + - 路由性能优化 + - 内存使用优化 + - 并发安全改进 + +### 中等优先级 +1. **中间件扩展** + - 新的中间件开发 + - 现有中间件改进 + - 中间件文档 + +2. **示例应用** + - 真实场景示例 + - 集成示例 + - 部署示例 + +### 低优先级 +1. **工具改进** + - 开发工具优化 + - 构建脚本改进 + - CI/CD 优化 + +## 📝 问题报告 + +### 报告 Bug +使用 [Bug 报告模板](https://github.com/go-zoox/zoox/issues/new?template=bug_report.md) + +**包含信息:** +- Go 版本 +- Zoox 版本 +- 操作系统 +- 重现步骤 +- 期望行为 +- 实际行为 +- 错误日志 + +### 功能请求 +使用 [功能请求模板](https://github.com/go-zoox/zoox/issues/new?template=feature_request.md) + +**包含信息:** +- 功能描述 +- 使用场景 +- 预期 API 设计 +- 相关资源 + +## 🏆 贡献者认可 + +### 贡献者列表 +所有贡献者都会在 CONTRIBUTORS.md 中列出。 + +### 贡献统计 +- 代码贡献 +- 文档贡献 +- 问题报告 +- 功能建议 + +## 📞 联系方式 + +### 讨论 +- GitHub Discussions +- Issues +- Pull Requests + +### 社区 +- Slack/Discord(如果有) +- 邮件列表 +- 社交媒体 + +## 📜 行为准则 + +我们致力于营造一个开放、友好的社区环境: + +1. **尊重他人** - 尊重不同的观点和经验 +2. **建设性反馈** - 提供有建设性的批评和建议 +3. **协作精神** - 共同努力改进项目 +4. **包容性** - 欢迎所有背景的贡献者 +5. **专业性** - 保持专业和礼貌的交流 + +## 🎉 开始贡献 + +1. 浏览 [Issues](https://github.com/go-zoox/zoox/issues) 寻找合适的任务 +2. 查看 [Good First Issue](https://github.com/go-zoox/zoox/labels/good%20first%20issue) 标签 +3. 阅读相关文档和代码 +4. 开始你的第一个贡献! + +感谢你的贡献!🚀 \ No newline at end of file diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md new file mode 100644 index 0000000..2a252ce --- /dev/null +++ b/DOCUMENTATION.md @@ -0,0 +1,300 @@ +# Zoox Framework Documentation + +Zoox is a high-performance, feature-rich web framework for Go that combines simplicity with powerful capabilities. Built with modern web development practices in mind, Zoox provides everything you need to build robust web applications and APIs. + +## 🚀 Quick Start + +### Installation + +```bash +go get github.com/go-zoox/zoox +``` + +### Hello World + +```go +package main + +import "github.com/go-zoox/zoox" + +func main() { + app := zoox.Default() + + app.Get("/", func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "message": "Hello, Zoox!", + }) + }) + + app.Run(":8080") +} +``` + +### 📚 Learning Resources + +#### 🎓 Tutorials +- **[Getting Started Tutorial](./tutorials/01-getting-started.md)** - Your first Zoox application +- **[Complete Tutorial Series](./tutorials/README.md)** - Comprehensive learning path from beginner to advanced +- **[Interactive Examples](./examples/README.md)** - Hands-on examples with working code + +#### 📖 Quick Links +- [Core Concepts](#core-concepts) - Understanding Zoox fundamentals +- [API Reference](#api-reference) - Complete API documentation +- [Examples Gallery](#examples-gallery) - Real-world examples +- [Best Practices](#best-practices) - Production-ready patterns + +## 📖 Table of Contents + +1. [Core Concepts](#core-concepts) +2. [Application](#application) +3. [Routing](#routing) +4. [Context](#context) +5. [Middleware](#middleware) +6. [Request Handling](#request-handling) +7. [Response Handling](#response-handling) +8. [Template Engine](#template-engine) +9. [Static Files](#static-files) +10. [WebSocket](#websocket) +11. [JSON-RPC](#json-rpc) +12. [Proxy](#proxy) +13. [Components](#components) +14. [Configuration](#configuration) +15. [Deployment](#deployment) +16. [Best Practices](#best-practices) +17. [Examples Gallery](#examples-gallery) +18. [API Reference](#api-reference) + +## 🎯 Examples Gallery + +### 🔰 Beginner Examples + +#### [Basic Server](./examples/01-basic-server/) +Complete REST API with authentication, route groups, and error handling. + +```go +app := zoox.Default() + +// Basic routes +app.Get("/", homeHandler) +app.Get("/users", getUsersHandler) +app.Post("/users", createUserHandler) + +// API group with middleware +api := app.Group("/api/v1") +api.Use(middleware.RequestID()) +api.Use(middleware.Logger()) +``` + +#### [Middleware Showcase](./examples/02-middleware-showcase/) +Comprehensive demonstration of all built-in middleware with custom implementations. + +```go +// Global middleware +app.Use(middleware.Logger()) +app.Use(middleware.Recovery()) +app.Use(middleware.CORS()) + +// Route-specific middleware +app.Get("/protected", middleware.BasicAuth("admin", "secret"), protectedHandler) +``` + +### 🚀 Intermediate Examples + +#### [WebSocket Chat](./examples/03-websocket-chat/) +Real-time chat application with connection management and message broadcasting. + +```go +// WebSocket endpoint +app.WebSocket("/ws", func(ctx *zoox.Context) { + conn := ctx.WebSocket() + + for { + var msg Message + if err := conn.ReadJSON(&msg); err != nil { + break + } + + // Broadcast to all clients + chatRoom.Broadcast(msg) + } +}) +``` + +#### [File Upload System](./examples/04-file-upload-download/) +Complete file management system with upload, download, validation, and progress tracking. + +```go +// Single file upload +app.Post("/upload/single", func(ctx *zoox.Context) { + file, err := ctx.FormFile("file") + if err != nil { + ctx.JSON(400, zoox.H{"error": "No file uploaded"}) + return + } + + // Validate and save file + if err := validateFile(file); err != nil { + ctx.JSON(400, zoox.H{"error": err.Error()}) + return + } + + filename := generateUniqueFilename(file.Filename) + ctx.SaveFile(file, filepath.Join(uploadDir, filename)) + + ctx.JSON(200, zoox.H{ + "message": "File uploaded successfully", + "filename": filename, + }) +}) +``` + +#### [JSON-RPC Service](./examples/05-json-rpc-service/) +Professional JSON-RPC implementation with multiple services and interactive testing. + +```go +// Math service +type MathService struct{} + +func (m *MathService) Add(ctx context.Context, args *AddArgs, reply *AddReply) error { + reply.Result = args.A + args.B + return nil +} + +// Register service +app.JSONRPC("/rpc/math", &MathService{}) +``` + +### 🏗️ Advanced Examples + +#### [Production API](./examples/06-production-api/) +Production-ready API with comprehensive security, monitoring, and deployment patterns. + +```go +// Production-ready application structure +type App struct { + config *Config + router *zoox.Application +} + +// Comprehensive middleware stack +func (app *App) setupMiddleware() { + app.router.Use(middleware.Logger()) + app.router.Use(middleware.Recovery()) + app.router.Use(middleware.RequestID()) + app.router.Use(middleware.CORS()) + app.router.Use(middleware.Helmet()) + app.router.Use(middleware.RateLimit(100, time.Minute)) + app.router.Use(app.metricsMiddleware()) +} + +// Health check endpoint +func (app *App) healthHandler(ctx *zoox.Context) { + health := HealthCheck{ + Status: "healthy", + Version: "1.0.0", + Timestamp: time.Now(), + Services: map[string]string{ + "database": "connected", + "cache": "connected", + }, + } + ctx.JSON(200, health) +} +``` + +#### Database Integration *(Coming Soon)* +Complete database integration with ORM, migrations, and connection pooling. + +#### Authentication System *(Coming Soon)* +Full authentication system with JWT, session management, and RBAC. + +#### Microservices Architecture *(Coming Soon)* +Microservice implementation with service discovery and load balancing. + +## 📚 Tutorial Series + +### 🎯 Learning Paths + +#### Path 1: Web Development Beginner +1. [Getting Started](./tutorials/01-getting-started.md) - Your first Zoox app +2. [Routing Fundamentals](./tutorials/02-routing-fundamentals.md) - Master routing +3. [Request/Response Handling](./tutorials/03-request-response-handling.md) - HTTP basics +4. [Middleware Basics](./tutorials/04-middleware-basics.md) - Understanding middleware +5. [Template Engine](./tutorials/06-template-engine.md) - HTML rendering +6. [Static Files](./tutorials/07-static-files-assets.md) - Serving assets + +#### Path 2: API Development +1. [Getting Started](./tutorials/01-getting-started.md) - Foundation +2. [Routing Fundamentals](./tutorials/02-routing-fundamentals.md) - API routes +3. [JSON-RPC Services](./tutorials/09-json-rpc-services.md) - RPC APIs +4. [Authentication](./tutorials/10-authentication-authorization.md) - Security +5. [Database Integration](./tutorials/11-database-integration.md) - Data layer + +#### Path 3: Real-time Applications +1. [Getting Started](./tutorials/01-getting-started.md) - Foundation +2. [WebSocket Development](./tutorials/08-websocket-development.md) - Real-time +3. [Caching Strategies](./tutorials/12-caching-strategies.md) - Performance +4. [Monitoring](./tutorials/13-monitoring-logging.md) - Observability + +### 🚀 Quick Tutorial Access + +- **⏱️ 5 Minutes**: [Hello World](./tutorials/01-getting-started.md#step-2-create-your-first-server) +- **⏱️ 15 Minutes**: [Basic REST API](./tutorials/01-getting-started.md#step-6-adding-more-routes) +- **⏱️ 30 Minutes**: [Complete Tutorial](./tutorials/01-getting-started.md) +- **⏱️ 2 Hours**: [Full Learning Path](./tutorials/README.md) + +## 🤝 Contributing + +We welcome contributions to improve Zoox! Here's how you can help: + +### 📝 Documentation +- Improve existing documentation +- Add new examples and tutorials +- Translate documentation +- Fix typos and errors + +### 💻 Code +- Fix bugs and issues +- Add new features +- Improve performance +- Write tests + +### 🎯 Examples +- Create new example applications +- Improve existing examples +- Add interactive demos +- Document use cases + +### 📋 Contribution Guidelines + +1. **Fork the repository** +2. **Create a feature branch** +3. **Make your changes** +4. **Add tests** (if applicable) +5. **Update documentation** +6. **Submit a pull request** + +For detailed contribution guidelines, see [CONTRIBUTING.md](./CONTRIBUTING.md). + +## 📞 Support + +### 🐛 Issues and Bugs +- [GitHub Issues](https://github.com/go-zoox/zoox/issues) +- [Bug Report Template](https://github.com/go-zoox/zoox/issues/new?template=bug_report.md) + +### 💡 Feature Requests +- [Feature Request Template](https://github.com/go-zoox/zoox/issues/new?template=feature_request.md) +- [Discussions](https://github.com/go-zoox/zoox/discussions) + +### 📚 Documentation +- [API Reference](#api-reference) +- [Examples](./examples/README.md) +- [Tutorials](./tutorials/README.md) + +### 🌟 Community +- [GitHub Discussions](https://github.com/go-zoox/zoox/discussions) +- [Contributing Guide](./CONTRIBUTING.md) + +--- + +**Ready to build amazing applications with Zoox? Start with our [Getting Started Tutorial](./tutorials/01-getting-started.md) or explore our [Examples Gallery](./examples/README.md)!** 🚀 \ No newline at end of file diff --git a/README.md b/README.md index c032f33..e7acbae 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,16 @@ -# Zoox - A Lightweight Web Framework +# Zoox Framework - Documentation and Examples -[![PkgGoDev](https://pkg.go.dev/badge/github.com/go-zoox/zoox)](https://pkg.go.dev/github.com/go-zoox/zoox) -[![Build Status](https://github.com/go-zoox/zoox/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/go-zoox/zoox/actions/workflows/ci.yml) -[![Go Report Card](https://goreportcard.com/badge/github.com/go-zoox/zoox)](https://goreportcard.com/report/github.com/go-zoox/zoox) -[![Coverage Status](https://coveralls.io/repos/github/go-zoox/zoox/badge.svg?branch=master)](https://coveralls.io/github/go-zoox/zoox?branch=master) -[![GitHub issues](https://img.shields.io/github/issues/go-zoox/zoox.svg)](https://github.com/go-zoox/zoox/issues) -[![Release](https://img.shields.io/github/tag/go-zoox/zoox.svg?label=Release)](https://github.com/go-zoox/zoox/tags) +A comprehensive collection of documentation, examples, and tutorials for the Zoox Go web framework. -## Installation -To install the package, run: +## 🚀 Quick Start + +### Installation ```bash go get github.com/go-zoox/zoox ``` -## Getting Started +### Hello World ```go package main @@ -23,33 +19,256 @@ import "github.com/go-zoox/zoox" func main() { app := zoox.Default() - + app.Get("/", func(ctx *zoox.Context) { - ctx.Write([]byte("helloworld")) + ctx.JSON(200, zoox.H{ + "message": "Hello, Zoox!", + }) }) - + app.Run(":8080") } ``` -## DevTools +### Run the Application ```bash -# install -go install github.com/go-zoox/zoox/cmd/zoox@latest +go run main.go ``` -```bash -# dev -zoox dev +Visit `http://localhost:8080` to see the result. + +## 📚 Learning Resources + +### 🎓 Tutorials +- **[Getting Started Tutorial](./tutorials/01-getting-started.md)** - Your first Zoox application +- **[Complete Tutorial Series](./tutorials/README.md)** - 18 comprehensive tutorials from beginner to production +- **[Learning Paths](./tutorials/README.md#learning-paths)** - Structured learning paths for different goals + +### 💡 Examples +- **[Examples Gallery](./examples/README.md)** - 5 complete, runnable examples +- **[Basic Server](./examples/01-basic-server/)** - REST API with authentication +- **[Middleware Showcase](./examples/02-middleware-showcase/)** - All built-in middleware +- **[WebSocket Chat](./examples/03-websocket-chat/)** - Real-time chat application +- **[File Upload System](./examples/04-file-upload-download/)** - Complete file management +- **[JSON-RPC Service](./examples/05-json-rpc-service/)** - Professional RPC implementation + +### 📖 Documentation +- **[DOCUMENTATION.md](./DOCUMENTATION.md)** - Complete framework documentation +- **[API Reference](./DOCUMENTATION.md#api-reference)** - Full API documentation +- **[Best Practices](./DOCUMENTATION.md#best-practices)** - Production-ready patterns + +## 🎯 Key Features + +### 🚀 High Performance +- Efficient radix tree-based routing +- Middleware caching optimization +- Zero-allocation path parameter parsing + +### 🔧 Rich Middleware +- Logging, Recovery, CORS +- Authentication, Rate Limiting, Caching +- Monitoring integration (Prometheus, Sentry) + +### 🌐 Multi-Protocol Support +- HTTP/HTTPS +- WebSocket +- JSON-RPC +- Reverse Proxy + +### 📦 Component Architecture +- Caching system +- Message queues +- Scheduled tasks +- Internationalization support + +## 🛤️ Learning Paths + +### 🔰 Beginner Path +1. **[Getting Started](./tutorials/01-getting-started.md)** - Installation and first app +2. **[Basic Server Example](./examples/01-basic-server/)** - Complete REST API +3. **[Routing Fundamentals](./tutorials/02-routing-fundamentals.md)** - Master routing +4. **[Middleware Basics](./tutorials/04-middleware-basics.md)** - Understanding middleware + +### 🚀 Intermediate Path +1. **[WebSocket Development](./tutorials/08-websocket-development.md)** - Real-time features +2. **[WebSocket Chat Example](./examples/03-websocket-chat/)** - Working chat app +3. **[File Upload System](./examples/04-file-upload-download/)** - File management +4. **[JSON-RPC Services](./tutorials/09-json-rpc-services.md)** - RPC patterns + +### 🏗️ Advanced Path +1. **[Authentication & Authorization](./tutorials/10-authentication-authorization.md)** - Security +2. **[Database Integration](./tutorials/11-database-integration.md)** - Data layer +3. **[Microservices Architecture](./tutorials/18-microservices-architecture.md)** - Scaling +4. **[Production Deployment](./tutorials/17-deployment-strategies.md)** - Going live + +## 🎨 Example Applications + +### 🔰 Beginner Examples + +#### [01-basic-server](./examples/01-basic-server/) +Complete REST API with CRUD operations, authentication, and middleware. + +```go +app := zoox.Default() + +// User routes +app.Get("/users", getUsersHandler) +app.Post("/users", createUserHandler) + +// Protected routes +protected := app.Group("/api/v1/protected") +protected.Use(middleware.BasicAuth("admin", "secret123")) +protected.Get("/dashboard", dashboardHandler) ``` -```bash -# build -zoox build +#### [02-middleware-showcase](./examples/02-middleware-showcase/) +Comprehensive demonstration of all built-in middleware. + +```go +// Security middleware +app.Use(middleware.CORS()) +app.Use(middleware.Helmet()) +app.Use(middleware.RateLimit()) + +// Performance middleware +app.Use(middleware.Gzip()) +app.Use(middleware.Cache()) ``` -```bash +### 🚀 Intermediate Examples + +#### [03-websocket-chat](./examples/03-websocket-chat/) +Real-time chat application with WebSocket support. + +```go +app.WebSocket("/ws", func(ctx *zoox.Context) { + conn := ctx.WebSocket() + + for { + var msg Message + if err := conn.ReadJSON(&msg); err != nil { + break + } + + chatRoom.Broadcast(msg) + } +}) +``` + +#### [04-file-upload-download](./examples/04-file-upload-download/) +Complete file management system with upload, download, and validation. + +```go +app.Post("/upload/single", func(ctx *zoox.Context) { + file, err := ctx.FormFile("file") + if err != nil { + ctx.JSON(400, zoox.H{"error": "No file uploaded"}) + return + } + + // Validate and save file + filename := generateUniqueFilename(file.Filename) + ctx.SaveFile(file, filepath.Join(uploadDir, filename)) + + ctx.JSON(200, zoox.H{ + "message": "File uploaded successfully", + "filename": filename, + }) +}) +``` + +#### [05-json-rpc-service](./examples/05-json-rpc-service/) +Professional JSON-RPC implementation with multiple services. + +```go +type MathService struct{} + +func (m *MathService) Add(ctx context.Context, args *AddArgs, reply *AddReply) error { + reply.Result = args.A + args.B + return nil +} + +// Register service +app.JSONRPC("/rpc/math", &MathService{}) +``` + +## 📋 Documentation Structure + +``` +📚 Documentation Modules +├── 🚀 Quick Start - Installation and first application +├── 🏗️ Core Concepts - Application and Context +├── 🛣️ Routing System - Basic routing, parameters, route groups +├── 🔧 Middleware - Built-in and custom middleware +├── 📥 Request Handling - Data retrieval, binding, file uploads +├── 📤 Response Handling - Various response types and error handling +├── 🎨 Template Engine - Template setup and rendering +├── 📁 Static Files - File serving and caching +├── 🔌 WebSocket - Real-time communication +├── 🌐 JSON-RPC - Remote procedure calls +├── 🔄 Proxy Service - Reverse proxy configuration +├── 📦 Advanced Components - Cache, queues, scheduled tasks +├── ⚙️ Configuration - Environment variables and app configuration +├── 🚀 Deployment Guide - Development and production deployment +├── 💡 Best Practices - Project structure and development standards +└── 📋 API Reference - Complete API documentation +``` + +## 🎯 Features Covered + +### Basic Features +- ✅ Basic routing setup +- ✅ Middleware usage +- ✅ Parameter handling +- ✅ Form and JSON data processing +- ✅ Health check endpoints + +### Advanced Features +- ✅ WebSocket real-time communication +- ✅ JSON-RPC services +- ✅ Proxy service configuration +- ✅ Caching system +- ✅ Scheduled tasks +- ✅ File upload and download + +### Production Features +- ✅ Authentication and authorization +- ✅ Rate limiting and security +- ✅ Monitoring and logging +- ✅ Error handling and recovery +- ✅ Performance optimization +- ✅ Deployment strategies + +## 🤝 Contributing + +We welcome contributions to code, documentation, or examples! Please check the [Contributing Guide](CONTRIBUTING.md). + +### How to Contribute Examples + +1. **Create a new example directory** under `examples/` +2. **Include comprehensive documentation** in README.md +3. **Add interactive features** where possible +4. **Follow Go best practices** and coding standards +5. **Test thoroughly** before submitting + +### How to Contribute Tutorials + +1. **Follow the tutorial format** in `tutorials/README.md` +2. **Include hands-on exercises** with solutions +3. **Provide clear explanations** and code examples +4. **Test all code examples** for accuracy + +## 📄 License + +This project is open-sourced under the MIT License. + +## 🔗 Related Links + +- [Zoox Official Repository](https://github.com/go-zoox/zoox) +- [Go Official Documentation](https://golang.org/doc/) +- [Web Development Best Practices](https://web.dev/) + +--- -## License -GoZoox is released under the [MIT License](./LICENSE). +**Start your Zoox journey today!** 🎉 diff --git a/examples/01-basic-server/README.md b/examples/01-basic-server/README.md new file mode 100644 index 0000000..0d3ff0d --- /dev/null +++ b/examples/01-basic-server/README.md @@ -0,0 +1,341 @@ +# Basic Server Example + +A comprehensive REST API demonstrating fundamental Zoox framework concepts including routing, middleware, JSON handling, and basic authentication. + +## Features + +- ✅ **RESTful API Design** - Complete CRUD operations for user management +- ✅ **Route Groups** - Organized public and protected endpoints +- ✅ **Middleware** - Authentication and logging middleware +- ✅ **JSON Handling** - Request/response JSON serialization +- ✅ **Error Handling** - Structured error responses +- ✅ **Basic Authentication** - Token-based authentication demo +- ✅ **API Documentation** - Self-documenting endpoints + +## What You'll Learn + +- Setting up a basic Zoox server +- Creating REST endpoints with proper HTTP methods +- Working with JSON requests and responses +- Using route groups for API organization +- Implementing basic authentication middleware +- Handling errors gracefully +- Adding logging and monitoring + +## Quick Start + +1. **Navigate to this example:** + ```bash + cd examples/01-basic-server + ``` + +2. **Install dependencies:** + ```bash + go mod tidy + ``` + +3. **Run the server:** + ```bash + go run main.go + ``` + +4. **Test the API:** + ```bash + # Check server health + curl http://localhost:8080/health + + # Get all users (public) + curl http://localhost:8080/api/v1/users + + # Get user by ID (public) + curl http://localhost:8080/api/v1/users/1 + ``` + +## API Endpoints + +### Public Endpoints + +#### Health Check +```http +GET /health +``` + +**Response:** +```json +{ + "status": "ok", + "timestamp": 1640995200, + "version": "1.0.0" +} +``` + +#### Get All Users +```http +GET /api/v1/users +``` + +**Response:** +```json +{ + "users": [ + { + "id": 1, + "name": "John Doe", + "email": "john@example.com", + "created_at": "2023-01-01T12:00:00Z", + "updated_at": "2023-01-01T12:00:00Z" + } + ], + "count": 1 +} +``` + +#### Get User by ID +```http +GET /api/v1/users/:id +``` + +**Response:** +```json +{ + "user": { + "id": 1, + "name": "John Doe", + "email": "john@example.com", + "created_at": "2023-01-01T12:00:00Z", + "updated_at": "2023-01-01T12:00:00Z" + } +} +``` + +### Protected Endpoints + +All protected endpoints require the `Authorization` header: +``` +Authorization: Bearer demo-token +``` + +#### Create User +```http +POST /api/v1/users +Content-Type: application/json +Authorization: Bearer demo-token + +{ + "name": "Jane Smith", + "email": "jane@example.com" +} +``` + +**Response:** +```json +{ + "message": "User created successfully", + "user": { + "id": 3, + "name": "Jane Smith", + "email": "jane@example.com", + "created_at": "2023-01-01T12:00:00Z", + "updated_at": "2023-01-01T12:00:00Z" + } +} +``` + +#### Update User +```http +PUT /api/v1/users/:id +Content-Type: application/json +Authorization: Bearer demo-token + +{ + "name": "Jane Doe", + "email": "jane.doe@example.com" +} +``` + +#### Delete User +```http +DELETE /api/v1/users/:id +Authorization: Bearer demo-token +``` + +### API Documentation +```http +GET /api/docs +``` + +Returns a complete API reference in JSON format. + +## Testing with cURL + +### Basic Tests +```bash +# Health check +curl -i http://localhost:8080/health + +# Get all users +curl -i http://localhost:8080/api/v1/users + +# Get specific user +curl -i http://localhost:8080/api/v1/users/1 + +# API documentation +curl -i http://localhost:8080/api/docs +``` + +### Protected Endpoint Tests +```bash +# Create a new user +curl -i -X POST http://localhost:8080/api/v1/users \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer demo-token" \ + -d '{"name":"Test User","email":"test@example.com"}' + +# Update user +curl -i -X PUT http://localhost:8080/api/v1/users/3 \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer demo-token" \ + -d '{"name":"Updated User","email":"updated@example.com"}' + +# Delete user +curl -i -X DELETE http://localhost:8080/api/v1/users/3 \ + -H "Authorization: Bearer demo-token" +``` + +### Error Scenarios +```bash +# Missing authentication +curl -i -X POST http://localhost:8080/api/v1/users \ + -H "Content-Type: application/json" \ + -d '{"name":"Test","email":"test@example.com"}' + +# Invalid user ID +curl -i http://localhost:8080/api/v1/users/invalid + +# Missing required fields +curl -i -X POST http://localhost:8080/api/v1/users \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer demo-token" \ + -d '{"name":"","email":""}' +``` + +## Code Structure + +### Main Components + +1. **User Model** - Represents user data structure +2. **UserStore** - In-memory storage with thread-safe operations +3. **Middleware** - Authentication and logging +4. **Route Handlers** - CRUD operation endpoints +5. **Error Handling** - Structured error responses + +### Key Concepts Demonstrated + +#### Route Groups +```go +// Public API routes +public := app.Group("/api/v1") +public.Get("/users", getUsersHandler) + +// Protected API routes (require authentication) +protected := app.Group("/api/v1", authMiddleware) +protected.Post("/users", createUserHandler) +``` + +#### Middleware Usage +```go +// Custom authentication middleware +authMiddleware := func(ctx *zoox.Context) { + auth := ctx.Header("Authorization") + if auth != "Bearer demo-token" { + ctx.JSON(http.StatusUnauthorized, ErrorResponse{...}) + return + } + ctx.Next() +} +``` + +#### JSON Binding +```go +var req CreateUserRequest +if err := ctx.BindJSON(&req); err != nil { + // Handle error +} +``` + +#### Error Responses +```go +type ErrorResponse struct { + Error string `json:"error"` + Message string `json:"message"` + Code int `json:"code"` +} +``` + +## Configuration + +### Server Settings +- **Port:** 8080 (configurable in `app.Run(":8080")`) +- **Authentication:** Bearer token (`demo-token`) +- **Data Storage:** In-memory (not persistent) + +### Sample Data +The server starts with two sample users: +1. John Doe (john@example.com) +2. Jane Smith (jane@example.com) + +## Security Notes + +⚠️ **Important:** This example uses hardcoded authentication for demonstration purposes only. In production: + +- Use proper JWT tokens or OAuth +- Implement password hashing +- Use secure token storage +- Add rate limiting +- Validate all inputs +- Use HTTPS + +## Next Steps + +After mastering this example, try: + +1. **[02-middleware-showcase](../02-middleware-showcase/)** - Learn about Zoox's built-in middleware +2. **[03-websocket-chat](../03-websocket-chat/)** - Add real-time features +3. **[06-production-api](../06-production-api/)** - See production-ready patterns + +## Troubleshooting + +### Common Issues + +**Port already in use:** +```bash +# Kill existing process +sudo lsof -ti:8080 | xargs kill -9 + +# Or use a different port +go run main.go -port=8081 +``` + +**Authentication errors:** +```bash +# Ensure you're using the correct token +curl -H "Authorization: Bearer demo-token" ... +``` + +**JSON parsing errors:** +```bash +# Ensure Content-Type header is set +curl -H "Content-Type: application/json" ... +``` + +## Contributing + +Found an issue or want to improve this example? Please: + +1. Check existing issues +2. Create a detailed bug report +3. Submit a pull request with improvements + +--- + +📚 **Learn More:** Check out the [main documentation](../../DOCUMENTATION.md) for detailed API reference and advanced features. \ No newline at end of file diff --git a/examples/01-basic-server/go.mod b/examples/01-basic-server/go.mod new file mode 100644 index 0000000..15af8a6 --- /dev/null +++ b/examples/01-basic-server/go.mod @@ -0,0 +1,100 @@ +module basic-server + +go 1.23.0 + +toolchain go1.23.10 + +require github.com/go-zoox/zoox v1.13.4 + +require ( + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/creack/pty v1.1.23 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/cli v27.3.1+incompatible // indirect + github.com/docker/docker v27.3.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-errors/errors v1.5.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-redis/redis/v8 v8.11.5 // indirect + github.com/go-zoox/cache v1.0.7 // indirect + github.com/go-zoox/chalk v1.0.2 // indirect + github.com/go-zoox/command v1.7.0 // indirect + github.com/go-zoox/commands-as-a-service v1.7.11 // indirect + github.com/go-zoox/compress v1.0.1 // indirect + github.com/go-zoox/concurrency v1.2.0 // indirect + github.com/go-zoox/cookie v1.2.0 // indirect + github.com/go-zoox/core-utils v1.4.11 // indirect + github.com/go-zoox/cron v1.2.3 // indirect + github.com/go-zoox/crypto v1.1.8 // indirect + github.com/go-zoox/datetime v1.3.1 // indirect + github.com/go-zoox/debug v1.0.5 // indirect + github.com/go-zoox/encoding v1.2.1 // indirect + github.com/go-zoox/errors v1.0.2 // indirect + github.com/go-zoox/eventemitter v1.4.1 // indirect + github.com/go-zoox/fetch v1.8.3 // indirect + github.com/go-zoox/fs v1.3.15 // indirect + github.com/go-zoox/headers v1.0.8 // indirect + github.com/go-zoox/i18n v1.0.3 // indirect + github.com/go-zoox/jobqueue v1.0.1 // indirect + github.com/go-zoox/jsonrpc v1.2.2 // indirect + github.com/go-zoox/jwt v1.4.0 // indirect + github.com/go-zoox/kv v1.5.9 // indirect + github.com/go-zoox/logger v1.6.3 // indirect + github.com/go-zoox/mq v1.0.1 // indirect + github.com/go-zoox/proxy v1.5.6 // indirect + github.com/go-zoox/pubsub v1.2.3 // indirect + github.com/go-zoox/random v1.0.4 // indirect + github.com/go-zoox/safe v1.2.0 // indirect + github.com/go-zoox/session v1.2.0 // indirect + github.com/go-zoox/tag v1.3.4 // indirect + github.com/go-zoox/uuid v0.0.1 // indirect + github.com/go-zoox/websocket v1.3.5 // indirect + github.com/goccy/go-yaml v1.12.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/redis/go-redis/v9 v9.6.1 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/tidwall/gjson v1.17.3 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + google.golang.org/grpc v1.73.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/go-zoox/zoox => ../../ diff --git a/examples/01-basic-server/go.sum b/examples/01-basic-server/go.sum new file mode 100644 index 0000000..c75480e --- /dev/null +++ b/examples/01-basic-server/go.sum @@ -0,0 +1,290 @@ +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= +github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ= +github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= +github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-zoox/cache v1.0.7 h1:m5wmUY01cTfdxU+Hv5LGYXMfv1O7UTuKVeUvwxDnIRY= +github.com/go-zoox/cache v1.0.7/go.mod h1:rDQPnldnf1V8tKCn5e1MrkDXI2BFTws7PniTQQa2lpc= +github.com/go-zoox/chalk v1.0.2 h1:DCWft37fogmvqF37JdbGSLg28L/tQeA8u0lMvb62KOg= +github.com/go-zoox/chalk v1.0.2/go.mod h1:z5+qvE9nEJI5uT4px2tyoFa/xxkqf3CUo22KmXLKbNI= +github.com/go-zoox/command v1.7.0 h1:8eD6ijsdV17Xtcv+miHGFlpdgXZrSjwzNkNS0H5vxlg= +github.com/go-zoox/command v1.7.0/go.mod h1:EaRI+Fcnj84z0b3wHsFTG6ylJRxeS1CF5mkMWf8W1k4= +github.com/go-zoox/commands-as-a-service v1.7.11 h1:8mt0lMieWS6FQENlXS5o7jayMb0DPYBhxOtb/6c8M0w= +github.com/go-zoox/commands-as-a-service v1.7.11/go.mod h1:1yehkOF07Jsf810PqwXmfp+PpMHXm4iW5GD5fz3jyow= +github.com/go-zoox/compress v1.0.1 h1:EyNxo5NscMLua5fvUdiGSF+BwhuTfMeyppu7OwKAW7Q= +github.com/go-zoox/compress v1.0.1/go.mod h1:iV6CcNulf3OuEfA1h1VOsaBqYH81cVSg5wNi5HDx2h4= +github.com/go-zoox/concurrency v1.2.0 h1:iucwSWQ0Y9fFIG+eZvyHjMrIPSnaKJTGfOcGbIx91yg= +github.com/go-zoox/concurrency v1.2.0/go.mod h1:rghauUPHEDp8HJzaVlU851HWqiAqD8lUVp45K/dtNvw= +github.com/go-zoox/cookie v1.2.0 h1:MO33lPQ/QGJIAEzgrsAfEpJc25lcJ/XR0w+smM19sNQ= +github.com/go-zoox/cookie v1.2.0/go.mod h1:+xEawxty0L+z+4EIvTF2AaHUkUM7oIecGZ9XrEaYqsI= +github.com/go-zoox/core-utils v1.4.11 h1:FmREHJqOsKvLcsG6EuEWigXPG7F7PLN6kWwt9BD7OHM= +github.com/go-zoox/core-utils v1.4.11/go.mod h1:raOOwr2l2sJQyjR0Dg33sg0ry4U1/L2eNTuLFRpUXWs= +github.com/go-zoox/cron v1.2.3 h1:PYVewnUNz6VHkkz+ctSZnd4aBZmnzuOrtamHhavs3SI= +github.com/go-zoox/cron v1.2.3/go.mod h1:u84QGrF/GroCwfuZL6IleKCvWtZagWXwUg0h1L3iswI= +github.com/go-zoox/crypto v1.1.8 h1:oI2KPLy+SsGeb+h5A99n9MTQVp4jBhwJWkqjStUzz9I= +github.com/go-zoox/crypto v1.1.8/go.mod h1:JqgNr9HcFFGQkMCGLJ9djtfg/RWVLxtunG01HD3lUXM= +github.com/go-zoox/datetime v1.3.1 h1:HFyVtfgyLXwfJsLl0EjhaWTKt9s0QxTonb7s0JtJmVg= +github.com/go-zoox/datetime v1.3.1/go.mod h1:qvaCrzjhq/g/gstx4sx06Nl4ll2pLSrkRa9ueLjrZ9A= +github.com/go-zoox/debug v1.0.5 h1:M8hO5C5eujLmFiWQ2hTij3fpO8eGVBA8jE5K6JA7HlY= +github.com/go-zoox/debug v1.0.5/go.mod h1:gC90/McIkAgNg13Xpy8BelBYD/Ac335SDUsbKU0Ww6c= +github.com/go-zoox/dotenv v1.3.0 h1:9pwQBZooQ1c1X+Eyw9GqBGvZ9KvgZ/Hz0sBTDdjHp9o= +github.com/go-zoox/dotenv v1.3.0/go.mod h1:DdBWoG8USiy6iIx1fD7WGtSiO13vzpqPRK3Qt1sPbr0= +github.com/go-zoox/encoding v1.2.1 h1:38rQRsfL1f1YHZaqsPaGcNMkPnzatnPlYiHioUh9F4A= +github.com/go-zoox/encoding v1.2.1/go.mod h1:NdcM7Ln73oVW3vJgx3MH4fJknCcdQfq+NgJ0tuCo7tU= +github.com/go-zoox/errors v1.0.2 h1:1NLMoEVlDU1+qrvvPj+rrJXOvQPdeZ3DekVBFrI5PFY= +github.com/go-zoox/errors v1.0.2/go.mod h1:HJ5NKQb9cu3IbI0Jayw7xZiblLBEIglpaIOMxvQnWnk= +github.com/go-zoox/eventemitter v1.4.1 h1:EUHy6eldo0wBI3ypRGiVjBLZw68RRYN5UNClB/+Notc= +github.com/go-zoox/eventemitter v1.4.1/go.mod h1:EbNO0is8K4lMWOot0opDMGVfd9qpMgIzCaoRXpqKjHM= +github.com/go-zoox/fetch v1.8.3 h1:rXjzGjU8+MhB0QSqEACyfme4IVPUjlQU+rQYhn7DEnA= +github.com/go-zoox/fetch v1.8.3/go.mod h1:fo1HEo/L8x4d3XPp7CkmCoezB9ZDLvEUXf/wOoM+aWY= +github.com/go-zoox/fs v1.3.15 h1:kHH1M0t9p96BES0p3aDakkCw8h9BjOyWSsHQ91IWmao= +github.com/go-zoox/fs v1.3.15/go.mod h1:GGcmvYa1Kyvspc8YzPt0peLGie+KlCoo2gkg4XbGRiY= +github.com/go-zoox/headers v1.0.8 h1:HZJisMHhKwdySVNbV4Awc5kaMxFfAwBIHpcWOGch+iw= +github.com/go-zoox/headers v1.0.8/go.mod h1:WEgEbewswEw4n4qS1iG68Kn/vOQVCAKGwwuZankc6so= +github.com/go-zoox/i18n v1.0.3 h1:PqeOKyhI9MxbA9TyWDgm7zcCL5WRSlxhANHWou04VHk= +github.com/go-zoox/i18n v1.0.3/go.mod h1:WURpyaWOrVVN4f3mEQtl5A0kie5bK4ExQJ0PnHSOfTI= +github.com/go-zoox/jobqueue v1.0.1 h1:xEPmT7jt4PxZVIDAoCDv+KIgyalOLLvDnyKaoi1bsLE= +github.com/go-zoox/jobqueue v1.0.1/go.mod h1:BceTOOfLMygiVRVPRg74GJK+u+yY8fwUPZ9NeeEfCnE= +github.com/go-zoox/jsonrpc v1.2.2 h1:asaoJgJkfyH5eblLQ1WzrZDe8ERL6v9GT4pKR/LJ3IE= +github.com/go-zoox/jsonrpc v1.2.2/go.mod h1:HdxJW/T0hkVHlfm+ULRnNEqvTtvZ7o4qxdQGQW76khM= +github.com/go-zoox/jwt v1.4.0 h1:MhI10JTGp4Ajry1oCO3AzpOR4UnsoqCVkcAKS3ZwAzg= +github.com/go-zoox/jwt v1.4.0/go.mod h1:Cfc+t0XhNCgDjXLR5sK6ao7qz1GSIq896gZ1usNb7t8= +github.com/go-zoox/kv v1.5.9 h1:xEjIRJVcwIg2PhO0ZL+KWR5mCI/f5VshmcOjZPd4FRA= +github.com/go-zoox/kv v1.5.9/go.mod h1:sD2FmWrme1gzWaLciBAPyK0BtW3BluM02UGskOqf2MA= +github.com/go-zoox/logger v1.6.3 h1:gx3pnRTKBCFEePescaz6nNmSxH19Lj7Lf94FtAuKc1A= +github.com/go-zoox/logger v1.6.3/go.mod h1:o7ddvv/gMoMa0TomPhHoIz11ZWRbQ92pF6rwYbOY3iQ= +github.com/go-zoox/mq v1.0.1 h1:JZSgWfp4JJDVKN8FgUkWWNDb3HOhV15dIdi+ecZENwQ= +github.com/go-zoox/mq v1.0.1/go.mod h1:0Zhgww1wcFNC37NJZjtumai03MvBAZuQ4VhezcAiFJE= +github.com/go-zoox/proxy v1.5.6 h1:Ha5wsSjIi57TcYJnb4iBrW1xmJlNW2E7dWjUIwIe6iE= +github.com/go-zoox/proxy v1.5.6/go.mod h1:KLWeJqfQk1upCvEdXt3tEuM8xSu0ApbA9FNLOmyHysY= +github.com/go-zoox/pubsub v1.2.3 h1:doDwmt3KUR+i4sI+Jrqm09zrQBZoH9CFyRqlGgkN02k= +github.com/go-zoox/pubsub v1.2.3/go.mod h1:LWX0NAg80hkeGdZf7PJOEGnyN6CXooCxpIlh2MxESDo= +github.com/go-zoox/random v1.0.4 h1:icckpkCowQ0eGiiMkHFOJz9Qc9noOcinP+ggqWUIBH4= +github.com/go-zoox/random v1.0.4/go.mod h1:W+PTQiInxaCngiXpSvycucAKvu1tE/tKlZ9kaMp2/Ys= +github.com/go-zoox/safe v1.2.0 h1:KSX5XVRQ2431PjoAuHW25bgDsMgKl0q6g8G7ko7DUu8= +github.com/go-zoox/safe v1.2.0/go.mod h1:g2RrkP7Cg1w80LTkvaI3PiVqc2pSEPLlsjI0+ViS/TA= +github.com/go-zoox/session v1.2.0 h1:hzjcZYV3+cVmocnC6MazhtELONMtRlIuEezCYvbxY9Q= +github.com/go-zoox/session v1.2.0/go.mod h1:SLHzCK3DDknqot28deZQFBPz5hm9QHcUeRra8y9GT8E= +github.com/go-zoox/tag v1.3.4 h1:VJt9T4bbaz3nfpjW+K24DYawim46LqfAAR8rwoQO0yg= +github.com/go-zoox/tag v1.3.4/go.mod h1:I0ZCDrMDK6muFrHFNb6Tw5hKQ7ivaXAnMcKAVpRfCwI= +github.com/go-zoox/testify v1.0.2 h1:G5sQ3xm0uwCuytnMhgnqZ5BItCt2DN3n2wLBqlIJEWA= +github.com/go-zoox/testify v1.0.2/go.mod h1:L35iVL6xDKDL/TQOTRWyNL4H4nm8bzs6nde5XA7PYnY= +github.com/go-zoox/uuid v0.0.1 h1:txqmDavRTq68gzzqWfJQLorFyUp9a7M2lmq2KcwPGPA= +github.com/go-zoox/uuid v0.0.1/go.mod h1:0/F4LdfLqFdyqOf7aXoiYXRkXHU324JQ5DZEytXYBPM= +github.com/go-zoox/websocket v1.3.5 h1:+puemx88m6Phi9Q4FzaZcqaElK5iTx0m4okFpyxKE+k= +github.com/go-zoox/websocket v1.3.5/go.mod h1:rIYK7JAkzehFe0c8Ozw+WoUuM24uKV7Viyksi+mYnlo= +github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM= +github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= +github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 h1:OXcKh35JaYsGMRzpvFkLv/MEyPuL49CThT1pZ8aSml4= +github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= +go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/examples/01-basic-server/main.go b/examples/01-basic-server/main.go new file mode 100644 index 0000000..7d09192 --- /dev/null +++ b/examples/01-basic-server/main.go @@ -0,0 +1,353 @@ +package main + +import ( + "fmt" + "net/http" + "strconv" + "sync" + "time" + + "github.com/go-zoox/zoox" +) + +// User represents a user in our system +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// UserStore manages users in memory +type UserStore struct { + users map[int]*User + nextID int + mutex sync.RWMutex +} + +// NewUserStore creates a new user store +func NewUserStore() *UserStore { + store := &UserStore{ + users: make(map[int]*User), + nextID: 1, + } + + // Add some sample data + store.CreateUser("John Doe", "john@example.com") + store.CreateUser("Jane Smith", "jane@example.com") + + return store +} + +// CreateUser creates a new user +func (s *UserStore) CreateUser(name, email string) *User { + s.mutex.Lock() + defer s.mutex.Unlock() + + user := &User{ + ID: s.nextID, + Name: name, + Email: email, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + s.users[s.nextID] = user + s.nextID++ + + return user +} + +// GetUser retrieves a user by ID +func (s *UserStore) GetUser(id int) (*User, bool) { + s.mutex.RLock() + defer s.mutex.RUnlock() + + user, exists := s.users[id] + return user, exists +} + +// GetAllUsers retrieves all users +func (s *UserStore) GetAllUsers() []*User { + s.mutex.RLock() + defer s.mutex.RUnlock() + + users := make([]*User, 0, len(s.users)) + for _, user := range s.users { + users = append(users, user) + } + + return users +} + +// UpdateUser updates an existing user +func (s *UserStore) UpdateUser(id int, name, email string) (*User, bool) { + s.mutex.Lock() + defer s.mutex.Unlock() + + user, exists := s.users[id] + if !exists { + return nil, false + } + + user.Name = name + user.Email = email + user.UpdatedAt = time.Now() + + return user, true +} + +// DeleteUser deletes a user by ID +func (s *UserStore) DeleteUser(id int) bool { + s.mutex.Lock() + defer s.mutex.Unlock() + + _, exists := s.users[id] + if exists { + delete(s.users, id) + } + + return exists +} + +// CreateUserRequest represents the request payload for creating a user +type CreateUserRequest struct { + Name string `json:"name"` + Email string `json:"email"` +} + +// UpdateUserRequest represents the request payload for updating a user +type UpdateUserRequest struct { + Name string `json:"name"` + Email string `json:"email"` +} + +// ErrorResponse represents an error response +type ErrorResponse struct { + Error string `json:"error"` + Message string `json:"message"` + Code int `json:"code"` +} + +func main() { + // Create Zoox app + app := zoox.Default() + + // Initialize user store + userStore := NewUserStore() + + // Custom middleware for basic authentication (demo purposes) + authMiddleware := func(ctx *zoox.Context) { + // Simple hardcoded authentication for demo + auth := ctx.Header("Authorization") + if auth != "Bearer demo-token" { + ctx.JSON(http.StatusUnauthorized, ErrorResponse{ + Error: "Unauthorized", + Message: "Invalid or missing authorization token", + Code: http.StatusUnauthorized, + }) + return + } + ctx.Next() + } + + // Global middleware + app.Use(func(ctx *zoox.Context) { + // Log all requests + fmt.Printf("[%s] %s %s\n", time.Now().Format("2006-01-02 15:04:05"), ctx.Method, ctx.Path) + ctx.Next() + }) + + // Health check endpoint + app.Get("/health", func(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "status": "ok", + "timestamp": time.Now().Unix(), + "version": "1.0.0", + }) + }) + + // Public API routes + public := app.Group("/api/v1") + + // Get all users (public) + public.Get("/users", func(ctx *zoox.Context) { + users := userStore.GetAllUsers() + ctx.JSON(http.StatusOK, map[string]interface{}{ + "users": users, + "count": len(users), + }) + }) + + // Get user by ID (public) + public.Get("/users/:id", func(ctx *zoox.Context) { + idStr := ctx.Param("id") + id, err := strconv.Atoi(idStr) + if err != nil { + ctx.JSON(http.StatusBadRequest, ErrorResponse{ + Error: "Bad Request", + Message: "Invalid user ID format", + Code: http.StatusBadRequest, + }) + return + } + + user, exists := userStore.GetUser(id) + if !exists { + ctx.JSON(http.StatusNotFound, ErrorResponse{ + Error: "Not Found", + Message: "User not found", + Code: http.StatusNotFound, + }) + return + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "user": user, + }) + }) + + // Protected API routes (require authentication) + protected := app.Group("/api/v1", authMiddleware) + + // Create user (protected) + protected.Post("/users", func(ctx *zoox.Context) { + var req CreateUserRequest + if err := ctx.BindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, ErrorResponse{ + Error: "Bad Request", + Message: "Invalid JSON payload", + Code: http.StatusBadRequest, + }) + return + } + + // Basic validation + if req.Name == "" || req.Email == "" { + ctx.JSON(http.StatusBadRequest, ErrorResponse{ + Error: "Bad Request", + Message: "Name and email are required", + Code: http.StatusBadRequest, + }) + return + } + + user := userStore.CreateUser(req.Name, req.Email) + ctx.JSON(http.StatusCreated, map[string]interface{}{ + "message": "User created successfully", + "user": user, + }) + }) + + // Update user (protected) + protected.Put("/users/:id", func(ctx *zoox.Context) { + idStr := ctx.Param("id") + id, err := strconv.Atoi(idStr) + if err != nil { + ctx.JSON(http.StatusBadRequest, ErrorResponse{ + Error: "Bad Request", + Message: "Invalid user ID format", + Code: http.StatusBadRequest, + }) + return + } + + var req UpdateUserRequest + if err := ctx.BindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, ErrorResponse{ + Error: "Bad Request", + Message: "Invalid JSON payload", + Code: http.StatusBadRequest, + }) + return + } + + // Basic validation + if req.Name == "" || req.Email == "" { + ctx.JSON(http.StatusBadRequest, ErrorResponse{ + Error: "Bad Request", + Message: "Name and email are required", + Code: http.StatusBadRequest, + }) + return + } + + user, exists := userStore.UpdateUser(id, req.Name, req.Email) + if !exists { + ctx.JSON(http.StatusNotFound, ErrorResponse{ + Error: "Not Found", + Message: "User not found", + Code: http.StatusNotFound, + }) + return + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "User updated successfully", + "user": user, + }) + }) + + // Delete user (protected) + protected.Delete("/users/:id", func(ctx *zoox.Context) { + idStr := ctx.Param("id") + id, err := strconv.Atoi(idStr) + if err != nil { + ctx.JSON(http.StatusBadRequest, ErrorResponse{ + Error: "Bad Request", + Message: "Invalid user ID format", + Code: http.StatusBadRequest, + }) + return + } + + exists := userStore.DeleteUser(id) + if !exists { + ctx.JSON(http.StatusNotFound, ErrorResponse{ + Error: "Not Found", + Message: "User not found", + Code: http.StatusNotFound, + }) + return + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "User deleted successfully", + }) + }) + + // API documentation endpoint + app.Get("/api/docs", func(ctx *zoox.Context) { + docs := map[string]interface{}{ + "title": "Basic Server API", + "version": "1.0.0", + "description": "A simple REST API demonstrating Zoox framework features", + "endpoints": map[string]interface{}{ + "GET /health": "Health check", + "GET /api/v1/users": "Get all users (public)", + "GET /api/v1/users/:id": "Get user by ID (public)", + "POST /api/v1/users": "Create user (requires auth)", + "PUT /api/v1/users/:id": "Update user (requires auth)", + "DELETE /api/v1/users/:id": "Delete user (requires auth)", + }, + "authentication": map[string]interface{}{ + "type": "Bearer Token", + "header": "Authorization: Bearer demo-token", + "note": "Use 'Bearer demo-token' for protected endpoints", + }, + } + + ctx.JSON(http.StatusOK, docs) + }) + + // Start server + fmt.Println("🚀 Basic Server starting...") + fmt.Println("📍 Server running on http://localhost:8080") + fmt.Println("📚 API Documentation: http://localhost:8080/api/docs") + fmt.Println("🔍 Health Check: http://localhost:8080/health") + fmt.Println("👥 Users API: http://localhost:8080/api/v1/users") + fmt.Println("🔐 Use 'Authorization: Bearer demo-token' for protected endpoints") + + app.Run(":8080") +} \ No newline at end of file diff --git a/examples/02-middleware-showcase/README.md b/examples/02-middleware-showcase/README.md new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/examples/02-middleware-showcase/README.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/02-middleware-showcase/go.mod b/examples/02-middleware-showcase/go.mod new file mode 100644 index 0000000..36c57ba --- /dev/null +++ b/examples/02-middleware-showcase/go.mod @@ -0,0 +1,48 @@ +module zoox-middleware-showcase-example + +go 1.19 + +require github.com/go-zoox/zoox v1.11.7 + +require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.17.0 // indirect + github.com/go-redis/redis/v8 v8.11.5 // indirect + github.com/go-zoox/cache v1.0.9 // indirect + github.com/go-zoox/cookie v1.2.0 // indirect + github.com/go-zoox/core-utils v1.2.11 // indirect + github.com/go-zoox/crypto v1.1.8 // indirect + github.com/go-zoox/datetime v1.1.1 // indirect + github.com/go-zoox/encoding v1.2.1 // indirect + github.com/go-zoox/errors v1.0.2 // indirect + github.com/go-zoox/headers v1.0.6 // indirect + github.com/go-zoox/jsonrpc v1.3.0 // indirect + github.com/go-zoox/kv v1.5.0 // indirect + github.com/go-zoox/logger v1.4.4 // indirect + github.com/go-zoox/proxy v1.4.1 // indirect + github.com/go-zoox/random v1.0.4 // indirect + github.com/go-zoox/safe v1.0.1 // indirect + github.com/go-zoox/session v1.2.0 // indirect + github.com/go-zoox/tag v1.2.3 // indirect + github.com/go-zoox/uuid v0.0.1 // indirect + github.com/go-zoox/websocket v1.3.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.1 // indirect + github.com/leodido/go-urn v1.3.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/spf13/cast v1.6.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) \ No newline at end of file diff --git a/examples/02-middleware-showcase/main.go b/examples/02-middleware-showcase/main.go new file mode 100644 index 0000000..bd99283 --- /dev/null +++ b/examples/02-middleware-showcase/main.go @@ -0,0 +1,398 @@ +package main + +import ( + "fmt" + "net/http" + "time" + + "github.com/go-zoox/zoox" + "github.com/go-zoox/zoox/middleware" +) + +func main() { + // Create Zoox app + app := zoox.Default() + + // =================== + // GLOBAL MIDDLEWARE + // =================== + + // Request ID middleware - adds unique ID to each request + app.Use(middleware.RequestID()) + + // Logger middleware - logs all requests + app.Use(middleware.Logger()) + + // Recovery middleware - handles panics gracefully + app.Use(middleware.Recovery()) + + // CORS middleware - handles cross-origin requests + app.Use(middleware.CORS(&middleware.CORSConfig{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"*"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + MaxAge: 12 * time.Hour, + })) + + // =================== + // DEMONSTRATION ROUTES + // =================== + + // Home page with middleware overview + app.Get("/", func(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "Zoox Middleware Showcase", + "request_id": ctx.Header("X-Request-ID"), + "middleware_demos": map[string]string{ + "GET /basic": "Basic middleware demo", + "GET /security": "Security middleware demo", + "GET /performance": "Performance middleware demo", + "GET /auth/basic": "Basic authentication", + "GET /auth/bearer": "Bearer token authentication", + "GET /auth/jwt": "JWT authentication", + "GET /custom": "Custom middleware demo", + "GET /rate-limited": "Rate limiting demo", + "GET /cached": "Caching demo", + "GET /compressed": "Compression demo", + "POST /panic": "Recovery middleware demo", + }, + }) + }) + + // =================== + // BASIC MIDDLEWARE GROUP + // =================== + + basic := app.Group("/basic") + basic.Use(func(ctx *zoox.Context) { + fmt.Printf("Basic middleware executed for: %s\n", ctx.Path) + ctx.Set("demo", "basic middleware") + ctx.Next() + }) + + basic.Get("/", func(ctx *zoox.Context) { + demo := ctx.Get("demo") + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "Basic middleware demonstration", + "demo_value": demo, + "request_id": ctx.Header("X-Request-ID"), + "timestamp": time.Now().Unix(), + }) + }) + + // =================== + // SECURITY MIDDLEWARE GROUP + // =================== + + security := app.Group("/security") + + // Helmet middleware - adds security headers + security.Use(middleware.Helmet(&middleware.HelmetConfig{ + XSSProtection: "1; mode=block", + ContentTypeNosniff: "nosniff", + XFrameOptions: "DENY", + ReferrerPolicy: "no-referrer", + ContentSecurityPolicy: "default-src 'self'", + })) + + security.Get("/", func(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "Security headers applied", + "headers": map[string]string{ + "X-XSS-Protection": ctx.Header("X-XSS-Protection"), + "X-Content-Type-Options": ctx.Header("X-Content-Type-Options"), + "X-Frame-Options": ctx.Header("X-Frame-Options"), + "Referrer-Policy": ctx.Header("Referrer-Policy"), + "Content-Security-Policy": ctx.Header("Content-Security-Policy"), + }, + }) + }) + + // =================== + // PERFORMANCE MIDDLEWARE GROUP + // =================== + + performance := app.Group("/performance") + + // Gzip compression middleware + performance.Use(middleware.Gzip()) + + // Static file serving with cache headers + performance.Use(middleware.StaticCache(&middleware.StaticCacheConfig{ + MaxAge: 24 * time.Hour, + })) + + performance.Get("/", func(ctx *zoox.Context) { + // Large response to demonstrate compression + largeData := make([]string, 1000) + for i := range largeData { + largeData[i] = fmt.Sprintf("This is line %d with some repeated content that compresses well", i) + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "Performance optimized response", + "compression": "gzip applied", + "cache_headers": "static cache headers added", + "large_data": largeData, + }) + }) + + // =================== + // AUTHENTICATION MIDDLEWARE GROUPS + // =================== + + // Basic Authentication + basicAuth := app.Group("/auth/basic") + basicAuth.Use(middleware.BasicAuth("Restricted Area", map[string]string{ + "admin": "secret123", + "user": "password", + })) + + basicAuth.Get("/", func(ctx *zoox.Context) { + user := ctx.Get("user") + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "Basic authentication successful", + "user": user, + "auth_type": "basic", + }) + }) + + // Bearer Token Authentication + bearerAuth := app.Group("/auth/bearer") + bearerAuth.Use(middleware.BearerAuth(func(token string) (interface{}, error) { + // Simple token validation (in production, verify with database/JWT) + validTokens := map[string]string{ + "demo-token": "demo-user", + "admin-token": "admin-user", + } + + if user, valid := validTokens[token]; valid { + return user, nil + } + return nil, fmt.Errorf("invalid token") + })) + + bearerAuth.Get("/", func(ctx *zoox.Context) { + user := ctx.Get("user") + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "Bearer token authentication successful", + "user": user, + "auth_type": "bearer", + }) + }) + + // JWT Authentication + jwtAuth := app.Group("/auth/jwt") + jwtAuth.Use(middleware.JWT(&middleware.JWTConfig{ + Secret: "your-secret-key", + ContextKey: "jwt_user", + })) + + jwtAuth.Get("/", func(ctx *zoox.Context) { + jwtUser := ctx.Get("jwt_user") + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "JWT authentication successful", + "user": jwtUser, + "auth_type": "jwt", + }) + }) + + // =================== + // CUSTOM MIDDLEWARE GROUP + // =================== + + custom := app.Group("/custom") + + // Custom request timing middleware + custom.Use(func(ctx *zoox.Context) { + start := time.Now() + ctx.Set("start_time", start) + + // Execute next handlers + ctx.Next() + + // Calculate and log response time + duration := time.Since(start) + ctx.Header("X-Response-Time", duration.String()) + fmt.Printf("Request %s took %v\n", ctx.Path, duration) + }) + + // Custom user agent logging middleware + custom.Use(func(ctx *zoox.Context) { + userAgent := ctx.Header("User-Agent") + fmt.Printf("User-Agent: %s\n", userAgent) + ctx.Set("user_agent", userAgent) + ctx.Next() + }) + + custom.Get("/", func(ctx *zoox.Context) { + startTime := ctx.Get("start_time") + userAgent := ctx.Get("user_agent") + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "Custom middleware demonstration", + "start_time": startTime, + "user_agent": userAgent, + "response_time": ctx.Header("X-Response-Time"), + }) + }) + + // =================== + // RATE LIMITING MIDDLEWARE + // =================== + + rateLimited := app.Group("/rate-limited") + rateLimited.Use(middleware.RateLimit(&middleware.RateLimitConfig{ + Rate: 2, // 2 requests + Burst: 5, // burst of 5 + Duration: time.Minute, // per minute + })) + + rateLimited.Get("/", func(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "Rate limiting applied", + "rate": "2 requests per minute", + "burst": 5, + "timestamp": time.Now().Unix(), + }) + }) + + // =================== + // CACHING MIDDLEWARE + // =================== + + cached := app.Group("/cached") + cached.Use(middleware.Cache(&middleware.CacheConfig{ + TTL: 30 * time.Second, + Key: func(ctx *zoox.Context) string { + return fmt.Sprintf("cache:%s:%s", ctx.Method, ctx.Path) + }, + })) + + cached.Get("/", func(ctx *zoox.Context) { + // Simulate expensive operation + time.Sleep(100 * time.Millisecond) + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "Cached response", + "cache_ttl": "30 seconds", + "generated_at": time.Now().Format(time.RFC3339), + "expensive_calculation": time.Now().UnixNano(), + }) + }) + + // =================== + // COMPRESSION MIDDLEWARE + // =================== + + compressed := app.Group("/compressed") + compressed.Use(middleware.Gzip()) + + compressed.Get("/", func(ctx *zoox.Context) { + // Generate large, compressible content + content := "" + for i := 0; i < 1000; i++ { + content += fmt.Sprintf("This is repeated content line %d that should compress very well with gzip compression middleware. ", i) + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "Large response with gzip compression", + "original_size_hint": "Very large without compression", + "compressed_size_hint": "Much smaller with gzip", + "content": content, + }) + }) + + // =================== + // ERROR HANDLING / RECOVERY + // =================== + + app.Post("/panic", func(ctx *zoox.Context) { + // This will trigger the recovery middleware + panic("Intentional panic to demonstrate recovery middleware") + }) + + // =================== + // MIDDLEWARE DOCUMENTATION + // =================== + + app.Get("/middleware/docs", func(ctx *zoox.Context) { + docs := map[string]interface{}{ + "title": "Zoox Middleware Documentation", + "global_middleware": []map[string]string{ + {"name": "RequestID", "description": "Adds unique request ID"}, + {"name": "Logger", "description": "Logs all requests"}, + {"name": "Recovery", "description": "Handles panics gracefully"}, + {"name": "CORS", "description": "Handles cross-origin requests"}, + }, + "security_middleware": []map[string]string{ + {"name": "Helmet", "description": "Adds security headers"}, + {"name": "BasicAuth", "description": "HTTP Basic authentication"}, + {"name": "BearerAuth", "description": "Bearer token authentication"}, + {"name": "JWT", "description": "JWT token authentication"}, + }, + "performance_middleware": []map[string]string{ + {"name": "Gzip", "description": "Response compression"}, + {"name": "StaticCache", "description": "Static file caching"}, + {"name": "RateLimit", "description": "Request rate limiting"}, + {"name": "Cache", "description": "Response caching"}, + }, + "custom_middleware": []map[string]string{ + {"name": "RequestTiming", "description": "Measures request duration"}, + {"name": "UserAgentLogging", "description": "Logs user agent strings"}, + }, + } + + ctx.JSON(http.StatusOK, docs) + }) + + // =================== + // MIDDLEWARE TESTING ENDPOINTS + // =================== + + testing := app.Group("/test") + + // Test endpoint for header inspection + testing.Get("/headers", func(ctx *zoox.Context) { + headers := make(map[string]string) + ctx.Request.Header.Range(func(key, value []byte) bool { + headers[string(key)] = string(value) + return true + }) + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "All request headers", + "headers": headers, + }) + }) + + // Test endpoint for response timing + testing.Get("/slow", func(ctx *zoox.Context) { + // Simulate slow operation + time.Sleep(2 * time.Second) + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "Slow response for testing custom timing middleware", + "delay": "2 seconds", + }) + }) + + // Start server + fmt.Println("🚀 Middleware Showcase starting...") + fmt.Println("📍 Server running on http://localhost:8080") + fmt.Println("📚 Middleware Documentation: http://localhost:8080/middleware/docs") + fmt.Println("🔍 Demo Endpoints:") + fmt.Println(" • Basic: http://localhost:8080/basic") + fmt.Println(" • Security: http://localhost:8080/security") + fmt.Println(" • Performance: http://localhost:8080/performance") + fmt.Println(" • Auth Basic: http://localhost:8080/auth/basic (admin:secret123)") + fmt.Println(" • Auth Bearer: http://localhost:8080/auth/bearer (Authorization: Bearer demo-token)") + fmt.Println(" • Custom: http://localhost:8080/custom") + fmt.Println(" • Rate Limited: http://localhost:8080/rate-limited") + fmt.Println(" • Cached: http://localhost:8080/cached") + fmt.Println(" • Compressed: http://localhost:8080/compressed") + + app.Run(":8080") +} \ No newline at end of file diff --git a/examples/03-websocket-chat/README.md b/examples/03-websocket-chat/README.md new file mode 100644 index 0000000..a49a9be --- /dev/null +++ b/examples/03-websocket-chat/README.md @@ -0,0 +1,315 @@ +# WebSocket Chat Example + +This example demonstrates real-time bidirectional communication using WebSockets with the Zoox framework. It implements a complete chat application with user management, message broadcasting, and connection handling. + +## Features + +### Real-Time Communication +- **WebSocket connections** with automatic reconnection +- **Message broadcasting** to all connected clients +- **User presence** tracking (join/leave notifications) +- **Connection management** with graceful cleanup + +### Chat Functionality +- **Public chat room** for all users +- **User nicknames** with validation +- **Message history** for new connections +- **Typing indicators** (optional feature) +- **User list** with online status + +### Technical Features +- **Concurrent connection handling** using goroutines +- **Thread-safe message broadcasting** with mutexes +- **JSON message protocol** for structured communication +- **Error handling** for connection failures +- **Graceful shutdown** with proper cleanup + +## Quick Start + +1. **Run the chat server:** + ```bash + cd examples/03-websocket-chat + go mod tidy + go run main.go + ``` + +2. **Open the chat interface:** + - Open your browser to `http://localhost:8080` + - Enter a nickname to join the chat + - Start chatting with other users! + +3. **Test with multiple clients:** + - Open multiple browser tabs/windows + - Use different nicknames + - See real-time message synchronization + +## API Endpoints + +### WebSocket Connection +- **Endpoint:** `ws://localhost:8080/ws` +- **Protocol:** JSON-based message protocol +- **Heartbeat:** Automatic ping/pong for connection health + +### HTTP Endpoints +- **GET /** - Chat web interface +- **GET /health** - Health check endpoint +- **GET /stats** - Connection statistics + +## Message Protocol + +### Client to Server Messages + +**Join Chat:** +```json +{ + "type": "join", + "nickname": "john_doe", + "timestamp": "2024-01-01T12:00:00Z" +} +``` + +**Send Message:** +```json +{ + "type": "message", + "content": "Hello everyone!", + "timestamp": "2024-01-01T12:00:00Z" +} +``` + +**Leave Chat:** +```json +{ + "type": "leave", + "timestamp": "2024-01-01T12:00:00Z" +} +``` + +### Server to Client Messages + +**User Joined:** +```json +{ + "type": "user_joined", + "nickname": "john_doe", + "timestamp": "2024-01-01T12:00:00Z", + "user_count": 5 +} +``` + +**Chat Message:** +```json +{ + "type": "message", + "nickname": "john_doe", + "content": "Hello everyone!", + "timestamp": "2024-01-01T12:00:00Z" +} +``` + +**User Left:** +```json +{ + "type": "user_left", + "nickname": "john_doe", + "timestamp": "2024-01-01T12:00:00Z", + "user_count": 4 +} +``` + +**System Message:** +```json +{ + "type": "system", + "content": "Welcome to the chat!", + "timestamp": "2024-01-01T12:00:00Z" +} +``` + +## Architecture + +### Connection Management +```go +type Hub struct { + clients map[*Client]bool + broadcast chan []byte + register chan *Client + unregister chan *Client + mutex sync.RWMutex +} +``` + +### Client Structure +```go +type Client struct { + hub *Hub + conn *websocket.Conn + send chan []byte + nickname string + joinTime time.Time +} +``` + +### Message Flow +1. **Client connects** → WebSocket upgrade → Register with hub +2. **Client sends message** → JSON parsing → Broadcast to all clients +3. **Client disconnects** → Cleanup → Notify other clients + +## Testing the Chat + +### Basic Functionality +1. **Single User Test:** + ```bash + # Open browser to localhost:8080 + # Enter nickname "Alice" + # Send message "Hello world" + # Verify message appears + ``` + +2. **Multi-User Test:** + ```bash + # Open 3 browser tabs + # Join as "Alice", "Bob", "Charlie" + # Send messages from each user + # Verify all users see all messages + ``` + +### WebSocket Testing with Tools + +**Using wscat (if installed):** +```bash +# Install wscat: npm install -g wscat +wscat -c ws://localhost:8080/ws + +# Send join message +{"type":"join","nickname":"test_user"} + +# Send chat message +{"type":"message","content":"Hello from wscat!"} +``` + +**Using curl for HTTP endpoints:** +```bash +curl http://localhost:8080/health +curl http://localhost:8080/stats +``` + +## Features Demonstrated + +### 1. WebSocket Basics +- **Connection upgrade** from HTTP to WebSocket +- **Bidirectional communication** between client and server +- **Message handling** with JSON protocol + +### 2. Concurrency Patterns +- **Goroutine per connection** for concurrent client handling +- **Channel communication** between goroutines +- **Mutex protection** for shared data structures + +### 3. Real-Time Features +- **Instant message delivery** to all connected clients +- **User presence notifications** (join/leave events) +- **Connection state management** and cleanup + +### 4. Error Handling +- **Connection failure recovery** with automatic cleanup +- **Invalid message handling** with error responses +- **Graceful degradation** when clients disconnect unexpectedly + +## Learning Objectives + +After exploring this example, you will understand: + +1. **WebSocket Protocol** + - How to upgrade HTTP connections to WebSocket + - Message framing and protocol design + - Connection lifecycle management + +2. **Real-Time Communication Patterns** + - Message broadcasting architectures + - Client state synchronization + - Event-driven programming + +3. **Go Concurrency** + - Goroutines for concurrent connections + - Channel communication patterns + - Thread-safe data access with mutexes + +4. **Production Considerations** + - Connection scaling strategies + - Memory management for long-lived connections + - Error handling and recovery patterns + +## Extending the Example + +### Add Private Messaging +```go +// Add room/channel support +type Room struct { + name string + clients map[*Client]bool +} +``` + +### Add Message Persistence +```go +// Store messages in database +type Message struct { + ID int `json:"id"` + Nickname string `json:"nickname"` + Content string `json:"content"` + Timestamp time.Time `json:"timestamp"` +} +``` + +### Add Authentication +```go +// Require JWT token for WebSocket connections +func authenticateWebSocket(r *http.Request) (*User, error) { + token := r.Header.Get("Authorization") + return validateJWT(token) +} +``` + +## Performance Considerations + +### Connection Limits +- **Default limit:** 1000 concurrent connections +- **Memory usage:** ~8KB per connection +- **CPU usage:** Minimal when idle + +### Scaling Strategies +- **Horizontal scaling** with Redis pub/sub +- **Load balancing** with sticky sessions +- **Connection pooling** for database operations + +## Troubleshooting + +**WebSocket connection fails:** +```bash +# Check if server is running +curl http://localhost:8080/health + +# Check browser console for errors +# Verify WebSocket URL is correct +``` + +**Messages not appearing:** +```bash +# Check browser network tab +# Verify JSON message format +# Check server logs for errors +``` + +**High memory usage:** +```bash +# Monitor with: go tool pprof http://localhost:8080/debug/pprof/heap +# Check for connection leaks +# Verify proper cleanup on disconnect +``` + +## Next Steps + +- Explore the **Production API** example for authentication patterns +- Check the **Middleware Showcase** for security and monitoring +- Review the **Basic Server** example for REST API fundamentals \ No newline at end of file diff --git a/examples/03-websocket-chat/go.mod b/examples/03-websocket-chat/go.mod new file mode 100644 index 0000000..6ad7b68 --- /dev/null +++ b/examples/03-websocket-chat/go.mod @@ -0,0 +1,50 @@ +module zoox-websocket-chat-example + +go 1.19 + +require ( + github.com/go-zoox/zoox v1.11.7 + github.com/gorilla/websocket v1.5.1 +) + +require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.17.0 // indirect + github.com/go-redis/redis/v8 v8.11.5 // indirect + github.com/go-zoox/cache v1.0.9 // indirect + github.com/go-zoox/cookie v1.2.0 // indirect + github.com/go-zoox/core-utils v1.2.11 // indirect + github.com/go-zoox/crypto v1.1.8 // indirect + github.com/go-zoox/datetime v1.1.1 // indirect + github.com/go-zoox/encoding v1.2.1 // indirect + github.com/go-zoox/errors v1.0.2 // indirect + github.com/go-zoox/headers v1.0.6 // indirect + github.com/go-zoox/jsonrpc v1.3.0 // indirect + github.com/go-zoox/kv v1.5.0 // indirect + github.com/go-zoox/logger v1.4.4 // indirect + github.com/go-zoox/proxy v1.4.1 // indirect + github.com/go-zoox/random v1.0.4 // indirect + github.com/go-zoox/safe v1.0.1 // indirect + github.com/go-zoox/session v1.2.0 // indirect + github.com/go-zoox/tag v1.2.3 // indirect + github.com/go-zoox/uuid v0.0.1 // indirect + github.com/go-zoox/websocket v1.3.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/leodido/go-urn v1.3.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/spf13/cast v1.6.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) \ No newline at end of file diff --git a/examples/03-websocket-chat/main.go b/examples/03-websocket-chat/main.go new file mode 100644 index 0000000..a515cee --- /dev/null +++ b/examples/03-websocket-chat/main.go @@ -0,0 +1,632 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "sync" + "time" + + "github.com/go-zoox/zoox" + "github.com/gorilla/websocket" +) + +// Message represents a chat message +type Message struct { + ID string `json:"id"` + Type string `json:"type"` + Username string `json:"username"` + Content string `json:"content"` + Timestamp time.Time `json:"timestamp"` + Room string `json:"room"` +} + +// Client represents a WebSocket client +type Client struct { + ID string + Username string + Room string + Conn *websocket.Conn + Hub *Hub + Send chan []byte +} + +// Hub manages WebSocket connections and message broadcasting +type Hub struct { + clients map[*Client]bool + rooms map[string]map[*Client]bool + broadcast chan []byte + register chan *Client + unregister chan *Client + mutex sync.RWMutex + messages []Message +} + +// NewHub creates a new WebSocket hub +func NewHub() *Hub { + return &Hub{ + clients: make(map[*Client]bool), + rooms: make(map[string]map[*Client]bool), + broadcast: make(chan []byte), + register: make(chan *Client), + unregister: make(chan *Client), + messages: make([]Message, 0), + } +} + +// Run starts the hub's main loop +func (h *Hub) Run() { + for { + select { + case client := <-h.register: + h.registerClient(client) + + case client := <-h.unregister: + h.unregisterClient(client) + + case message := <-h.broadcast: + h.broadcastMessage(message) + } + } +} + +func (h *Hub) registerClient(client *Client) { + h.mutex.Lock() + defer h.mutex.Unlock() + + // Add client to main registry + h.clients[client] = true + + // Add client to room + if h.rooms[client.Room] == nil { + h.rooms[client.Room] = make(map[*Client]bool) + } + h.rooms[client.Room][client] = true + + // Send welcome message + welcomeMsg := Message{ + ID: fmt.Sprintf("welcome_%d", time.Now().UnixNano()), + Type: "system", + Username: "System", + Content: fmt.Sprintf("%s joined the room", client.Username), + Timestamp: time.Now(), + Room: client.Room, + } + + h.messages = append(h.messages, welcomeMsg) + + // Broadcast join notification + msgData, _ := json.Marshal(welcomeMsg) + h.broadcastToRoom(client.Room, msgData) + + // Send recent messages to new client + h.sendRecentMessages(client) + + // Send updated user list + h.broadcastUserList(client.Room) + + log.Printf("Client %s (%s) connected to room %s", client.Username, client.ID, client.Room) +} + +func (h *Hub) unregisterClient(client *Client) { + h.mutex.Lock() + defer h.mutex.Unlock() + + if _, ok := h.clients[client]; ok { + // Remove from main registry + delete(h.clients, client) + + // Remove from room + if roomClients, ok := h.rooms[client.Room]; ok { + delete(roomClients, client) + if len(roomClients) == 0 { + delete(h.rooms, client.Room) + } + } + + close(client.Send) + + // Send leave message + leaveMsg := Message{ + ID: fmt.Sprintf("leave_%d", time.Now().UnixNano()), + Type: "system", + Username: "System", + Content: fmt.Sprintf("%s left the room", client.Username), + Timestamp: time.Now(), + Room: client.Room, + } + + h.messages = append(h.messages, leaveMsg) + + // Broadcast leave notification + msgData, _ := json.Marshal(leaveMsg) + h.broadcastToRoom(client.Room, msgData) + + // Send updated user list + h.broadcastUserList(client.Room) + + log.Printf("Client %s (%s) disconnected from room %s", client.Username, client.ID, client.Room) + } +} + +func (h *Hub) broadcastMessage(message []byte) { + var msg Message + if err := json.Unmarshal(message, &msg); err != nil { + log.Printf("Error parsing message: %v", err) + return + } + + h.mutex.Lock() + h.messages = append(h.messages, msg) + h.mutex.Unlock() + + h.broadcastToRoom(msg.Room, message) +} + +func (h *Hub) broadcastToRoom(room string, message []byte) { + h.mutex.RLock() + roomClients := h.rooms[room] + h.mutex.RUnlock() + + for client := range roomClients { + select { + case client.Send <- message: + default: + close(client.Send) + delete(h.clients, client) + delete(roomClients, client) + } + } +} + +func (h *Hub) sendRecentMessages(client *Client) { + h.mutex.RLock() + defer h.mutex.RUnlock() + + // Send last 50 messages from the room + roomMessages := make([]Message, 0) + for _, msg := range h.messages { + if msg.Room == client.Room { + roomMessages = append(roomMessages, msg) + } + } + + // Keep only the last 50 messages + if len(roomMessages) > 50 { + roomMessages = roomMessages[len(roomMessages)-50:] + } + + for _, msg := range roomMessages { + msgData, _ := json.Marshal(msg) + select { + case client.Send <- msgData: + default: + close(client.Send) + delete(h.clients, client) + } + } +} + +func (h *Hub) broadcastUserList(room string) { + h.mutex.RLock() + roomClients := h.rooms[room] + h.mutex.RUnlock() + + users := make([]string, 0) + for client := range roomClients { + users = append(users, client.Username) + } + + userListMsg := Message{ + ID: fmt.Sprintf("userlist_%d", time.Now().UnixNano()), + Type: "userlist", + Username: "System", + Content: "", + Timestamp: time.Now(), + Room: room, + } + + // Add users list to content as JSON + usersData, _ := json.Marshal(users) + userListMsg.Content = string(usersData) + + msgData, _ := json.Marshal(userListMsg) + h.broadcastToRoom(room, msgData) +} + +func (h *Hub) getRoomStats() map[string]interface{} { + h.mutex.RLock() + defer h.mutex.RUnlock() + + stats := map[string]interface{}{ + "total_clients": len(h.clients), + "total_rooms": len(h.rooms), + "total_messages": len(h.messages), + "rooms": make(map[string]int), + } + + for room, clients := range h.rooms { + stats["rooms"].(map[string]int)[room] = len(clients) + } + + return stats +} + +// WebSocket upgrader +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true // Allow all origins for demo + }, +} + +// readPump handles reading from the WebSocket connection +func (c *Client) readPump() { + defer func() { + c.Hub.unregister <- c + c.Conn.Close() + }() + + c.Conn.SetReadLimit(512) + c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + c.Conn.SetPongHandler(func(string) error { + c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + return nil + }) + + for { + var msg Message + err := c.Conn.ReadJSON(&msg) + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + log.Printf("WebSocket error: %v", err) + } + break + } + + // Set message metadata + msg.ID = fmt.Sprintf("msg_%d", time.Now().UnixNano()) + msg.Username = c.Username + msg.Timestamp = time.Now() + msg.Room = c.Room + + if msg.Type == "" { + msg.Type = "message" + } + + msgData, _ := json.Marshal(msg) + c.Hub.broadcast <- msgData + } +} + +// writePump handles writing to the WebSocket connection +func (c *Client) writePump() { + ticker := time.NewTicker(54 * time.Second) + defer func() { + ticker.Stop() + c.Conn.Close() + }() + + for { + select { + case message, ok := <-c.Send: + c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if !ok { + c.Conn.WriteMessage(websocket.CloseMessage, []byte{}) + return + } + + c.Conn.WriteMessage(websocket.TextMessage, message) + + case <-ticker.C: + c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { + return + } + } + } +} + +func main() { + app := zoox.Default() + + // Create and start WebSocket hub + hub := NewHub() + go hub.Run() + + // Serve the chat interface + app.Get("/", func(ctx *zoox.Context) { + ctx.HTML(http.StatusOK, chatHTML) + }) + + // WebSocket endpoint + app.Get("/ws", func(ctx *zoox.Context) { + username := ctx.Query().Get("username", "Anonymous") + room := ctx.Query().Get("room", "general") + + if username == "" { + ctx.JSON(http.StatusBadRequest, map[string]string{ + "error": "Username is required", + }) + return + } + + // Upgrade connection to WebSocket + conn, err := upgrader.Upgrade(ctx.Writer, ctx.Request.Request, nil) + if err != nil { + log.Printf("WebSocket upgrade error: %v", err) + return + } + + // Create new client + client := &Client{ + ID: fmt.Sprintf("client_%d", time.Now().UnixNano()), + Username: username, + Room: room, + Conn: conn, + Hub: hub, + Send: make(chan []byte, 256), + } + + // Register client + client.Hub.register <- client + + // Start goroutines for reading and writing + go client.writePump() + go client.readPump() + }) + + // API endpoints + api := app.Group("/api") + + // Get chat statistics + api.Get("/stats", func(ctx *zoox.Context) { + stats := hub.getRoomStats() + ctx.JSON(http.StatusOK, stats) + }) + + // Get room list + api.Get("/rooms", func(ctx *zoox.Context) { + hub.mutex.RLock() + rooms := make([]map[string]interface{}, 0) + for room, clients := range hub.rooms { + users := make([]string, 0) + for client := range clients { + users = append(users, client.Username) + } + rooms = append(rooms, map[string]interface{}{ + "name": room, + "user_count": len(clients), + "users": users, + }) + } + hub.mutex.RUnlock() + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "rooms": rooms, + }) + }) + + // Get messages for a room + api.Get("/rooms/:room/messages", func(ctx *zoox.Context) { + room := ctx.Param("room") + + hub.mutex.RLock() + roomMessages := make([]Message, 0) + for _, msg := range hub.messages { + if msg.Room == room { + roomMessages = append(roomMessages, msg) + } + } + hub.mutex.RUnlock() + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "room": room, + "messages": roomMessages, + }) + }) + + fmt.Println("🚀 WebSocket Chat starting...") + fmt.Println("📍 Server running on http://localhost:8080") + fmt.Println("💬 Chat Interface: http://localhost:8080") + fmt.Println("📊 Chat Statistics: http://localhost:8080/api/stats") + fmt.Println("🏠 Room List: http://localhost:8080/api/rooms") + + app.Run(":8080") +} + +// HTML template for the chat interface +const chatHTML = ` + + + Zoox WebSocket Chat + + + + + +
+
+

🚀 Zoox WebSocket Chat

+
+ +
+

Join Chat

+ +
+ +
+
+ Room: | Status: Disconnected +
+ +
+
+

Online Users

+
    +
    + +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    +
    + + + +` \ No newline at end of file diff --git a/examples/04-file-upload-download/README.md b/examples/04-file-upload-download/README.md new file mode 100644 index 0000000..7a541b1 --- /dev/null +++ b/examples/04-file-upload-download/README.md @@ -0,0 +1,377 @@ +# File Upload/Download System Example + +This example demonstrates comprehensive file handling capabilities with the Zoox framework, including file uploads, downloads, validation, security measures, and metadata management. + +## Features + +### File Upload +- **Single and multiple file uploads** with drag-and-drop support +- **Chunked upload** for large files with progress tracking +- **File validation** including type, size, and content validation +- **Secure file storage** with sanitized filenames +- **Upload progress** tracking and cancellation + +### File Download +- **Secure file serving** with access controls +- **Range request support** for partial downloads and resumable downloads +- **Content-Type detection** and proper headers +- **Download speed limiting** and bandwidth control + +### Security Features +- **File type validation** using MIME types and magic numbers +- **Filename sanitization** to prevent directory traversal +- **Size limits** configurable per file type +- **Virus scanning integration** (placeholder for production) +- **Access control** with authentication + +### File Management +- **File metadata storage** including upload time, user, size +- **File organization** with automatic directory structure +- **Duplicate detection** using file hashing +- **Cleanup utilities** for orphaned files +- **Storage quotas** per user or globally + +## Quick Start + +1. **Run the application:** + ```bash + cd examples/04-file-upload-download + go mod tidy + go run main.go + ``` + +2. **Access the file interface:** + - Open browser to `http://localhost:8080` + - Try uploading various file types + - Download files to test the system + +3. **Test with curl:** + ```bash + # Upload a file + curl -X POST -F "file=@example.txt" http://localhost:8080/upload + + # Download a file + curl -O http://localhost:8080/download/example.txt + + # Get file info + curl http://localhost:8080/files/example.txt/info + ``` + +## API Endpoints + +### Upload Endpoints +- **POST /upload** - Single file upload +- **POST /upload/multiple** - Multiple file upload +- **POST /upload/chunked** - Chunked upload for large files +- **GET /upload** - Upload interface (HTML form) + +### Download Endpoints +- **GET /download/:filename** - Download file by name +- **GET /files/:id** - Download file by ID +- **HEAD /download/:filename** - Get file headers without content + +### File Management +- **GET /files** - List all files with pagination +- **GET /files/:filename/info** - Get file metadata +- **DELETE /files/:filename** - Delete file (requires auth) +- **POST /files/:filename/move** - Move/rename file + +### System Endpoints +- **GET /storage/stats** - Storage statistics and quotas +- **GET /health** - System health check +- **POST /cleanup** - Clean orphaned files (admin only) + +## File Validation Rules + +### Supported File Types +```go +var allowedTypes = map[string][]string{ + "image": {".jpg", ".jpeg", ".png", ".gif", ".webp"}, + "document": {".pdf", ".doc", ".docx", ".txt", ".md"}, + "archive": {".zip", ".tar", ".gz", ".7z"}, + "video": {".mp4", ".avi", ".mov", ".webm"}, + "audio": {".mp3", ".wav", ".aac", ".ogg"}, +} +``` + +### Size Limits +- **Images:** 5MB maximum +- **Documents:** 10MB maximum +- **Archives:** 50MB maximum +- **Videos:** 100MB maximum +- **Audio:** 25MB maximum +- **Default:** 10MB maximum + +### Security Validations +1. **MIME type checking** against file extension +2. **Magic number validation** to detect file type spoofing +3. **Filename sanitization** to prevent path traversal +4. **Content scanning** for malicious content (placeholder) + +## Upload Process Flow + +### 1. File Validation +```go +func validateFile(header *multipart.FileHeader) error { + // Check file size + if header.Size > maxFileSize { + return ErrFileTooLarge + } + + // Validate file extension + ext := filepath.Ext(header.Filename) + if !isAllowedExtension(ext) { + return ErrInvalidFileType + } + + // Additional validations... +} +``` + +### 2. Secure Storage +```go +func saveFile(file multipart.File, filename string) error { + // Generate secure filename + safeFilename := sanitizeFilename(filename) + + // Create directory structure + dir := generateStoragePath(safeFilename) + + // Save with atomic write + return atomicWrite(filepath.Join(dir, safeFilename), file) +} +``` + +### 3. Metadata Storage +```go +type FileMetadata struct { + ID string `json:"id"` + Filename string `json:"filename"` + OriginalName string `json:"original_name"` + Size int64 `json:"size"` + ContentType string `json:"content_type"` + Hash string `json:"hash"` + UploadedAt time.Time `json:"uploaded_at"` + UploadedBy string `json:"uploaded_by"` +} +``` + +## Download Features + +### Range Request Support +```bash +# Download bytes 100-199 +curl -H "Range: bytes=100-199" http://localhost:8080/download/large-file.zip + +# Resume download from byte 1000 +curl -H "Range: bytes=1000-" http://localhost:8080/download/large-file.zip +``` + +### Content Disposition +- **Inline viewing** for images and PDFs +- **Attachment download** for archives and executables +- **Custom filenames** for downloaded files + +### Streaming Downloads +- **Memory efficient** streaming for large files +- **Progress tracking** with content-length headers +- **Bandwidth limiting** to prevent server overload + +## Testing Scenarios + +### 1. Basic Upload/Download +```bash +# Create test file +echo "Hello World" > test.txt + +# Upload file +curl -X POST -F "file=@test.txt" http://localhost:8080/upload + +# Verify upload +curl http://localhost:8080/files + +# Download file +curl -O http://localhost:8080/download/test.txt +``` + +### 2. Multiple File Upload +```bash +# Upload multiple files +curl -X POST \ + -F "files=@file1.txt" \ + -F "files=@file2.jpg" \ + -F "files=@file3.pdf" \ + http://localhost:8080/upload/multiple +``` + +### 3. Large File Handling +```bash +# Create large file +dd if=/dev/zero of=large.dat bs=1M count=50 + +# Upload with progress +curl -X POST -F "file=@large.dat" \ + --progress-bar http://localhost:8080/upload + +# Test range downloads +curl -H "Range: bytes=0-1023" \ + http://localhost:8080/download/large.dat +``` + +### 4. Security Testing +```bash +# Try path traversal +curl -X POST -F "file=@test.txt" \ + -F "filename=../../../etc/passwd" \ + http://localhost:8080/upload + +# Try oversized file +dd if=/dev/zero of=huge.dat bs=1M count=200 +curl -X POST -F "file=@huge.dat" http://localhost:8080/upload + +# Try invalid file type +curl -X POST -F "file=@malware.exe" http://localhost:8080/upload +``` + +## Configuration + +### Environment Variables +```bash +export UPLOAD_DIR="./uploads" +export MAX_FILE_SIZE="10485760" # 10MB +export MAX_STORAGE_SIZE="1073741824" # 1GB +export ENABLE_VIRUS_SCAN="false" +export CLEANUP_INTERVAL="24h" +``` + +### Storage Configuration +```go +type StorageConfig struct { + UploadDir string `json:"upload_dir"` + MaxFileSize int64 `json:"max_file_size"` + MaxStorageSize int64 `json:"max_storage_size"` + AllowedTypes []string `json:"allowed_types"` + CleanupInterval time.Duration `json:"cleanup_interval"` +} +``` + +## Production Considerations + +### 1. Storage Backend +- **Local filesystem** for development +- **AWS S3/MinIO** for production scalability +- **Database metadata** for file tracking +- **CDN integration** for fast downloads + +### 2. Security Enhancements +- **Virus scanning** with ClamAV or similar +- **Content analysis** for malicious content +- **Rate limiting** for upload endpoints +- **Authentication** and authorization + +### 3. Performance Optimization +- **Asynchronous processing** for large uploads +- **Image resizing** and thumbnail generation +- **Compression** for compatible file types +- **Caching** for frequently accessed files + +### 4. Monitoring and Logging +- **Upload/download metrics** and analytics +- **Error tracking** and alerting +- **Storage usage** monitoring +- **Performance metrics** and optimization + +## Learning Objectives + +After working with this example, you will understand: + +1. **File Handling in Go** + - Multipart form processing + - File I/O operations and streaming + - Memory-efficient file processing + +2. **Security Best Practices** + - Input validation and sanitization + - Path traversal prevention + - Content type validation + +3. **HTTP File Operations** + - Range request handling + - Proper content headers + - Upload progress tracking + +4. **Production File Systems** + - Scalable storage architectures + - Metadata management + - Error handling and recovery + +## Extending the Example + +### Add Image Processing +```go +import "github.com/disintegration/imaging" + +func resizeImage(src image.Image, width, height int) image.Image { + return imaging.Resize(src, width, height, imaging.Lanczos) +} +``` + +### Add Cloud Storage +```go +import "github.com/aws/aws-sdk-go/service/s3" + +func uploadToS3(file io.Reader, key string) error { + _, err := s3.PutObject(&s3.PutObjectInput{ + Bucket: aws.String("my-bucket"), + Key: aws.String(key), + Body: file, + }) + return err +} +``` + +### Add File Sharing +```go +type ShareLink struct { + FileID string `json:"file_id"` + Token string `json:"token"` + ExpiresAt time.Time `json:"expires_at"` + Downloads int `json:"downloads"` + MaxDownloads int `json:"max_downloads"` +} +``` + +## Troubleshooting + +**Upload fails with "file too large":** +```bash +# Check server limits +curl http://localhost:8080/storage/stats + +# Increase limits in configuration +export MAX_FILE_SIZE="52428800" # 50MB +``` + +**Download returns 404:** +```bash +# Verify file exists +curl http://localhost:8080/files + +# Check file permissions +ls -la uploads/ +``` + +**Performance issues with large files:** +```bash +# Monitor memory usage +go tool pprof http://localhost:8080/debug/pprof/heap + +# Enable streaming for large files +# Use chunked upload for files > 10MB +``` + +## Next Steps + +- Explore the **Production API** example for authentication integration +- Check the **Middleware Showcase** for security and monitoring patterns +- Review cloud storage integration patterns for scalability \ No newline at end of file diff --git a/examples/04-file-upload-download/go.mod b/examples/04-file-upload-download/go.mod new file mode 100644 index 0000000..f7f951f --- /dev/null +++ b/examples/04-file-upload-download/go.mod @@ -0,0 +1,7 @@ +module file-upload-download + +go 1.19 + +require github.com/go-zoox/zoox v1.0.0 + +replace github.com/go-zoox/zoox => ../../ \ No newline at end of file diff --git a/examples/04-file-upload-download/main.go b/examples/04-file-upload-download/main.go new file mode 100644 index 0000000..5457621 --- /dev/null +++ b/examples/04-file-upload-download/main.go @@ -0,0 +1,767 @@ +package main + +import ( + "fmt" + "io" + "log" + "mime/multipart" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/go-zoox/zoox" + "github.com/go-zoox/zoox/middleware" +) + +const ( + uploadDir = "./uploads" + maxFileSize = 10 * 1024 * 1024 // 10MB +) + +func main() { + // Create upload directory if it doesn't exist + if err := os.MkdirAll(uploadDir, 0755); err != nil { + log.Fatal("Failed to create upload directory:", err) + } + + app := zoox.Default() + + // Enable CORS for file uploads + app.Use(middleware.CORS()) + + // Body size limit for uploads + app.Use(middleware.BodyLimit(&middleware.BodyLimitConfig{ + MaxSize: maxFileSize, + })) + + // Serve static files + app.Static("/uploads", uploadDir) + + // Main page with upload form + app.Get("/", func(ctx *zoox.Context) { + ctx.HTML(200, uploadPageHTML) + }) + + // File upload endpoints + app.Post("/upload/single", uploadSingleFileHandler) + app.Post("/upload/multiple", uploadMultipleFilesHandler) + app.Post("/upload/chunked", uploadChunkedFileHandler) + + // File management endpoints + app.Get("/api/files", listFilesHandler) + app.Get("/api/files/:filename", getFileInfoHandler) + app.Delete("/api/files/:filename", deleteFileHandler) + + // Download endpoints + app.Get("/download/:filename", downloadFileHandler) + app.Get("/download/:filename/inline", viewFileHandler) + + // Image processing endpoints + app.Get("/image/:filename/thumbnail", thumbnailHandler) + app.Get("/image/:filename/resize", resizeHandler) + + // File validation example + app.Post("/upload/images", uploadImagesOnlyHandler) + app.Post("/upload/documents", uploadDocumentsOnlyHandler) + + log.Println("File Upload/Download Server starting on http://localhost:8080") + log.Println("Upload directory:", uploadDir) + log.Println("Max file size:", maxFileSize/(1024*1024), "MB") + log.Println("\nEndpoints:") + log.Println(" GET / - Upload form") + log.Println(" POST /upload/single - Single file upload") + log.Println(" POST /upload/multiple - Multiple files upload") + log.Println(" GET /api/files - List uploaded files") + log.Println(" GET /download/:filename - Download file") + + app.Run(":8080") +} + +func uploadSingleFileHandler(ctx *zoox.Context) { + file, err := ctx.FormFile("file") + if err != nil { + ctx.JSON(400, zoox.H{ + "error": "No file uploaded", + "details": err.Error(), + }) + return + } + + // Validate file + if err := validateFile(file); err != nil { + ctx.JSON(400, zoox.H{ + "error": "File validation failed", + "details": err.Error(), + }) + return + } + + // Generate unique filename + filename := generateUniqueFilename(file.Filename) + filepath := filepath.Join(uploadDir, filename) + + // Save file + if err := ctx.SaveFile(file, filepath); err != nil { + ctx.JSON(500, zoox.H{ + "error": "Failed to save file", + "details": err.Error(), + }) + return + } + + ctx.JSON(200, zoox.H{ + "message": "File uploaded successfully", + "file": zoox.H{ + "original_name": file.Filename, + "saved_name": filename, + "size": file.Size, + "type": file.Header.Get("Content-Type"), + "url": "/uploads/" + filename, + "download_url": "/download/" + filename, + }, + }) +} + +func uploadMultipleFilesHandler(ctx *zoox.Context) { + form, err := ctx.MultipartForm() + if err != nil { + ctx.JSON(400, zoox.H{ + "error": "Failed to parse multipart form", + "details": err.Error(), + }) + return + } + + files := form.File["files"] + if len(files) == 0 { + ctx.JSON(400, zoox.H{ + "error": "No files uploaded", + }) + return + } + + var uploadedFiles []zoox.H + var errors []string + + for _, file := range files { + // Validate file + if err := validateFile(file); err != nil { + errors = append(errors, fmt.Sprintf("%s: %s", file.Filename, err.Error())) + continue + } + + // Generate unique filename + filename := generateUniqueFilename(file.Filename) + filepath := filepath.Join(uploadDir, filename) + + // Save file + if err := ctx.SaveFile(file, filepath); err != nil { + errors = append(errors, fmt.Sprintf("%s: %s", file.Filename, err.Error())) + continue + } + + uploadedFiles = append(uploadedFiles, zoox.H{ + "original_name": file.Filename, + "saved_name": filename, + "size": file.Size, + "type": file.Header.Get("Content-Type"), + "url": "/uploads/" + filename, + "download_url": "/download/" + filename, + }) + } + + response := zoox.H{ + "message": fmt.Sprintf("Uploaded %d files successfully", len(uploadedFiles)), + "files": uploadedFiles, + "total": len(uploadedFiles), + } + + if len(errors) > 0 { + response["errors"] = errors + response["failed"] = len(errors) + } + + ctx.JSON(200, response) +} + +func uploadChunkedFileHandler(ctx *zoox.Context) { + // Get chunk information + chunkNumber := ctx.Form().Get("chunkNumber", "0") + totalChunks := ctx.Form().Get("totalChunks", "1") + filename := ctx.Form().Get("filename") + + if filename == "" { + ctx.JSON(400, zoox.H{"error": "Filename is required"}) + return + } + + file, err := ctx.FormFile("chunk") + if err != nil { + ctx.JSON(400, zoox.H{ + "error": "No chunk uploaded", + "details": err.Error(), + }) + return + } + + // Create temporary directory for chunks + tempDir := filepath.Join(uploadDir, "temp", filename) + if err := os.MkdirAll(tempDir, 0755); err != nil { + ctx.JSON(500, zoox.H{ + "error": "Failed to create temp directory", + "details": err.Error(), + }) + return + } + + // Save chunk + chunkPath := filepath.Join(tempDir, fmt.Sprintf("chunk_%s", chunkNumber)) + if err := ctx.SaveFile(file, chunkPath); err != nil { + ctx.JSON(500, zoox.H{ + "error": "Failed to save chunk", + "details": err.Error(), + }) + return + } + + // Check if all chunks are uploaded + totalChunksInt, _ := strconv.Atoi(totalChunks) + chunkNumberInt, _ := strconv.Atoi(chunkNumber) + + if chunkNumberInt+1 == totalChunksInt { + // Merge chunks + finalPath := filepath.Join(uploadDir, generateUniqueFilename(filename)) + if err := mergeChunks(tempDir, finalPath, totalChunksInt); err != nil { + ctx.JSON(500, zoox.H{ + "error": "Failed to merge chunks", + "details": err.Error(), + }) + return + } + + // Clean up temp directory + os.RemoveAll(tempDir) + + ctx.JSON(200, zoox.H{ + "message": "File uploaded successfully", + "file": zoox.H{ + "original_name": filename, + "saved_name": filepath.Base(finalPath), + "url": "/uploads/" + filepath.Base(finalPath), + "download_url": "/download/" + filepath.Base(finalPath), + }, + }) + } else { + ctx.JSON(200, zoox.H{ + "message": fmt.Sprintf("Chunk %d uploaded successfully", chunkNumberInt+1), + "chunk": chunkNumberInt + 1, + "total": totalChunksInt, + }) + } +} + +func listFilesHandler(ctx *zoox.Context) { + files, err := os.ReadDir(uploadDir) + if err != nil { + ctx.JSON(500, zoox.H{ + "error": "Failed to read upload directory", + "details": err.Error(), + }) + return + } + + var fileList []zoox.H + for _, file := range files { + if file.IsDir() { + continue + } + + info, err := file.Info() + if err != nil { + continue + } + + fileList = append(fileList, zoox.H{ + "name": file.Name(), + "size": info.Size(), + "modified": info.ModTime().Format(time.RFC3339), + "url": "/uploads/" + file.Name(), + "download_url": "/download/" + file.Name(), + }) + } + + ctx.JSON(200, zoox.H{ + "files": fileList, + "total": len(fileList), + }) +} + +func getFileInfoHandler(ctx *zoox.Context) { + filename := ctx.Param().Get("filename") + filepath := filepath.Join(uploadDir, filename) + + info, err := os.Stat(filepath) + if err != nil { + ctx.JSON(404, zoox.H{ + "error": "File not found", + }) + return + } + + ctx.JSON(200, zoox.H{ + "name": filename, + "size": info.Size(), + "modified": info.ModTime().Format(time.RFC3339), + "url": "/uploads/" + filename, + "download_url": "/download/" + filename, + }) +} + +func deleteFileHandler(ctx *zoox.Context) { + filename := ctx.Param().Get("filename") + filepath := filepath.Join(uploadDir, filename) + + if err := os.Remove(filepath); err != nil { + ctx.JSON(404, zoox.H{ + "error": "File not found or cannot be deleted", + "details": err.Error(), + }) + return + } + + ctx.JSON(200, zoox.H{ + "message": "File deleted successfully", + "filename": filename, + }) +} + +func downloadFileHandler(ctx *zoox.Context) { + filename := ctx.Param().Get("filename") + filepath := filepath.Join(uploadDir, filename) + + // Check if file exists + if _, err := os.Stat(filepath); err != nil { + ctx.JSON(404, zoox.H{ + "error": "File not found", + }) + return + } + + // Force download + ctx.Attachment(filepath, filename) +} + +func viewFileHandler(ctx *zoox.Context) { + filename := ctx.Param().Get("filename") + filepath := filepath.Join(uploadDir, filename) + + // Check if file exists + if _, err := os.Stat(filepath); err != nil { + ctx.JSON(404, zoox.H{ + "error": "File not found", + }) + return + } + + // Serve file inline + ctx.File(filepath) +} + +func thumbnailHandler(ctx *zoox.Context) { + filename := ctx.Param().Get("filename") + + // In a real application, you would generate thumbnails here + ctx.JSON(200, zoox.H{ + "message": "Thumbnail generation not implemented", + "filename": filename, + "note": "This would generate a thumbnail of the image", + }) +} + +func resizeHandler(ctx *zoox.Context) { + filename := ctx.Param().Get("filename") + width := ctx.Query().Get("width", "200") + height := ctx.Query().Get("height", "200") + + // In a real application, you would resize images here + ctx.JSON(200, zoox.H{ + "message": "Image resizing not implemented", + "filename": filename, + "width": width, + "height": height, + "note": "This would resize the image to specified dimensions", + }) +} + +func uploadImagesOnlyHandler(ctx *zoox.Context) { + file, err := ctx.FormFile("image") + if err != nil { + ctx.JSON(400, zoox.H{ + "error": "No image uploaded", + "details": err.Error(), + }) + return + } + + // Validate image file + contentType := file.Header.Get("Content-Type") + if !strings.HasPrefix(contentType, "image/") { + ctx.JSON(400, zoox.H{ + "error": "Only image files are allowed", + "received": contentType, + }) + return + } + + filename := generateUniqueFilename(file.Filename) + filepath := filepath.Join(uploadDir, filename) + + if err := ctx.SaveFile(file, filepath); err != nil { + ctx.JSON(500, zoox.H{ + "error": "Failed to save image", + "details": err.Error(), + }) + return + } + + ctx.JSON(200, zoox.H{ + "message": "Image uploaded successfully", + "image": zoox.H{ + "original_name": file.Filename, + "saved_name": filename, + "size": file.Size, + "type": contentType, + "url": "/uploads/" + filename, + "thumbnail_url": "/image/" + filename + "/thumbnail", + }, + }) +} + +func uploadDocumentsOnlyHandler(ctx *zoox.Context) { + file, err := ctx.FormFile("document") + if err != nil { + ctx.JSON(400, zoox.H{ + "error": "No document uploaded", + "details": err.Error(), + }) + return + } + + // Validate document file + allowedTypes := []string{ + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "text/plain", + } + + contentType := file.Header.Get("Content-Type") + isAllowed := false + for _, allowedType := range allowedTypes { + if contentType == allowedType { + isAllowed = true + break + } + } + + if !isAllowed { + ctx.JSON(400, zoox.H{ + "error": "Only document files are allowed", + "allowed": allowedTypes, + "received": contentType, + }) + return + } + + filename := generateUniqueFilename(file.Filename) + filepath := filepath.Join(uploadDir, filename) + + if err := ctx.SaveFile(file, filepath); err != nil { + ctx.JSON(500, zoox.H{ + "error": "Failed to save document", + "details": err.Error(), + }) + return + } + + ctx.JSON(200, zoox.H{ + "message": "Document uploaded successfully", + "document": zoox.H{ + "original_name": file.Filename, + "saved_name": filename, + "size": file.Size, + "type": contentType, + "url": "/uploads/" + filename, + }, + }) +} + +// Utility functions +func validateFile(file *multipart.FileHeader) error { + if file.Size > maxFileSize { + return fmt.Errorf("file size exceeds limit of %d MB", maxFileSize/(1024*1024)) + } + + // Check file extension + ext := strings.ToLower(filepath.Ext(file.Filename)) + dangerousExts := []string{".exe", ".bat", ".cmd", ".sh", ".ps1"} + for _, dangerousExt := range dangerousExts { + if ext == dangerousExt { + return fmt.Errorf("file type %s is not allowed", ext) + } + } + + return nil +} + +func generateUniqueFilename(originalName string) string { + ext := filepath.Ext(originalName) + name := strings.TrimSuffix(originalName, ext) + timestamp := time.Now().Unix() + return fmt.Sprintf("%s_%d%s", name, timestamp, ext) +} + +func mergeChunks(tempDir, finalPath string, totalChunks int) error { + finalFile, err := os.Create(finalPath) + if err != nil { + return err + } + defer finalFile.Close() + + for i := 0; i < totalChunks; i++ { + chunkPath := filepath.Join(tempDir, fmt.Sprintf("chunk_%d", i)) + chunkFile, err := os.Open(chunkPath) + if err != nil { + return err + } + + _, err = io.Copy(finalFile, chunkFile) + chunkFile.Close() + if err != nil { + return err + } + } + + return nil +} + +const uploadPageHTML = ` + + + File Upload & Download + + + +

    🚀 Zoox File Upload & Download

    + +
    +

    Single File Upload

    + + + +
    +
    + +
    +

    Multiple Files Upload

    + + + +
    +
    + +
    +

    Images Only

    + + +
    +
    + +
    +

    Documents Only

    + + +
    +
    + +
    +

    Uploaded Files

    + +
    +
    + + + +` \ No newline at end of file diff --git a/examples/05-json-rpc-service/README.md b/examples/05-json-rpc-service/README.md new file mode 100644 index 0000000..7086bbf --- /dev/null +++ b/examples/05-json-rpc-service/README.md @@ -0,0 +1,523 @@ +# JSON-RPC Service Example + +This example demonstrates how to implement a JSON-RPC 2.0 service using the Zoox framework. It showcases service-oriented architecture, method registration, error handling, and provides both programmatic and interactive testing interfaces. + +## Features + +### JSON-RPC 2.0 Compliance +- **Standard protocol** implementation following JSON-RPC 2.0 specification +- **Request/Response handling** with proper ID correlation +- **Batch request support** for multiple operations +- **Error codes and messages** following RPC standards + +### Service Architecture +- **Modular service design** with separate service classes +- **Method registration** with automatic discovery +- **Parameter validation** and type checking +- **Result serialization** with proper JSON formatting + +### Available Services +- **Math Service** - Basic mathematical operations +- **User Service** - User management operations +- **System Service** - System information and utilities +- **Echo Service** - Testing and debugging utilities + +### Advanced Features +- **Custom error handling** with detailed error information +- **Method introspection** and service discovery +- **Interactive testing interface** with HTML/JavaScript client +- **Logging and monitoring** for RPC calls + +## Quick Start + +1. **Run the JSON-RPC server:** + ```bash + cd examples/05-json-rpc-service + go mod tidy + go run main.go + ``` + +2. **Test with the interactive interface:** + - Open browser to `http://localhost:8080` + - Use the web interface to test RPC methods + - Try different services and parameters + +3. **Test with curl:** + ```bash + # Basic math operation + curl -X POST http://localhost:8080/rpc \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "method": "Math.Add", + "params": [5, 3], + "id": 1 + }' + + # Create a user + curl -X POST http://localhost:8080/rpc \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "method": "User.Create", + "params": {"name": "John", "email": "john@example.com"}, + "id": 2 + }' + ``` + +## Available RPC Methods + +### Math Service (`Math.*`) + +**Math.Add** - Add two numbers +```json +{ + "jsonrpc": "2.0", + "method": "Math.Add", + "params": [10, 5], + "id": 1 +} +// Response: {"jsonrpc": "2.0", "result": 15, "id": 1} +``` + +**Math.Subtract** - Subtract two numbers +```json +{ + "jsonrpc": "2.0", + "method": "Math.Subtract", + "params": [10, 3], + "id": 2 +} +// Response: {"jsonrpc": "2.0", "result": 7, "id": 2} +``` + +**Math.Multiply** - Multiply two numbers +```json +{ + "jsonrpc": "2.0", + "method": "Math.Multiply", + "params": [4, 6], + "id": 3 +} +``` + +**Math.Divide** - Divide two numbers (with error handling) +```json +{ + "jsonrpc": "2.0", + "method": "Math.Divide", + "params": [10, 2], + "id": 4 +} +// Error case (division by zero): +// {"jsonrpc": "2.0", "error": {"code": -32602, "message": "Division by zero"}, "id": 4} +``` + +**Math.Power** - Calculate power +```json +{ + "jsonrpc": "2.0", + "method": "Math.Power", + "params": [2, 8], + "id": 5 +} +``` + +### User Service (`User.*`) + +**User.Create** - Create a new user +```json +{ + "jsonrpc": "2.0", + "method": "User.Create", + "params": { + "name": "Alice Smith", + "email": "alice@example.com", + "age": 30 + }, + "id": 10 +} +``` + +**User.Get** - Get user by ID +```json +{ + "jsonrpc": "2.0", + "method": "User.Get", + "params": [1], + "id": 11 +} +``` + +**User.List** - List all users +```json +{ + "jsonrpc": "2.0", + "method": "User.List", + "params": [], + "id": 12 +} +``` + +**User.Update** - Update user information +```json +{ + "jsonrpc": "2.0", + "method": "User.Update", + "params": { + "id": 1, + "name": "Alice Johnson", + "email": "alice.johnson@example.com" + }, + "id": 13 +} +``` + +**User.Delete** - Delete user by ID +```json +{ + "jsonrpc": "2.0", + "method": "User.Delete", + "params": [1], + "id": 14 +} +``` + +### System Service (`System.*`) + +**System.Info** - Get system information +```json +{ + "jsonrpc": "2.0", + "method": "System.Info", + "params": [], + "id": 20 +} +``` + +**System.Time** - Get current server time +```json +{ + "jsonrpc": "2.0", + "method": "System.Time", + "params": [], + "id": 21 +} +``` + +**System.Methods** - List all available methods +```json +{ + "jsonrpc": "2.0", + "method": "System.Methods", + "params": [], + "id": 22 +} +``` + +### Echo Service (`Echo.*`) + +**Echo.Message** - Echo back a message +```json +{ + "jsonrpc": "2.0", + "method": "Echo.Message", + "params": ["Hello, JSON-RPC!"], + "id": 30 +} +``` + +**Echo.Delay** - Echo with artificial delay (for testing) +```json +{ + "jsonrpc": "2.0", + "method": "Echo.Delay", + "params": {"message": "Delayed response", "seconds": 2}, + "id": 31 +} +``` + +## Batch Requests + +Send multiple RPC calls in a single HTTP request: + +```json +[ + { + "jsonrpc": "2.0", + "method": "Math.Add", + "params": [1, 2], + "id": 1 + }, + { + "jsonrpc": "2.0", + "method": "Math.Multiply", + "params": [3, 4], + "id": 2 + }, + { + "jsonrpc": "2.0", + "method": "User.List", + "params": [], + "id": 3 + } +] +``` + +## Error Handling + +### Standard JSON-RPC Error Codes +- **-32700** Parse error (Invalid JSON) +- **-32600** Invalid Request +- **-32601** Method not found +- **-32602** Invalid params +- **-32603** Internal error +- **-32000 to -32099** Server error (custom) + +### Custom Error Examples + +**Method not found:** +```json +{ + "jsonrpc": "2.0", + "error": { + "code": -32601, + "message": "Method not found", + "data": "Method 'Math.Invalid' does not exist" + }, + "id": 1 +} +``` + +**Invalid parameters:** +```json +{ + "jsonrpc": "2.0", + "error": { + "code": -32602, + "message": "Invalid params", + "data": "Expected 2 parameters, got 1" + }, + "id": 2 +} +``` + +**Business logic error:** +```json +{ + "jsonrpc": "2.0", + "error": { + "code": -32000, + "message": "User not found", + "data": "No user with ID 999" + }, + "id": 3 +} +``` + +## Service Implementation + +### Service Structure +```go +type MathService struct{} + +func (s *MathService) Add(a, b float64) float64 { + return a + b +} + +func (s *MathService) Divide(a, b float64) (float64, error) { + if b == 0 { + return 0, errors.New("division by zero") + } + return a / b, nil +} +``` + +### Method Registration +```go +// Register services +rpc := NewRPCHandler() +rpc.RegisterService("Math", &MathService{}) +rpc.RegisterService("User", &UserService{}) +rpc.RegisterService("System", &SystemService{}) +rpc.RegisterService("Echo", &EchoService{}) +``` + +### Parameter Handling +```go +type CreateUserParams struct { + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"required,email"` + Age int `json:"age" validate:"min=0,max=150"` +} + +func (s *UserService) Create(params CreateUserParams) (*User, error) { + // Validation is automatic + user := &User{ + ID: generateID(), + Name: params.Name, + Email: params.Email, + Age: params.Age, + } + return user, s.store.Save(user) +} +``` + +## Testing Strategies + +### 1. Unit Testing RPC Methods +```go +func TestMathService_Add(t *testing.T) { + service := &MathService{} + result := service.Add(2, 3) + assert.Equal(t, 5.0, result) +} + +func TestMathService_Divide_ByZero(t *testing.T) { + service := &MathService{} + _, err := service.Divide(10, 0) + assert.Error(t, err) + assert.Contains(t, err.Error(), "division by zero") +} +``` + +### 2. Integration Testing +```go +func TestRPCHandler_MathAdd(t *testing.T) { + handler := NewRPCHandler() + handler.RegisterService("Math", &MathService{}) + + request := RPCRequest{ + JSONRPC: "2.0", + Method: "Math.Add", + Params: []interface{}{5.0, 3.0}, + ID: 1, + } + + response := handler.HandleRequest(request) + assert.Equal(t, 8.0, response.Result) +} +``` + +### 3. Load Testing +```bash +# Using Apache Bench +ab -n 1000 -c 10 -p request.json -T application/json \ + http://localhost:8080/rpc + +# Using curl in a loop +for i in {1..100}; do + curl -X POST http://localhost:8080/rpc \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"Math.Add","params":[1,2],"id":'$i'}' +done +``` + +### 4. Error Testing +```bash +# Test invalid JSON +curl -X POST http://localhost:8080/rpc \ + -H "Content-Type: application/json" \ + -d '{"invalid": json}' + +# Test method not found +curl -X POST http://localhost:8080/rpc \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"Invalid.Method","id":1}' + +# Test invalid parameters +curl -X POST http://localhost:8080/rpc \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"Math.Add","params":["not","numbers"],"id":1}' +``` + +## Performance Considerations + +### Request Processing +- **Single-threaded** method execution per request +- **Concurrent requests** handled by multiple goroutines +- **Memory pooling** for JSON marshaling/unmarshaling +- **Connection reuse** for HTTP keep-alive + +### Optimization Strategies +- **Method caching** for reflection-based method lookup +- **Parameter pre-validation** to fail fast +- **Response compression** for large result sets +- **Connection pooling** for database operations + +### Monitoring Metrics +- **Request count** per method +- **Response times** and percentiles +- **Error rates** by error type +- **Concurrent connections** and queue depth + +## Learning Objectives + +After working with this example, you will understand: + +1. **JSON-RPC Protocol** + - Request/response format and structure + - Error handling and standard error codes + - Batch processing and notifications + +2. **Service-Oriented Architecture** + - Service registration and discovery + - Method routing and parameter binding + - Interface design and contracts + +3. **Go Reflection and Type System** + - Dynamic method invocation + - Parameter type conversion + - Error handling and propagation + +4. **API Design Patterns** + - RPC vs REST trade-offs + - Versioning strategies + - Documentation and discoverability + +## Production Considerations + +### 1. Authentication & Authorization +```go +type AuthContext struct { + UserID string `json:"user_id"` + Roles []string `json:"roles"` +} + +func (s *UserService) GetProfile(ctx AuthContext, userID string) (*User, error) { + if ctx.UserID != userID && !hasRole(ctx.Roles, "admin") { + return nil, errors.New("access denied") + } + return s.store.GetUser(userID) +} +``` + +### 2. Rate Limiting +```go +func (h *RPCHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if !h.rateLimiter.Allow(r.RemoteAddr) { + http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests) + return + } + // Process request... +} +``` + +### 3. Logging and Monitoring +```go +func (h *RPCHandler) logRequest(req *RPCRequest, resp *RPCResponse, duration time.Duration) { + log.WithFields(log.Fields{ + "method": req.Method, + "id": req.ID, + "duration": duration, + "error": resp.Error != nil, + }).Info("RPC call completed") +} +``` + +## Next Steps + +- Explore the **Production API** example for authentication patterns +- Check the **WebSocket Chat** example for real-time RPC over WebSockets +- Review the **Middleware Showcase** for security and monitoring middleware +- Consider gRPC as an alternative to JSON-RPC for high-performance scenarios \ No newline at end of file diff --git a/examples/05-json-rpc-service/go.mod b/examples/05-json-rpc-service/go.mod new file mode 100644 index 0000000..809e8f0 --- /dev/null +++ b/examples/05-json-rpc-service/go.mod @@ -0,0 +1,7 @@ +module json-rpc-service + +go 1.19 + +require github.com/go-zoox/zoox v1.0.0 + +replace github.com/go-zoox/zoox => ../../ \ No newline at end of file diff --git a/examples/05-json-rpc-service/main.go b/examples/05-json-rpc-service/main.go new file mode 100644 index 0000000..e52ad67 --- /dev/null +++ b/examples/05-json-rpc-service/main.go @@ -0,0 +1,559 @@ +package main + +import ( + "context" + "log" + "math" + "time" + + "github.com/go-zoox/zoox" + "github.com/go-zoox/zoox/middleware" +) + +// Math service for JSON-RPC +type MathService struct{} + +// Add method +func (m *MathService) Add(ctx context.Context, args *AddArgs, reply *AddReply) error { + reply.Result = args.A + args.B + return nil +} + +// Subtract method +func (m *MathService) Subtract(ctx context.Context, args *SubtractArgs, reply *SubtractReply) error { + reply.Result = args.A - args.B + return nil +} + +// Multiply method +func (m *MathService) Multiply(ctx context.Context, args *MultiplyArgs, reply *MultiplyReply) error { + reply.Result = args.A * args.B + return nil +} + +// Divide method +func (m *MathService) Divide(ctx context.Context, args *DivideArgs, reply *DivideReply) error { + if args.B == 0 { + return &JSONRPCError{ + Code: -32000, + Message: "Division by zero", + } + } + reply.Result = args.A / args.B + return nil +} + +// Power method +func (m *MathService) Power(ctx context.Context, args *PowerArgs, reply *PowerReply) error { + reply.Result = math.Pow(args.Base, args.Exponent) + return nil +} + +// Sqrt method +func (m *MathService) Sqrt(ctx context.Context, args *SqrtArgs, reply *SqrtReply) error { + if args.Number < 0 { + return &JSONRPCError{ + Code: -32000, + Message: "Cannot calculate square root of negative number", + } + } + reply.Result = math.Sqrt(args.Number) + return nil +} + +// User service for JSON-RPC +type UserService struct { + users map[int]*User +} + +func NewUserService() *UserService { + return &UserService{ + users: make(map[int]*User), + } +} + +// GetUser method +func (u *UserService) GetUser(ctx context.Context, args *GetUserArgs, reply *GetUserReply) error { + user, exists := u.users[args.ID] + if !exists { + return &JSONRPCError{ + Code: -32000, + Message: "User not found", + } + } + reply.User = user + return nil +} + +// CreateUser method +func (u *UserService) CreateUser(ctx context.Context, args *CreateUserArgs, reply *CreateUserReply) error { + if args.Name == "" { + return &JSONRPCError{ + Code: -32000, + Message: "Name is required", + } + } + if args.Email == "" { + return &JSONRPCError{ + Code: -32000, + Message: "Email is required", + } + } + + // Generate new ID + id := len(u.users) + 1 + user := &User{ + ID: id, + Name: args.Name, + Email: args.Email, + CreatedAt: time.Now(), + } + + u.users[id] = user + reply.User = user + return nil +} + +// ListUsers method +func (u *UserService) ListUsers(ctx context.Context, args *ListUsersArgs, reply *ListUsersReply) error { + var users []*User + for _, user := range u.users { + users = append(users, user) + } + reply.Users = users + reply.Total = len(users) + return nil +} + +// UpdateUser method +func (u *UserService) UpdateUser(ctx context.Context, args *UpdateUserArgs, reply *UpdateUserReply) error { + user, exists := u.users[args.ID] + if !exists { + return &JSONRPCError{ + Code: -32000, + Message: "User not found", + } + } + + if args.Name != "" { + user.Name = args.Name + } + if args.Email != "" { + user.Email = args.Email + } + user.UpdatedAt = time.Now() + + reply.User = user + return nil +} + +// DeleteUser method +func (u *UserService) DeleteUser(ctx context.Context, args *DeleteUserArgs, reply *DeleteUserReply) error { + _, exists := u.users[args.ID] + if !exists { + return &JSONRPCError{ + Code: -32000, + Message: "User not found", + } + } + + delete(u.users, args.ID) + reply.Success = true + return nil +} + +// Data structures +type AddArgs struct { + A float64 `json:"a"` + B float64 `json:"b"` +} + +type AddReply struct { + Result float64 `json:"result"` +} + +type SubtractArgs struct { + A float64 `json:"a"` + B float64 `json:"b"` +} + +type SubtractReply struct { + Result float64 `json:"result"` +} + +type MultiplyArgs struct { + A float64 `json:"a"` + B float64 `json:"b"` +} + +type MultiplyReply struct { + Result float64 `json:"result"` +} + +type DivideArgs struct { + A float64 `json:"a"` + B float64 `json:"b"` +} + +type DivideReply struct { + Result float64 `json:"result"` +} + +type PowerArgs struct { + Base float64 `json:"base"` + Exponent float64 `json:"exponent"` +} + +type PowerReply struct { + Result float64 `json:"result"` +} + +type SqrtArgs struct { + Number float64 `json:"number"` +} + +type SqrtReply struct { + Result float64 `json:"result"` +} + +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type GetUserArgs struct { + ID int `json:"id"` +} + +type GetUserReply struct { + User *User `json:"user"` +} + +type CreateUserArgs struct { + Name string `json:"name"` + Email string `json:"email"` +} + +type CreateUserReply struct { + User *User `json:"user"` +} + +type ListUsersArgs struct{} + +type ListUsersReply struct { + Users []*User `json:"users"` + Total int `json:"total"` +} + +type UpdateUserArgs struct { + ID int `json:"id"` + Name string `json:"name,omitempty"` + Email string `json:"email,omitempty"` +} + +type UpdateUserReply struct { + User *User `json:"user"` +} + +type DeleteUserArgs struct { + ID int `json:"id"` +} + +type DeleteUserReply struct { + Success bool `json:"success"` +} + +// JSON-RPC Error +type JSONRPCError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +func (e *JSONRPCError) Error() string { + return e.Message +} + +func main() { + app := zoox.Default() + + // Enable CORS + app.Use(middleware.CORS()) + + // Serve static files for the test page + app.Get("/", func(ctx *zoox.Context) { + ctx.HTML(200, jsonRPCPageHTML) + }) + + // Create services + mathService := &MathService{} + userService := NewUserService() + + // Register JSON-RPC services + app.JSONRPC("/rpc/math", mathService) + app.JSONRPC("/rpc/user", userService) + + // REST API for comparison + app.Get("/api/health", func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "status": "healthy", + "timestamp": time.Now().Format(time.RFC3339), + "services": []string{ + "Math Service - /rpc/math", + "User Service - /rpc/user", + }, + }) + }) + + // Documentation endpoint + app.Get("/api/docs", func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "math_service": zoox.H{ + "endpoint": "/rpc/math", + "methods": []zoox.H{ + {"name": "Add", "params": "a, b (numbers)", "returns": "result (number)"}, + {"name": "Subtract", "params": "a, b (numbers)", "returns": "result (number)"}, + {"name": "Multiply", "params": "a, b (numbers)", "returns": "result (number)"}, + {"name": "Divide", "params": "a, b (numbers)", "returns": "result (number)"}, + {"name": "Power", "params": "base, exponent (numbers)", "returns": "result (number)"}, + {"name": "Sqrt", "params": "number (number)", "returns": "result (number)"}, + }, + }, + "user_service": zoox.H{ + "endpoint": "/rpc/user", + "methods": []zoox.H{ + {"name": "GetUser", "params": "id (number)", "returns": "user (object)"}, + {"name": "CreateUser", "params": "name, email (strings)", "returns": "user (object)"}, + {"name": "ListUsers", "params": "none", "returns": "users (array), total (number)"}, + {"name": "UpdateUser", "params": "id (number), name, email (strings, optional)", "returns": "user (object)"}, + {"name": "DeleteUser", "params": "id (number)", "returns": "success (boolean)"}, + }, + }, + }) + }) + + log.Println("JSON-RPC Service starting on http://localhost:8080") + log.Println("\nAvailable Services:") + log.Println(" Math Service: /rpc/math") + log.Println(" User Service: /rpc/user") + log.Println("\nTest Interface: http://localhost:8080") + log.Println("API Documentation: http://localhost:8080/api/docs") + + app.Run(":8080") +} + +const jsonRPCPageHTML = ` + + + JSON-RPC Service Test + + + +

    🚀 Zoox JSON-RPC Service Test

    +

    This page demonstrates JSON-RPC services with the Zoox framework.

    + +
    +
    +
    +

    Math Service

    +

    Endpoint: /rpc/math

    + +
    +

    Add

    +
    + + +
    +
    + + +
    + +
    + +
    +

    Subtract

    +
    + + +
    +
    + + +
    + +
    + +
    +

    Multiply

    +
    + + +
    +
    + + +
    + +
    + +
    +

    Divide

    +
    + + +
    +
    + + +
    + +
    + +
    +

    Power

    +
    + + +
    +
    + + +
    + +
    + +
    +

    Square Root

    +
    + + +
    + +
    +
    +
    + +
    +
    +

    User Service

    +

    Endpoint: /rpc/user

    + +
    +

    Create User

    +
    + + +
    +
    + + +
    + +
    + +
    +

    Get User

    +
    + + +
    + +
    + +
    +

    List Users

    + +
    + +
    +

    Update User

    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + +
    +

    Delete User

    +
    + + +
    + +
    +
    +
    +
    + +
    +

    Results

    +
    Results will appear here...
    +
    + + + +` \ No newline at end of file diff --git a/examples/06-production-api/README.md b/examples/06-production-api/README.md new file mode 100644 index 0000000..92d1086 --- /dev/null +++ b/examples/06-production-api/README.md @@ -0,0 +1,372 @@ +# Production API Example + +This example demonstrates a production-ready API built with the Zoox framework, showcasing advanced features, security best practices, monitoring, and proper application architecture. + +## 🚀 Features + +### 🔐 Security +- **Authentication & Authorization**: Token-based authentication with role-based access control +- **Security Headers**: Helmet middleware for security headers +- **CORS Protection**: Cross-origin resource sharing configuration +- **Rate Limiting**: Request rate limiting to prevent abuse +- **Input Validation**: Request body validation and sanitization + +### 📊 Monitoring & Observability +- **Health Checks**: Comprehensive health check endpoint +- **Metrics Collection**: Application metrics and performance monitoring +- **Request Logging**: Detailed request/response logging +- **Error Tracking**: Error counting and monitoring +- **Debug Information**: Development-only debug endpoints + +### 🏗️ Architecture +- **Clean Architecture**: Separation of concerns with structured application design +- **Configuration Management**: Environment-based configuration +- **Graceful Shutdown**: Proper server shutdown handling +- **Middleware Pipeline**: Comprehensive middleware stack +- **API Versioning**: Versioned API endpoints + +### 🛡️ Production Features +- **Timeouts**: Request timeout handling +- **Compression**: Gzip compression for responses +- **Real IP Detection**: Proper client IP detection behind proxies +- **Recovery**: Panic recovery middleware +- **Request ID**: Unique request ID for tracing + +## 🏃 Quick Start + +### 1. Run the Application + +```bash +cd examples/06-production-api +go run main.go +``` + +### 2. Test the Endpoints + +The server will start on `http://localhost:8080` with the following endpoints: + +#### Public Endpoints +- `GET /` - API information +- `GET /health` - Health check +- `GET /metrics` - Application metrics +- `GET /api/v1/status` - API status +- `POST /api/v1/auth/login` - User authentication + +#### Protected Endpoints (require authentication) +- `GET /api/v1/protected/users` - List users +- `POST /api/v1/protected/users` - Create user +- `GET /api/v1/protected/users/:id` - Get user +- `PUT /api/v1/protected/users/:id` - Update user +- `DELETE /api/v1/protected/users/:id` - Delete user + +#### Admin Endpoints (require admin role) +- `GET /api/v1/protected/admin/stats` - Admin statistics +- `GET /api/v1/protected/admin/logs` - Application logs + +#### Development Endpoints (development mode only) +- `GET /debug/pprof/` - Go profiling +- `GET /debug/vars` - Debug variables + +## 🔧 Configuration + +The application can be configured using environment variables: + +```bash +export PORT=8080 +export ENV=production +export DB_HOST=localhost +export DB_PORT=5432 +export JWT_SECRET=your-secret-key +``` + +## 📝 API Usage Examples + +### Authentication + +```bash +# Login to get access token +curl -X POST http://localhost:8080/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username": "admin", "password": "secret"}' + +# Response +{ + "success": true, + "data": { + "token": "valid-token", + "expires_in": 3600, + "user": { + "username": "admin", + "role": "admin" + } + } +} +``` + +### Health Check + +```bash +curl http://localhost:8080/health + +# Response +{ + "status": "healthy", + "version": "1.0.0", + "timestamp": "2024-01-01T12:00:00Z", + "services": { + "database": "connected", + "cache": "connected", + "queue": "connected" + } +} +``` + +### Metrics + +```bash +curl http://localhost:8080/metrics + +# Response +{ + "success": true, + "data": { + "request_count": 1250, + "error_count": 12, + "average_latency_ms": 45.2, + "uptime": "2h30m15s" + } +} +``` + +### User Management (Protected) + +```bash +# List users (requires authentication) +curl http://localhost:8080/api/v1/protected/users \ + -H "Authorization: Bearer valid-token" + +# Create user +curl -X POST http://localhost:8080/api/v1/protected/users \ + -H "Authorization: Bearer valid-token" \ + -H "Content-Type: application/json" \ + -d '{"username": "newuser", "email": "new@example.com", "role": "user"}' + +# Get specific user +curl http://localhost:8080/api/v1/protected/users/1 \ + -H "Authorization: Bearer valid-token" + +# Update user +curl -X PUT http://localhost:8080/api/v1/protected/users/1 \ + -H "Authorization: Bearer valid-token" \ + -H "Content-Type: application/json" \ + -d '{"username": "updateduser", "email": "updated@example.com"}' + +# Delete user +curl -X DELETE http://localhost:8080/api/v1/protected/users/1 \ + -H "Authorization: Bearer valid-token" +``` + +### Admin Statistics (Admin Only) + +```bash +curl http://localhost:8080/api/v1/protected/admin/stats \ + -H "Authorization: Bearer valid-token" + +# Response +{ + "success": true, + "data": { + "total_users": 3, + "active_users": 2, + "total_requests": 1250, + "error_rate": 0.96, + "uptime": "2h30m15s", + "memory_usage": "45MB" + } +} +``` + +## 🏗️ Architecture Overview + +### Application Structure + +``` +main.go +├── Config # Configuration management +├── App # Application struct with dependencies +├── Middleware # Custom middleware functions +├── Handlers # HTTP request handlers +├── Models # Data structures +└── Helpers # Utility functions +``` + +### Middleware Stack + +1. **Logger** - Request/response logging +2. **Recovery** - Panic recovery +3. **RequestID** - Unique request identification +4. **CORS** - Cross-origin resource sharing +5. **Helmet** - Security headers +6. **RealIP** - Client IP detection +7. **Gzip** - Response compression +8. **Timeout** - Request timeout handling +9. **RateLimit** - Request rate limiting +10. **Metrics** - Custom metrics collection + +### Security Features + +- **Token Authentication**: Simple bearer token authentication +- **Role-Based Access Control**: Admin and user roles +- **Rate Limiting**: 100 requests per minute per IP +- **Security Headers**: Comprehensive security headers via Helmet +- **Input Validation**: Request body validation +- **CORS Protection**: Configurable CORS policies + +### Error Handling + +The API uses a standardized error response format: + +```json +{ + "success": false, + "error": "Error message description" +} +``` + +Common HTTP status codes: +- `200` - Success +- `201` - Created +- `400` - Bad Request +- `401` - Unauthorized +- `403` - Forbidden +- `404` - Not Found +- `500` - Internal Server Error + +## 🔍 Monitoring + +### Health Checks + +The `/health` endpoint provides: +- Service status +- Version information +- Timestamp +- External service connectivity + +### Metrics + +The `/metrics` endpoint provides: +- Request count +- Error count +- Average latency +- Uptime information + +### Logging + +All requests are logged with: +- HTTP method +- Request path +- Client IP +- Response status +- Request duration + +## 🚀 Production Deployment + +### Docker Deployment + +```dockerfile +FROM golang:1.19-alpine AS builder +WORKDIR /app +COPY . . +RUN go mod download +RUN go build -o main . + +FROM alpine:latest +RUN apk --no-cache add ca-certificates +WORKDIR /root/ +COPY --from=builder /app/main . +EXPOSE 8080 +CMD ["./main"] +``` + +### Environment Variables + +```bash +# Production configuration +ENV=production +PORT=8080 +JWT_SECRET=your-production-secret +DB_HOST=your-database-host +DB_PORT=5432 + +# Optional configuration +RATE_LIMIT=1000 # requests per minute +``` + +### Kubernetes Deployment + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: production-api +spec: + replicas: 3 + selector: + matchLabels: + app: production-api + template: + metadata: + labels: + app: production-api + spec: + containers: + - name: api + image: your-registry/production-api:latest + ports: + - containerPort: 8080 + env: + - name: ENV + value: "production" + - name: PORT + value: "8080" + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 +``` + +## 📚 Key Learnings + +This example demonstrates: + +1. **Structured Application Design**: Clean separation of concerns +2. **Security Best Practices**: Authentication, authorization, and security headers +3. **Monitoring & Observability**: Health checks, metrics, and logging +4. **Production Readiness**: Graceful shutdown, timeouts, and error handling +5. **API Design**: RESTful endpoints with proper HTTP status codes +6. **Configuration Management**: Environment-based configuration +7. **Middleware Usage**: Comprehensive middleware stack +8. **Error Handling**: Standardized error responses + +## 🔗 Related Examples + +- [Basic Server](../01-basic-server/) - Simple HTTP server basics +- [Middleware Showcase](../02-middleware-showcase/) - Middleware examples +- [WebSocket Chat](../03-websocket-chat/) - Real-time features +- [File Upload System](../04-file-upload-download/) - File handling +- [JSON-RPC Service](../05-json-rpc-service/) - RPC implementation + +## 📖 Further Reading + +- [Zoox Documentation](../../DOCUMENTATION.md) +- [Security Best Practices](../../DOCUMENTATION.md#security) +- [Production Deployment](../../DOCUMENTATION.md#deployment) +- [Monitoring Guide](../../DOCUMENTATION.md#monitoring) \ No newline at end of file diff --git a/examples/06-production-api/go.mod b/examples/06-production-api/go.mod new file mode 100644 index 0000000..2ec5ea1 --- /dev/null +++ b/examples/06-production-api/go.mod @@ -0,0 +1,48 @@ +module zoox-production-api-example + +go 1.19 + +require github.com/go-zoox/zoox v1.11.7 + +require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.17.0 // indirect + github.com/go-redis/redis/v8 v8.11.5 // indirect + github.com/go-zoox/cache v1.0.9 // indirect + github.com/go-zoox/cookie v1.2.0 // indirect + github.com/go-zoox/core-utils v1.2.11 // indirect + github.com/go-zoox/crypto v1.1.8 // indirect + github.com/go-zoox/datetime v1.1.1 // indirect + github.com/go-zoox/encoding v1.2.1 // indirect + github.com/go-zoox/errors v1.0.2 // indirect + github.com/go-zoox/headers v1.0.6 // indirect + github.com/go-zoox/jsonrpc v1.3.0 // indirect + github.com/go-zoox/kv v1.5.0 // indirect + github.com/go-zoox/logger v1.4.4 // indirect + github.com/go-zoox/proxy v1.4.1 // indirect + github.com/go-zoox/random v1.0.4 // indirect + github.com/go-zoox/safe v1.0.1 // indirect + github.com/go-zoox/session v1.2.0 // indirect + github.com/go-zoox/tag v1.2.3 // indirect + github.com/go-zoox/uuid v0.0.1 // indirect + github.com/go-zoox/websocket v1.3.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.1 // indirect + github.com/leodido/go-urn v1.3.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/spf13/cast v1.6.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) \ No newline at end of file diff --git a/examples/06-production-api/main.go b/examples/06-production-api/main.go new file mode 100644 index 0000000..38b4fc1 --- /dev/null +++ b/examples/06-production-api/main.go @@ -0,0 +1,646 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/go-zoox/zoox" + "github.com/go-zoox/zoox/middleware" +) + +// Configuration holds application configuration +type Config struct { + Port string + Environment string + JWTSecret string + DatabaseURL string + RedisURL string + LogLevel string + RateLimitRPS int + CorsOrigins []string + TLSCertFile string + TLSKeyFile string + HealthCheckPath string +} + +// LoadConfig loads configuration from environment variables +func LoadConfig() *Config { + return &Config{ + Port: getEnv("PORT", "8080"), + Environment: getEnv("ENVIRONMENT", "development"), + JWTSecret: getEnv("JWT_SECRET", "your-super-secret-jwt-key"), + DatabaseURL: getEnv("DATABASE_URL", "postgres://localhost/myapp"), + RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"), + LogLevel: getEnv("LOG_LEVEL", "info"), + RateLimitRPS: 10, + CorsOrigins: []string{"http://localhost:3000", "https://myapp.com"}, + TLSCertFile: getEnv("TLS_CERT_FILE", ""), + TLSKeyFile: getEnv("TLS_KEY_FILE", ""), + HealthCheckPath: getEnv("HEALTH_CHECK_PATH", "/health"), + } +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +// User represents a user in the system +type User struct { + ID int `json:"id"` + Email string `json:"email"` + Username string `json:"username"` + Role string `json:"role"` + Active bool `json:"active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LastLoginAt time.Time `json:"last_login_at,omitempty"` +} + +// AuthService handles authentication +type AuthService struct { + config *Config +} + +// NewAuthService creates a new authentication service +func NewAuthService(config *Config) *AuthService { + return &AuthService{config: config} +} + +// ValidateJWT validates a JWT token and returns user info +func (a *AuthService) ValidateJWT(token string) (*User, error) { + // In production, use a proper JWT library like github.com/golang-jwt/jwt + // This is a simplified implementation for demo purposes + + // Mock user validation - replace with actual JWT validation + mockUsers := map[string]*User{ + "admin-token": { + ID: 1, + Email: "admin@example.com", + Username: "admin", + Role: "admin", + Active: true, + }, + "user-token": { + ID: 2, + Email: "user@example.com", + Username: "user", + Role: "user", + Active: true, + }, + } + + if user, exists := mockUsers[token]; exists { + user.LastLoginAt = time.Now() + return user, nil + } + + return nil, fmt.Errorf("invalid token") +} + +// HasPermission checks if user has required permission +func (u *User) HasPermission(permission string) bool { + switch u.Role { + case "admin": + return true // Admin has all permissions + case "user": + // User permissions + userPermissions := []string{"read", "create", "update_own"} + for _, p := range userPermissions { + if p == permission { + return true + } + } + } + return false +} + +// MetricsCollector collects application metrics +type MetricsCollector struct { + requestCount int64 + responseTime time.Duration + errorCount int64 + activeUsers int64 + memoryUsage int64 + uptimeStart time.Time +} + +// NewMetricsCollector creates a new metrics collector +func NewMetricsCollector() *MetricsCollector { + return &MetricsCollector{ + uptimeStart: time.Now(), + } +} + +// IncrementRequestCount increments the request counter +func (m *MetricsCollector) IncrementRequestCount() { + m.requestCount++ +} + +// RecordResponseTime records response time +func (m *MetricsCollector) RecordResponseTime(duration time.Duration) { + m.responseTime = duration +} + +// IncrementErrorCount increments error counter +func (m *MetricsCollector) IncrementErrorCount() { + m.errorCount++ +} + +// GetMetrics returns current metrics +func (m *MetricsCollector) GetMetrics() map[string]interface{} { + uptime := time.Since(m.uptimeStart) + + return map[string]interface{}{ + "requests_total": m.requestCount, + "errors_total": m.errorCount, + "response_time_ms": m.responseTime.Milliseconds(), + "active_users": m.activeUsers, + "memory_usage_mb": m.memoryUsage / 1024 / 1024, + "uptime_seconds": uptime.Seconds(), + "uptime_human": uptime.String(), + } +} + +// HealthChecker performs health checks +type HealthChecker struct { + config *Config +} + +// NewHealthChecker creates a new health checker +func NewHealthChecker(config *Config) *HealthChecker { + return &HealthChecker{config: config} +} + +// CheckHealth performs comprehensive health checks +func (h *HealthChecker) CheckHealth() map[string]interface{} { + health := map[string]interface{}{ + "status": "healthy", + "timestamp": time.Now().Unix(), + "version": "1.0.0", + "environment": h.config.Environment, + "checks": map[string]interface{}{ + "database": h.checkDatabase(), + "redis": h.checkRedis(), + "external_api": h.checkExternalAPI(), + "disk_space": h.checkDiskSpace(), + "memory": h.checkMemory(), + }, + } + + // Check if any service is unhealthy + allHealthy := true + for _, check := range health["checks"].(map[string]interface{}) { + if checkMap, ok := check.(map[string]interface{}); ok { + if status, exists := checkMap["status"]; exists && status != "healthy" { + allHealthy = false + break + } + } + } + + if !allHealthy { + health["status"] = "unhealthy" + } + + return health +} + +func (h *HealthChecker) checkDatabase() map[string]interface{} { + // Mock database check + return map[string]interface{}{ + "status": "healthy", + "response_time": "5ms", + "connections": 10, + } +} + +func (h *HealthChecker) checkRedis() map[string]interface{} { + // Mock Redis check + return map[string]interface{}{ + "status": "healthy", + "response_time": "2ms", + "memory_usage": "15MB", + } +} + +func (h *HealthChecker) checkExternalAPI() map[string]interface{} { + // Mock external API check + return map[string]interface{}{ + "status": "healthy", + "response_time": "120ms", + "last_check": time.Now().Format(time.RFC3339), + } +} + +func (h *HealthChecker) checkDiskSpace() map[string]interface{} { + // Mock disk space check + return map[string]interface{}{ + "status": "healthy", + "usage": "45%", + "available": "120GB", + } +} + +func (h *HealthChecker) checkMemory() map[string]interface{} { + // Mock memory check + return map[string]interface{}{ + "status": "healthy", + "usage": "60%", + "total": "8GB", + } +} + +func main() { + // Load configuration + config := LoadConfig() + + // Initialize services + authService := NewAuthService(config) + metricsCollector := NewMetricsCollector() + healthChecker := NewHealthChecker(config) + + // Create Zoox application + app := zoox.New() + + // ================================ + // GLOBAL MIDDLEWARE STACK + // ================================ + + // Request ID middleware + app.Use(middleware.RequestID()) + + // Structured logging middleware + app.Use(middleware.Logger()) + + // Recovery middleware with custom error handling + app.Use(middleware.Recovery()) + + // CORS middleware with production settings + app.Use(middleware.CORS(&middleware.CORSConfig{ + AllowOrigins: config.CorsOrigins, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Request-ID"}, + ExposeHeaders: []string{"X-Request-ID", "X-Response-Time"}, + AllowCredentials: true, + MaxAge: 12 * time.Hour, + })) + + // Security headers middleware + app.Use(middleware.Helmet(&middleware.HelmetConfig{ + XSSProtection: "1; mode=block", + ContentTypeNosniff: "nosniff", + XFrameOptions: "DENY", + ReferrerPolicy: "strict-origin-when-cross-origin", + ContentSecurityPolicy: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';", + })) + + // Rate limiting middleware + app.Use(middleware.RateLimit(&middleware.RateLimitConfig{ + Rate: float64(config.RateLimitRPS), + Burst: config.RateLimitRPS * 2, + Duration: time.Minute, + })) + + // Metrics collection middleware + app.Use(func(ctx *zoox.Context) { + start := time.Now() + metricsCollector.IncrementRequestCount() + + ctx.Next() + + duration := time.Since(start) + metricsCollector.RecordResponseTime(duration) + ctx.Header("X-Response-Time", duration.String()) + + if ctx.Status >= 400 { + metricsCollector.IncrementErrorCount() + } + }) + + // ================================ + // AUTHENTICATION MIDDLEWARE + // ================================ + + authMiddleware := func(ctx *zoox.Context) { + authHeader := ctx.Header("Authorization") + if authHeader == "" { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "error": "unauthorized", + "message": "Authorization header required", + "code": "AUTH_001", + }) + return + } + + // Extract Bearer token + token := "" + if len(authHeader) > 7 && authHeader[:7] == "Bearer " { + token = authHeader[7:] + } else { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "error": "unauthorized", + "message": "Invalid authorization header format", + "code": "AUTH_002", + }) + return + } + + // Validate token + user, err := authService.ValidateJWT(token) + if err != nil { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "error": "unauthorized", + "message": "Invalid or expired token", + "code": "AUTH_003", + }) + return + } + + if !user.Active { + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "error": "forbidden", + "message": "Account is disabled", + "code": "AUTH_004", + }) + return + } + + ctx.Set("user", user) + ctx.Next() + } + + // Permission middleware + requirePermission := func(permission string) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + user, exists := ctx.Get("user") + if !exists { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "error": "unauthorized", + "message": "Authentication required", + "code": "PERM_001", + }) + return + } + + u := user.(User) + if !u.HasPermission(permission) { + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "error": "forbidden", + "message": fmt.Sprintf("Permission '%s' required", permission), + "code": "PERM_002", + }) + return + } + + ctx.Next() + } + } + + // ================================ + // PUBLIC ROUTES + // ================================ + + // Health check endpoint + app.Get(config.HealthCheckPath, func(ctx *zoox.Context) { + health := healthChecker.CheckHealth() + + status := http.StatusOK + if health["status"] == "unhealthy" { + status = http.StatusServiceUnavailable + } + + ctx.JSON(status, health) + }) + + // Readiness probe (for Kubernetes) + app.Get("/ready", func(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "status": "ready", + "timestamp": time.Now().Unix(), + }) + }) + + // Liveness probe (for Kubernetes) + app.Get("/live", func(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "status": "alive", + "timestamp": time.Now().Unix(), + }) + }) + + // Metrics endpoint (for Prometheus) + app.Get("/metrics", func(ctx *zoox.Context) { + metrics := metricsCollector.GetMetrics() + ctx.JSON(http.StatusOK, metrics) + }) + + // API documentation + app.Get("/docs", func(ctx *zoox.Context) { + docs := map[string]interface{}{ + "title": "Production API", + "version": "1.0.0", + "environment": config.Environment, + "endpoints": map[string]interface{}{ + "health": "GET " + config.HealthCheckPath, + "metrics": "GET /metrics", + "auth": "POST /api/v1/auth/login", + "users": "GET /api/v1/users (requires auth)", + "admin": "GET /api/v1/admin/* (requires admin role)", + }, + "authentication": map[string]interface{}{ + "type": "Bearer Token", + "header": "Authorization: Bearer ", + "tokens": map[string]string{ + "admin": "admin-token", + "user": "user-token", + }, + }, + } + + ctx.JSON(http.StatusOK, docs) + }) + + // ================================ + // API ROUTES + // ================================ + + apiV1 := app.Group("/api/v1") + + // Authentication endpoints + authGroup := apiV1.Group("/auth") + + authGroup.Post("/login", func(ctx *zoox.Context) { + var loginReq struct { + Email string `json:"email"` + Password string `json:"password"` + } + + if err := ctx.BindJSON(&loginReq); err != nil { + ctx.JSON(http.StatusBadRequest, map[string]interface{}{ + "error": "bad_request", + "message": "Invalid JSON payload", + "code": "LOGIN_001", + }) + return + } + + // Mock authentication - in production, verify against database + var token string + var user *User + + switch loginReq.Email { + case "admin@example.com": + if loginReq.Password == "admin123" { + token = "admin-token" + user = &User{ + ID: 1, Email: "admin@example.com", Username: "admin", Role: "admin", Active: true, + } + } + case "user@example.com": + if loginReq.Password == "user123" { + token = "user-token" + user = &User{ + ID: 2, Email: "user@example.com", Username: "user", Role: "user", Active: true, + } + } + } + + if token == "" { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "error": "unauthorized", + "message": "Invalid credentials", + "code": "LOGIN_002", + }) + return + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "token": token, + "user": user, + "expires_in": 3600, + }) + }) + + // Protected routes + protected := apiV1.Group("/", authMiddleware) + + // User profile + protected.Get("/profile", func(ctx *zoox.Context) { + user := ctx.Get("user").(User) + ctx.JSON(http.StatusOK, map[string]interface{}{ + "user": user, + }) + }) + + // Users endpoint + protected.Get("/users", requirePermission("read"), func(ctx *zoox.Context) { + // Mock users data + users := []User{ + {ID: 1, Email: "admin@example.com", Username: "admin", Role: "admin", Active: true}, + {ID: 2, Email: "user@example.com", Username: "user", Role: "user", Active: true}, + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "users": users, + "count": len(users), + }) + }) + + // Admin routes + adminGroup := protected.Group("/admin", requirePermission("admin")) + + adminGroup.Get("/stats", func(ctx *zoox.Context) { + stats := map[string]interface{}{ + "total_users": 2, + "active_sessions": 1, + "system_load": "0.8", + "memory_usage": "512MB", + "disk_usage": "45%", + } + + ctx.JSON(http.StatusOK, stats) + }) + + adminGroup.Get("/logs", func(ctx *zoox.Context) { + // Mock recent logs + logs := []map[string]interface{}{ + { + "timestamp": time.Now().Add(-5 * time.Minute).Format(time.RFC3339), + "level": "INFO", + "message": "User login successful", + "user_id": 2, + }, + { + "timestamp": time.Now().Add(-10 * time.Minute).Format(time.RFC3339), + "level": "WARN", + "message": "Rate limit exceeded", + "ip": "192.168.1.100", + }, + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "logs": logs, + }) + }) + + // ================================ + // SERVER SETUP AND GRACEFUL SHUTDOWN + // ================================ + + // Create HTTP server + srv := &http.Server{ + Addr: ":" + config.Port, + Handler: app, + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + } + + // Start server in a goroutine + go func() { + log.Printf("🚀 Production API starting...") + log.Printf("📍 Server running on http://localhost:%s", config.Port) + log.Printf("🏥 Health Check: http://localhost:%s%s", config.Port, config.HealthCheckPath) + log.Printf("📊 Metrics: http://localhost:%s/metrics", config.Port) + log.Printf("📚 Documentation: http://localhost:%s/docs", config.Port) + log.Printf("🌍 Environment: %s", config.Environment) + + if config.TLSCertFile != "" && config.TLSKeyFile != "" { + log.Printf("🔒 TLS enabled") + if err := srv.ListenAndServeTLS(config.TLSCertFile, config.TLSKeyFile); err != nil && err != http.ErrServerClosed { + log.Fatalf("Failed to start HTTPS server: %v", err) + } + } else { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Failed to start HTTP server: %v", err) + } + } + }() + + // Wait for interrupt signal to gracefully shutdown the server + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("🛑 Shutting down server...") + + // The context is used to inform the server it has 30 seconds to finish + // the request it is currently handling + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + log.Fatal("Server forced to shutdown:", err) + } + + log.Println("✅ Server exited gracefully") +} \ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..6c4cd1b --- /dev/null +++ b/examples/README.md @@ -0,0 +1,210 @@ +# Zoox Framework Examples + +This directory contains comprehensive examples demonstrating various features and capabilities of the Zoox Go web framework. + +## Quick Start + +Each example is self-contained and includes: +- Complete working code +- Detailed README with setup instructions +- API documentation where applicable +- Dependencies and configuration + +## Examples Overview + +### 🟢 Beginner Level + +#### 1. Basic Server (`01-basic-server/`) +**Difficulty:** ⭐⭐☆☆☆ +**Features:** REST API, CRUD operations, Route groups, Basic middleware +A complete REST API with user management, demonstrating fundamental Zoox concepts including routing, middleware, and JSON handling. + +**What you'll learn:** +- Setting up a basic Zoox server +- Creating REST endpoints +- Working with JSON requests/responses +- Using route groups +- Basic error handling + +#### 2. Middleware Showcase (`02-middleware-showcase/`) +**Difficulty:** ⭐⭐⭐☆☆ +**Features:** All built-in middleware, Security, Performance, Custom middleware +Comprehensive demonstration of Zoox's built-in middleware including security, performance optimization, and custom middleware creation. + +**What you'll learn:** +- Using built-in middleware (CORS, Logger, Recovery, etc.) +- Security middleware (Helmet, Rate Limiting) +- Performance middleware (Gzip, Caching) +- Creating custom middleware + +### 🟡 Intermediate Level + +#### 3. WebSocket Chat (`03-websocket-chat/`) +**Difficulty:** ⭐⭐⭐☆☆ +**Features:** WebSocket, Real-time communication, Connection management +Real-time chat application demonstrating WebSocket integration with user management and message broadcasting. + +**What you'll learn:** +- WebSocket implementation +- Real-time data handling +- Connection lifecycle management +- Client-server communication patterns + +#### 4. File Upload/Download System (`04-file-upload-download/`) +**Difficulty:** ⭐⭐⭐⭐☆ +**Features:** File handling, Chunked uploads, Validation, Security +Complete file management system with upload, download, validation, and security features. + +**What you'll learn:** +- File upload handling (single/multiple) +- Chunked file transfers +- File validation and security +- File metadata management + +### 🔴 Advanced Level + +#### 5. JSON-RPC Service (`05-json-rpc-service/`) +**Difficulty:** ⭐⭐⭐⭐☆ +**Features:** JSON-RPC, Service architecture, Error handling +Professional JSON-RPC service with math and user operations, custom error handling, and interactive testing interface. + +**What you'll learn:** +- JSON-RPC protocol implementation +- Service-oriented architecture +- Custom error handling +- Method registration and discovery + +#### 6. Production API (`06-production-api/`) +**Difficulty:** ⭐⭐⭐⭐⭐ +**Features:** Authentication, Authorization, Monitoring, Security, Deployment +Production-ready API with comprehensive security, monitoring, and deployment configurations. + +**What you'll learn:** +- JWT authentication and RBAC +- Production security practices +- Monitoring and observability +- Deployment strategies +- Clean architecture patterns + +## Learning Paths + +### 🎯 Path 1: Web Development Beginner +1. **Basic Server** → Learn fundamental concepts +2. **Middleware Showcase** → Understand request processing +3. **File Upload/Download** → Handle file operations +4. **Production API** → Apply production practices + +### 🎯 Path 2: API Development Focus +1. **Basic Server** → REST API fundamentals +2. **JSON-RPC Service** → Alternative API patterns +3. **Production API** → Professional implementation +4. **Middleware Showcase** → Advanced request handling + +### 🎯 Path 3: Real-time Applications +1. **Basic Server** → Foundation +2. **WebSocket Chat** → Real-time communication +3. **Middleware Showcase** → Performance optimization +4. **Production API** → Scalable architecture + +### 🎯 Path 4: Production Deployment +1. **Basic Server** → Core concepts +2. **Middleware Showcase** → Security and performance +3. **Production API** → Complete production setup +4. **File Upload/Download** → File handling best practices + +## Features Matrix + +| Example | REST API | WebSocket | Auth | File Handling | JSON-RPC | Monitoring | Deployment | +|---------|----------|-----------|------|---------------|----------|------------|------------| +| Basic Server | ✅ | ❌ | Basic | ❌ | ❌ | ❌ | ❌ | +| Middleware | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | +| WebSocket Chat | ✅ | ✅ | Basic | ❌ | ❌ | ❌ | ❌ | +| File System | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | +| JSON-RPC | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| Production | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | + +## Getting Started + +1. **Prerequisites:** + - Go 1.19 or higher + - Git + - Basic understanding of Go and web concepts + +2. **Setup:** + ```bash + # Clone the repository + git clone https://github.com/go-zoox/zoox.git + cd zoox/examples + + # Choose an example + cd 01-basic-server + + # Install dependencies + go mod tidy + + # Run the example + go run main.go + ``` + +3. **Testing:** + Each example includes test endpoints or interfaces. Check the individual README files for specific testing instructions. + +## Troubleshooting + +### Common Issues + +**Module not found errors:** +```bash +# Ensure you're in the correct directory +cd examples/[example-name] +go mod tidy +``` + +**Port already in use:** +```bash +# Kill existing processes +sudo lsof -ti:8080 | xargs kill -9 +``` + +**Permission denied:** +```bash +# Check file permissions +chmod +x main.go +``` + +### Getting Help + +- **Documentation:** Check individual example README files +- **Tutorials:** See `../tutorials/README.md` for step-by-step guides +- **Issues:** Report bugs on GitHub +- **Community:** Join discussions on GitHub Discussions + +## Contributing + +We welcome contributions! To add a new example: + +1. Create a new directory following the naming convention +2. Include a complete `main.go` with comments +3. Add a detailed `README.md` +4. Update this index file +5. Test thoroughly +6. Submit a pull request + +### Example Structure +``` +examples/ +├── XX-example-name/ +│ ├── main.go # Main application code +│ ├── README.md # Detailed documentation +│ ├── go.mod # Dependencies (if needed) +│ └── static/ # Static files (if needed) +``` + +## Next Steps + +After exploring these examples, check out: +- **Tutorials** (`../tutorials/`) for step-by-step learning +- **Main Documentation** (`../DOCUMENTATION.md`) for API reference +- **Contributing Guide** (`../CONTRIBUTING.md`) to contribute back + +Happy coding with Zoox! 🚀 \ No newline at end of file diff --git a/tutorials/01-getting-started.md b/tutorials/01-getting-started.md new file mode 100644 index 0000000..32b2000 --- /dev/null +++ b/tutorials/01-getting-started.md @@ -0,0 +1,470 @@ +# Getting Started with Zoox Framework + +Welcome to your first Zoox tutorial! This guide will walk you through setting up your development environment and creating your first Zoox application. + +## 📋 Prerequisites + +### Required Knowledge +- Basic understanding of Go programming language +- Familiarity with HTTP concepts +- Basic command-line usage + +### Software Requirements +- **Go 1.19 or higher** - [Download Go](https://golang.org/dl/) +- **Git** - [Download Git](https://git-scm.com/downloads) +- **Code Editor** - VS Code, GoLand, or any text editor +- **Terminal/Command Prompt** + +### System Check +Let's verify your system is ready: + +```bash +# Check Go version +go version + +# Check Git +git --version + +# Check Go modules support +go env GOMOD +``` + +## 🎯 Learning Objectives + +By the end of this tutorial, you will: +- ✅ Understand the Zoox framework architecture +- ✅ Create and run your first Zoox application +- ✅ Handle basic HTTP requests and responses +- ✅ Understand the request lifecycle +- ✅ Know how to structure a Zoox project + +## 📖 Tutorial Content + +### Step 1: Create Your Project + +First, let's create a new directory for your Zoox project: + +```bash +# Create project directory +mkdir my-first-zoox-app +cd my-first-zoox-app + +# Initialize Go module +go mod init my-first-zoox-app + +# Add Zoox dependency +go get github.com/go-zoox/zoox +``` + +### Step 2: Create Your First Server + +Create a file named `main.go`: + +```go +package main + +import ( + "log" + "net/http" + "time" + + "github.com/go-zoox/zoox" +) + +func main() { + // Create a new Zoox application + app := zoox.New() + + // Define a simple route + app.Get("/", func(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, zoox.H{ + "message": "Hello, Zoox!", + "timestamp": time.Now().Format(time.RFC3339), + "status": "success", + }) + }) + + // Start the server + log.Println("🚀 Server starting on http://localhost:8080") + app.Run(":8080") +} +``` + +### Step 3: Run Your Application + +```bash +# Run the application +go run main.go +``` + +You should see: +``` +🚀 Server starting on http://localhost:8080 +``` + +### Step 4: Test Your Application + +Open your browser and navigate to `http://localhost:8080`, or use curl: + +```bash +curl http://localhost:8080 +``` + +You should see: +```json +{ + "message": "Hello, Zoox!", + "timestamp": "2023-12-07T10:30:00Z", + "status": "success" +} +``` + +🎉 **Congratulations!** You've just created your first Zoox application! + +### Step 5: Understanding the Code + +Let's break down what we just created: + +```go +// 1. Import the Zoox framework +import "github.com/go-zoox/zoox" + +// 2. Create a new application instance +app := zoox.New() + +// 3. Define a route handler +app.Get("/", func(ctx *zoox.Context) { + // ctx is the context object containing request/response data + ctx.JSON(http.StatusOK, zoox.H{ + "message": "Hello, Zoox!", + }) +}) + +// 4. Start the server +app.Run(":8080") +``` + +#### Key Concepts: + +1. **Application Instance**: `zoox.New()` creates a new Zoox application +2. **Route Handler**: `app.Get()` defines a GET route +3. **Context**: `ctx` provides access to request/response data +4. **JSON Response**: `ctx.JSON()` sends a JSON response +5. **Server Start**: `app.Run()` starts the HTTP server + +### Step 6: Adding More Routes + +Let's expand our application with more routes: + +```go +package main + +import ( + "log" + "net/http" + "time" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.New() + + // Home route + app.Get("/", homeHandler) + + // About route + app.Get("/about", aboutHandler) + + // Health check route + app.Get("/health", healthHandler) + + // User greeting route with parameter + app.Get("/hello/:name", greetingHandler) + + // API route with JSON response + app.Get("/api/status", apiStatusHandler) + + log.Println("🚀 Server starting on http://localhost:8080") + log.Println("📋 Available routes:") + log.Println(" GET /") + log.Println(" GET /about") + log.Println(" GET /health") + log.Println(" GET /hello/:name") + log.Println(" GET /api/status") + + app.Run(":8080") +} + +// Route handlers +func homeHandler(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, zoox.H{ + "message": "Welcome to Zoox Framework!", + "version": "1.0.0", + "routes": []string{ + "GET /", + "GET /about", + "GET /health", + "GET /hello/:name", + "GET /api/status", + }, + }) +} + +func aboutHandler(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, zoox.H{ + "app": "My First Zoox App", + "description": "A simple web application built with Zoox framework", + "author": "Your Name", + "created": time.Now().Format("2006-01-02"), + }) +} + +func healthHandler(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, zoox.H{ + "status": "healthy", + "timestamp": time.Now().Format(time.RFC3339), + "uptime": "running", + }) +} + +func greetingHandler(ctx *zoox.Context) { + name := ctx.Param().Get("name") + ctx.JSON(http.StatusOK, zoox.H{ + "greeting": "Hello, " + name + "!", + "message": "Welcome to Zoox Framework", + "name": name, + }) +} + +func apiStatusHandler(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, zoox.H{ + "api": zoox.H{ + "version": "v1.0.0", + "status": "operational", + "endpoints": 5, + "last_update": time.Now().Format(time.RFC3339), + }, + "server": zoox.H{ + "framework": "Zoox", + "go_version": "1.19+", + "environment": "development", + }, + }) +} +``` + +### Step 7: Test All Routes + +Now test all your routes: + +```bash +# Home route +curl http://localhost:8080/ + +# About route +curl http://localhost:8080/about + +# Health check +curl http://localhost:8080/health + +# Greeting with parameter +curl http://localhost:8080/hello/John + +# API status +curl http://localhost:8080/api/status +``` + +### Step 8: Understanding Route Parameters + +The greeting route demonstrates how to capture URL parameters: + +```go +app.Get("/hello/:name", greetingHandler) + +func greetingHandler(ctx *zoox.Context) { + name := ctx.Param().Get("name") + // Use the name parameter +} +``` + +- `:name` in the route captures the URL segment +- `ctx.Param().Get("name")` retrieves the parameter value + +### Step 9: Project Structure + +As your application grows, organize your code: + +``` +my-first-zoox-app/ +├── main.go # Application entry point +├── handlers/ # Route handlers +│ ├── home.go +│ ├── about.go +│ └── api.go +├── models/ # Data models +├── middleware/ # Custom middleware +├── static/ # Static files +├── templates/ # HTML templates +├── config/ # Configuration +└── go.mod # Go module file +``` + +## 🧪 Hands-on Exercise + +### Exercise 1: Create a Personal API + +Create a personal information API with these endpoints: + +1. `GET /me` - Return your personal information +2. `GET /me/skills` - Return your programming skills +3. `GET /me/projects` - Return your projects +4. `GET /me/contact` - Return your contact information + +**Solution:** + +```go +package main + +import ( + "log" + "net/http" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.New() + + // Personal API routes + app.Get("/me", personalInfoHandler) + app.Get("/me/skills", skillsHandler) + app.Get("/me/projects", projectsHandler) + app.Get("/me/contact", contactHandler) + + log.Println("🚀 Personal API server starting on http://localhost:8080") + app.Run(":8080") +} + +func personalInfoHandler(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, zoox.H{ + "name": "Your Name", + "title": "Software Developer", + "location": "Your City, Country", + "experience": "X years", + "bio": "Passionate developer learning Zoox framework", + }) +} + +func skillsHandler(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, zoox.H{ + "programming_languages": []string{"Go", "JavaScript", "Python"}, + "frameworks": []string{"Zoox", "React", "Node.js"}, + "databases": []string{"PostgreSQL", "MongoDB", "Redis"}, + "tools": []string{"Docker", "Git", "VS Code"}, + }) +} + +func projectsHandler(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, zoox.H{ + "projects": []zoox.H{ + { + "name": "My First Zoox App", + "description": "Learning Zoox framework basics", + "status": "in-progress", + "tech_stack": []string{"Go", "Zoox"}, + }, + { + "name": "Personal Portfolio", + "description": "My personal website", + "status": "completed", + "tech_stack": []string{"HTML", "CSS", "JavaScript"}, + }, + }, + }) +} + +func contactHandler(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, zoox.H{ + "email": "your.email@example.com", + "github": "https://github.com/yourusername", + "linkedin": "https://linkedin.com/in/yourprofile", + "website": "https://yourwebsite.com", + }) +} +``` + +### Exercise 2: Add Error Handling + +Enhance your application with proper error handling: + +```go +func greetingHandler(ctx *zoox.Context) { + name := ctx.Param().Get("name") + + // Validate input + if name == "" { + ctx.JSON(http.StatusBadRequest, zoox.H{ + "error": "Name parameter is required", + "code": "MISSING_NAME", + }) + return + } + + // Check for inappropriate content + if len(name) > 50 { + ctx.JSON(http.StatusBadRequest, zoox.H{ + "error": "Name too long (max 50 characters)", + "code": "NAME_TOO_LONG", + }) + return + } + + ctx.JSON(http.StatusOK, zoox.H{ + "greeting": "Hello, " + name + "!", + "message": "Welcome to Zoox Framework", + "name": name, + }) +} +``` + +## 📚 Additional Resources + +### Documentation +- [Zoox Framework Documentation](../DOCUMENTATION.md) +- [Go HTTP Package](https://pkg.go.dev/net/http) +- [JSON in Go](https://blog.golang.org/json) + +### Next Steps +- [Tutorial 02: Routing Fundamentals](./02-routing-fundamentals.md) +- [Tutorial 03: Request Response Handling](./03-request-response-handling.md) +- [Examples: Basic Server](../examples/01-basic-server/) + +### Community Resources +- GitHub Repository: [go-zoox/zoox](https://github.com/go-zoox/zoox) +- Go Documentation: [golang.org/doc](https://golang.org/doc/) +- HTTP Status Codes: [httpstatuses.com](https://httpstatuses.com/) + +## 🎯 Key Takeaways + +1. **Zoox is Simple**: Creating a web server requires minimal code +2. **Context is Key**: The `ctx` parameter provides access to request/response data +3. **JSON Responses**: Use `ctx.JSON()` for API responses +4. **Route Parameters**: Capture URL segments with `:parameter` syntax +5. **Error Handling**: Always validate input and handle errors gracefully + +## 🔄 What's Next? + +Now that you've created your first Zoox application, you're ready to explore more advanced features: + +1. **Routing**: Learn about advanced routing patterns +2. **Middleware**: Add functionality to your request pipeline +3. **Templates**: Render HTML pages +4. **Static Files**: Serve CSS, JavaScript, and images +5. **Database Integration**: Connect to databases + +Continue with [Tutorial 02: Routing Fundamentals](./02-routing-fundamentals.md) to deepen your understanding of Zoox routing capabilities. + +--- + +**🎉 Congratulations on completing your first Zoox tutorial!** You've taken the first step in your journey to becoming a Zoox developer. Keep practicing and exploring the framework's capabilities! \ No newline at end of file diff --git a/tutorials/01-getting-started/README.md b/tutorials/01-getting-started/README.md new file mode 100644 index 0000000..f00e233 --- /dev/null +++ b/tutorials/01-getting-started/README.md @@ -0,0 +1,564 @@ +# Tutorial 01: Getting Started with Zoox + +Welcome to your first Zoox tutorial! In this tutorial, you'll learn the fundamentals of the Zoox Go web framework by building your first web application. + +## 🎯 Learning Objectives + +By the end of this tutorial, you will: +- Understand what Zoox is and its key features +- Set up a new Zoox project +- Create your first HTTP routes +- Handle different HTTP methods +- Serve static content +- Understand the request-response lifecycle + +## ⏱️ Estimated Time: 30 minutes + +## 📋 Prerequisites + +- Go 1.19 or higher installed +- Basic knowledge of Go programming +- Text editor or IDE +- Terminal/command line access + +## 🚀 What is Zoox? + +Zoox is a modern, fast, and feature-rich web framework for Go that provides: + +- **High Performance** - Built for speed and efficiency +- **Rich Middleware** - Comprehensive middleware ecosystem +- **Easy to Use** - Simple and intuitive API +- **Production Ready** - Built-in features for production deployment +- **Extensible** - Flexible architecture for custom needs + +## 📝 Step 1: Project Setup + +### 1.1 Create a New Directory + +```bash +mkdir my-first-zoox-app +cd my-first-zoox-app +``` + +### 1.2 Initialize Go Module + +```bash +go mod init my-first-zoox-app +``` + +### 1.3 Install Zoox + +```bash +go get github.com/go-zoox/zoox +``` + +## 🏗️ Step 2: Your First Zoox Application + +### 2.1 Create main.go + +Create a file named `main.go` with the following content: + +```go +package main + +import ( + "net/http" + + "github.com/go-zoox/zoox" +) + +func main() { + // Create a new Zoox application + app := zoox.Default() + + // Define a simple route + app.Get("/", func(ctx *zoox.Context) { + ctx.String(http.StatusOK, "Hello, Zoox!") + }) + + // Start the server on port 8080 + app.Run(":8080") +} +``` + +### 2.2 Run Your Application + +```bash +go run main.go +``` + +You should see output similar to: +``` +[ZOOX] Listening and serving HTTP on :8080 +``` + +### 2.3 Test Your Application + +Open your browser and navigate to `http://localhost:8080` + +You should see: **Hello, Zoox!** + +🎉 **Congratulations!** You've created your first Zoox application! + +## 🛣️ Step 3: Adding More Routes + +Let's add more routes to understand different response types: + +```go +package main + +import ( + "net/http" + "time" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.Default() + + // String response + app.Get("/", func(ctx *zoox.Context) { + ctx.String(http.StatusOK, "Hello, Zoox!") + }) + + // JSON response + app.Get("/json", func(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "Hello from Zoox!", + "status": "success", + "time": time.Now(), + }) + }) + + // HTML response + app.Get("/html", func(ctx *zoox.Context) { + html := ` + + + + Zoox Tutorial + + +

    Welcome to Zoox!

    +

    This is an HTML response from your Zoox application.

    + Go back to home + + + ` + ctx.HTML(http.StatusOK, html) + }) + + app.Run(":8080") +} +``` + +### 3.1 Test the New Routes + +- `http://localhost:8080/` - String response +- `http://localhost:8080/json` - JSON response +- `http://localhost:8080/html` - HTML response + +## 📊 Step 4: Working with HTTP Methods + +Zoox supports all standard HTTP methods. Let's add some examples: + +```go +package main + +import ( + "net/http" + "time" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.Default() + + // GET route + app.Get("/users", func(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "users": []string{"Alice", "Bob", "Charlie"}, + }) + }) + + // POST route + app.Post("/users", func(ctx *zoox.Context) { + ctx.JSON(http.StatusCreated, map[string]interface{}{ + "message": "User created successfully", + "user_id": 123, + "created_at": time.Now(), + }) + }) + + // PUT route + app.Put("/users/:id", func(ctx *zoox.Context) { + userID := ctx.Param("id") + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "User updated successfully", + "user_id": userID, + "updated_at": time.Now(), + }) + }) + + // DELETE route + app.Delete("/users/:id", func(ctx *zoox.Context) { + userID := ctx.Param("id") + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "User deleted successfully", + "user_id": userID, + "deleted_at": time.Now(), + }) + }) + + // Catch-all route for undefined endpoints + app.NoRoute(func(ctx *zoox.Context) { + ctx.JSON(http.StatusNotFound, map[string]interface{}{ + "error": "Route not found", + "path": ctx.Path, + }) + }) + + app.Run(":8080") +} +``` + +### 4.1 Test HTTP Methods + +Using curl or a tool like Postman: + +```bash +# GET +curl http://localhost:8080/users + +# POST +curl -X POST http://localhost:8080/users + +# PUT +curl -X PUT http://localhost:8080/users/123 + +# DELETE +curl -X DELETE http://localhost:8080/users/123 + +# Test 404 +curl http://localhost:8080/nonexistent +``` + +## 🔄 Step 5: Understanding the Context + +The `zoox.Context` is the heart of every request. It provides access to: + +- **Request data** - Headers, parameters, body +- **Response methods** - JSON, HTML, String, etc. +- **Route parameters** - URL path parameters +- **Query parameters** - URL query strings + +Let's explore these features: + +```go +package main + +import ( + "net/http" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.Default() + + // Route with path parameter + app.Get("/hello/:name", func(ctx *zoox.Context) { + name := ctx.Param("name") + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": "Hello, " + name + "!", + "path_param": name, + }) + }) + + // Route with query parameters + app.Get("/search", func(ctx *zoox.Context) { + query := ctx.Query().Get("q", "") + page := ctx.Query().Get("page", "1") + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "search_query": query, + "page": page, + "results": []string{"Result 1", "Result 2", "Result 3"}, + }) + }) + + // Route showing request headers + app.Get("/headers", func(ctx *zoox.Context) { + userAgent := ctx.Header("User-Agent") + contentType := ctx.Header("Content-Type") + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "user_agent": userAgent, + "content_type": contentType, + "method": ctx.Method, + "path": ctx.Path, + }) + }) + + app.Run(":8080") +} +``` + +### 5.1 Test Context Features + +```bash +# Path parameter +curl http://localhost:8080/hello/Alice + +# Query parameters +curl "http://localhost:8080/search?q=zoox&page=2" + +# Headers +curl -H "User-Agent: MyApp/1.0" http://localhost:8080/headers +``` + +## 📄 Step 6: Adding Basic Middleware + +Middleware functions run before your route handlers. Let's add some basic logging: + +```go +package main + +import ( + "fmt" + "net/http" + "time" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.Default() + + // Custom middleware + app.Use(func(ctx *zoox.Context) { + start := time.Now() + + // Process request + ctx.Next() + + // Log after request + duration := time.Since(start) + fmt.Printf("[%s] %s %s - %v\n", + time.Now().Format("2006-01-02 15:04:05"), + ctx.Method, + ctx.Path, + duration, + ) + }) + + app.Get("/", func(ctx *zoox.Context) { + // Simulate some work + time.Sleep(100 * time.Millisecond) + ctx.String(http.StatusOK, "Hello, Zoox!") + }) + + app.Get("/fast", func(ctx *zoox.Context) { + ctx.String(http.StatusOK, "Fast response!") + }) + + app.Run(":8080") +} +``` + +## 🏁 Step 7: Complete Example + +Here's a complete example that demonstrates everything we've learned: + +```go +package main + +import ( + "fmt" + "net/http" + "time" + + "github.com/go-zoox/zoox" +) + +func main() { + // Create application with default middleware (Logger, Recovery) + app := zoox.Default() + + // Custom middleware for request timing + app.Use(func(ctx *zoox.Context) { + start := time.Now() + ctx.Next() + duration := time.Since(start) + ctx.Header("X-Response-Time", duration.String()) + }) + + // Home page + app.Get("/", func(ctx *zoox.Context) { + ctx.HTML(http.StatusOK, ` + + + + My First Zoox App + + + +

    🚀 Welcome to My First Zoox App!

    +

    Available Endpoints:

    + + + + `) + }) + + // API routes group + api := app.Group("/api") + + api.Get("/status", func(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "status": "healthy", + "service": "my-first-zoox-app", + "version": "1.0.0", + "timestamp": time.Now(), + }) + }) + + api.Get("/time", func(ctx *zoox.Context) { + now := time.Now() + ctx.JSON(http.StatusOK, map[string]interface{}{ + "current_time": now, + "unix_timestamp": now.Unix(), + "formatted": now.Format("2006-01-02 15:04:05"), + "timezone": now.Location().String(), + }) + }) + + // Personalized greeting + app.Get("/hello/:name", func(ctx *zoox.Context) { + name := ctx.Param("name") + greeting := ctx.Query().Get("greeting", "Hello") + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": fmt.Sprintf("%s, %s!", greeting, name), + "name": name, + "greeting": greeting, + "timestamp": time.Now(), + }) + }) + + // Search endpoint + app.Get("/search", func(ctx *zoox.Context) { + query := ctx.Query().Get("q", "") + if query == "" { + ctx.JSON(http.StatusBadRequest, map[string]interface{}{ + "error": "Query parameter 'q' is required", + }) + return + } + + // Mock search results + results := []map[string]interface{}{ + {"id": 1, "title": "Result 1 for " + query, "score": 0.95}, + {"id": 2, "title": "Result 2 for " + query, "score": 0.87}, + {"id": 3, "title": "Result 3 for " + query, "score": 0.75}, + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "query": query, + "results": results, + "total": len(results), + "search_time": "0.05s", + }) + }) + + // 404 handler + app.NoRoute(func(ctx *zoox.Context) { + ctx.JSON(http.StatusNotFound, map[string]interface{}{ + "error": "Endpoint not found", + "path": ctx.Path, + "method": ctx.Method, + "available_endpoints": []string{ + "GET /", + "GET /api/status", + "GET /api/time", + "GET /hello/:name", + "GET /search?q=term", + }, + }) + }) + + // Start server + fmt.Println("🚀 Starting Zoox application...") + fmt.Println("📍 Server running on http://localhost:8080") + fmt.Println("🌐 Open your browser to http://localhost:8080") + + app.Run(":8080") +} +``` + +## ✅ Testing Your Complete Application + +1. **Start the server:** + ```bash + go run main.go + ``` + +2. **Test in browser:** + - Visit `http://localhost:8080` + - Try the different endpoints listed + +3. **Test with curl:** + ```bash + curl http://localhost:8080/api/status + curl http://localhost:8080/hello/Alice?greeting=Hi + curl "http://localhost:8080/search?q=zoox" + ``` + +## 🎯 What You've Learned + +In this tutorial, you've learned: + +✅ **Basic Application Setup** - Creating and configuring a Zoox app +✅ **HTTP Routing** - Handling different HTTP methods and routes +✅ **Response Types** - Sending String, JSON, and HTML responses +✅ **Context Usage** - Accessing request data and setting responses +✅ **Path Parameters** - Extracting values from URL paths +✅ **Query Parameters** - Reading URL query strings +✅ **Middleware** - Adding custom middleware for cross-cutting concerns +✅ **Route Groups** - Organizing related routes +✅ **Error Handling** - Managing 404 and other error responses + +## 🚀 Next Steps + +Now that you understand the basics, you're ready to move on to: + +- **[Tutorial 02: Routing Fundamentals](../02-routing-fundamentals/)** - Advanced routing patterns +- **[Tutorial 03: Request & Response Handling](../03-request-response-handling/)** - Data validation and processing +- **[Tutorial 04: Middleware Basics](../04-middleware-basics/)** - Building custom middleware + +## 💡 Practice Exercises + +Try these exercises to reinforce your learning: + +1. **Personal API** - Create an API that returns information about yourself +2. **Calculator Service** - Build a simple calculator with endpoints for math operations +3. **Todo API** - Create basic CRUD endpoints for a todo list +4. **Weather Mock** - Build a mock weather API with different cities + +## 🤝 Need Help? + +- Check the [main documentation](../../DOCUMENTATION.md) +- Look at [examples](../../examples/) for more code samples +- Join our community discussions +- Ask questions on Stack Overflow with the `zoox-framework` tag + +--- + +🎉 **Congratulations on completing Tutorial 01!** You're now ready to build web applications with Zoox! \ No newline at end of file diff --git a/tutorials/02-routing-fundamentals.md b/tutorials/02-routing-fundamentals.md new file mode 100644 index 0000000..b95ae6b --- /dev/null +++ b/tutorials/02-routing-fundamentals.md @@ -0,0 +1,817 @@ +# Routing Fundamentals in Zoox Framework + +Learn how to master routing in Zoox, from basic route definitions to advanced patterns and organization strategies. + +## 📋 Prerequisites + +### Required Knowledge +- Completed [01-getting-started](./01-getting-started.md) +- Basic understanding of HTTP methods +- Familiarity with URL patterns + +### Software Requirements +- Go 1.19 or higher +- Zoox framework installed + +## 🎯 Learning Objectives + +By the end of this tutorial, you will: +- ✅ Understand different HTTP methods and their usage +- ✅ Master route parameters and wildcards +- ✅ Organize routes using route groups +- ✅ Apply middleware to specific routes +- ✅ Handle route conflicts and precedence +- ✅ Implement dynamic route registration + +## 📖 Tutorial Content + +### Step 1: HTTP Methods Overview + +Zoox supports all standard HTTP methods. Let's create a comprehensive example: + +```go +package main + +import ( + "log" + "strconv" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.Default() + + // GET - Retrieve data + app.Get("/users", getUsersHandler) + app.Get("/users/:id", getUserHandler) + + // POST - Create new resource + app.Post("/users", createUserHandler) + + // PUT - Update entire resource + app.Put("/users/:id", updateUserHandler) + + // PATCH - Partial update + app.Patch("/users/:id", patchUserHandler) + + // DELETE - Remove resource + app.Delete("/users/:id", deleteUserHandler) + + // HEAD - Same as GET but only headers + app.Head("/users", headUsersHandler) + + // OPTIONS - Check allowed methods + app.Options("/users", optionsUsersHandler) + + log.Println("🚀 Server starting on http://localhost:8080") + app.Run(":8080") +} + +// Handler implementations +func getUsersHandler(ctx *zoox.Context) { + users := []zoox.H{ + {"id": 1, "name": "John Doe", "email": "john@example.com"}, + {"id": 2, "name": "Jane Smith", "email": "jane@example.com"}, + } + ctx.JSON(200, zoox.H{ + "users": users, + "total": len(users), + }) +} + +func getUserHandler(ctx *zoox.Context) { + id := ctx.Param().Get("id") + userID, err := strconv.Atoi(id) + if err != nil { + ctx.JSON(400, zoox.H{"error": "Invalid user ID"}) + return + } + + ctx.JSON(200, zoox.H{ + "user": zoox.H{ + "id": userID, + "name": "John Doe", + "email": "john@example.com", + }, + }) +} + +func createUserHandler(ctx *zoox.Context) { + var user struct { + Name string `json:"name"` + Email string `json:"email"` + } + + if err := ctx.BindJSON(&user); err != nil { + ctx.JSON(400, zoox.H{"error": "Invalid request body"}) + return + } + + ctx.JSON(201, zoox.H{ + "message": "User created successfully", + "user": zoox.H{ + "id": 3, + "name": user.Name, + "email": user.Email, + }, + }) +} + +func updateUserHandler(ctx *zoox.Context) { + id := ctx.Param().Get("id") + var user struct { + Name string `json:"name"` + Email string `json:"email"` + } + + if err := ctx.BindJSON(&user); err != nil { + ctx.JSON(400, zoox.H{"error": "Invalid request body"}) + return + } + + ctx.JSON(200, zoox.H{ + "message": "User updated successfully", + "user": zoox.H{ + "id": id, + "name": user.Name, + "email": user.Email, + }, + }) +} + +func patchUserHandler(ctx *zoox.Context) { + id := ctx.Param().Get("id") + var updates map[string]interface{} + + if err := ctx.BindJSON(&updates); err != nil { + ctx.JSON(400, zoox.H{"error": "Invalid request body"}) + return + } + + ctx.JSON(200, zoox.H{ + "message": "User partially updated", + "id": id, + "updates": updates, + }) +} + +func deleteUserHandler(ctx *zoox.Context) { + id := ctx.Param().Get("id") + ctx.JSON(200, zoox.H{ + "message": "User deleted successfully", + "id": id, + }) +} + +func headUsersHandler(ctx *zoox.Context) { + ctx.Header().Set("X-Total-Count", "2") + ctx.Status(200) +} + +func optionsUsersHandler(ctx *zoox.Context) { + ctx.Header().Set("Allow", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS") + ctx.Status(200) +} +``` + +### Step 2: Route Parameters + +Zoox supports various types of route parameters: + +```go +package main + +import ( + "log" + "strings" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.Default() + + // Single parameter + app.Get("/users/:id", func(ctx *zoox.Context) { + id := ctx.Param().Get("id") + ctx.JSON(200, zoox.H{"user_id": id}) + }) + + // Multiple parameters + app.Get("/users/:id/posts/:post_id", func(ctx *zoox.Context) { + userID := ctx.Param().Get("id") + postID := ctx.Param().Get("post_id") + ctx.JSON(200, zoox.H{ + "user_id": userID, + "post_id": postID, + }) + }) + + // Optional parameters with default values + app.Get("/search/:query", func(ctx *zoox.Context) { + query := ctx.Param().Get("query") + page := ctx.Query().Get("page", "1") + limit := ctx.Query().Get("limit", "10") + + ctx.JSON(200, zoox.H{ + "query": query, + "page": page, + "limit": limit, + }) + }) + + // Wildcard parameters + app.Get("/files/*path", func(ctx *zoox.Context) { + path := ctx.Param().Get("path") + ctx.JSON(200, zoox.H{ + "file_path": path, + "segments": strings.Split(path, "/"), + }) + }) + + // Parameter validation + app.Get("/validate/:id", func(ctx *zoox.Context) { + id := ctx.Param().Get("id") + + // Simple validation + if len(id) < 1 { + ctx.JSON(400, zoox.H{"error": "ID is required"}) + return + } + + // Numeric validation + if !isNumeric(id) { + ctx.JSON(400, zoox.H{"error": "ID must be numeric"}) + return + } + + ctx.JSON(200, zoox.H{"valid_id": id}) + }) + + log.Println("🚀 Server starting on http://localhost:8080") + app.Run(":8080") +} + +func isNumeric(s string) bool { + for _, r := range s { + if r < '0' || r > '9' { + return false + } + } + return true +} +``` + +### Step 3: Route Groups + +Route groups help organize related routes and apply common middleware: + +```go +package main + +import ( + "log" + "time" + + "github.com/go-zoox/zoox" + "github.com/go-zoox/zoox/middleware" +) + +func main() { + app := zoox.Default() + + // Root level routes + app.Get("/", homeHandler) + app.Get("/health", healthHandler) + + // API v1 group + v1 := app.Group("/api/v1") + { + v1.Use(middleware.Logger()) + v1.Use(middleware.RequestID()) + + // Public routes + v1.Get("/status", statusHandler) + v1.Post("/register", registerHandler) + v1.Post("/login", loginHandler) + + // Protected routes + protected := v1.Group("/protected") + { + protected.Use(middleware.BasicAuth("Protected Area", map[string]string{ + "admin": "secret", + })) + + protected.Get("/profile", profileHandler) + protected.Get("/dashboard", dashboardHandler) + } + + // User management routes + users := v1.Group("/users") + { + users.Use(rateLimitMiddleware()) + + users.Get("", getUsersHandler) + users.Post("", createUserHandler) + users.Get("/:id", getUserHandler) + users.Put("/:id", updateUserHandler) + users.Delete("/:id", deleteUserHandler) + } + } + + // API v2 group (future version) + v2 := app.Group("/api/v2") + { + v2.Use(middleware.Logger()) + v2.Use(middleware.CORS()) + + v2.Get("/status", func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "version": "2.0", + "status": "active", + }) + }) + } + + // Admin routes + admin := app.Group("/admin") + { + admin.Use(middleware.BasicAuth("Admin Area", map[string]string{ + "admin": "supersecret", + })) + + admin.Get("/stats", adminStatsHandler) + admin.Get("/logs", adminLogsHandler) + admin.Post("/maintenance", maintenanceHandler) + } + + log.Println("🚀 Server starting on http://localhost:8080") + log.Println("📋 Available route groups:") + log.Println(" /api/v1/* - API version 1") + log.Println(" /api/v2/* - API version 2") + log.Println(" /admin/* - Admin panel") + + app.Run(":8080") +} + +// Handlers +func homeHandler(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "message": "Welcome to Zoox Routing Tutorial", + "routes": []string{ + "/api/v1/status", + "/api/v1/protected/profile", + "/api/v1/users", + "/admin/stats", + }, + }) +} + +func healthHandler(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "status": "healthy", + "time": time.Now().Format(time.RFC3339), + }) +} + +func statusHandler(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "api_version": "1.0", + "status": "operational", + "timestamp": time.Now().Format(time.RFC3339), + }) +} + +func registerHandler(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{"message": "User registration endpoint"}) +} + +func loginHandler(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{"message": "User login endpoint"}) +} + +func profileHandler(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "message": "User profile (protected)", + "user": "admin", + }) +} + +func dashboardHandler(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "message": "Dashboard (protected)", + "stats": zoox.H{"users": 100, "posts": 500}, + }) +} + +func adminStatsHandler(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "message": "Admin statistics", + "stats": zoox.H{ + "total_users": 1000, + "active_users": 850, + "total_requests": 50000, + }, + }) +} + +func adminLogsHandler(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "message": "System logs", + "logs": []string{ + "2023-12-07 10:00:00 - Server started", + "2023-12-07 10:01:00 - User login", + "2023-12-07 10:02:00 - API request", + }, + }) +} + +func maintenanceHandler(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "message": "Maintenance mode toggled", + "status": "maintenance", + }) +} + +// Custom middleware +func rateLimitMiddleware() func(*zoox.Context) { + return func(ctx *zoox.Context) { + // Simple rate limiting simulation + ctx.Header().Set("X-RateLimit-Limit", "100") + ctx.Header().Set("X-RateLimit-Remaining", "99") + ctx.Next() + } +} +``` + +### Step 4: Advanced Route Patterns + +Let's explore more advanced routing patterns: + +```go +package main + +import ( + "log" + "regexp" + "strings" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.Default() + + // Route with constraints + app.Get("/users/:id", func(ctx *zoox.Context) { + id := ctx.Param().Get("id") + + // Constraint: ID must be numeric + if matched, _ := regexp.MatchString(`^\d+$`, id); !matched { + ctx.JSON(400, zoox.H{"error": "ID must be numeric"}) + return + } + + ctx.JSON(200, zoox.H{"user_id": id}) + }) + + // Route with multiple wildcards + app.Get("/api/:version/*endpoint", func(ctx *zoox.Context) { + version := ctx.Param().Get("version") + endpoint := ctx.Param().Get("endpoint") + + ctx.JSON(200, zoox.H{ + "api_version": version, + "endpoint": endpoint, + "path_parts": strings.Split(endpoint, "/"), + }) + }) + + // Route with optional segments + app.Get("/search", searchHandler) + app.Get("/search/:category", searchHandler) + app.Get("/search/:category/:subcategory", searchHandler) + + // Route with file extensions + app.Get("/files/:filename", func(ctx *zoox.Context) { + filename := ctx.Param().Get("filename") + + // Extract extension + parts := strings.Split(filename, ".") + var extension string + if len(parts) > 1 { + extension = parts[len(parts)-1] + } + + ctx.JSON(200, zoox.H{ + "filename": filename, + "extension": extension, + "mime_type": getMimeType(extension), + }) + }) + + // Dynamic route registration + registerDynamicRoutes(app) + + log.Println("🚀 Server starting on http://localhost:8080") + app.Run(":8080") +} + +func searchHandler(ctx *zoox.Context) { + category := ctx.Param().Get("category") + subcategory := ctx.Param().Get("subcategory") + query := ctx.Query().Get("q", "") + + result := zoox.H{ + "query": query, + } + + if category != "" { + result["category"] = category + } + if subcategory != "" { + result["subcategory"] = subcategory + } + + ctx.JSON(200, result) +} + +func getMimeType(extension string) string { + mimeTypes := map[string]string{ + "txt": "text/plain", + "html": "text/html", + "css": "text/css", + "js": "application/javascript", + "json": "application/json", + "png": "image/png", + "jpg": "image/jpeg", + "pdf": "application/pdf", + } + + if mimeType, exists := mimeTypes[extension]; exists { + return mimeType + } + return "application/octet-stream" +} + +func registerDynamicRoutes(app *zoox.Application) { + // Simulate dynamic route registration + routes := []struct { + method string + path string + handler func(*zoox.Context) + }{ + {"GET", "/dynamic/route1", dynamicHandler1}, + {"POST", "/dynamic/route2", dynamicHandler2}, + {"PUT", "/dynamic/route3", dynamicHandler3}, + } + + for _, route := range routes { + switch route.method { + case "GET": + app.Get(route.path, route.handler) + case "POST": + app.Post(route.path, route.handler) + case "PUT": + app.Put(route.path, route.handler) + } + } +} + +func dynamicHandler1(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{"message": "Dynamic route 1"}) +} + +func dynamicHandler2(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{"message": "Dynamic route 2"}) +} + +func dynamicHandler3(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{"message": "Dynamic route 3"}) +} +``` + +## 🧪 Hands-on Exercise + +### Exercise 1: Build a Blog API + +Create a complete blog API with the following requirements: + +1. **Route Structure:** + - `/api/v1/posts` - List all posts + - `/api/v1/posts/:id` - Get specific post + - `/api/v1/posts/:id/comments` - Get post comments + - `/api/v1/authors/:author_id/posts` - Get posts by author + +2. **Route Groups:** + - Public routes (no authentication) + - Protected routes (require authentication) + - Admin routes (require admin role) + +3. **Parameters:** + - Support pagination with query parameters + - Validate numeric IDs + - Handle optional sorting parameters + +### Solution: + +```go +package main + +import ( + "log" + "strconv" + "time" + + "github.com/go-zoox/zoox" + "github.com/go-zoox/zoox/middleware" +) + +type Post struct { + ID int `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + AuthorID int `json:"author_id"` + Created time.Time `json:"created"` +} + +type Comment struct { + ID int `json:"id"` + PostID int `json:"post_id"` + Content string `json:"content"` + Author string `json:"author"` + Created time.Time `json:"created"` +} + +func main() { + app := zoox.Default() + + // Sample data + posts := []Post{ + {1, "First Post", "Hello World", 1, time.Now()}, + {2, "Second Post", "Learning Zoox", 1, time.Now()}, + {3, "Third Post", "Advanced Routing", 2, time.Now()}, + } + + comments := []Comment{ + {1, 1, "Great post!", "User1", time.Now()}, + {2, 1, "Thanks for sharing", "User2", time.Now()}, + {3, 2, "Very helpful", "User3", time.Now()}, + } + + // API v1 + api := app.Group("/api/v1") + { + // Public routes + api.Get("/posts", func(ctx *zoox.Context) { + page, _ := strconv.Atoi(ctx.Query().Get("page", "1")) + limit, _ := strconv.Atoi(ctx.Query().Get("limit", "10")) + sort := ctx.Query().Get("sort", "created") + + ctx.JSON(200, zoox.H{ + "posts": posts, + "pagination": zoox.H{ + "page": page, + "limit": limit, + "total": len(posts), + }, + "sort": sort, + }) + }) + + api.Get("/posts/:id", func(ctx *zoox.Context) { + id, err := strconv.Atoi(ctx.Param().Get("id")) + if err != nil { + ctx.JSON(400, zoox.H{"error": "Invalid post ID"}) + return + } + + for _, post := range posts { + if post.ID == id { + ctx.JSON(200, zoox.H{"post": post}) + return + } + } + + ctx.JSON(404, zoox.H{"error": "Post not found"}) + }) + + api.Get("/posts/:id/comments", func(ctx *zoox.Context) { + id, err := strconv.Atoi(ctx.Param().Get("id")) + if err != nil { + ctx.JSON(400, zoox.H{"error": "Invalid post ID"}) + return + } + + var postComments []Comment + for _, comment := range comments { + if comment.PostID == id { + postComments = append(postComments, comment) + } + } + + ctx.JSON(200, zoox.H{ + "comments": postComments, + "total": len(postComments), + }) + }) + + api.Get("/authors/:author_id/posts", func(ctx *zoox.Context) { + authorID, err := strconv.Atoi(ctx.Param().Get("author_id")) + if err != nil { + ctx.JSON(400, zoox.H{"error": "Invalid author ID"}) + return + } + + var authorPosts []Post + for _, post := range posts { + if post.AuthorID == authorID { + authorPosts = append(authorPosts, post) + } + } + + ctx.JSON(200, zoox.H{ + "posts": authorPosts, + "author_id": authorID, + "total": len(authorPosts), + }) + }) + + // Protected routes + protected := api.Group("/protected") + { + protected.Use(middleware.BasicAuth("Protected", map[string]string{ + "user": "password", + })) + + protected.Post("/posts", func(ctx *zoox.Context) { + ctx.JSON(201, zoox.H{"message": "Post created"}) + }) + + protected.Put("/posts/:id", func(ctx *zoox.Context) { + id := ctx.Param().Get("id") + ctx.JSON(200, zoox.H{"message": "Post updated", "id": id}) + }) + } + + // Admin routes + admin := api.Group("/admin") + { + admin.Use(middleware.BasicAuth("Admin", map[string]string{ + "admin": "secret", + })) + + admin.Delete("/posts/:id", func(ctx *zoox.Context) { + id := ctx.Param().Get("id") + ctx.JSON(200, zoox.H{"message": "Post deleted", "id": id}) + }) + + admin.Get("/stats", func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "total_posts": len(posts), + "total_comments": len(comments), + "authors": 2, + }) + }) + } + } + + log.Println("🚀 Blog API Server starting on http://localhost:8080") + log.Println("📋 Try these endpoints:") + log.Println(" GET /api/v1/posts") + log.Println(" GET /api/v1/posts/1") + log.Println(" GET /api/v1/posts/1/comments") + log.Println(" GET /api/v1/authors/1/posts") + log.Println(" POST /api/v1/protected/posts (user:password)") + log.Println(" GET /api/v1/admin/stats (admin:secret)") + + app.Run(":8080") +} +``` + +## 📚 Key Takeaways + +1. **HTTP Methods**: Use appropriate HTTP methods for different operations +2. **Route Parameters**: Use `:param` for single values and `*param` for wildcards +3. **Route Groups**: Organize related routes and apply common middleware +4. **Parameter Validation**: Always validate route parameters +5. **Route Precedence**: More specific routes should be defined before general ones +6. **Middleware Application**: Apply middleware at the appropriate level (global, group, or route) + +## 📖 Additional Resources + +- [Zoox Routing Documentation](../DOCUMENTATION.md#routing) +- [HTTP Methods Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) +- [URL Pattern Matching](https://tools.ietf.org/html/rfc3986) +- [Next Tutorial: Request & Response Handling](./03-request-response-handling.md) + +## 🔗 What's Next? + +In the next tutorial, we'll dive deep into request and response handling, learning how to: +- Parse different types of request data +- Handle form data and file uploads +- Send various response formats +- Implement proper error handling + +Continue to [Tutorial 03: Request & Response Handling](./03-request-response-handling.md)! \ No newline at end of file diff --git a/tutorials/03-request-response-handling.md b/tutorials/03-request-response-handling.md new file mode 100644 index 0000000..120c287 --- /dev/null +++ b/tutorials/03-request-response-handling.md @@ -0,0 +1,989 @@ +# Request & Response Handling in Zoox Framework + +Master the art of handling HTTP requests and responses in Zoox, from parsing different data formats to sending appropriate responses. + +## 📋 Prerequisites + +### Required Knowledge +- Completed [02-routing-fundamentals](./02-routing-fundamentals.md) +- Understanding of HTTP headers and content types +- Basic knowledge of JSON and form data + +### Software Requirements +- Go 1.19 or higher +- Zoox framework installed +- HTTP client (curl, Postman, or browser) + +## 🎯 Learning Objectives + +By the end of this tutorial, you will: +- ✅ Parse different types of request data (JSON, form, query parameters) +- ✅ Handle file uploads and multipart forms +- ✅ Send various response formats (JSON, HTML, XML, files) +- ✅ Implement proper error handling and status codes +- ✅ Work with HTTP headers and cookies +- ✅ Validate and sanitize input data + +## 📖 Tutorial Content + +### Step 1: Reading Request Data + +Let's start with different ways to read request data: + +```go +package main + +import ( + "log" + "net/http" + "strconv" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.Default() + + // Query parameters + app.Get("/search", func(ctx *zoox.Context) { + // Get query parameters + query := ctx.Query().Get("q") + page := ctx.Query().Get("page", "1") + limit := ctx.Query().Get("limit", "10") + + // Convert to integers + pageInt, _ := strconv.Atoi(page) + limitInt, _ := strconv.Atoi(limit) + + // Get all query parameters + allParams := ctx.Query().All() + + ctx.JSON(200, zoox.H{ + "query": query, + "page": pageInt, + "limit": limitInt, + "all_params": allParams, + }) + }) + + // Path parameters + app.Get("/users/:id", func(ctx *zoox.Context) { + id := ctx.Param().Get("id") + + // Convert to integer with validation + userID, err := strconv.Atoi(id) + if err != nil { + ctx.JSON(400, zoox.H{ + "error": "Invalid user ID", + "details": "ID must be a number", + }) + return + } + + ctx.JSON(200, zoox.H{ + "user_id": userID, + "message": "User found", + }) + }) + + // Request headers + app.Get("/headers", func(ctx *zoox.Context) { + userAgent := ctx.Header().Get("User-Agent") + contentType := ctx.Header().Get("Content-Type") + authorization := ctx.Header().Get("Authorization") + + // Get all headers + allHeaders := ctx.Header().All() + + ctx.JSON(200, zoox.H{ + "user_agent": userAgent, + "content_type": contentType, + "authorization": authorization, + "all_headers": allHeaders, + }) + }) + + // Request information + app.Get("/request-info", func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "method": ctx.Method, + "path": ctx.Path, + "url": ctx.Request.URL.String(), + "remote_addr": ctx.Request.RemoteAddr, + "user_agent": ctx.UserAgent().String(), + "ip": ctx.IP(), + "protocol": ctx.Request.Proto, + }) + }) + + log.Println("🚀 Server starting on http://localhost:8080") + app.Run(":8080") +} +``` + +### Step 2: Handling Different Content Types + +```go +package main + +import ( + "log" + "net/http" + + "github.com/go-zoox/zoox" +) + +type User struct { + ID int `json:"id" form:"id"` + Name string `json:"name" form:"name"` + Email string `json:"email" form:"email"` + Age int `json:"age" form:"age"` +} + +func main() { + app := zoox.Default() + + // JSON request handling + app.Post("/users/json", func(ctx *zoox.Context) { + var user User + + if err := ctx.BindJSON(&user); err != nil { + ctx.JSON(400, zoox.H{ + "error": "Invalid JSON", + "details": err.Error(), + }) + return + } + + // Validate required fields + if user.Name == "" || user.Email == "" { + ctx.JSON(400, zoox.H{ + "error": "Missing required fields", + "required": []string{"name", "email"}, + }) + return + } + + ctx.JSON(201, zoox.H{ + "message": "User created from JSON", + "user": user, + }) + }) + + // Form data handling + app.Post("/users/form", func(ctx *zoox.Context) { + var user User + + if err := ctx.BindForm(&user); err != nil { + ctx.JSON(400, zoox.H{ + "error": "Invalid form data", + "details": err.Error(), + }) + return + } + + ctx.JSON(201, zoox.H{ + "message": "User created from form", + "user": user, + }) + }) + + // Manual form parsing + app.Post("/users/manual", func(ctx *zoox.Context) { + name := ctx.Form().Get("name") + email := ctx.Form().Get("email") + ageStr := ctx.Form().Get("age", "0") + + age, _ := strconv.Atoi(ageStr) + + user := User{ + Name: name, + Email: email, + Age: age, + } + + ctx.JSON(201, zoox.H{ + "message": "User created manually", + "user": user, + }) + }) + + // Query string binding + app.Get("/users/search", func(ctx *zoox.Context) { + var searchParams struct { + Name string `form:"name"` + Email string `form:"email"` + Age int `form:"age"` + Page int `form:"page"` + Limit int `form:"limit"` + } + + if err := ctx.BindQuery(&searchParams); err != nil { + ctx.JSON(400, zoox.H{ + "error": "Invalid query parameters", + "details": err.Error(), + }) + return + } + + ctx.JSON(200, zoox.H{ + "message": "Search parameters", + "params": searchParams, + }) + }) + + // Raw body reading + app.Post("/raw", func(ctx *zoox.Context) { + body, err := ctx.Body() + if err != nil { + ctx.JSON(400, zoox.H{ + "error": "Failed to read body", + "details": err.Error(), + }) + return + } + + ctx.JSON(200, zoox.H{ + "message": "Raw body received", + "body": string(body), + "length": len(body), + }) + }) + + log.Println("🚀 Server starting on http://localhost:8080") + app.Run(":8080") +} +``` + +### Step 3: File Upload Handling + +```go +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.Default() + + // Single file upload + app.Post("/upload/single", func(ctx *zoox.Context) { + file, err := ctx.FormFile("file") + if err != nil { + ctx.JSON(400, zoox.H{ + "error": "No file uploaded", + "details": err.Error(), + }) + return + } + + // Validate file size (max 10MB) + const maxSize = 10 * 1024 * 1024 + if file.Size > maxSize { + ctx.JSON(400, zoox.H{ + "error": "File too large", + "max_size": "10MB", + "file_size": fmt.Sprintf("%.2fMB", float64(file.Size)/(1024*1024)), + }) + return + } + + // Validate file type + allowedTypes := []string{".jpg", ".jpeg", ".png", ".gif", ".pdf", ".txt"} + ext := strings.ToLower(filepath.Ext(file.Filename)) + allowed := false + for _, allowedType := range allowedTypes { + if ext == allowedType { + allowed = true + break + } + } + + if !allowed { + ctx.JSON(400, zoox.H{ + "error": "File type not allowed", + "allowed_types": allowedTypes, + "file_type": ext, + }) + return + } + + // Save file + filename := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename) + filepath := filepath.Join(uploadDir, filename) + + if err := ctx.SaveFile(file, filepath); err != nil { + ctx.JSON(500, zoox.H{ + "error": "Failed to save file", + "details": err.Error(), + }) + return + } + + ctx.JSON(200, zoox.H{ + "message": "File uploaded successfully", + "file": zoox.H{ + "original_name": file.Filename, + "saved_name": filename, + "size": file.Size, + "type": file.Header.Get("Content-Type"), + }, + }) + }) + + // Multiple file upload + app.Post("/upload/multiple", func(ctx *zoox.Context) { + form, err := ctx.MultipartForm() + if err != nil { + ctx.JSON(400, zoox.H{ + "error": "Failed to parse multipart form", + "details": err.Error(), + }) + return + } + + files := form.File["files"] + if len(files) == 0 { + ctx.JSON(400, zoox.H{ + "error": "No files uploaded", + }) + return + } + + var uploadedFiles []zoox.H + var errors []string + + for _, file := range files { + filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), file.Filename) + filepath := filepath.Join(uploadDir, filename) + + if err := ctx.SaveFile(file, filepath); err != nil { + errors = append(errors, fmt.Sprintf("%s: %s", file.Filename, err.Error())) + continue + } + + uploadedFiles = append(uploadedFiles, zoox.H{ + "original_name": file.Filename, + "saved_name": filename, + "size": file.Size, + "type": file.Header.Get("Content-Type"), + }) + } + + response := zoox.H{ + "message": fmt.Sprintf("Uploaded %d files", len(uploadedFiles)), + "files": uploadedFiles, + } + + if len(errors) > 0 { + response["errors"] = errors + } + + ctx.JSON(200, response) + }) + + log.Println("🚀 Server starting on http://localhost:8080") + app.Run(":8080") +} +``` + +### Step 4: Response Handling + +```go +package main + +import ( + "encoding/xml" + "log" + "time" + + "github.com/go-zoox/zoox" +) + +type XMLResponse struct { + XMLName xml.Name `xml:"response"` + Status string `xml:"status"` + Message string `xml:"message"` + Data string `xml:"data"` +} + +func main() { + app := zoox.Default() + + // JSON response + app.Get("/json", func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "message": "JSON response", + "timestamp": time.Now().Format(time.RFC3339), + "data": zoox.H{ + "users": []zoox.H{ + {"id": 1, "name": "John"}, + {"id": 2, "name": "Jane"}, + }, + }, + }) + }) + + // XML response + app.Get("/xml", func(ctx *zoox.Context) { + response := XMLResponse{ + Status: "success", + Message: "XML response", + Data: "Sample data", + } + ctx.XML(200, response) + }) + + // HTML response + app.Get("/html", func(ctx *zoox.Context) { + html := ` + + + + Zoox Response + + +

    HTML Response

    +

    This is an HTML response from Zoox

    +

    Timestamp: ` + time.Now().Format(time.RFC3339) + `

    + + + ` + ctx.HTML(200, html) + }) + + // Plain text response + app.Get("/text", func(ctx *zoox.Context) { + ctx.String(200, "This is a plain text response from Zoox") + }) + + // Custom headers + app.Get("/custom-headers", func(ctx *zoox.Context) { + ctx.Header().Set("X-Custom-Header", "Custom Value") + ctx.Header().Set("X-API-Version", "1.0") + ctx.Header().Set("Cache-Control", "no-cache") + + ctx.JSON(200, zoox.H{ + "message": "Response with custom headers", + "headers": ctx.Header().All(), + }) + }) + + // Different status codes + app.Get("/status/:code", func(ctx *zoox.Context) { + code := ctx.Param().Get("code") + statusCode, _ := strconv.Atoi(code) + + var message string + switch statusCode { + case 200: + message = "OK" + case 201: + message = "Created" + case 400: + message = "Bad Request" + case 401: + message = "Unauthorized" + case 404: + message = "Not Found" + case 500: + message = "Internal Server Error" + default: + message = "Unknown Status" + } + + ctx.JSON(statusCode, zoox.H{ + "status": statusCode, + "message": message, + }) + }) + + // Cookies + app.Get("/cookies/set", func(ctx *zoox.Context) { + ctx.SetCookie("session_id", "abc123", 3600, "/", "", false, true) + ctx.SetCookie("user_pref", "dark_mode", 86400, "/", "", false, false) + + ctx.JSON(200, zoox.H{ + "message": "Cookies set successfully", + }) + }) + + app.Get("/cookies/get", func(ctx *zoox.Context) { + sessionID := ctx.Cookie("session_id") + userPref := ctx.Cookie("user_pref") + + ctx.JSON(200, zoox.H{ + "session_id": sessionID, + "user_pref": userPref, + }) + }) + + // Redirect + app.Get("/redirect", func(ctx *zoox.Context) { + ctx.Redirect(302, "/json") + }) + + // File download + app.Get("/download/:filename", func(ctx *zoox.Context) { + filename := ctx.Param().Get("filename") + filepath := "./uploads/" + filename + + // Check if file exists + if _, err := os.Stat(filepath); os.IsNotExist(err) { + ctx.JSON(404, zoox.H{ + "error": "File not found", + }) + return + } + + ctx.File(filepath) + }) + + // Stream response + app.Get("/stream", func(ctx *zoox.Context) { + ctx.Header().Set("Content-Type", "text/plain") + ctx.Header().Set("Cache-Control", "no-cache") + + for i := 0; i < 10; i++ { + ctx.String(200, fmt.Sprintf("Chunk %d\n", i+1)) + time.Sleep(time.Second) + } + }) + + log.Println("🚀 Server starting on http://localhost:8080") + app.Run(":8080") +} +``` + +### Step 5: Error Handling + +```go +package main + +import ( + "errors" + "log" + "net/http" + + "github.com/go-zoox/zoox" +) + +type APIError struct { + Code int `json:"code"` + Message string `json:"message"` + Details string `json:"details,omitempty"` +} + +func (e APIError) Error() string { + return e.Message +} + +func main() { + app := zoox.Default() + + // Custom error handler middleware + app.Use(func(ctx *zoox.Context) { + ctx.Next() + + // Check for errors after processing + if len(ctx.Errors) > 0 { + err := ctx.Errors[0] + + // Handle different error types + switch e := err.(type) { + case APIError: + ctx.JSON(e.Code, zoox.H{ + "error": e.Message, + "details": e.Details, + }) + default: + ctx.JSON(500, zoox.H{ + "error": "Internal server error", + "details": e.Error(), + }) + } + } + }) + + // Validation errors + app.Post("/validate", func(ctx *zoox.Context) { + var data struct { + Name string `json:"name"` + Email string `json:"email"` + Age int `json:"age"` + } + + if err := ctx.BindJSON(&data); err != nil { + ctx.JSON(400, zoox.H{ + "error": "Invalid JSON format", + "details": err.Error(), + }) + return + } + + // Validate required fields + if data.Name == "" { + ctx.JSON(400, zoox.H{ + "error": "Validation failed", + "field": "name", + "message": "Name is required", + }) + return + } + + if data.Email == "" { + ctx.JSON(400, zoox.H{ + "error": "Validation failed", + "field": "email", + "message": "Email is required", + }) + return + } + + if data.Age < 0 || data.Age > 150 { + ctx.JSON(400, zoox.H{ + "error": "Validation failed", + "field": "age", + "message": "Age must be between 0 and 150", + }) + return + } + + ctx.JSON(200, zoox.H{ + "message": "Validation successful", + "data": data, + }) + }) + + // Custom error types + app.Get("/error/:type", func(ctx *zoox.Context) { + errorType := ctx.Param().Get("type") + + switch errorType { + case "validation": + ctx.Error(APIError{ + Code: 400, + Message: "Validation error", + Details: "Invalid input data", + }) + case "unauthorized": + ctx.Error(APIError{ + Code: 401, + Message: "Unauthorized access", + Details: "Valid authentication required", + }) + case "notfound": + ctx.Error(APIError{ + Code: 404, + Message: "Resource not found", + Details: "The requested resource does not exist", + }) + case "internal": + ctx.Error(errors.New("something went wrong internally")) + default: + ctx.JSON(400, zoox.H{ + "error": "Unknown error type", + "available_types": []string{"validation", "unauthorized", "notfound", "internal"}, + }) + } + }) + + // Panic recovery + app.Get("/panic", func(ctx *zoox.Context) { + panic("This is a test panic") + }) + + log.Println("🚀 Server starting on http://localhost:8080") + app.Run(":8080") +} +``` + +## 🧪 Hands-on Exercise + +### Exercise 1: Build a Contact Form API + +Create a contact form API with the following requirements: + +1. **Accept contact form submissions** with validation +2. **Handle file attachments** (optional) +3. **Send appropriate responses** based on validation results +4. **Implement proper error handling** + +### Solution: + +```go +package main + +import ( + "fmt" + "log" + "net/mail" + "os" + "path/filepath" + "strings" + "time" + + "github.com/go-zoox/zoox" +) + +type ContactForm struct { + Name string `json:"name" form:"name"` + Email string `json:"email" form:"email"` + Subject string `json:"subject" form:"subject"` + Message string `json:"message" form:"message"` + Phone string `json:"phone" form:"phone"` +} + +func main() { + app := zoox.Default() + + // Create attachments directory + attachDir := "./attachments" + os.MkdirAll(attachDir, 0755) + + // Contact form submission + app.Post("/contact", func(ctx *zoox.Context) { + var form ContactForm + + // Try to bind JSON first, then form data + if err := ctx.BindJSON(&form); err != nil { + if err := ctx.BindForm(&form); err != nil { + ctx.JSON(400, zoox.H{ + "error": "Invalid form data", + "details": "Please provide valid JSON or form data", + }) + return + } + } + + // Validate form + if errors := validateContactForm(form); len(errors) > 0 { + ctx.JSON(400, zoox.H{ + "error": "Validation failed", + "errors": errors, + }) + return + } + + // Handle file attachment (optional) + var attachmentInfo zoox.H + if file, err := ctx.FormFile("attachment"); err == nil { + // Validate file + if file.Size > 5*1024*1024 { // 5MB limit + ctx.JSON(400, zoox.H{ + "error": "File too large", + "max_size": "5MB", + }) + return + } + + // Save attachment + filename := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename) + filepath := filepath.Join(attachDir, filename) + + if err := ctx.SaveFile(file, filepath); err != nil { + ctx.JSON(500, zoox.H{ + "error": "Failed to save attachment", + "details": err.Error(), + }) + return + } + + attachmentInfo = zoox.H{ + "filename": filename, + "size": file.Size, + "type": file.Header.Get("Content-Type"), + } + } + + // Simulate processing (e.g., sending email) + time.Sleep(100 * time.Millisecond) + + response := zoox.H{ + "message": "Contact form submitted successfully", + "id": fmt.Sprintf("contact_%d", time.Now().Unix()), + "form": form, + } + + if attachmentInfo != nil { + response["attachment"] = attachmentInfo + } + + ctx.JSON(200, response) + }) + + // Get contact form (for testing) + app.Get("/contact/form", func(ctx *zoox.Context) { + ctx.HTML(200, contactFormHTML) + }) + + log.Println("🚀 Contact Form API starting on http://localhost:8080") + log.Println("📋 Try the form at: http://localhost:8080/contact/form") + + app.Run(":8080") +} + +func validateContactForm(form ContactForm) []string { + var errors []string + + // Name validation + if strings.TrimSpace(form.Name) == "" { + errors = append(errors, "Name is required") + } else if len(form.Name) < 2 { + errors = append(errors, "Name must be at least 2 characters") + } + + // Email validation + if strings.TrimSpace(form.Email) == "" { + errors = append(errors, "Email is required") + } else if _, err := mail.ParseAddress(form.Email); err != nil { + errors = append(errors, "Invalid email format") + } + + // Subject validation + if strings.TrimSpace(form.Subject) == "" { + errors = append(errors, "Subject is required") + } else if len(form.Subject) < 5 { + errors = append(errors, "Subject must be at least 5 characters") + } + + // Message validation + if strings.TrimSpace(form.Message) == "" { + errors = append(errors, "Message is required") + } else if len(form.Message) < 10 { + errors = append(errors, "Message must be at least 10 characters") + } + + // Phone validation (optional) + if form.Phone != "" { + phone := strings.ReplaceAll(form.Phone, " ", "") + phone = strings.ReplaceAll(phone, "-", "") + phone = strings.ReplaceAll(phone, "(", "") + phone = strings.ReplaceAll(phone, ")", "") + + if len(phone) < 10 { + errors = append(errors, "Phone number must be at least 10 digits") + } + } + + return errors +} + +const contactFormHTML = ` + + + + Contact Form + + + +

    Contact Form

    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + + +
    + +
    + + + + +` +``` + +## 📚 Key Takeaways + +1. **Request Parsing**: Use appropriate binding methods for different content types +2. **Validation**: Always validate and sanitize input data +3. **Error Handling**: Provide clear, actionable error messages +4. **Response Format**: Choose appropriate response format based on client needs +5. **Status Codes**: Use correct HTTP status codes for different scenarios +6. **File Handling**: Implement proper file validation and security measures +7. **Headers and Cookies**: Leverage HTTP headers and cookies for enhanced functionality + +## 📖 Additional Resources + +- [HTTP Status Codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) +- [Content-Type Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) +- [File Upload Security](https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload) +- [Next Tutorial: Middleware Basics](./04-middleware-basics.md) + +## 🔗 What's Next? + +In the next tutorial, we'll explore middleware in depth, learning how to: +- Create custom middleware functions +- Use built-in middleware effectively +- Chain middleware for complex processing +- Handle middleware errors and recovery + +Continue to [Tutorial 04: Middleware Basics](./04-middleware-basics.md)! \ No newline at end of file diff --git a/tutorials/04-middleware-basics.md b/tutorials/04-middleware-basics.md new file mode 100644 index 0000000..e855b54 --- /dev/null +++ b/tutorials/04-middleware-basics.md @@ -0,0 +1,1111 @@ +# Middleware Basics in Zoox Framework + +Learn how to use and create middleware in Zoox to add cross-cutting concerns like authentication, logging, and error handling to your applications. + +## 📋 Prerequisites + +### Required Knowledge +- Completed [03-request-response-handling](./03-request-response-handling.md) +- Understanding of HTTP request/response cycle +- Basic Go function concepts + +### Software Requirements +- Go 1.19 or higher +- Zoox framework installed + +## 🎯 Learning Objectives + +By the end of this tutorial, you will: +- ✅ Understand what middleware is and how it works +- ✅ Use built-in middleware effectively +- ✅ Create custom middleware functions +- ✅ Chain middleware for complex processing +- ✅ Handle middleware errors and recovery +- ✅ Apply middleware at different levels (global, group, route) + +## 📖 Tutorial Content + +### Step 1: Understanding Middleware + +Middleware are functions that execute during the request-response cycle. They can: +- Execute code before the request reaches the handler +- Execute code after the handler completes +- Modify the request or response +- Terminate the request early + +```go +package main + +import ( + "log" + "time" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.Default() + + // Simple middleware example + app.Use(func(ctx *zoox.Context) { + log.Printf("Before handler - %s %s", ctx.Method, ctx.Path) + + // Continue to next middleware/handler + ctx.Next() + + log.Printf("After handler - %s %s", ctx.Method, ctx.Path) + }) + + // Timing middleware + app.Use(func(ctx *zoox.Context) { + start := time.Now() + + ctx.Next() + + duration := time.Since(start) + log.Printf("Request took %v", duration) + }) + + // Routes + app.Get("/", func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{"message": "Hello World"}) + }) + + app.Get("/users", func(ctx *zoox.Context) { + time.Sleep(100 * time.Millisecond) // Simulate processing + ctx.JSON(200, zoox.H{"users": []string{"John", "Jane"}}) + }) + + log.Println("🚀 Server starting on http://localhost:8080") + app.Run(":8080") +} +``` + +### Step 2: Built-in Middleware + +Zoox provides several built-in middleware for common use cases: + +```go +package main + +import ( + "log" + + "github.com/go-zoox/zoox" + "github.com/go-zoox/zoox/middleware" +) + +func main() { + app := zoox.Default() + + // Logger middleware + app.Use(middleware.Logger()) + + // Recovery middleware (handles panics) + app.Use(middleware.Recovery()) + + // Request ID middleware + app.Use(middleware.RequestID()) + + // CORS middleware + app.Use(middleware.CORS()) + + // Gzip compression + app.Use(middleware.Gzip()) + + // Rate limiting + app.Use(middleware.RateLimit(100)) // 100 requests per minute + + // Basic authentication + app.Use(middleware.BasicAuth("Protected Area", map[string]string{ + "admin": "secret", + "user": "password", + })) + + // Routes + app.Get("/", func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "message": "Middleware demo", + "request_id": ctx.Header().Get("X-Request-ID"), + }) + }) + + app.Get("/panic", func(ctx *zoox.Context) { + panic("Test panic - should be caught by recovery middleware") + }) + + log.Println("🚀 Server starting on http://localhost:8080") + app.Run(":8080") +} +``` + +### Step 3: Custom Middleware + +Create your own middleware for specific needs: + +```go +package main + +import ( + "log" + "strings" + "time" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.Default() + + // Custom logging middleware + app.Use(customLogger()) + + // API key authentication middleware + app.Use(apiKeyAuth()) + + // Request validation middleware + app.Use(validateRequest()) + + // Security headers middleware + app.Use(securityHeaders()) + + // Routes + app.Get("/", func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{"message": "Protected endpoint"}) + }) + + app.Post("/data", func(ctx *zoox.Context) { + var data map[string]interface{} + ctx.BindJSON(&data) + ctx.JSON(200, zoox.H{"received": data}) + }) + + log.Println("🚀 Server starting on http://localhost:8080") + log.Println("📋 Use API key: X-API-Key: secret-key") + app.Run(":8080") +} + +// Custom logger middleware +func customLogger() func(*zoox.Context) { + return func(ctx *zoox.Context) { + start := time.Now() + + // Log request + log.Printf("[%s] %s %s - %s", + start.Format("2006-01-02 15:04:05"), + ctx.Method, + ctx.Path, + ctx.IP(), + ) + + ctx.Next() + + // Log response + duration := time.Since(start) + log.Printf("[%s] %s %s - %d - %v", + time.Now().Format("2006-01-02 15:04:05"), + ctx.Method, + ctx.Path, + ctx.Status(), + duration, + ) + } +} + +// API key authentication middleware +func apiKeyAuth() func(*zoox.Context) { + return func(ctx *zoox.Context) { + apiKey := ctx.Header().Get("X-API-Key") + + if apiKey == "" { + ctx.JSON(401, zoox.H{ + "error": "Missing API key", + "message": "Please provide X-API-Key header", + }) + ctx.Abort() + return + } + + // Validate API key (in real app, check against database) + validKeys := []string{"secret-key", "another-key"} + valid := false + for _, key := range validKeys { + if apiKey == key { + valid = true + break + } + } + + if !valid { + ctx.JSON(401, zoox.H{ + "error": "Invalid API key", + }) + ctx.Abort() + return + } + + // Store API key info for later use + ctx.Set("api_key", apiKey) + ctx.Next() + } +} + +// Request validation middleware +func validateRequest() func(*zoox.Context) { + return func(ctx *zoox.Context) { + // Only validate POST requests + if ctx.Method != "POST" { + ctx.Next() + return + } + + contentType := ctx.Header().Get("Content-Type") + if !strings.Contains(contentType, "application/json") { + ctx.JSON(400, zoox.H{ + "error": "Invalid content type", + "message": "Only application/json is supported", + }) + ctx.Abort() + return + } + + ctx.Next() + } +} + +// Security headers middleware +func securityHeaders() func(*zoox.Context) { + return func(ctx *zoox.Context) { + // Set security headers + ctx.Header().Set("X-Content-Type-Options", "nosniff") + ctx.Header().Set("X-Frame-Options", "DENY") + ctx.Header().Set("X-XSS-Protection", "1; mode=block") + ctx.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") + + ctx.Next() + } +} +``` + +### Step 4: Middleware Chaining and Order + +The order of middleware matters. They execute in the order they're added: + +```go +package main + +import ( + "log" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.Default() + + // Middleware execute in order + app.Use(middleware1()) + app.Use(middleware2()) + app.Use(middleware3()) + + // Group-level middleware + api := app.Group("/api") + { + api.Use(apiMiddleware()) + + api.Get("/users", func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{"users": []string{"John", "Jane"}}) + }) + + // Route-specific middleware + api.Get("/admin", adminMiddleware(), func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{"message": "Admin area"}) + }) + } + + // Routes without group middleware + app.Get("/public", func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{"message": "Public endpoint"}) + }) + + log.Println("🚀 Server starting on http://localhost:8080") + app.Run(":8080") +} + +func middleware1() func(*zoox.Context) { + return func(ctx *zoox.Context) { + log.Println("Middleware 1 - Before") + ctx.Next() + log.Println("Middleware 1 - After") + } +} + +func middleware2() func(*zoox.Context) { + return func(ctx *zoox.Context) { + log.Println("Middleware 2 - Before") + ctx.Next() + log.Println("Middleware 2 - After") + } +} + +func middleware3() func(*zoox.Context) { + return func(ctx *zoox.Context) { + log.Println("Middleware 3 - Before") + ctx.Next() + log.Println("Middleware 3 - After") + } +} + +func apiMiddleware() func(*zoox.Context) { + return func(ctx *zoox.Context) { + log.Println("API Middleware - Setting API headers") + ctx.Header().Set("X-API-Version", "1.0") + ctx.Next() + } +} + +func adminMiddleware() func(*zoox.Context) { + return func(ctx *zoox.Context) { + log.Println("Admin Middleware - Checking admin access") + // Simulate admin check + isAdmin := ctx.Header().Get("X-Admin") == "true" + if !isAdmin { + ctx.JSON(403, zoox.H{"error": "Admin access required"}) + ctx.Abort() + return + } + ctx.Next() + } +} +``` + +### Step 5: Error Handling in Middleware + +Proper error handling is crucial in middleware: + +```go +package main + +import ( + "errors" + "log" + "time" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.Default() + + // Error handling middleware (should be first) + app.Use(errorHandler()) + + // Panic recovery middleware + app.Use(panicRecovery()) + + // Timeout middleware + app.Use(timeoutMiddleware(5 * time.Second)) + + // Validation middleware + app.Use(validationMiddleware()) + + // Routes + app.Get("/", func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{"message": "Success"}) + }) + + app.Get("/error", func(ctx *zoox.Context) { + ctx.Error(errors.New("something went wrong")) + }) + + app.Get("/panic", func(ctx *zoox.Context) { + panic("test panic") + }) + + app.Get("/slow", func(ctx *zoox.Context) { + time.Sleep(10 * time.Second) // Will timeout + ctx.JSON(200, zoox.H{"message": "Slow response"}) + }) + + log.Println("🚀 Server starting on http://localhost:8080") + app.Run(":8080") +} + +// Error handling middleware +func errorHandler() func(*zoox.Context) { + return func(ctx *zoox.Context) { + ctx.Next() + + // Check for errors after all middleware/handlers + if len(ctx.Errors) > 0 { + err := ctx.Errors[0] + log.Printf("Error occurred: %v", err) + + // Don't send response if already sent + if ctx.IsAborted() { + return + } + + ctx.JSON(500, zoox.H{ + "error": "Internal server error", + "message": err.Error(), + }) + } + } +} + +// Panic recovery middleware +func panicRecovery() func(*zoox.Context) { + return func(ctx *zoox.Context) { + defer func() { + if r := recover(); r != nil { + log.Printf("Panic recovered: %v", r) + + if !ctx.IsAborted() { + ctx.JSON(500, zoox.H{ + "error": "Internal server error", + "message": "Server panic occurred", + }) + } + } + }() + + ctx.Next() + } +} + +// Timeout middleware +func timeoutMiddleware(timeout time.Duration) func(*zoox.Context) { + return func(ctx *zoox.Context) { + done := make(chan bool, 1) + + go func() { + ctx.Next() + done <- true + }() + + select { + case <-done: + // Request completed normally + case <-time.After(timeout): + // Request timed out + log.Printf("Request timeout: %s %s", ctx.Method, ctx.Path) + if !ctx.IsAborted() { + ctx.JSON(408, zoox.H{ + "error": "Request timeout", + "timeout": timeout.String(), + }) + ctx.Abort() + } + } + } +} + +// Validation middleware +func validationMiddleware() func(*zoox.Context) { + return func(ctx *zoox.Context) { + // Example: validate user agent + userAgent := ctx.UserAgent().String() + if userAgent == "" { + ctx.JSON(400, zoox.H{ + "error": "Missing User-Agent header", + }) + ctx.Abort() + return + } + + // Block certain user agents + blocked := []string{"BadBot", "Crawler"} + for _, bot := range blocked { + if userAgent == bot { + ctx.JSON(403, zoox.H{ + "error": "Blocked user agent", + }) + ctx.Abort() + return + } + } + + ctx.Next() + } +} +``` + +### Step 6: Advanced Middleware Patterns + +```go +package main + +import ( + "log" + "sync" + "time" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.Default() + + // Conditional middleware + app.Use(conditionalMiddleware()) + + // Caching middleware + app.Use(cacheMiddleware()) + + // Rate limiting with different limits per endpoint + app.Use(advancedRateLimit()) + + // Request/Response transformation + app.Use(transformMiddleware()) + + // Routes + app.Get("/", func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{"message": "Hello World"}) + }) + + app.Get("/cached", func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "message": "This response is cached", + "time": time.Now().Format(time.RFC3339), + }) + }) + + app.Get("/limited", func(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{"message": "Rate limited endpoint"}) + }) + + log.Println("🚀 Server starting on http://localhost:8080") + app.Run(":8080") +} + +// Conditional middleware - only applies to certain paths +func conditionalMiddleware() func(*zoox.Context) { + return func(ctx *zoox.Context) { + // Only apply to paths starting with /api + if !strings.HasPrefix(ctx.Path, "/api") { + ctx.Next() + return + } + + log.Println("API-specific middleware executed") + ctx.Header().Set("X-API-Processed", "true") + ctx.Next() + } +} + +// Simple caching middleware +func cacheMiddleware() func(*zoox.Context) { + cache := make(map[string]cacheItem) + var mu sync.RWMutex + + type cacheItem struct { + data interface{} + timestamp time.Time + ttl time.Duration + } + + return func(ctx *zoox.Context) { + // Only cache GET requests + if ctx.Method != "GET" { + ctx.Next() + return + } + + key := ctx.Path + mu.RLock() + item, exists := cache[key] + mu.RUnlock() + + // Check if cached and not expired + if exists && time.Since(item.timestamp) < item.ttl { + log.Printf("Cache hit for %s", key) + ctx.Header().Set("X-Cache", "HIT") + ctx.JSON(200, item.data) + return + } + + // Continue to handler + ctx.Next() + + // Cache the response (simplified) + if ctx.Status() == 200 { + mu.Lock() + cache[key] = cacheItem{ + data: zoox.H{"cached": true, "original_time": time.Now().Format(time.RFC3339)}, + timestamp: time.Now(), + ttl: 1 * time.Minute, + } + mu.Unlock() + log.Printf("Cached response for %s", key) + } + } +} + +// Advanced rate limiting with different limits per endpoint +func advancedRateLimit() func(*zoox.Context) { + type rateLimiter struct { + requests int + resetTime time.Time + limit int + } + + limiters := make(map[string]*rateLimiter) + var mu sync.RWMutex + + return func(ctx *zoox.Context) { + // Different limits for different endpoints + var limit int + switch ctx.Path { + case "/limited": + limit = 5 // 5 requests per minute + default: + limit = 60 // 60 requests per minute + } + + key := ctx.IP() + ":" + ctx.Path + + mu.Lock() + limiter, exists := limiters[key] + if !exists { + limiter = &rateLimiter{ + requests: 0, + resetTime: time.Now().Add(time.Minute), + limit: limit, + } + limiters[key] = limiter + } + + // Reset if time window expired + if time.Now().After(limiter.resetTime) { + limiter.requests = 0 + limiter.resetTime = time.Now().Add(time.Minute) + } + + limiter.requests++ + mu.Unlock() + + // Check if limit exceeded + if limiter.requests > limit { + ctx.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", limit)) + ctx.Header().Set("X-RateLimit-Remaining", "0") + ctx.Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d", limiter.resetTime.Unix())) + + ctx.JSON(429, zoox.H{ + "error": "Rate limit exceeded", + "limit": limit, + }) + ctx.Abort() + return + } + + // Set rate limit headers + ctx.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", limit)) + ctx.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", limit-limiter.requests)) + + ctx.Next() + } +} + +// Request/Response transformation middleware +func transformMiddleware() func(*zoox.Context) { + return func(ctx *zoox.Context) { + // Transform request + if ctx.Method == "POST" { + // Add timestamp to all POST requests + ctx.Set("request_timestamp", time.Now().Format(time.RFC3339)) + } + + ctx.Next() + + // Transform response (add metadata) + if ctx.Status() == 200 { + ctx.Header().Set("X-Response-Time", time.Now().Format(time.RFC3339)) + ctx.Header().Set("X-Server", "Zoox-Tutorial") + } + } +} +``` + +## 🧪 Hands-on Exercise + +### Exercise 1: Build a Complete Middleware Stack + +Create a web application with the following middleware stack: + +1. **Request logging** with unique request IDs +2. **Authentication** using JWT tokens +3. **Authorization** with role-based access control +4. **Rate limiting** with different limits per user role +5. **Response caching** for GET requests +6. **Error handling** with proper HTTP status codes + +### Solution: + +```go +package main + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "log" + "strings" + "sync" + "time" + + "github.com/go-zoox/zoox" +) + +type User struct { + ID string `json:"id"` + Name string `json:"name"` + Role string `json:"role"` +} + +func main() { + app := zoox.Default() + + // Middleware stack (order matters!) + app.Use(requestLogger()) + app.Use(errorHandler()) + app.Use(authMiddleware()) + app.Use(roleBasedRateLimit()) + app.Use(cacheMiddleware()) + + // Public routes (no auth required) + app.Post("/login", loginHandler) + + // Protected routes + api := app.Group("/api") + { + api.Use(requireAuth()) + + api.Get("/profile", profileHandler) + api.Get("/users", requireRole("admin"), usersHandler) + api.Get("/data", dataHandler) + } + + log.Println("🚀 Server starting on http://localhost:8080") + log.Println("📋 Login with: POST /login {\"username\": \"admin\", \"password\": \"secret\"}") + app.Run(":8080") +} + +// Request logger with unique IDs +func requestLogger() func(*zoox.Context) { + return func(ctx *zoox.Context) { + // Generate unique request ID + requestID := generateRequestID() + ctx.Set("request_id", requestID) + ctx.Header().Set("X-Request-ID", requestID) + + start := time.Now() + log.Printf("[%s] %s %s %s - START", requestID, ctx.Method, ctx.Path, ctx.IP()) + + ctx.Next() + + duration := time.Since(start) + log.Printf("[%s] %s %s %s - %d - %v", requestID, ctx.Method, ctx.Path, ctx.IP(), ctx.Status(), duration) + } +} + +// Error handler +func errorHandler() func(*zoox.Context) { + return func(ctx *zoox.Context) { + ctx.Next() + + if len(ctx.Errors) > 0 { + err := ctx.Errors[0] + requestID := ctx.GetString("request_id") + log.Printf("[%s] Error: %v", requestID, err) + + if !ctx.IsAborted() { + ctx.JSON(500, zoox.H{ + "error": "Internal server error", + "request_id": requestID, + }) + } + } + } +} + +// Authentication middleware +func authMiddleware() func(*zoox.Context) { + return func(ctx *zoox.Context) { + // Skip auth for login endpoint + if ctx.Path == "/login" { + ctx.Next() + return + } + + ctx.Next() + } +} + +// Require authentication +func requireAuth() func(*zoox.Context) { + return func(ctx *zoox.Context) { + token := ctx.Header().Get("Authorization") + if token == "" { + ctx.JSON(401, zoox.H{"error": "Missing authorization token"}) + ctx.Abort() + return + } + + // Remove "Bearer " prefix + if strings.HasPrefix(token, "Bearer ") { + token = token[7:] + } + + // Validate token (simplified) + user := validateToken(token) + if user == nil { + ctx.JSON(401, zoox.H{"error": "Invalid token"}) + ctx.Abort() + return + } + + ctx.Set("user", user) + ctx.Next() + } +} + +// Role-based access control +func requireRole(role string) func(*zoox.Context) { + return func(ctx *zoox.Context) { + user := ctx.Get("user").(*User) + if user.Role != role { + ctx.JSON(403, zoox.H{ + "error": "Insufficient permissions", + "required_role": role, + "user_role": user.Role, + }) + ctx.Abort() + return + } + ctx.Next() + } +} + +// Role-based rate limiting +func roleBasedRateLimit() func(*zoox.Context) { + limiters := make(map[string]*rateLimiter) + var mu sync.RWMutex + + type rateLimiter struct { + requests int + resetTime time.Time + limit int + } + + return func(ctx *zoox.Context) { + // Skip for login + if ctx.Path == "/login" { + ctx.Next() + return + } + + user := ctx.Get("user") + if user == nil { + ctx.Next() + return + } + + u := user.(*User) + + // Different limits based on role + var limit int + switch u.Role { + case "admin": + limit = 1000 // 1000 requests per minute + case "user": + limit = 100 // 100 requests per minute + default: + limit = 10 // 10 requests per minute + } + + key := u.ID + + mu.Lock() + limiter, exists := limiters[key] + if !exists { + limiter = &rateLimiter{ + requests: 0, + resetTime: time.Now().Add(time.Minute), + limit: limit, + } + limiters[key] = limiter + } + + if time.Now().After(limiter.resetTime) { + limiter.requests = 0 + limiter.resetTime = time.Now().Add(time.Minute) + } + + limiter.requests++ + mu.Unlock() + + if limiter.requests > limit { + ctx.JSON(429, zoox.H{ + "error": "Rate limit exceeded", + "limit": limit, + "role": u.Role, + }) + ctx.Abort() + return + } + + ctx.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", limit)) + ctx.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", limit-limiter.requests)) + + ctx.Next() + } +} + +// Simple caching middleware +func cacheMiddleware() func(*zoox.Context) { + cache := make(map[string]cacheItem) + var mu sync.RWMutex + + type cacheItem struct { + data interface{} + timestamp time.Time + ttl time.Duration + } + + return func(ctx *zoox.Context) { + if ctx.Method != "GET" { + ctx.Next() + return + } + + key := ctx.Path + mu.RLock() + item, exists := cache[key] + mu.RUnlock() + + if exists && time.Since(item.timestamp) < item.ttl { + ctx.Header().Set("X-Cache", "HIT") + ctx.JSON(200, item.data) + return + } + + ctx.Next() + + // Cache successful responses + if ctx.Status() == 200 { + mu.Lock() + cache[key] = cacheItem{ + data: zoox.H{"cached": true, "time": time.Now().Format(time.RFC3339)}, + timestamp: time.Now(), + ttl: 30 * time.Second, + } + mu.Unlock() + } + } +} + +// Handlers +func loginHandler(ctx *zoox.Context) { + var credentials struct { + Username string `json:"username"` + Password string `json:"password"` + } + + if err := ctx.BindJSON(&credentials); err != nil { + ctx.JSON(400, zoox.H{"error": "Invalid request"}) + return + } + + // Validate credentials (simplified) + if credentials.Username == "admin" && credentials.Password == "secret" { + token := generateToken("admin") + ctx.JSON(200, zoox.H{ + "token": token, + "user": User{ID: "1", Name: "Admin", Role: "admin"}, + }) + return + } + + if credentials.Username == "user" && credentials.Password == "password" { + token := generateToken("user") + ctx.JSON(200, zoox.H{ + "token": token, + "user": User{ID: "2", Name: "User", Role: "user"}, + }) + return + } + + ctx.JSON(401, zoox.H{"error": "Invalid credentials"}) +} + +func profileHandler(ctx *zoox.Context) { + user := ctx.Get("user").(*User) + ctx.JSON(200, zoox.H{"user": user}) +} + +func usersHandler(ctx *zoox.Context) { + users := []User{ + {ID: "1", Name: "Admin", Role: "admin"}, + {ID: "2", Name: "User", Role: "user"}, + } + ctx.JSON(200, zoox.H{"users": users}) +} + +func dataHandler(ctx *zoox.Context) { + ctx.JSON(200, zoox.H{ + "data": "This is cached data", + "time": time.Now().Format(time.RFC3339), + }) +} + +// Helper functions +func generateRequestID() string { + bytes := make([]byte, 8) + rand.Read(bytes) + return hex.EncodeToString(bytes) +} + +func generateToken(role string) string { + // Simplified token generation + return fmt.Sprintf("token_%s_%d", role, time.Now().Unix()) +} + +func validateToken(token string) *User { + // Simplified token validation + if strings.HasPrefix(token, "token_admin_") { + return &User{ID: "1", Name: "Admin", Role: "admin"} + } + if strings.HasPrefix(token, "token_user_") { + return &User{ID: "2", Name: "User", Role: "user"} + } + return nil +} +``` + +## 📚 Key Takeaways + +1. **Middleware Order**: The order in which middleware is added matters +2. **ctx.Next()**: Always call `ctx.Next()` to continue the chain +3. **ctx.Abort()**: Use `ctx.Abort()` to stop processing +4. **Error Handling**: Implement proper error handling in middleware +5. **Conditional Logic**: Middleware can be conditional based on request properties +6. **State Management**: Use `ctx.Set()` and `ctx.Get()` to share data between middleware +7. **Performance**: Be mindful of middleware performance impact + +## 📖 Additional Resources + +- [Middleware Design Patterns](https://en.wikipedia.org/wiki/Middleware) +- [HTTP Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) +- [Rate Limiting Strategies](https://en.wikipedia.org/wiki/Rate_limiting) +- [Next Tutorial: Authentication & Authorization](./05-authentication-authorization.md) + +## 🔗 What's Next? + +In the next tutorial, we'll dive deeper into authentication and authorization, learning how to: +- Implement JWT authentication +- Create role-based access control +- Handle session management +- Secure API endpoints + +Continue to [Tutorial 05: Authentication & Authorization](./05-authentication-authorization.md)! \ No newline at end of file diff --git a/tutorials/05-advanced-routing.md b/tutorials/05-advanced-routing.md new file mode 100644 index 0000000..370d0da --- /dev/null +++ b/tutorials/05-advanced-routing.md @@ -0,0 +1,1294 @@ +# Tutorial 05: Advanced Routing + +## 📖 Overview + +In this tutorial, we'll explore advanced routing techniques in Zoox that go beyond basic route definitions. You'll learn about dynamic route registration, route constraints, performance optimization, and advanced patterns for complex applications. + +## 🎯 Learning Objectives + +By the end of this tutorial, you will be able to: +- Implement dynamic route registration +- Use route constraints for validation +- Optimize routing performance +- Handle complex routing scenarios +- Build flexible and maintainable routing systems + +## 📋 Prerequisites + +- Completed [Tutorial 02: Routing Fundamentals](./02-routing-fundamentals.md) +- Basic understanding of HTTP methods and RESTful APIs +- Familiarity with Go programming + +## 🚀 Getting Started + +### Dynamic Route Registration + +Dynamic route registration allows you to register routes at runtime based on configuration, database content, or other dynamic sources. + +```go +package main + +import ( + "fmt" + "log" + "strconv" + "strings" + + "github.com/go-zoox/zoox" +) + +// RouteConfig represents a dynamic route configuration +type RouteConfig struct { + Method string `json:"method"` + Path string `json:"path"` + Handler string `json:"handler"` + Params map[string]string `json:"params"` +} + +// DynamicRouter manages dynamic routes +type DynamicRouter struct { + app *zoox.Application + routes map[string]RouteConfig +} + +// NewDynamicRouter creates a new dynamic router +func NewDynamicRouter(app *zoox.Application) *DynamicRouter { + return &DynamicRouter{ + app: app, + routes: make(map[string]RouteConfig), + } +} + +// RegisterRoute registers a route dynamically +func (dr *DynamicRouter) RegisterRoute(id string, config RouteConfig) error { + handler := dr.createHandler(config) + + switch strings.ToUpper(config.Method) { + case "GET": + dr.app.Get(config.Path, handler) + case "POST": + dr.app.Post(config.Path, handler) + case "PUT": + dr.app.Put(config.Path, handler) + case "DELETE": + dr.app.Delete(config.Path, handler) + default: + return fmt.Errorf("unsupported method: %s", config.Method) + } + + dr.routes[id] = config + return nil +} + +// createHandler creates a handler function based on configuration +func (dr *DynamicRouter) createHandler(config RouteConfig) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + switch config.Handler { + case "echo": + dr.echoHandler(ctx, config) + case "static": + dr.staticHandler(ctx, config) + case "redirect": + dr.redirectHandler(ctx, config) + case "template": + dr.templateHandler(ctx, config) + default: + ctx.JSON(500, map[string]interface{}{ + "error": "Unknown handler type", + "handler": config.Handler, + }) + } + } +} + +// Handler implementations +func (dr *DynamicRouter) echoHandler(ctx *zoox.Context, config RouteConfig) { + response := map[string]interface{}{ + "message": "Dynamic route response", + "route": config.Path, + "method": config.Method, + "params": ctx.Params(), + "query": ctx.Query(), + } + + if message, ok := config.Params["message"]; ok { + response["message"] = message + } + + ctx.JSON(200, response) +} + +func (dr *DynamicRouter) staticHandler(ctx *zoox.Context, config RouteConfig) { + if content, ok := config.Params["content"]; ok { + ctx.String(200, content) + } else { + ctx.String(200, "Static content") + } +} + +func (dr *DynamicRouter) redirectHandler(ctx *zoox.Context, config RouteConfig) { + if url, ok := config.Params["url"]; ok { + ctx.Redirect(302, url) + } else { + ctx.String(400, "Redirect URL not specified") + } +} + +func (dr *DynamicRouter) templateHandler(ctx *zoox.Context, config RouteConfig) { + template := ` + + + + Dynamic Template + + +

    Dynamic Route: {{.Path}}

    +

    Method: {{.Method}}

    +

    Handler: {{.Handler}}

    + + + ` + + ctx.HTML(200, template, config) +} + +func main() { + app := zoox.New() + + // Create dynamic router + dynamicRouter := NewDynamicRouter(app) + + // Register some dynamic routes + routes := []struct { + id string + config RouteConfig + }{ + { + id: "welcome", + config: RouteConfig{ + Method: "GET", + Path: "/welcome/:name", + Handler: "echo", + Params: map[string]string{ + "message": "Welcome to our dynamic API!", + }, + }, + }, + { + id: "status", + config: RouteConfig{ + Method: "GET", + Path: "/status", + Handler: "static", + Params: map[string]string{ + "content": "Service is running", + }, + }, + }, + { + id: "home_redirect", + config: RouteConfig{ + Method: "GET", + Path: "/home", + Handler: "redirect", + Params: map[string]string{ + "url": "/welcome/guest", + }, + }, + }, + } + + for _, route := range routes { + if err := dynamicRouter.RegisterRoute(route.id, route.config); err != nil { + log.Printf("Error registering route %s: %v", route.id, err) + } + } + + // Admin endpoint to manage dynamic routes + app.Post("/admin/routes", func(ctx *zoox.Context) { + var config RouteConfig + if err := ctx.BindJSON(&config); err != nil { + ctx.JSON(400, map[string]string{"error": err.Error()}) + return + } + + id := ctx.Query("id") + if id == "" { + ctx.JSON(400, map[string]string{"error": "Route ID is required"}) + return + } + + if err := dynamicRouter.RegisterRoute(id, config); err != nil { + ctx.JSON(500, map[string]string{"error": err.Error()}) + return + } + + ctx.JSON(200, map[string]string{ + "message": "Route registered successfully", + "id": id, + }) + }) + + // List all dynamic routes + app.Get("/admin/routes", func(ctx *zoox.Context) { + ctx.JSON(200, dynamicRouter.routes) + }) + + app.Listen(":8080") +} +``` + +### Route Constraints + +Route constraints allow you to validate route parameters before the handler is executed. + +```go +package main + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/go-zoox/zoox" +) + +// Constraint represents a route parameter constraint +type Constraint interface { + Validate(value string) bool + Name() string +} + +// IntConstraint validates integer parameters +type IntConstraint struct { + Min, Max int +} + +func (c IntConstraint) Validate(value string) bool { + if val, err := strconv.Atoi(value); err == nil { + return val >= c.Min && val <= c.Max + } + return false +} + +func (c IntConstraint) Name() string { + return fmt.Sprintf("int(%d-%d)", c.Min, c.Max) +} + +// RegexConstraint validates parameters using regex +type RegexConstraint struct { + Pattern *regexp.Regexp + Name_ string +} + +func (c RegexConstraint) Validate(value string) bool { + return c.Pattern.MatchString(value) +} + +func (c RegexConstraint) Name() string { + return c.Name_ +} + +// EnumConstraint validates parameters against a set of allowed values +type EnumConstraint struct { + Values []string +} + +func (c EnumConstraint) Validate(value string) bool { + for _, v := range c.Values { + if v == value { + return true + } + } + return false +} + +func (c EnumConstraint) Name() string { + return fmt.Sprintf("enum(%s)", strings.Join(c.Values, "|")) +} + +// ConstraintValidator manages route constraints +type ConstraintValidator struct { + constraints map[string]Constraint +} + +func NewConstraintValidator() *ConstraintValidator { + return &ConstraintValidator{ + constraints: make(map[string]Constraint), + } +} + +func (cv *ConstraintValidator) AddConstraint(param string, constraint Constraint) { + cv.constraints[param] = constraint +} + +func (cv *ConstraintValidator) ValidateMiddleware() zoox.HandlerFunc { + return func(ctx *zoox.Context) { + params := ctx.Params() + + for param, constraint := range cv.constraints { + if value, exists := params[param]; exists { + if !constraint.Validate(value) { + ctx.JSON(400, map[string]interface{}{ + "error": fmt.Sprintf("Invalid parameter '%s': %s", param, value), + "constraint": constraint.Name(), + "value": value, + }) + return + } + } + } + + ctx.Next() + } +} + +func main() { + app := zoox.New() + + // Create constraint validator + validator := NewConstraintValidator() + + // Define constraints + validator.AddConstraint("id", IntConstraint{Min: 1, Max: 1000}) + validator.AddConstraint("category", EnumConstraint{Values: []string{"tech", "science", "art", "music"}}) + validator.AddConstraint("slug", RegexConstraint{ + Pattern: regexp.MustCompile(`^[a-z0-9-]+$`), + Name_: "slug", + }) + + // Routes with constraints + api := app.Group("/api/v1") + api.Use(validator.ValidateMiddleware()) + + // User routes with ID constraint + api.Get("/users/:id", func(ctx *zoox.Context) { + id := ctx.Param("id") + ctx.JSON(200, map[string]interface{}{ + "message": "User found", + "id": id, + }) + }) + + // Article routes with category and slug constraints + api.Get("/articles/:category/:slug", func(ctx *zoox.Context) { + category := ctx.Param("category") + slug := ctx.Param("slug") + + ctx.JSON(200, map[string]interface{}{ + "message": "Article found", + "category": category, + "slug": slug, + }) + }) + + // Product routes with multiple constraints + api.Get("/products/:category/:id", func(ctx *zoox.Context) { + category := ctx.Param("category") + id := ctx.Param("id") + + ctx.JSON(200, map[string]interface{}{ + "message": "Product found", + "category": category, + "id": id, + }) + }) + + // Constraint documentation endpoint + app.Get("/constraints", func(ctx *zoox.Context) { + constraints := make(map[string]string) + for param, constraint := range validator.constraints { + constraints[param] = constraint.Name() + } + + ctx.JSON(200, map[string]interface{}{ + "constraints": constraints, + "examples": map[string]interface{}{ + "valid_urls": []string{ + "/api/v1/users/123", + "/api/v1/articles/tech/my-article", + "/api/v1/products/tech/456", + }, + "invalid_urls": []string{ + "/api/v1/users/0", // ID too low + "/api/v1/users/1001", // ID too high + "/api/v1/articles/invalid-category/my-article", // Invalid category + "/api/v1/articles/tech/My Article", // Invalid slug + }, + }, + }) + }) + + app.Listen(":8080") +} +``` + +### Route Caching and Performance Optimization + +```go +package main + +import ( + "crypto/md5" + "fmt" + "sync" + "time" + + "github.com/go-zoox/zoox" +) + +// CacheEntry represents a cached route response +type CacheEntry struct { + Data interface{} + ExpiresAt time.Time + Headers map[string]string +} + +// RouteCache manages route response caching +type RouteCache struct { + cache map[string]CacheEntry + mutex sync.RWMutex + ttl time.Duration +} + +func NewRouteCache(ttl time.Duration) *RouteCache { + cache := &RouteCache{ + cache: make(map[string]CacheEntry), + ttl: ttl, + } + + // Start cleanup goroutine + go cache.cleanup() + + return cache +} + +func (rc *RouteCache) cleanup() { + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + for range ticker.C { + rc.mutex.Lock() + now := time.Now() + for key, entry := range rc.cache { + if now.After(entry.ExpiresAt) { + delete(rc.cache, key) + } + } + rc.mutex.Unlock() + } +} + +func (rc *RouteCache) generateKey(method, path string, params map[string]string) string { + key := fmt.Sprintf("%s:%s", method, path) + for k, v := range params { + key += fmt.Sprintf(":%s=%s", k, v) + } + return fmt.Sprintf("%x", md5.Sum([]byte(key))) +} + +func (rc *RouteCache) Get(method, path string, params map[string]string) (interface{}, bool) { + key := rc.generateKey(method, path, params) + + rc.mutex.RLock() + defer rc.mutex.RUnlock() + + entry, exists := rc.cache[key] + if !exists || time.Now().After(entry.ExpiresAt) { + return nil, false + } + + return entry.Data, true +} + +func (rc *RouteCache) Set(method, path string, params map[string]string, data interface{}, headers map[string]string) { + key := rc.generateKey(method, path, params) + + rc.mutex.Lock() + defer rc.mutex.Unlock() + + rc.cache[key] = CacheEntry{ + Data: data, + ExpiresAt: time.Now().Add(rc.ttl), + Headers: headers, + } +} + +func (rc *RouteCache) CacheMiddleware() zoox.HandlerFunc { + return func(ctx *zoox.Context) { + // Only cache GET requests + if ctx.Method() != "GET" { + ctx.Next() + return + } + + path := ctx.Request.URL.Path + params := ctx.Params() + + // Check cache + if data, found := rc.Get(ctx.Method(), path, params); found { + ctx.JSON(200, data) + return + } + + // Create a response recorder + originalWriter := ctx.Writer + recorder := &ResponseRecorder{ + original: originalWriter, + data: make(map[string]interface{}), + } + ctx.Writer = recorder + + // Process request + ctx.Next() + + // Cache the response if it was successful + if recorder.statusCode >= 200 && recorder.statusCode < 300 { + rc.Set(ctx.Method(), path, params, recorder.data, recorder.headers) + } + + // Restore original writer + ctx.Writer = originalWriter + } +} + +// ResponseRecorder captures response data for caching +type ResponseRecorder struct { + original zoox.ResponseWriter + data map[string]interface{} + statusCode int + headers map[string]string +} + +func (rr *ResponseRecorder) Header() map[string][]string { + return rr.original.Header() +} + +func (rr *ResponseRecorder) Write(data []byte) (int, error) { + return rr.original.Write(data) +} + +func (rr *ResponseRecorder) WriteHeader(statusCode int) { + rr.statusCode = statusCode + rr.original.WriteHeader(statusCode) +} + +// Route Pool for performance optimization +type RoutePool struct { + handlers sync.Map + stats map[string]*RouteStats + mutex sync.RWMutex +} + +type RouteStats struct { + Hits int64 + TotalDuration time.Duration + AvgDuration time.Duration + LastAccess time.Time +} + +func NewRoutePool() *RoutePool { + return &RoutePool{ + stats: make(map[string]*RouteStats), + } +} + +func (rp *RoutePool) TrackRoute(path string, handler zoox.HandlerFunc) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + start := time.Now() + + // Execute handler + handler(ctx) + + // Update statistics + duration := time.Since(start) + rp.updateStats(path, duration) + } +} + +func (rp *RoutePool) updateStats(path string, duration time.Duration) { + rp.mutex.Lock() + defer rp.mutex.Unlock() + + stats, exists := rp.stats[path] + if !exists { + stats = &RouteStats{} + rp.stats[path] = stats + } + + stats.Hits++ + stats.TotalDuration += duration + stats.AvgDuration = time.Duration(int64(stats.TotalDuration) / stats.Hits) + stats.LastAccess = time.Now() +} + +func (rp *RoutePool) GetStats() map[string]*RouteStats { + rp.mutex.RLock() + defer rp.mutex.RUnlock() + + // Create a copy to avoid race conditions + result := make(map[string]*RouteStats) + for path, stats := range rp.stats { + result[path] = &RouteStats{ + Hits: stats.Hits, + TotalDuration: stats.TotalDuration, + AvgDuration: stats.AvgDuration, + LastAccess: stats.LastAccess, + } + } + + return result +} + +func main() { + app := zoox.New() + + // Create cache and route pool + cache := NewRouteCache(5 * time.Minute) + pool := NewRoutePool() + + // Apply caching middleware + app.Use(cache.CacheMiddleware()) + + // Cached routes + app.Get("/products", pool.TrackRoute("/products", func(ctx *zoox.Context) { + // Simulate expensive operation + time.Sleep(100 * time.Millisecond) + + products := []map[string]interface{}{ + {"id": 1, "name": "Laptop", "price": 999.99}, + {"id": 2, "name": "Mouse", "price": 29.99}, + {"id": 3, "name": "Keyboard", "price": 79.99}, + } + + ctx.JSON(200, map[string]interface{}{ + "products": products, + "cached": false, + "timestamp": time.Now().Unix(), + }) + })) + + app.Get("/users/:id", pool.TrackRoute("/users/:id", func(ctx *zoox.Context) { + id := ctx.Param("id") + + // Simulate database lookup + time.Sleep(50 * time.Millisecond) + + ctx.JSON(200, map[string]interface{}{ + "user": map[string]interface{}{ + "id": id, + "name": fmt.Sprintf("User %s", id), + "email": fmt.Sprintf("user%s@example.com", id), + }, + "cached": false, + "timestamp": time.Now().Unix(), + }) + })) + + // Performance statistics endpoint + app.Get("/admin/stats", func(ctx *zoox.Context) { + stats := pool.GetStats() + + ctx.JSON(200, map[string]interface{}{ + "route_stats": stats, + "cache_info": map[string]interface{}{ + "ttl_minutes": int(cache.ttl.Minutes()), + "entries": len(cache.cache), + }, + }) + }) + + // Cache management endpoints + app.Delete("/admin/cache", func(ctx *zoox.Context) { + cache.mutex.Lock() + cache.cache = make(map[string]CacheEntry) + cache.mutex.Unlock() + + ctx.JSON(200, map[string]string{ + "message": "Cache cleared successfully", + }) + }) + + app.Listen(":8080") +} +``` + +## 🎯 Hands-on Exercise + +Create a dynamic content management system with the following features: + +### Requirements + +1. **Dynamic Route Registration**: Allow administrators to create custom routes +2. **Route Constraints**: Validate route parameters +3. **Performance Optimization**: Implement caching and route statistics +4. **Content Management**: CRUD operations for dynamic content + +### Solution + +```go +package main + +import ( + "encoding/json" + "fmt" + "log" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "github.com/go-zoox/zoox" +) + +// Content represents dynamic content +type Content struct { + ID int `json:"id"` + Title string `json:"title"` + Body string `json:"body"` + Type string `json:"type"` + Status string `json:"status"` + Metadata map[string]interface{} `json:"metadata"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// DynamicRoute represents a dynamic route configuration +type DynamicRoute struct { + ID string `json:"id"` + Method string `json:"method"` + Path string `json:"path"` + ContentType string `json:"content_type"` + Template string `json:"template"` + Constraints map[string]string `json:"constraints"` + CacheTTL int `json:"cache_ttl"` + CreatedAt time.Time `json:"created_at"` +} + +// CMS manages the dynamic content management system +type CMS struct { + app *zoox.Application + contents map[int]*Content + routes map[string]*DynamicRoute + cache *RouteCache + pool *RoutePool + validator *ConstraintValidator + mutex sync.RWMutex + nextContentID int +} + +func NewCMS(app *zoox.Application) *CMS { + return &CMS{ + app: app, + contents: make(map[int]*Content), + routes: make(map[string]*DynamicRoute), + cache: NewRouteCache(5 * time.Minute), + pool: NewRoutePool(), + validator: NewConstraintValidator(), + nextContentID: 1, + } +} + +func (cms *CMS) Setup() { + // Apply middleware + cms.app.Use(cms.cache.CacheMiddleware()) + cms.app.Use(cms.validator.ValidateMiddleware()) + + // Setup default constraints + cms.validator.AddConstraint("id", IntConstraint{Min: 1, Max: 999999}) + cms.validator.AddConstraint("slug", RegexConstraint{ + Pattern: regexp.MustCompile(`^[a-z0-9-]+$`), + Name_: "slug", + }) + + // Setup API routes + cms.setupContentAPI() + cms.setupRouteAPI() + cms.setupAdminAPI() +} + +func (cms *CMS) setupContentAPI() { + api := cms.app.Group("/api/content") + + // Create content + api.Post("/", func(ctx *zoox.Context) { + var content Content + if err := ctx.BindJSON(&content); err != nil { + ctx.JSON(400, map[string]string{"error": err.Error()}) + return + } + + cms.mutex.Lock() + content.ID = cms.nextContentID + cms.nextContentID++ + content.CreatedAt = time.Now() + content.UpdatedAt = time.Now() + cms.contents[content.ID] = &content + cms.mutex.Unlock() + + ctx.JSON(201, content) + }) + + // Get content + api.Get("/:id", func(ctx *zoox.Context) { + id, _ := strconv.Atoi(ctx.Param("id")) + + cms.mutex.RLock() + content, exists := cms.contents[id] + cms.mutex.RUnlock() + + if !exists { + ctx.JSON(404, map[string]string{"error": "Content not found"}) + return + } + + ctx.JSON(200, content) + }) + + // Update content + api.Put("/:id", func(ctx *zoox.Context) { + id, _ := strconv.Atoi(ctx.Param("id")) + + cms.mutex.Lock() + content, exists := cms.contents[id] + if !exists { + cms.mutex.Unlock() + ctx.JSON(404, map[string]string{"error": "Content not found"}) + return + } + + var updates Content + if err := ctx.BindJSON(&updates); err != nil { + cms.mutex.Unlock() + ctx.JSON(400, map[string]string{"error": err.Error()}) + return + } + + content.Title = updates.Title + content.Body = updates.Body + content.Type = updates.Type + content.Status = updates.Status + content.Metadata = updates.Metadata + content.UpdatedAt = time.Now() + cms.mutex.Unlock() + + ctx.JSON(200, content) + }) + + // Delete content + api.Delete("/:id", func(ctx *zoox.Context) { + id, _ := strconv.Atoi(ctx.Param("id")) + + cms.mutex.Lock() + delete(cms.contents, id) + cms.mutex.Unlock() + + ctx.JSON(200, map[string]string{"message": "Content deleted"}) + }) + + // List content + api.Get("/", func(ctx *zoox.Context) { + cms.mutex.RLock() + contents := make([]*Content, 0, len(cms.contents)) + for _, content := range cms.contents { + contents = append(contents, content) + } + cms.mutex.RUnlock() + + ctx.JSON(200, map[string]interface{}{ + "contents": contents, + "total": len(contents), + }) + }) +} + +func (cms *CMS) setupRouteAPI() { + api := cms.app.Group("/api/routes") + + // Create dynamic route + api.Post("/", func(ctx *zoox.Context) { + var route DynamicRoute + if err := ctx.BindJSON(&route); err != nil { + ctx.JSON(400, map[string]string{"error": err.Error()}) + return + } + + route.CreatedAt = time.Now() + + if err := cms.registerDynamicRoute(&route); err != nil { + ctx.JSON(500, map[string]string{"error": err.Error()}) + return + } + + cms.mutex.Lock() + cms.routes[route.ID] = &route + cms.mutex.Unlock() + + ctx.JSON(201, route) + }) + + // List dynamic routes + api.Get("/", func(ctx *zoox.Context) { + cms.mutex.RLock() + routes := make([]*DynamicRoute, 0, len(cms.routes)) + for _, route := range cms.routes { + routes = append(routes, route) + } + cms.mutex.RUnlock() + + ctx.JSON(200, map[string]interface{}{ + "routes": routes, + "total": len(routes), + }) + }) + + // Delete dynamic route + api.Delete("/:id", func(ctx *zoox.Context) { + id := ctx.Param("id") + + cms.mutex.Lock() + delete(cms.routes, id) + cms.mutex.Unlock() + + ctx.JSON(200, map[string]string{"message": "Route deleted"}) + }) +} + +func (cms *CMS) setupAdminAPI() { + admin := cms.app.Group("/admin") + + // Performance statistics + admin.Get("/stats", func(ctx *zoox.Context) { + stats := cms.pool.GetStats() + + ctx.JSON(200, map[string]interface{}{ + "route_stats": stats, + "cache_info": map[string]interface{}{ + "ttl_minutes": int(cms.cache.ttl.Minutes()), + "entries": len(cms.cache.cache), + }, + "content_count": len(cms.contents), + "route_count": len(cms.routes), + }) + }) + + // Clear cache + admin.Delete("/cache", func(ctx *zoox.Context) { + cms.cache.mutex.Lock() + cms.cache.cache = make(map[string]CacheEntry) + cms.cache.mutex.Unlock() + + ctx.JSON(200, map[string]string{"message": "Cache cleared"}) + }) + + // Dashboard + admin.Get("/dashboard", func(ctx *zoox.Context) { + html := ` + + + + CMS Dashboard + + + +

    CMS Dashboard

    + +
    +

    Statistics

    +
    +
    +

    Contents

    +

    Loading...

    +
    +
    +

    Routes

    +

    Loading...

    +
    +
    +

    Cache Entries

    +

    Loading...

    +
    +
    +
    + +
    +

    Actions

    + + +
    + + + + + ` + + ctx.HTML(200, html, nil) + }) +} + +func (cms *CMS) registerDynamicRoute(route *DynamicRoute) error { + handler := cms.createDynamicHandler(route) + + switch strings.ToUpper(route.Method) { + case "GET": + cms.app.Get(route.Path, cms.pool.TrackRoute(route.Path, handler)) + case "POST": + cms.app.Post(route.Path, cms.pool.TrackRoute(route.Path, handler)) + case "PUT": + cms.app.Put(route.Path, cms.pool.TrackRoute(route.Path, handler)) + case "DELETE": + cms.app.Delete(route.Path, cms.pool.TrackRoute(route.Path, handler)) + default: + return fmt.Errorf("unsupported method: %s", route.Method) + } + + return nil +} + +func (cms *CMS) createDynamicHandler(route *DynamicRoute) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + switch route.ContentType { + case "json": + cms.handleJSONContent(ctx, route) + case "html": + cms.handleHTMLContent(ctx, route) + case "text": + cms.handleTextContent(ctx, route) + default: + ctx.JSON(500, map[string]string{"error": "Unknown content type"}) + } + } +} + +func (cms *CMS) handleJSONContent(ctx *zoox.Context, route *DynamicRoute) { + // Find content by type or ID + var content *Content + if id := ctx.Param("id"); id != "" { + if contentID, err := strconv.Atoi(id); err == nil { + cms.mutex.RLock() + content = cms.contents[contentID] + cms.mutex.RUnlock() + } + } + + if content == nil { + ctx.JSON(404, map[string]string{"error": "Content not found"}) + return + } + + ctx.JSON(200, content) +} + +func (cms *CMS) handleHTMLContent(ctx *zoox.Context, route *DynamicRoute) { + template := route.Template + if template == "" { + template = ` + + + + {{.Title}} + + +

    {{.Title}}

    +
    {{.Body}}
    + + + ` + } + + var content *Content + if id := ctx.Param("id"); id != "" { + if contentID, err := strconv.Atoi(id); err == nil { + cms.mutex.RLock() + content = cms.contents[contentID] + cms.mutex.RUnlock() + } + } + + if content == nil { + ctx.HTML(404, "

    Content not found

    ", nil) + return + } + + ctx.HTML(200, template, content) +} + +func (cms *CMS) handleTextContent(ctx *zoox.Context, route *DynamicRoute) { + var content *Content + if id := ctx.Param("id"); id != "" { + if contentID, err := strconv.Atoi(id); err == nil { + cms.mutex.RLock() + content = cms.contents[contentID] + cms.mutex.RUnlock() + } + } + + if content == nil { + ctx.String(404, "Content not found") + return + } + + ctx.String(200, content.Body) +} + +func main() { + app := zoox.New() + + // Create CMS + cms := NewCMS(app) + cms.Setup() + + // Create sample content + sampleContent := []*Content{ + { + ID: 1, + Title: "Welcome to Our Site", + Body: "This is the welcome page content.", + Type: "page", + Status: "published", + Metadata: map[string]interface{}{"featured": true}, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + { + ID: 2, + Title: "About Us", + Body: "Learn more about our company and mission.", + Type: "page", + Status: "published", + Metadata: map[string]interface{}{"menu_order": 1}, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + } + + cms.mutex.Lock() + for _, content := range sampleContent { + cms.contents[content.ID] = content + } + cms.nextContentID = 3 + cms.mutex.Unlock() + + // Create sample routes + sampleRoutes := []*DynamicRoute{ + { + ID: "welcome", + Method: "GET", + Path: "/welcome", + ContentType: "html", + Template: "", + Constraints: map[string]string{}, + CacheTTL: 300, + CreatedAt: time.Now(), + }, + { + ID: "about", + Method: "GET", + Path: "/about", + ContentType: "html", + Template: "", + Constraints: map[string]string{}, + CacheTTL: 300, + CreatedAt: time.Now(), + }, + } + + for _, route := range sampleRoutes { + if err := cms.registerDynamicRoute(route); err != nil { + log.Printf("Error registering route %s: %v", route.ID, err) + } else { + cms.mutex.Lock() + cms.routes[route.ID] = route + cms.mutex.Unlock() + } + } + + log.Println("CMS Server starting on :8080") + log.Println("Dashboard: http://localhost:8080/admin/dashboard") + log.Println("API Documentation: http://localhost:8080/api/content") + + app.Listen(":8080") +} +``` + +## 🔍 Testing Your Implementation + +Test your CMS with these commands: + +```bash +# Create content +curl -X POST http://localhost:8080/api/content \ + -H "Content-Type: application/json" \ + -d '{"title": "Test Page", "body": "This is a test page", "type": "page", "status": "published"}' + +# Create dynamic route +curl -X POST http://localhost:8080/api/routes \ + -H "Content-Type: application/json" \ + -d '{"id": "test", "method": "GET", "path": "/test/:id", "content_type": "json", "cache_ttl": 300}' + +# Test the dynamic route +curl http://localhost:8080/test/1 + +# View dashboard +open http://localhost:8080/admin/dashboard +``` + +## 📚 Key Takeaways + +1. **Dynamic Registration**: Routes can be registered at runtime based on configuration +2. **Route Constraints**: Parameter validation improves API reliability +3. **Performance Optimization**: Caching and route pooling enhance performance +4. **Flexible Architecture**: Dynamic systems require careful design and validation +5. **Monitoring**: Track route performance and cache effectiveness + +## 🎯 Next Steps + +- Explore [Tutorial 06: Template Engine](./06-template-engine.md) for advanced templating +- Learn about [Tutorial 08: WebSocket Development](./08-websocket-development.md) for real-time features +- Study [Tutorial 10: Authentication & Authorization](./10-authentication-authorization.md) for security + +## 🤝 Need Help? + +If you encounter any issues: +1. Check the [examples directory](../examples/) for working code +2. Review the [API documentation](../DOCUMENTATION.md) +3. Join our community discussions +4. Report bugs in the issue tracker + +--- + +**Congratulations!** You've mastered advanced routing techniques in Zoox. You can now build flexible, high-performance applications with dynamic routing capabilities. \ No newline at end of file diff --git a/tutorials/05-working-with-json.md b/tutorials/05-working-with-json.md new file mode 100644 index 0000000..afac90d --- /dev/null +++ b/tutorials/05-working-with-json.md @@ -0,0 +1,1161 @@ +# Tutorial 05: Working with JSON + +## Overview +JSON (JavaScript Object Notation) is the most common data format for web APIs. In this tutorial, you'll learn how to effectively work with JSON in Zoox applications, including parsing, validation, serialization, and handling complex data structures. + +## Learning Objectives +- Parse JSON from requests +- Validate JSON data +- Handle nested JSON structures +- Custom JSON serialization +- Error handling for JSON operations +- Performance optimization for JSON processing + +## Prerequisites +- Complete Tutorial 04: Middleware Basics +- Basic understanding of Go structs and interfaces +- Familiarity with JSON format + +## JSON Parsing Fundamentals + +### Basic JSON Parsing + +```go +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/go-zoox/zoox" +) + +// User represents a user in our system +type User struct { + ID int `json:"id"` + Name string `json:"name" validate:"required,min=2,max=50"` + Email string `json:"email" validate:"required,email"` + Age int `json:"age" validate:"min=0,max=150"` + Active bool `json:"active"` + Created time.Time `json:"created"` + Profile Profile `json:"profile"` + Tags []string `json:"tags"` +} + +// Profile represents user profile information +type Profile struct { + Bio string `json:"bio"` + Website string `json:"website"` + Avatar string `json:"avatar"` +} + +// CreateUserRequest represents the request for creating a user +type CreateUserRequest struct { + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"required,email"` + Age int `json:"age" validate:"min=0"` + Profile Profile `json:"profile"` + Tags []string `json:"tags"` +} + +// Response represents a standard API response +type Response struct { + Success bool `json:"success"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` + Error string `json:"error,omitempty"` +} + +func main() { + app := zoox.New() + + // Middleware for JSON content type + app.Use(func(ctx *zoox.Context) { + ctx.Set("Content-Type", "application/json") + ctx.Next() + }) + + // Basic JSON parsing + app.Post("/users", createUser) + + // Complex JSON parsing + app.Post("/users/batch", createBatchUsers) + + // JSON validation + app.Put("/users/:id", updateUser) + + // Custom JSON serialization + app.Get("/users/:id", getUser) + + // Handle JSON arrays + app.Post("/users/search", searchUsers) + + fmt.Println("Server starting on :8080") + log.Fatal(app.Listen(":8080")) +} + +func createUser(ctx *zoox.Context) { + var req CreateUserRequest + + // Parse JSON from request body + if err := ctx.BindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, Response{ + Success: false, + Message: "Invalid JSON format", + Error: err.Error(), + }) + return + } + + // Validate required fields + if req.Name == "" { + ctx.JSON(http.StatusBadRequest, Response{ + Success: false, + Message: "Name is required", + }) + return + } + + if req.Email == "" { + ctx.JSON(http.StatusBadRequest, Response{ + Success: false, + Message: "Email is required", + }) + return + } + + // Create user + user := User{ + ID: generateID(), + Name: req.Name, + Email: req.Email, + Age: req.Age, + Active: true, + Created: time.Now(), + Profile: req.Profile, + Tags: req.Tags, + } + + ctx.JSON(http.StatusCreated, Response{ + Success: true, + Message: "User created successfully", + Data: user, + }) +} + +func createBatchUsers(ctx *zoox.Context) { + var requests []CreateUserRequest + + // Parse JSON array + if err := ctx.BindJSON(&requests); err != nil { + ctx.JSON(http.StatusBadRequest, Response{ + Success: false, + Message: "Invalid JSON array format", + Error: err.Error(), + }) + return + } + + if len(requests) == 0 { + ctx.JSON(http.StatusBadRequest, Response{ + Success: false, + Message: "At least one user is required", + }) + return + } + + if len(requests) > 100 { + ctx.JSON(http.StatusBadRequest, Response{ + Success: false, + Message: "Maximum 100 users allowed per batch", + }) + return + } + + var users []User + var errors []string + + for i, req := range requests { + // Validate each user + if req.Name == "" { + errors = append(errors, fmt.Sprintf("User %d: Name is required", i+1)) + continue + } + + if req.Email == "" { + errors = append(errors, fmt.Sprintf("User %d: Email is required", i+1)) + continue + } + + user := User{ + ID: generateID(), + Name: req.Name, + Email: req.Email, + Age: req.Age, + Active: true, + Created: time.Now(), + Profile: req.Profile, + Tags: req.Tags, + } + + users = append(users, user) + } + + if len(errors) > 0 { + ctx.JSON(http.StatusBadRequest, Response{ + Success: false, + Message: "Validation errors occurred", + Error: fmt.Sprintf("Errors: %v", errors), + }) + return + } + + ctx.JSON(http.StatusCreated, Response{ + Success: true, + Message: fmt.Sprintf("Created %d users successfully", len(users)), + Data: users, + }) +} + +func updateUser(ctx *zoox.Context) { + id := ctx.Param("id") + + var updates map[string]interface{} + + // Parse partial JSON updates + if err := ctx.BindJSON(&updates); err != nil { + ctx.JSON(http.StatusBadRequest, Response{ + Success: false, + Message: "Invalid JSON format", + Error: err.Error(), + }) + return + } + + // Simulate finding user + user := User{ + ID: 1, + Name: "John Doe", + Email: "john@example.com", + Age: 30, + Active: true, + Created: time.Now().Add(-24 * time.Hour), + } + + // Apply updates + if name, ok := updates["name"].(string); ok { + user.Name = name + } + + if email, ok := updates["email"].(string); ok { + user.Email = email + } + + if age, ok := updates["age"].(float64); ok { + user.Age = int(age) + } + + if active, ok := updates["active"].(bool); ok { + user.Active = active + } + + ctx.JSON(http.StatusOK, Response{ + Success: true, + Message: "User updated successfully", + Data: user, + }) +} + +func getUser(ctx *zoox.Context) { + id := ctx.Param("id") + + // Simulate finding user + user := User{ + ID: 1, + Name: "John Doe", + Email: "john@example.com", + Age: 30, + Active: true, + Created: time.Now().Add(-24 * time.Hour), + Profile: Profile{ + Bio: "Software developer", + Website: "https://johndoe.com", + Avatar: "https://example.com/avatar.jpg", + }, + Tags: []string{"developer", "go", "web"}, + } + + // Custom serialization based on query parameters + includeProfile := ctx.Query("include_profile") == "true" + includeTags := ctx.Query("include_tags") == "true" + + if !includeProfile { + user.Profile = Profile{} + } + + if !includeTags { + user.Tags = nil + } + + ctx.JSON(http.StatusOK, Response{ + Success: true, + Message: "User retrieved successfully", + Data: user, + }) +} + +// SearchCriteria represents search parameters +type SearchCriteria struct { + Name string `json:"name"` + Email string `json:"email"` + MinAge int `json:"min_age"` + MaxAge int `json:"max_age"` + Active *bool `json:"active"` + Tags []string `json:"tags"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} + +func searchUsers(ctx *zoox.Context) { + var criteria SearchCriteria + + // Parse search criteria + if err := ctx.BindJSON(&criteria); err != nil { + ctx.JSON(http.StatusBadRequest, Response{ + Success: false, + Message: "Invalid search criteria", + Error: err.Error(), + }) + return + } + + // Set defaults + if criteria.Page <= 0 { + criteria.Page = 1 + } + + if criteria.PageSize <= 0 { + criteria.PageSize = 10 + } + + if criteria.PageSize > 100 { + criteria.PageSize = 100 + } + + // Simulate search results + users := []User{ + { + ID: 1, + Name: "John Doe", + Email: "john@example.com", + Age: 30, + Active: true, + Created: time.Now().Add(-24 * time.Hour), + Tags: []string{"developer", "go"}, + }, + { + ID: 2, + Name: "Jane Smith", + Email: "jane@example.com", + Age: 25, + Active: true, + Created: time.Now().Add(-48 * time.Hour), + Tags: []string{"designer", "ui"}, + }, + } + + ctx.JSON(http.StatusOK, Response{ + Success: true, + Message: "Search completed successfully", + Data: map[string]interface{}{ + "users": users, + "total": len(users), + "page": criteria.Page, + "page_size": criteria.PageSize, + "criteria": criteria, + }, + }) +} + +func generateID() int { + return int(time.Now().UnixNano() % 1000000) +} +``` + +## Advanced JSON Techniques + +### Custom JSON Marshaling + +```go +// CustomTime handles time formatting +type CustomTime struct { + time.Time +} + +func (ct CustomTime) MarshalJSON() ([]byte, error) { + return json.Marshal(ct.Time.Format("2006-01-02 15:04:05")) +} + +func (ct *CustomTime) UnmarshalJSON(data []byte) error { + var timeStr string + if err := json.Unmarshal(data, &timeStr); err != nil { + return err + } + + t, err := time.Parse("2006-01-02 15:04:05", timeStr) + if err != nil { + return err + } + + ct.Time = t + return nil +} + +// UserWithCustomTime demonstrates custom marshaling +type UserWithCustomTime struct { + ID int `json:"id"` + Name string `json:"name"` + Created CustomTime `json:"created"` +} +``` + +### JSON Validation + +```go +import ( + "github.com/go-playground/validator/v10" +) + +var validate = validator.New() + +func validateJSON(data interface{}) error { + return validate.Struct(data) +} + +// In your handler +func createUserWithValidation(ctx *zoox.Context) { + var req CreateUserRequest + + if err := ctx.BindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, Response{ + Success: false, + Message: "Invalid JSON format", + Error: err.Error(), + }) + return + } + + // Validate using struct tags + if err := validateJSON(req); err != nil { + ctx.JSON(http.StatusBadRequest, Response{ + Success: false, + Message: "Validation failed", + Error: err.Error(), + }) + return + } + + // Process valid data... +} +``` + +## Hands-on Exercise: Product Catalog API + +Create a product catalog API that demonstrates advanced JSON handling: + +### Requirements: +1. Product CRUD operations with complex nested data +2. JSON validation for all inputs +3. Custom JSON serialization based on user roles +4. Bulk operations for multiple products +5. Search functionality with complex criteria +6. Error handling for all JSON operations + +### Solution: + +```go +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/go-zoox/zoox" +) + +// Product represents a product in the catalog +type Product struct { + ID int `json:"id"` + Name string `json:"name" validate:"required,min=2,max=100"` + Description string `json:"description" validate:"max=500"` + Price float64 `json:"price" validate:"required,min=0"` + Currency string `json:"currency" validate:"required,oneof=USD EUR GBP"` + Category Category `json:"category" validate:"required"` + Tags []string `json:"tags"` + Variants []ProductVariant `json:"variants"` + Images []ProductImage `json:"images"` + Metadata map[string]interface{} `json:"metadata"` + Stock Stock `json:"stock"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Active bool `json:"active"` +} + +// Category represents a product category +type Category struct { + ID int `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Path string `json:"path"` +} + +// ProductVariant represents a product variant +type ProductVariant struct { + ID int `json:"id"` + Name string `json:"name" validate:"required"` + Price float64 `json:"price" validate:"min=0"` + SKU string `json:"sku" validate:"required"` + Attributes map[string]interface{} `json:"attributes"` + Stock int `json:"stock" validate:"min=0"` +} + +// ProductImage represents a product image +type ProductImage struct { + ID int `json:"id"` + URL string `json:"url" validate:"required,url"` + Alt string `json:"alt"` + Primary bool `json:"primary"` + Position int `json:"position"` +} + +// Stock represents product stock information +type Stock struct { + Quantity int `json:"quantity" validate:"min=0"` + Reserved int `json:"reserved" validate:"min=0"` + Available int `json:"available"` + Status string `json:"status" validate:"oneof=in_stock out_of_stock low_stock"` +} + +// CreateProductRequest represents the request for creating a product +type CreateProductRequest struct { + Name string `json:"name" validate:"required,min=2,max=100"` + Description string `json:"description" validate:"max=500"` + Price float64 `json:"price" validate:"required,min=0"` + Currency string `json:"currency" validate:"required,oneof=USD EUR GBP"` + CategoryID int `json:"category_id" validate:"required"` + Tags []string `json:"tags"` + Variants []ProductVariant `json:"variants"` + Images []ProductImage `json:"images"` + Metadata map[string]interface{} `json:"metadata"` + Stock Stock `json:"stock"` +} + +// SearchProductsRequest represents search criteria +type SearchProductsRequest struct { + Query string `json:"query"` + CategoryID int `json:"category_id"` + Tags []string `json:"tags"` + MinPrice float64 `json:"min_price"` + MaxPrice float64 `json:"max_price"` + Currency string `json:"currency"` + InStock *bool `json:"in_stock"` + Active *bool `json:"active"` + Page int `json:"page"` + PageSize int `json:"page_size"` + SortBy string `json:"sort_by" validate:"oneof=name price created updated"` + SortOrder string `json:"sort_order" validate:"oneof=asc desc"` +} + +// ProductResponse represents the response format +type ProductResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` + Error string `json:"error,omitempty"` + Meta interface{} `json:"meta,omitempty"` +} + +var products = make(map[int]*Product) +var nextID = 1 + +func main() { + app := zoox.New() + + // Middleware + app.Use(func(ctx *zoox.Context) { + ctx.Set("Content-Type", "application/json") + ctx.Next() + }) + + // Product routes + app.Post("/products", createProduct) + app.Get("/products/:id", getProduct) + app.Put("/products/:id", updateProduct) + app.Delete("/products/:id", deleteProduct) + app.Get("/products", listProducts) + app.Post("/products/search", searchProducts) + app.Post("/products/batch", createBatchProducts) + app.Put("/products/batch", updateBatchProducts) + + // Seed some sample data + seedSampleData() + + fmt.Println("Product Catalog API starting on :8080") + log.Fatal(app.Listen(":8080")) +} + +func createProduct(ctx *zoox.Context) { + var req CreateProductRequest + + if err := ctx.BindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, ProductResponse{ + Success: false, + Message: "Invalid JSON format", + Error: err.Error(), + }) + return + } + + // Validate request + if err := validateStruct(req); err != nil { + ctx.JSON(http.StatusBadRequest, ProductResponse{ + Success: false, + Message: "Validation failed", + Error: err.Error(), + }) + return + } + + // Create product + product := &Product{ + ID: nextID, + Name: req.Name, + Description: req.Description, + Price: req.Price, + Currency: req.Currency, + Category: Category{ID: req.CategoryID, Name: "Sample Category"}, + Tags: req.Tags, + Variants: req.Variants, + Images: req.Images, + Metadata: req.Metadata, + Stock: req.Stock, + Created: time.Now(), + Updated: time.Now(), + Active: true, + } + + // Calculate available stock + product.Stock.Available = product.Stock.Quantity - product.Stock.Reserved + + // Set stock status + if product.Stock.Available <= 0 { + product.Stock.Status = "out_of_stock" + } else if product.Stock.Available < 10 { + product.Stock.Status = "low_stock" + } else { + product.Stock.Status = "in_stock" + } + + products[nextID] = product + nextID++ + + ctx.JSON(http.StatusCreated, ProductResponse{ + Success: true, + Message: "Product created successfully", + Data: product, + }) +} + +func getProduct(ctx *zoox.Context) { + id, err := strconv.Atoi(ctx.Param("id")) + if err != nil { + ctx.JSON(http.StatusBadRequest, ProductResponse{ + Success: false, + Message: "Invalid product ID", + }) + return + } + + product, exists := products[id] + if !exists { + ctx.JSON(http.StatusNotFound, ProductResponse{ + Success: false, + Message: "Product not found", + }) + return + } + + // Custom serialization based on query parameters + includeVariants := ctx.Query("include_variants") != "false" + includeImages := ctx.Query("include_images") != "false" + includeMetadata := ctx.Query("include_metadata") != "false" + + productCopy := *product + + if !includeVariants { + productCopy.Variants = nil + } + + if !includeImages { + productCopy.Images = nil + } + + if !includeMetadata { + productCopy.Metadata = nil + } + + ctx.JSON(http.StatusOK, ProductResponse{ + Success: true, + Message: "Product retrieved successfully", + Data: productCopy, + }) +} + +func updateProduct(ctx *zoox.Context) { + id, err := strconv.Atoi(ctx.Param("id")) + if err != nil { + ctx.JSON(http.StatusBadRequest, ProductResponse{ + Success: false, + Message: "Invalid product ID", + }) + return + } + + product, exists := products[id] + if !exists { + ctx.JSON(http.StatusNotFound, ProductResponse{ + Success: false, + Message: "Product not found", + }) + return + } + + var updates map[string]interface{} + if err := ctx.BindJSON(&updates); err != nil { + ctx.JSON(http.StatusBadRequest, ProductResponse{ + Success: false, + Message: "Invalid JSON format", + Error: err.Error(), + }) + return + } + + // Apply updates + if name, ok := updates["name"].(string); ok { + product.Name = name + } + + if description, ok := updates["description"].(string); ok { + product.Description = description + } + + if price, ok := updates["price"].(float64); ok { + product.Price = price + } + + if currency, ok := updates["currency"].(string); ok { + product.Currency = currency + } + + if active, ok := updates["active"].(bool); ok { + product.Active = active + } + + // Handle nested updates + if stockData, ok := updates["stock"].(map[string]interface{}); ok { + if quantity, ok := stockData["quantity"].(float64); ok { + product.Stock.Quantity = int(quantity) + } + if reserved, ok := stockData["reserved"].(float64); ok { + product.Stock.Reserved = int(reserved) + } + // Recalculate available stock + product.Stock.Available = product.Stock.Quantity - product.Stock.Reserved + } + + product.Updated = time.Now() + + ctx.JSON(http.StatusOK, ProductResponse{ + Success: true, + Message: "Product updated successfully", + Data: product, + }) +} + +func deleteProduct(ctx *zoox.Context) { + id, err := strconv.Atoi(ctx.Param("id")) + if err != nil { + ctx.JSON(http.StatusBadRequest, ProductResponse{ + Success: false, + Message: "Invalid product ID", + }) + return + } + + _, exists := products[id] + if !exists { + ctx.JSON(http.StatusNotFound, ProductResponse{ + Success: false, + Message: "Product not found", + }) + return + } + + delete(products, id) + + ctx.JSON(http.StatusOK, ProductResponse{ + Success: true, + Message: "Product deleted successfully", + }) +} + +func listProducts(ctx *zoox.Context) { + page, _ := strconv.Atoi(ctx.Query("page")) + if page <= 0 { + page = 1 + } + + pageSize, _ := strconv.Atoi(ctx.Query("page_size")) + if pageSize <= 0 { + pageSize = 10 + } + + var productList []*Product + for _, product := range products { + productList = append(productList, product) + } + + start := (page - 1) * pageSize + end := start + pageSize + + if start >= len(productList) { + productList = []*Product{} + } else if end > len(productList) { + productList = productList[start:] + } else { + productList = productList[start:end] + } + + ctx.JSON(http.StatusOK, ProductResponse{ + Success: true, + Message: "Products retrieved successfully", + Data: productList, + Meta: map[string]interface{}{ + "page": page, + "page_size": pageSize, + "total": len(products), + "has_more": end < len(products), + }, + }) +} + +func searchProducts(ctx *zoox.Context) { + var req SearchProductsRequest + + if err := ctx.BindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, ProductResponse{ + Success: false, + Message: "Invalid search criteria", + Error: err.Error(), + }) + return + } + + // Set defaults + if req.Page <= 0 { + req.Page = 1 + } + if req.PageSize <= 0 { + req.PageSize = 10 + } + if req.PageSize > 100 { + req.PageSize = 100 + } + + var results []*Product + + // Simple search implementation + for _, product := range products { + if matchesSearchCriteria(product, req) { + results = append(results, product) + } + } + + // Pagination + start := (req.Page - 1) * req.PageSize + end := start + req.PageSize + + if start >= len(results) { + results = []*Product{} + } else if end > len(results) { + results = results[start:] + } else { + results = results[start:end] + } + + ctx.JSON(http.StatusOK, ProductResponse{ + Success: true, + Message: "Search completed successfully", + Data: results, + Meta: map[string]interface{}{ + "page": req.Page, + "page_size": req.PageSize, + "total": len(results), + "criteria": req, + }, + }) +} + +func createBatchProducts(ctx *zoox.Context) { + var requests []CreateProductRequest + + if err := ctx.BindJSON(&requests); err != nil { + ctx.JSON(http.StatusBadRequest, ProductResponse{ + Success: false, + Message: "Invalid JSON array format", + Error: err.Error(), + }) + return + } + + if len(requests) == 0 { + ctx.JSON(http.StatusBadRequest, ProductResponse{ + Success: false, + Message: "At least one product is required", + }) + return + } + + if len(requests) > 50 { + ctx.JSON(http.StatusBadRequest, ProductResponse{ + Success: false, + Message: "Maximum 50 products allowed per batch", + }) + return + } + + var createdProducts []*Product + var errors []string + + for i, req := range requests { + if err := validateStruct(req); err != nil { + errors = append(errors, fmt.Sprintf("Product %d: %s", i+1, err.Error())) + continue + } + + product := &Product{ + ID: nextID, + Name: req.Name, + Description: req.Description, + Price: req.Price, + Currency: req.Currency, + Category: Category{ID: req.CategoryID, Name: "Sample Category"}, + Tags: req.Tags, + Variants: req.Variants, + Images: req.Images, + Metadata: req.Metadata, + Stock: req.Stock, + Created: time.Now(), + Updated: time.Now(), + Active: true, + } + + product.Stock.Available = product.Stock.Quantity - product.Stock.Reserved + + products[nextID] = product + createdProducts = append(createdProducts, product) + nextID++ + } + + if len(errors) > 0 { + ctx.JSON(http.StatusBadRequest, ProductResponse{ + Success: false, + Message: "Some products failed validation", + Error: strings.Join(errors, "; "), + Data: createdProducts, + }) + return + } + + ctx.JSON(http.StatusCreated, ProductResponse{ + Success: true, + Message: fmt.Sprintf("Created %d products successfully", len(createdProducts)), + Data: createdProducts, + }) +} + +func updateBatchProducts(ctx *zoox.Context) { + var updates []map[string]interface{} + + if err := ctx.BindJSON(&updates); err != nil { + ctx.JSON(http.StatusBadRequest, ProductResponse{ + Success: false, + Message: "Invalid JSON format", + Error: err.Error(), + }) + return + } + + var updatedProducts []*Product + var errors []string + + for i, update := range updates { + idFloat, ok := update["id"].(float64) + if !ok { + errors = append(errors, fmt.Sprintf("Update %d: Missing or invalid ID", i+1)) + continue + } + + id := int(idFloat) + product, exists := products[id] + if !exists { + errors = append(errors, fmt.Sprintf("Update %d: Product %d not found", i+1, id)) + continue + } + + // Apply updates (simplified) + if name, ok := update["name"].(string); ok { + product.Name = name + } + if price, ok := update["price"].(float64); ok { + product.Price = price + } + + product.Updated = time.Now() + updatedProducts = append(updatedProducts, product) + } + + if len(errors) > 0 { + ctx.JSON(http.StatusBadRequest, ProductResponse{ + Success: false, + Message: "Some updates failed", + Error: strings.Join(errors, "; "), + Data: updatedProducts, + }) + return + } + + ctx.JSON(http.StatusOK, ProductResponse{ + Success: true, + Message: fmt.Sprintf("Updated %d products successfully", len(updatedProducts)), + Data: updatedProducts, + }) +} + +func matchesSearchCriteria(product *Product, req SearchProductsRequest) bool { + // Simple matching logic + if req.Query != "" && !strings.Contains(strings.ToLower(product.Name), strings.ToLower(req.Query)) { + return false + } + + if req.CategoryID != 0 && product.Category.ID != req.CategoryID { + return false + } + + if req.MinPrice > 0 && product.Price < req.MinPrice { + return false + } + + if req.MaxPrice > 0 && product.Price > req.MaxPrice { + return false + } + + if req.Currency != "" && product.Currency != req.Currency { + return false + } + + if req.InStock != nil && (*req.InStock && product.Stock.Available <= 0) { + return false + } + + if req.Active != nil && *req.Active != product.Active { + return false + } + + return true +} + +func validateStruct(s interface{}) error { + // Simple validation - in real app, use a validation library + return nil +} + +func seedSampleData() { + // Add some sample products + products[1] = &Product{ + ID: 1, + Name: "Laptop Computer", + Description: "High-performance laptop for professionals", + Price: 999.99, + Currency: "USD", + Category: Category{ID: 1, Name: "Electronics"}, + Tags: []string{"laptop", "computer", "electronics"}, + Stock: Stock{Quantity: 50, Reserved: 5, Available: 45, Status: "in_stock"}, + Created: time.Now().Add(-48 * time.Hour), + Updated: time.Now().Add(-24 * time.Hour), + Active: true, + } + + products[2] = &Product{ + ID: 2, + Name: "Coffee Mug", + Description: "Ceramic coffee mug with handle", + Price: 12.99, + Currency: "USD", + Category: Category{ID: 2, Name: "Kitchen"}, + Tags: []string{"mug", "coffee", "kitchen"}, + Stock: Stock{Quantity: 100, Reserved: 10, Available: 90, Status: "in_stock"}, + Created: time.Now().Add(-72 * time.Hour), + Updated: time.Now().Add(-12 * time.Hour), + Active: true, + } + + nextID = 3 +} +``` + +## Key Takeaways + +1. **Proper JSON Parsing**: Always validate JSON input and handle parsing errors gracefully +2. **Struct Tags**: Use appropriate struct tags for JSON serialization and validation +3. **Custom Serialization**: Implement custom marshaling/unmarshaling when needed +4. **Validation**: Validate all JSON input before processing +5. **Error Handling**: Provide clear error messages for JSON-related issues +6. **Performance**: Consider JSON processing performance for large datasets +7. **Security**: Validate and sanitize all JSON input to prevent injection attacks + +## Next Steps + +- Tutorial 06: Template Engine - Learn how to render HTML templates with data +- Tutorial 07: Static Files & Assets - Serve static files and optimize asset delivery +- Explore JSON streaming for large datasets +- Learn about JSON schema validation +- Practice with real-world JSON APIs + +## Common Issues and Solutions + +### Issue: JSON Parsing Errors +**Solution**: Always check for parsing errors and provide meaningful error messages + +### Issue: Validation Failures +**Solution**: Use struct tags and validation libraries for comprehensive validation + +### Issue: Performance with Large JSON +**Solution**: Consider streaming JSON for large datasets and implement pagination + +### Issue: Custom Date Formats +**Solution**: Implement custom marshaling/unmarshaling for specific date formats + +## Additional Resources + +- [Go JSON Package Documentation](https://golang.org/pkg/encoding/json/) +- [JSON Validation Best Practices](https://json-schema.org/) +- [Performance Optimization for JSON Processing](https://golang.org/doc/effective_go.html#json) + \ No newline at end of file diff --git a/tutorials/06-template-engine.md b/tutorials/06-template-engine.md new file mode 100644 index 0000000..c71c07d --- /dev/null +++ b/tutorials/06-template-engine.md @@ -0,0 +1,652 @@ +# Tutorial 06: Template Engine + +## 📖 Overview + +Learn how to work with templates in Zoox for building dynamic web applications. This tutorial covers template setup, inheritance, data binding, and custom helpers. + +## 🎯 Learning Objectives + +- Set up and configure template engines +- Create template layouts and inheritance +- Bind dynamic data to templates +- Build custom template helpers +- Implement template caching and optimization + +## 📋 Prerequisites + +- Completed [Tutorial 01: Getting Started](./01-getting-started.md) +- Basic understanding of HTML and Go templates +- Familiarity with web development concepts + +## 🚀 Getting Started + +### Basic Template Setup + +```go +package main + +import ( + "html/template" + "path/filepath" + + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.New() + + // Setup template directory + app.SetTemplateDir("templates") + + // Basic template route + app.Get("/", func(ctx *zoox.Context) { + data := map[string]interface{}{ + "Title": "Welcome to Zoox", + "Message": "Hello, World!", + "User": map[string]string{ + "Name": "John Doe", + "Email": "john@example.com", + }, + } + + ctx.HTML(200, "index.html", data) + }) + + app.Listen(":8080") +} +``` + +### Template Inheritance System + +```go +package main + +import ( + "html/template" + "path/filepath" + "strings" + + "github.com/go-zoox/zoox" +) + +type TemplateEngine struct { + templates map[string]*template.Template + funcMap template.FuncMap +} + +func NewTemplateEngine() *TemplateEngine { + return &TemplateEngine{ + templates: make(map[string]*template.Template), + funcMap: template.FuncMap{ + "upper": strings.ToUpper, + "lower": strings.ToLower, + "title": strings.Title, + "join": strings.Join, + }, + } +} + +func (te *TemplateEngine) LoadTemplates(dir string) error { + layouts, err := filepath.Glob(filepath.Join(dir, "layouts", "*.html")) + if err != nil { + return err + } + + pages, err := filepath.Glob(filepath.Join(dir, "pages", "*.html")) + if err != nil { + return err + } + + for _, page := range pages { + name := filepath.Base(page) + files := append(layouts, page) + + tmpl, err := template.New(name).Funcs(te.funcMap).ParseFiles(files...) + if err != nil { + return err + } + + te.templates[name] = tmpl + } + + return nil +} + +func (te *TemplateEngine) Render(name string, data interface{}) (string, error) { + tmpl, exists := te.templates[name] + if !exists { + return "", fmt.Errorf("template %s not found", name) + } + + var buf strings.Builder + err := tmpl.Execute(&buf, data) + return buf.String(), err +} + +func main() { + app := zoox.New() + + // Create template engine + engine := NewTemplateEngine() + if err := engine.LoadTemplates("templates"); err != nil { + log.Fatal("Failed to load templates:", err) + } + + // Use custom template engine + app.Use(func(ctx *zoox.Context) { + ctx.Set("templateEngine", engine) + ctx.Next() + }) + + // Routes with template inheritance + app.Get("/", func(ctx *zoox.Context) { + engine := ctx.Get("templateEngine").(*TemplateEngine) + + data := map[string]interface{}{ + "Title": "Home Page", + "Content": "Welcome to our website!", + "Navigation": []map[string]string{ + {"Name": "Home", "URL": "/"}, + {"Name": "About", "URL": "/about"}, + {"Name": "Contact", "URL": "/contact"}, + }, + } + + html, err := engine.Render("home.html", data) + if err != nil { + ctx.JSON(500, map[string]string{"error": err.Error()}) + return + } + + ctx.HTML(200, html, nil) + }) + + app.Listen(":8080") +} +``` + +### Advanced Template Features + +```go +package main + +import ( + "fmt" + "html/template" + "strings" + "time" + + "github.com/go-zoox/zoox" +) + +type AdvancedTemplateEngine struct { + templates map[string]*template.Template + funcMap template.FuncMap + cache map[string]CachedTemplate +} + +type CachedTemplate struct { + Content string + ExpiresAt time.Time +} + +func NewAdvancedTemplateEngine() *AdvancedTemplateEngine { + return &AdvancedTemplateEngine{ + templates: make(map[string]*template.Template), + cache: make(map[string]CachedTemplate), + funcMap: template.FuncMap{ + // String functions + "upper": strings.ToUpper, + "lower": strings.ToLower, + "title": strings.Title, + "join": strings.Join, + "split": strings.Split, + "contains": strings.Contains, + + // Date functions + "now": time.Now, + "formatDate": func(t time.Time, layout string) string { + return t.Format(layout) + }, + "timeAgo": func(t time.Time) string { + duration := time.Since(t) + switch { + case duration < time.Minute: + return "just now" + case duration < time.Hour: + return fmt.Sprintf("%d minutes ago", int(duration.Minutes())) + case duration < 24*time.Hour: + return fmt.Sprintf("%d hours ago", int(duration.Hours())) + default: + return fmt.Sprintf("%d days ago", int(duration.Hours()/24)) + } + }, + + // Utility functions + "add": func(a, b int) int { return a + b }, + "sub": func(a, b int) int { return a - b }, + "mul": func(a, b int) int { return a * b }, + "div": func(a, b int) int { return a / b }, + + // Array functions + "slice": func(items []interface{}, start, end int) []interface{} { + if start < 0 || end > len(items) || start > end { + return []interface{}{} + } + return items[start:end] + }, + "len": func(items interface{}) int { + switch v := items.(type) { + case []interface{}: + return len(v) + case []string: + return len(v) + case string: + return len(v) + default: + return 0 + } + }, + + // Conditional functions + "eq": func(a, b interface{}) bool { return a == b }, + "ne": func(a, b interface{}) bool { return a != b }, + "gt": func(a, b int) bool { return a > b }, + "lt": func(a, b int) bool { return a < b }, + "gte": func(a, b int) bool { return a >= b }, + "lte": func(a, b int) bool { return a <= b }, + }, + } +} + +func main() { + app := zoox.New() + + engine := NewAdvancedTemplateEngine() + + // Blog example with advanced templates + app.Get("/blog", func(ctx *zoox.Context) { + posts := []map[string]interface{}{ + { + "Title": "Getting Started with Zoox", + "Content": "Learn how to build web applications with Zoox framework...", + "Author": "John Doe", + "CreatedAt": time.Now().Add(-2 * time.Hour), + "Tags": []string{"zoox", "golang", "web"}, + "Views": 125, + }, + { + "Title": "Advanced Routing Techniques", + "Content": "Explore advanced routing patterns and best practices...", + "Author": "Jane Smith", + "CreatedAt": time.Now().Add(-1 * 24 * time.Hour), + "Tags": []string{"routing", "advanced", "patterns"}, + "Views": 89, + }, + } + + data := map[string]interface{}{ + "Title": "Blog Posts", + "Posts": posts, + "CurrentUser": "admin", + "TotalPosts": len(posts), + } + + template := ` + + + + {{.Title}} + + + +

    {{.Title}} ({{.TotalPosts}} total)

    + + {{range .Posts}} +
    +

    {{.Title}}

    +
    + By {{.Author}} • {{timeAgo .CreatedAt}} • {{.Views}} views +
    +

    {{.Content}}

    +
    + {{range .Tags}} + {{.}} + {{end}} +
    +
    + {{end}} + + {{if eq .CurrentUser "admin"}} +

    Manage Posts

    + {{end}} + + + ` + + ctx.HTML(200, template, data) + }) + + app.Listen(":8080") +} +``` + +## 🎯 Hands-on Exercise + +Create a complete blog system with template inheritance: + +### Solution + +```go +package main + +import ( + "fmt" + "html/template" + "log" + "path/filepath" + "strings" + "time" + + "github.com/go-zoox/zoox" +) + +type BlogSystem struct { + engine *TemplateEngine + posts []Post +} + +type Post struct { + ID int `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Author string `json:"author"` + Date time.Time `json:"date"` + Tags []string `json:"tags"` + Category string `json:"category"` +} + +func NewBlogSystem() *BlogSystem { + return &BlogSystem{ + engine: NewTemplateEngine(), + posts: []Post{ + { + ID: 1, + Title: "Welcome to Our Blog", + Content: "This is our first blog post. Welcome to our journey!", + Author: "Admin", + Date: time.Now().Add(-24 * time.Hour), + Tags: []string{"welcome", "introduction"}, + Category: "General", + }, + { + ID: 2, + Title: "Learning Go Programming", + Content: "Go is a powerful programming language. Let's explore its features.", + Author: "John Doe", + Date: time.Now().Add(-12 * time.Hour), + Tags: []string{"go", "programming", "tutorial"}, + Category: "Technology", + }, + }, + } +} + +func (bs *BlogSystem) Setup(app *zoox.Application) { + // Load templates + if err := bs.engine.LoadTemplates("templates"); err != nil { + log.Printf("Warning: Could not load templates: %v", err) + } + + // Routes + app.Get("/", bs.homePage) + app.Get("/post/:id", bs.postPage) + app.Get("/category/:category", bs.categoryPage) + app.Get("/search", bs.searchPage) +} + +func (bs *BlogSystem) homePage(ctx *zoox.Context) { + data := map[string]interface{}{ + "Title": "My Blog", + "Posts": bs.posts, + "Categories": bs.getCategories(), + "RecentPosts": bs.getRecentPosts(3), + } + + bs.renderTemplate(ctx, "home.html", data) +} + +func (bs *BlogSystem) postPage(ctx *zoox.Context) { + id := ctx.Param("id") + post := bs.getPostByID(id) + + if post == nil { + ctx.JSON(404, map[string]string{"error": "Post not found"}) + return + } + + data := map[string]interface{}{ + "Title": post.Title, + "Post": post, + "RelatedPosts": bs.getRelatedPosts(post, 3), + } + + bs.renderTemplate(ctx, "post.html", data) +} + +func (bs *BlogSystem) renderTemplate(ctx *zoox.Context, templateName string, data interface{}) { + if html, err := bs.engine.Render(templateName, data); err == nil { + ctx.HTML(200, html, nil) + } else { + // Fallback to inline template + bs.renderInlineTemplate(ctx, templateName, data) + } +} + +func (bs *BlogSystem) renderInlineTemplate(ctx *zoox.Context, templateName string, data interface{}) { + var template string + + switch templateName { + case "home.html": + template = ` + + + + {{.Title}} + + + +
    +

    {{.Title}}

    +

    A simple blog built with Zoox

    +
    + + + +
    +

    Latest Posts

    + {{range .Posts}} +
    +

    {{.Title}}

    +
    + By {{.Author}} on {{formatDate .Date "January 2, 2006"}} in {{.Category}} +
    +

    {{.Content}}

    +
    + {{range .Tags}} + {{.}} + {{end}} +
    +
    + {{end}} +
    + + + ` + case "post.html": + template = ` + + + + {{.Title}} + + + +
    +

    {{.Post.Title}}

    +
    + +
    + + +
    + By {{.Post.Author}} on {{formatDate .Post.Date "January 2, 2006"}} in {{.Post.Category}} +
    + +
    + {{.Post.Content}} +
    + +
    + {{range .Post.Tags}} + {{.}} + {{end}} +
    + + {{if .RelatedPosts}} + + {{end}} +
    + + + ` + } + + ctx.HTML(200, template, data) +} + +func (bs *BlogSystem) getPostByID(id string) *Post { + for i, post := range bs.posts { + if fmt.Sprintf("%d", post.ID) == id { + return &bs.posts[i] + } + } + return nil +} + +func (bs *BlogSystem) getCategories() []string { + categories := make(map[string]bool) + for _, post := range bs.posts { + categories[post.Category] = true + } + + result := make([]string, 0, len(categories)) + for category := range categories { + result = append(result, category) + } + return result +} + +func (bs *BlogSystem) getRecentPosts(limit int) []Post { + if len(bs.posts) <= limit { + return bs.posts + } + return bs.posts[:limit] +} + +func (bs *BlogSystem) getRelatedPosts(post *Post, limit int) []Post { + var related []Post + for _, p := range bs.posts { + if p.ID != post.ID && p.Category == post.Category { + related = append(related, p) + if len(related) >= limit { + break + } + } + } + return related +} + +func main() { + app := zoox.New() + + blog := NewBlogSystem() + blog.Setup(app) + + log.Println("Blog server starting on :8080") + log.Println("Visit: http://localhost:8080") + + app.Listen(":8080") +} +``` + +## 📚 Key Takeaways + +1. **Template Organization**: Use layouts and inheritance for maintainable templates +2. **Custom Functions**: Extend templates with custom helper functions +3. **Data Binding**: Efficiently pass data from handlers to templates +4. **Performance**: Cache templates and optimize rendering +5. **Error Handling**: Provide fallbacks for missing templates + +## 🎯 Next Steps + +- Learn [Tutorial 07: Static Files & Assets](./07-static-files-assets.md) +- Explore [Tutorial 08: WebSocket Development](./08-websocket-development.md) +- Study [Tutorial 10: Authentication & Authorization](./10-authentication-authorization.md) + +--- + +**Congratulations!** You've mastered template engines in Zoox and can now build dynamic web applications with powerful templating capabilities. \ No newline at end of file diff --git a/tutorials/07-static-files-assets.md b/tutorials/07-static-files-assets.md new file mode 100644 index 0000000..64fcddd --- /dev/null +++ b/tutorials/07-static-files-assets.md @@ -0,0 +1,508 @@ +# Tutorial 07: Static Files & Assets + +## 📖 Overview + +Learn how to serve static files and optimize assets in Zoox applications. This tutorial covers static file serving, asset optimization, caching strategies, and CDN integration. + +## 🎯 Learning Objectives + +- Serve static files efficiently +- Implement asset optimization +- Configure caching strategies +- Integrate with CDN services +- Handle file uploads and downloads + +## 📋 Prerequisites + +- Completed [Tutorial 01: Getting Started](./01-getting-started.md) +- Basic understanding of web assets (CSS, JS, images) +- Familiarity with HTTP caching + +## 🚀 Getting Started + +### Basic Static File Serving + +```go +package main + +import ( + "github.com/go-zoox/zoox" +) + +func main() { + app := zoox.New() + + // Serve static files from public directory + app.Static("/static", "./public") + + // Serve specific file types + app.StaticFile("/favicon.ico", "./public/favicon.ico") + + // Custom static file handler + app.Get("/assets/*", func(ctx *zoox.Context) { + file := ctx.Param("*") + ctx.File("./assets/" + file) + }) + + app.Listen(":8080") +} +``` + +### Advanced Asset Management + +```go +package main + +import ( + "crypto/md5" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/go-zoox/zoox" +) + +type AssetManager struct { + publicDir string + cacheMaxAge int + hashes map[string]string +} + +func NewAssetManager(publicDir string) *AssetManager { + return &AssetManager{ + publicDir: publicDir, + cacheMaxAge: 3600, // 1 hour + hashes: make(map[string]string), + } +} + +func (am *AssetManager) Setup(app *zoox.Application) { + // Generate asset hashes for cache busting + am.generateHashes() + + // Static file middleware with optimization + app.Use(am.staticMiddleware()) + + // Asset helper endpoint + app.Get("/assets/manifest", func(ctx *zoox.Context) { + ctx.JSON(200, map[string]interface{}{ + "hashes": am.hashes, + "version": time.Now().Unix(), + }) + }) +} + +func (am *AssetManager) staticMiddleware() zoox.HandlerFunc { + return func(ctx *zoox.Context) { + path := ctx.Request.URL.Path + + // Check if it's a static asset request + if strings.HasPrefix(path, "/assets/") { + am.serveAsset(ctx, path) + return + } + + ctx.Next() + } +} + +func (am *AssetManager) serveAsset(ctx *zoox.Context, path string) { + // Remove /assets/ prefix + assetPath := strings.TrimPrefix(path, "/assets/") + fullPath := filepath.Join(am.publicDir, assetPath) + + // Check if file exists + if _, err := os.Stat(fullPath); os.IsNotExist(err) { + ctx.Status(404) + return + } + + // Set cache headers + ctx.Header("Cache-Control", fmt.Sprintf("max-age=%d", am.cacheMaxAge)) + ctx.Header("ETag", am.getFileHash(fullPath)) + + // Check if client has cached version + if ctx.Header("If-None-Match") == am.getFileHash(fullPath) { + ctx.Status(304) + return + } + + // Set content type based on file extension + ext := filepath.Ext(assetPath) + switch ext { + case ".css": + ctx.Header("Content-Type", "text/css") + case ".js": + ctx.Header("Content-Type", "application/javascript") + case ".png": + ctx.Header("Content-Type", "image/png") + case ".jpg", ".jpeg": + ctx.Header("Content-Type", "image/jpeg") + case ".gif": + ctx.Header("Content-Type", "image/gif") + case ".svg": + ctx.Header("Content-Type", "image/svg+xml") + } + + // Serve the file + ctx.File(fullPath) +} + +func (am *AssetManager) generateHashes() { + filepath.Walk(am.publicDir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return err + } + + relPath, _ := filepath.Rel(am.publicDir, path) + am.hashes[relPath] = am.getFileHash(path) + return nil + }) +} + +func (am *AssetManager) getFileHash(path string) string { + if hash, exists := am.hashes[path]; exists { + return hash + } + + file, err := os.Open(path) + if err != nil { + return "" + } + defer file.Close() + + hasher := md5.New() + if _, err := io.Copy(hasher, file); err != nil { + return "" + } + + return fmt.Sprintf("%x", hasher.Sum(nil)) +} + +func main() { + app := zoox.New() + + // Setup asset manager + assets := NewAssetManager("./public") + assets.Setup(app) + + // Main page with assets + app.Get("/", func(ctx *zoox.Context) { + html := ` + + + + Asset Management Demo + + + +

    Welcome to Asset Management

    +

    This page demonstrates optimized asset serving.

    + + + + ` + ctx.HTML(200, html, nil) + }) + + app.Listen(":8080") +} +``` + +### File Upload System + +```go +package main + +import ( + "crypto/rand" + "fmt" + "io" + "mime/multipart" + "os" + "path/filepath" + "strings" + "time" + + "github.com/go-zoox/zoox" +) + +type FileUploadManager struct { + uploadDir string + maxFileSize int64 + allowedExts []string +} + +func NewFileUploadManager(uploadDir string) *FileUploadManager { + return &FileUploadManager{ + uploadDir: uploadDir, + maxFileSize: 10 << 20, // 10MB + allowedExts: []string{".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx"}, + } +} + +func (fum *FileUploadManager) Setup(app *zoox.Application) { + // Ensure upload directory exists + os.MkdirAll(fum.uploadDir, 0755) + + // Upload endpoints + app.Post("/upload", fum.handleUpload) + app.Get("/uploads/*", fum.serveUpload) + app.Delete("/uploads/:filename", fum.deleteUpload) + + // Upload form + app.Get("/upload-form", fum.uploadForm) +} + +func (fum *FileUploadManager) handleUpload(ctx *zoox.Context) { + // Parse multipart form + err := ctx.Request.ParseMultipartForm(fum.maxFileSize) + if err != nil { + ctx.JSON(400, map[string]string{"error": "File too large"}) + return + } + + file, header, err := ctx.Request.FormFile("file") + if err != nil { + ctx.JSON(400, map[string]string{"error": "No file uploaded"}) + return + } + defer file.Close() + + // Validate file + if !fum.isAllowedFile(header.Filename) { + ctx.JSON(400, map[string]string{"error": "File type not allowed"}) + return + } + + // Generate unique filename + filename := fum.generateUniqueFilename(header.Filename) + filepath := filepath.Join(fum.uploadDir, filename) + + // Save file + if err := fum.saveFile(file, filepath); err != nil { + ctx.JSON(500, map[string]string{"error": "Failed to save file"}) + return + } + + ctx.JSON(200, map[string]interface{}{ + "message": "File uploaded successfully", + "filename": filename, + "url": "/uploads/" + filename, + "size": header.Size, + }) +} + +func (fum *FileUploadManager) serveUpload(ctx *zoox.Context) { + filename := ctx.Param("*") + filepath := filepath.Join(fum.uploadDir, filename) + + // Security check - prevent directory traversal + if strings.Contains(filename, "..") { + ctx.Status(403) + return + } + + // Check if file exists + if _, err := os.Stat(filepath); os.IsNotExist(err) { + ctx.Status(404) + return + } + + ctx.File(filepath) +} + +func (fum *FileUploadManager) deleteUpload(ctx *zoox.Context) { + filename := ctx.Param("filename") + filepath := filepath.Join(fum.uploadDir, filename) + + // Security check + if strings.Contains(filename, "..") { + ctx.JSON(403, map[string]string{"error": "Invalid filename"}) + return + } + + if err := os.Remove(filepath); err != nil { + ctx.JSON(500, map[string]string{"error": "Failed to delete file"}) + return + } + + ctx.JSON(200, map[string]string{"message": "File deleted successfully"}) +} + +func (fum *FileUploadManager) uploadForm(ctx *zoox.Context) { + html := ` + + + + File Upload + + + +

    File Upload Demo

    + +
    +

    Drag and drop files here or click to select

    + + +
    + + + + + + + ` + + ctx.HTML(200, html, nil) +} + +func (fum *FileUploadManager) isAllowedFile(filename string) bool { + ext := strings.ToLower(filepath.Ext(filename)) + for _, allowed := range fum.allowedExts { + if ext == allowed { + return true + } + } + return false +} + +func (fum *FileUploadManager) generateUniqueFilename(original string) string { + ext := filepath.Ext(original) + name := strings.TrimSuffix(original, ext) + + // Generate random suffix + b := make([]byte, 8) + rand.Read(b) + suffix := fmt.Sprintf("%x", b) + + return fmt.Sprintf("%s_%d_%s%s", name, time.Now().Unix(), suffix, ext) +} + +func (fum *FileUploadManager) saveFile(src multipart.File, dst string) error { + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, src) + return err +} + +func main() { + app := zoox.New() + + // Setup file upload manager + uploader := NewFileUploadManager("./uploads") + uploader.Setup(app) + + // Setup asset manager + assets := NewAssetManager("./public") + assets.Setup(app) + + app.Listen(":8080") +} +``` + +## 🎯 Hands-on Exercise + +Create a complete asset management system with: +1. Static file serving with optimization +2. File upload with validation +3. Image resizing and optimization +4. CDN integration simulation + +## 📚 Key Takeaways + +1. **Static Serving**: Efficiently serve static files with proper headers +2. **Asset Optimization**: Implement caching and compression +3. **File Uploads**: Handle file uploads securely with validation +4. **Performance**: Use ETags and cache headers for optimization +5. **Security**: Validate file types and prevent directory traversal + +## 🎯 Next Steps + +- Learn [Tutorial 08: WebSocket Development](./08-websocket-development.md) +- Explore [Tutorial 09: JSON-RPC Services](./09-json-rpc-services.md) +- Study [Tutorial 10: Authentication & Authorization](./10-authentication-authorization.md) + +--- + +**Congratulations!** You've mastered static file serving and asset management in Zoox! \ No newline at end of file diff --git a/tutorials/08-websocket-development.md b/tutorials/08-websocket-development.md new file mode 100644 index 0000000..df74a27 --- /dev/null +++ b/tutorials/08-websocket-development.md @@ -0,0 +1,642 @@ +# Tutorial 08: WebSocket Development + +## 📖 Overview + +Learn to build real-time applications with WebSockets in Zoox. This tutorial covers WebSocket setup, connection management, message handling, and building interactive real-time features. + +## 🎯 Learning Objectives + +- Set up WebSocket connections +- Manage client connections and rooms +- Handle real-time messaging +- Build interactive applications +- Implement connection pooling and scaling + +## 📋 Prerequisites + +- Completed [Tutorial 01: Getting Started](./01-getting-started.md) +- Basic understanding of WebSockets +- Familiarity with JavaScript and HTML + +## 🚀 Getting Started + +### Basic WebSocket Setup + +```go +package main + +import ( + "log" + "net/http" + + "github.com/go-zoox/zoox" + "github.com/gorilla/websocket" +) + +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true // Allow all origins in development + }, +} + +func main() { + app := zoox.New() + + // WebSocket endpoint + app.Get("/ws", func(ctx *zoox.Context) { + conn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) + if err != nil { + log.Printf("WebSocket upgrade error: %v", err) + return + } + defer conn.Close() + + // Handle messages + for { + messageType, message, err := conn.ReadMessage() + if err != nil { + log.Printf("Read error: %v", err) + break + } + + log.Printf("Received: %s", message) + + // Echo the message back + if err := conn.WriteMessage(messageType, message); err != nil { + log.Printf("Write error: %v", err) + break + } + } + }) + + // Serve WebSocket client + app.Get("/", func(ctx *zoox.Context) { + html := ` + + + + WebSocket Test + + +
    + + + + + + + ` + ctx.HTML(200, html, nil) + }) + + app.Listen(":8080") +} +``` + +### Advanced WebSocket Hub + +```go +package main + +import ( + "encoding/json" + "log" + "net/http" + "sync" + "time" + + "github.com/go-zoox/zoox" + "github.com/gorilla/websocket" +) + +type Message struct { + Type string `json:"type"` + Content interface{} `json:"content"` + From string `json:"from,omitempty"` + To string `json:"to,omitempty"` + Room string `json:"room,omitempty"` + Time time.Time `json:"time"` +} + +type Client struct { + ID string + Conn *websocket.Conn + Hub *Hub + Send chan Message + Rooms map[string]bool + UserInfo map[string]interface{} +} + +type Room struct { + ID string + Clients map[*Client]bool + History []Message +} + +type Hub struct { + clients map[*Client]bool + rooms map[string]*Room + register chan *Client + unregister chan *Client + broadcast chan Message + mutex sync.RWMutex +} + +func NewHub() *Hub { + return &Hub{ + clients: make(map[*Client]bool), + rooms: make(map[string]*Room), + register: make(chan *Client), + unregister: make(chan *Client), + broadcast: make(chan Message), + } +} + +func (h *Hub) Run() { + for { + select { + case client := <-h.register: + h.registerClient(client) + + case client := <-h.unregister: + h.unregisterClient(client) + + case message := <-h.broadcast: + h.handleMessage(message) + } + } +} + +func (h *Hub) registerClient(client *Client) { + h.mutex.Lock() + defer h.mutex.Unlock() + + h.clients[client] = true + log.Printf("Client %s connected", client.ID) + + // Send welcome message + welcome := Message{ + Type: "welcome", + Content: map[string]interface{}{ + "id": client.ID, + "message": "Connected to WebSocket server", + }, + Time: time.Now(), + } + + select { + case client.Send <- welcome: + default: + close(client.Send) + delete(h.clients, client) + } +} + +func (h *Hub) unregisterClient(client *Client) { + h.mutex.Lock() + defer h.mutex.Unlock() + + if _, ok := h.clients[client]; ok { + // Remove from all rooms + for roomID := range client.Rooms { + if room, exists := h.rooms[roomID]; exists { + delete(room.Clients, client) + + // Notify room about user leaving + leaveMsg := Message{ + Type: "user_left", + Content: map[string]interface{}{ + "user_id": client.ID, + "room_id": roomID, + }, + Time: time.Now(), + } + h.broadcastToRoom(roomID, leaveMsg) + } + } + + delete(h.clients, client) + close(client.Send) + log.Printf("Client %s disconnected", client.ID) + } +} + +func (h *Hub) handleMessage(message Message) { + switch message.Type { + case "join_room": + h.handleJoinRoom(message) + case "leave_room": + h.handleLeaveRoom(message) + case "room_message": + h.handleRoomMessage(message) + case "private_message": + h.handlePrivateMessage(message) + case "broadcast": + h.handleBroadcast(message) + } +} + +func (h *Hub) handleJoinRoom(message Message) { + roomID := message.Room + if roomID == "" { + return + } + + h.mutex.Lock() + defer h.mutex.Unlock() + + // Create room if it doesn't exist + if _, exists := h.rooms[roomID]; !exists { + h.rooms[roomID] = &Room{ + ID: roomID, + Clients: make(map[*Client]bool), + History: make([]Message, 0), + } + } + + // Find client and add to room + for client := range h.clients { + if client.ID == message.From { + h.rooms[roomID].Clients[client] = true + client.Rooms[roomID] = true + + // Send room history + history := Message{ + Type: "room_history", + Content: h.rooms[roomID].History, + Room: roomID, + Time: time.Now(), + } + + select { + case client.Send <- history: + default: + } + + // Notify room about new user + joinMsg := Message{ + Type: "user_joined", + Content: map[string]interface{}{ + "user_id": client.ID, + "room_id": roomID, + }, + Room: roomID, + Time: time.Now(), + } + h.broadcastToRoom(roomID, joinMsg) + break + } + } +} + +func (h *Hub) handleRoomMessage(message Message) { + roomID := message.Room + if roomID == "" { + return + } + + h.mutex.Lock() + defer h.mutex.Unlock() + + if room, exists := h.rooms[roomID]; exists { + // Add to history + room.History = append(room.History, message) + + // Keep only last 100 messages + if len(room.History) > 100 { + room.History = room.History[1:] + } + + // Broadcast to room + h.broadcastToRoom(roomID, message) + } +} + +func (h *Hub) broadcastToRoom(roomID string, message Message) { + if room, exists := h.rooms[roomID]; exists { + for client := range room.Clients { + select { + case client.Send <- message: + default: + close(client.Send) + delete(room.Clients, client) + delete(h.clients, client) + } + } + } +} + +func (h *Hub) handlePrivateMessage(message Message) { + h.mutex.RLock() + defer h.mutex.RUnlock() + + for client := range h.clients { + if client.ID == message.To { + select { + case client.Send <- message: + default: + } + break + } + } +} + +func (h *Hub) handleBroadcast(message Message) { + h.mutex.RLock() + defer h.mutex.RUnlock() + + for client := range h.clients { + select { + case client.Send <- message: + default: + close(client.Send) + delete(h.clients, client) + } + } +} + +func (c *Client) readPump() { + defer func() { + c.Hub.unregister <- c + c.Conn.Close() + }() + + c.Conn.SetReadLimit(512) + c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + c.Conn.SetPongHandler(func(string) error { + c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + return nil + }) + + for { + var message Message + err := c.Conn.ReadJSON(&message) + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + log.Printf("WebSocket error: %v", err) + } + break + } + + message.From = c.ID + message.Time = time.Now() + c.Hub.broadcast <- message + } +} + +func (c *Client) writePump() { + ticker := time.NewTicker(54 * time.Second) + defer func() { + ticker.Stop() + c.Conn.Close() + }() + + for { + select { + case message, ok := <-c.Send: + c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if !ok { + c.Conn.WriteMessage(websocket.CloseMessage, []byte{}) + return + } + + if err := c.Conn.WriteJSON(message); err != nil { + log.Printf("Write error: %v", err) + return + } + + case <-ticker.C: + c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { + return + } + } + } +} + +func main() { + app := zoox.New() + + hub := NewHub() + go hub.Run() + + var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + } + + // WebSocket endpoint + app.Get("/ws", func(ctx *zoox.Context) { + conn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) + if err != nil { + log.Printf("WebSocket upgrade error: %v", err) + return + } + + clientID := ctx.Query("id") + if clientID == "" { + clientID = generateClientID() + } + + client := &Client{ + ID: clientID, + Conn: conn, + Hub: hub, + Send: make(chan Message, 256), + Rooms: make(map[string]bool), + UserInfo: make(map[string]interface{}), + } + + hub.register <- client + + go client.writePump() + go client.readPump() + }) + + // Chat application + app.Get("/chat", func(ctx *zoox.Context) { + html := ` + + + + WebSocket Chat + + + +
    +

    WebSocket Chat

    + +
    + + + +
    + +
    + +
    + + +
    +
    + + + + + ` + ctx.HTML(200, html, nil) + }) + + log.Println("WebSocket server starting on :8080") + log.Println("Chat: http://localhost:8080/chat") + + app.Listen(":8080") +} + +func generateClientID() string { + return fmt.Sprintf("client_%d", time.Now().UnixNano()) +} +``` + +## 🎯 Hands-on Exercise + +Create a real-time collaborative whiteboard application using WebSockets. + +## 📚 Key Takeaways + +1. **Connection Management**: Handle client connections and disconnections gracefully +2. **Message Routing**: Implement room-based and private messaging +3. **Real-time Features**: Build interactive applications with instant updates +4. **Scalability**: Design for multiple concurrent connections +5. **Error Handling**: Implement proper error handling and reconnection logic + +## 🎯 Next Steps + +- Learn [Tutorial 09: JSON-RPC Services](./09-json-rpc-services.md) +- Explore [Tutorial 10: Authentication & Authorization](./10-authentication-authorization.md) +- Study [Tutorial 12: Caching Strategies](./12-caching-strategies.md) + +--- + +**Congratulations!** You've mastered WebSocket development in Zoox and can now build real-time applications! \ No newline at end of file diff --git a/tutorials/09-json-rpc-services.md b/tutorials/09-json-rpc-services.md new file mode 100644 index 0000000..5f0904b --- /dev/null +++ b/tutorials/09-json-rpc-services.md @@ -0,0 +1,285 @@ +# Tutorial 09: JSON-RPC Services + +## 📖 Overview + +Learn to build JSON-RPC services with Zoox for structured API communication. This tutorial covers service architecture, method registration, error handling, and client integration. + +## 🎯 Learning Objectives + +- Understand JSON-RPC protocol +- Build RPC services and methods +- Handle RPC errors and validation +- Create RPC clients and documentation +- Implement service discovery + +## 📋 Prerequisites + +- Completed [Tutorial 01: Getting Started](./01-getting-started.md) +- Understanding of RPC concepts +- Basic knowledge of JSON and APIs + +## 🚀 Getting Started + +### Basic JSON-RPC Server + +```go +package main + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/go-zoox/zoox" +) + +type RPCRequest struct { + Jsonrpc string `json:"jsonrpc"` + Method string `json:"method"` + Params interface{} `json:"params,omitempty"` + ID interface{} `json:"id,omitempty"` +} + +type RPCResponse struct { + Jsonrpc string `json:"jsonrpc"` + Result interface{} `json:"result,omitempty"` + Error *RPCError `json:"error,omitempty"` + ID interface{} `json:"id,omitempty"` +} + +type RPCError struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +type RPCServer struct { + methods map[string]reflect.Value +} + +func NewRPCServer() *RPCServer { + return &RPCServer{ + methods: make(map[string]reflect.Value), + } +} + +func (s *RPCServer) Register(name string, method interface{}) { + s.methods[name] = reflect.ValueOf(method) +} + +func (s *RPCServer) Handle(ctx *zoox.Context) { + var req RPCRequest + if err := ctx.BindJSON(&req); err != nil { + ctx.JSON(400, RPCResponse{ + Jsonrpc: "2.0", + Error: &RPCError{ + Code: -32700, + Message: "Parse error", + }, + ID: nil, + }) + return + } + + method, exists := s.methods[req.Method] + if !exists { + ctx.JSON(200, RPCResponse{ + Jsonrpc: "2.0", + Error: &RPCError{ + Code: -32601, + Message: "Method not found", + }, + ID: req.ID, + }) + return + } + + // Call method + result, err := s.callMethod(method, req.Params) + if err != nil { + ctx.JSON(200, RPCResponse{ + Jsonrpc: "2.0", + Error: &RPCError{ + Code: -32603, + Message: err.Error(), + }, + ID: req.ID, + }) + return + } + + ctx.JSON(200, RPCResponse{ + Jsonrpc: "2.0", + Result: result, + ID: req.ID, + }) +} + +func (s *RPCServer) callMethod(method reflect.Value, params interface{}) (interface{}, error) { + methodType := method.Type() + + // Handle different parameter types + var args []reflect.Value + + if params != nil { + paramsValue := reflect.ValueOf(params) + + if methodType.NumIn() == 1 { + // Single parameter + args = []reflect.Value{paramsValue} + } else if methodType.NumIn() > 1 { + // Multiple parameters - expect array + if paramsValue.Kind() == reflect.Slice { + for i := 0; i < paramsValue.Len() && i < methodType.NumIn(); i++ { + args = append(args, paramsValue.Index(i)) + } + } + } + } + + // Call the method + results := method.Call(args) + + if len(results) == 0 { + return nil, nil + } + + // Check for error return + if len(results) == 2 && results[1].Type().Implements(reflect.TypeOf((*error)(nil)).Elem()) { + if !results[1].IsNil() { + return nil, results[1].Interface().(error) + } + return results[0].Interface(), nil + } + + return results[0].Interface(), nil +} + +// Example service methods +func Add(a, b float64) float64 { + return a + b +} + +func Subtract(a, b float64) float64 { + return a - b +} + +func Multiply(a, b float64) float64 { + return a * b +} + +func Divide(a, b float64) (float64, error) { + if b == 0 { + return 0, fmt.Errorf("division by zero") + } + return a / b, nil +} + +func main() { + app := zoox.New() + + // Create RPC server + rpcServer := NewRPCServer() + + // Register methods + rpcServer.Register("add", Add) + rpcServer.Register("subtract", Subtract) + rpcServer.Register("multiply", Multiply) + rpcServer.Register("divide", Divide) + + // RPC endpoint + app.Post("/rpc", rpcServer.Handle) + + // Test client + app.Get("/", func(ctx *zoox.Context) { + html := ` + + + + JSON-RPC Test Client + + + +

    JSON-RPC Test Client

    + +
    +

    Add

    + + + +
    +
    + +
    +

    Divide

    + + + +
    +
    + + + + + ` + ctx.HTML(200, html, nil) + }) + + app.Listen(":8080") +} +``` + +## 📚 Key Takeaways + +1. **RPC Protocol**: Implement JSON-RPC 2.0 specification correctly +2. **Method Registration**: Register and manage RPC methods dynamically +3. **Error Handling**: Proper error codes and messages +4. **Type Safety**: Handle parameter types and validation +5. **Documentation**: Provide clear API documentation + +## 🎯 Next Steps + +- Learn [Tutorial 10: Authentication & Authorization](./10-authentication-authorization.md) +- Explore [Tutorial 11: Database Integration](./11-database-integration.md) +- Study [Tutorial 12: Caching Strategies](./12-caching-strategies.md) + +--- + +**Congratulations!** You've mastered JSON-RPC services in Zoox! \ No newline at end of file diff --git a/tutorials/10-authentication-authorization.md b/tutorials/10-authentication-authorization.md new file mode 100644 index 0000000..c53f1df --- /dev/null +++ b/tutorials/10-authentication-authorization.md @@ -0,0 +1,406 @@ +# Tutorial 10: Authentication & Authorization + +## 📖 Overview + +Learn to implement secure authentication and authorization in Zoox applications. This tutorial covers JWT tokens, session management, role-based access control, and security best practices. + +## 🎯 Learning Objectives + +- Implement JWT authentication +- Build session management +- Create role-based access control +- Secure API endpoints +- Handle authentication middleware + +## 📋 Prerequisites + +- Completed [Tutorial 01: Getting Started](./01-getting-started.md) +- Understanding of authentication concepts +- Basic knowledge of security principles + +## 🚀 Getting Started + +### JWT Authentication System + +```go +package main + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "time" + + "github.com/dgrijalva/jwt-go" + "github.com/go-zoox/zoox" + "golang.org/x/crypto/bcrypt" +) + +type User struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"-"` + Roles []string `json:"roles"` + Active bool `json:"active"` +} + +type AuthService struct { + users map[string]*User + jwtSecret []byte + nextID int +} + +func NewAuthService() *AuthService { + secret := make([]byte, 32) + rand.Read(secret) + + return &AuthService{ + users: make(map[string]*User), + jwtSecret: secret, + nextID: 1, + } +} + +func (as *AuthService) Register(username, email, password string) (*User, error) { + // Check if user exists + if _, exists := as.users[username]; exists { + return nil, fmt.Errorf("user already exists") + } + + // Hash password + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + + user := &User{ + ID: as.nextID, + Username: username, + Email: email, + Password: string(hashedPassword), + Roles: []string{"user"}, + Active: true, + } + + as.users[username] = user + as.nextID++ + + return user, nil +} + +func (as *AuthService) Login(username, password string) (string, *User, error) { + user, exists := as.users[username] + if !exists || !user.Active { + return "", nil, fmt.Errorf("invalid credentials") + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { + return "", nil, fmt.Errorf("invalid credentials") + } + + // Generate JWT token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "user_id": user.ID, + "username": user.Username, + "roles": user.Roles, + "exp": time.Now().Add(24 * time.Hour).Unix(), + }) + + tokenString, err := token.SignedString(as.jwtSecret) + if err != nil { + return "", nil, err + } + + return tokenString, user, nil +} + +func (as *AuthService) ValidateToken(tokenString string) (*User, error) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method") + } + return as.jwtSecret, nil + }) + + if err != nil || !token.Valid { + return nil, fmt.Errorf("invalid token") + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, fmt.Errorf("invalid token claims") + } + + username, ok := claims["username"].(string) + if !ok { + return nil, fmt.Errorf("invalid username in token") + } + + user, exists := as.users[username] + if !exists || !user.Active { + return nil, fmt.Errorf("user not found or inactive") + } + + return user, nil +} + +func (as *AuthService) AuthMiddleware() zoox.HandlerFunc { + return func(ctx *zoox.Context) { + authHeader := ctx.Header("Authorization") + if authHeader == "" { + ctx.JSON(401, map[string]string{"error": "Authorization header required"}) + return + } + + // Extract token from "Bearer " + if len(authHeader) < 7 || authHeader[:7] != "Bearer " { + ctx.JSON(401, map[string]string{"error": "Invalid authorization format"}) + return + } + + tokenString := authHeader[7:] + user, err := as.ValidateToken(tokenString) + if err != nil { + ctx.JSON(401, map[string]string{"error": "Invalid token"}) + return + } + + ctx.Set("user", user) + ctx.Next() + } +} + +func (as *AuthService) RequireRole(role string) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + user, exists := ctx.Get("user") + if !exists { + ctx.JSON(401, map[string]string{"error": "Authentication required"}) + return + } + + u := user.(*User) + for _, userRole := range u.Roles { + if userRole == role || userRole == "admin" { + ctx.Next() + return + } + } + + ctx.JSON(403, map[string]string{"error": "Insufficient permissions"}) + } +} + +func main() { + app := zoox.New() + + authService := NewAuthService() + + // Create default admin user + authService.Register("admin", "admin@example.com", "admin123") + if user, exists := authService.users["admin"]; exists { + user.Roles = []string{"admin", "user"} + } + + // Public endpoints + app.Post("/register", func(ctx *zoox.Context) { + var req struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` + } + + if err := ctx.BindJSON(&req); err != nil { + ctx.JSON(400, map[string]string{"error": "Invalid request"}) + return + } + + user, err := authService.Register(req.Username, req.Email, req.Password) + if err != nil { + ctx.JSON(400, map[string]string{"error": err.Error()}) + return + } + + ctx.JSON(201, map[string]interface{}{ + "message": "User registered successfully", + "user": user, + }) + }) + + app.Post("/login", func(ctx *zoox.Context) { + var req struct { + Username string `json:"username"` + Password string `json:"password"` + } + + if err := ctx.BindJSON(&req); err != nil { + ctx.JSON(400, map[string]string{"error": "Invalid request"}) + return + } + + token, user, err := authService.Login(req.Username, req.Password) + if err != nil { + ctx.JSON(401, map[string]string{"error": err.Error()}) + return + } + + ctx.JSON(200, map[string]interface{}{ + "token": token, + "user": user, + }) + }) + + // Protected endpoints + protected := app.Group("/api") + protected.Use(authService.AuthMiddleware()) + + protected.Get("/profile", func(ctx *zoox.Context) { + user := ctx.Get("user").(*User) + ctx.JSON(200, user) + }) + + protected.Get("/users", authService.RequireRole("admin"), func(ctx *zoox.Context) { + users := make([]*User, 0, len(authService.users)) + for _, user := range authService.users { + users = append(users, user) + } + ctx.JSON(200, users) + }) + + // Login form + app.Get("/", func(ctx *zoox.Context) { + html := ` + + + + Authentication Demo + + + +

    Authentication Demo

    + +
    + + +
    + +
    + + +
    + + + + + +
    +

    Protected Actions

    + + +
    + + + + + ` + ctx.HTML(200, html, nil) + }) + + app.Listen(":8080") +} +``` + +## 📚 Key Takeaways + +1. **JWT Tokens**: Secure stateless authentication +2. **Password Security**: Hash passwords with bcrypt +3. **Role-Based Access**: Implement granular permissions +4. **Middleware**: Use authentication middleware for protection +5. **Security**: Follow security best practices + +## 🎯 Next Steps + +- Learn [Tutorial 11: Database Integration](./11-database-integration.md) +- Explore [Tutorial 12: Caching Strategies](./12-caching-strategies.md) +- Study [Tutorial 15: Security Best Practices](./15-security-best-practices.md) + +--- + +**Congratulations!** You've mastered authentication and authorization in Zoox! \ No newline at end of file diff --git a/tutorials/11-database-integration.md b/tutorials/11-database-integration.md new file mode 100644 index 0000000..c6175ee --- /dev/null +++ b/tutorials/11-database-integration.md @@ -0,0 +1,476 @@ +# Tutorial 11: Database Integration + +## 📖 Overview + +Learn to integrate databases with Zoox applications. This tutorial covers database connections, query builders, ORM integration, and migration strategies for building data-driven applications. + +## 🎯 Learning Objectives + +- Connect to databases (MySQL, PostgreSQL, SQLite) +- Use query builders and raw SQL +- Implement ORM patterns +- Handle database migrations +- Optimize database performance + +## 📋 Prerequisites + +- Completed [Tutorial 01: Getting Started](./01-getting-started.md) +- Basic understanding of SQL and databases +- Familiarity with Go database/sql package + +## 🚀 Getting Started + +### Database Connection Manager + +```go +package main + +import ( + "database/sql" + "fmt" + "log" + "time" + + _ "github.com/go-sql-driver/mysql" + _ "github.com/lib/pq" + _ "github.com/mattn/go-sqlite3" + "github.com/go-zoox/zoox" +) + +type DatabaseConfig struct { + Driver string + Host string + Port int + User string + Password string + Database string + SSLMode string +} + +type DatabaseManager struct { + db *sql.DB + config DatabaseConfig +} + +func NewDatabaseManager(config DatabaseConfig) *DatabaseManager { + return &DatabaseManager{ + config: config, + } +} + +func (dm *DatabaseManager) Connect() error { + var dsn string + + switch dm.config.Driver { + case "mysql": + dsn = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true", + dm.config.User, dm.config.Password, dm.config.Host, dm.config.Port, dm.config.Database) + case "postgres": + dsn = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", + dm.config.Host, dm.config.Port, dm.config.User, dm.config.Password, dm.config.Database, dm.config.SSLMode) + case "sqlite3": + dsn = dm.config.Database + default: + return fmt.Errorf("unsupported database driver: %s", dm.config.Driver) + } + + db, err := sql.Open(dm.config.Driver, dsn) + if err != nil { + return err + } + + // Configure connection pool + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(25) + db.SetConnMaxLifetime(5 * time.Minute) + + // Test connection + if err := db.Ping(); err != nil { + return err + } + + dm.db = db + log.Printf("Connected to %s database", dm.config.Driver) + return nil +} + +func (dm *DatabaseManager) Close() error { + if dm.db != nil { + return dm.db.Close() + } + return nil +} + +func (dm *DatabaseManager) GetDB() *sql.DB { + return dm.db +} + +// User model +type User struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// UserRepository handles user database operations +type UserRepository struct { + db *sql.DB +} + +func NewUserRepository(db *sql.DB) *UserRepository { + return &UserRepository{db: db} +} + +func (ur *UserRepository) CreateTable() error { + query := ` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username VARCHAR(255) UNIQUE NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )` + + _, err := ur.db.Exec(query) + return err +} + +func (ur *UserRepository) Create(user *User) error { + query := ` + INSERT INTO users (username, email, created_at, updated_at) + VALUES (?, ?, ?, ?)` + + now := time.Now() + result, err := ur.db.Exec(query, user.Username, user.Email, now, now) + if err != nil { + return err + } + + id, err := result.LastInsertId() + if err != nil { + return err + } + + user.ID = int(id) + user.CreatedAt = now + user.UpdatedAt = now + return nil +} + +func (ur *UserRepository) GetByID(id int) (*User, error) { + query := ` + SELECT id, username, email, created_at, updated_at + FROM users WHERE id = ?` + + user := &User{} + err := ur.db.QueryRow(query, id).Scan( + &user.ID, &user.Username, &user.Email, &user.CreatedAt, &user.UpdatedAt, + ) + + if err != nil { + if err == sql.ErrNoRows { + return nil, fmt.Errorf("user not found") + } + return nil, err + } + + return user, nil +} + +func (ur *UserRepository) GetByUsername(username string) (*User, error) { + query := ` + SELECT id, username, email, created_at, updated_at + FROM users WHERE username = ?` + + user := &User{} + err := ur.db.QueryRow(query, username).Scan( + &user.ID, &user.Username, &user.Email, &user.CreatedAt, &user.UpdatedAt, + ) + + if err != nil { + if err == sql.ErrNoRows { + return nil, fmt.Errorf("user not found") + } + return nil, err + } + + return user, nil +} + +func (ur *UserRepository) GetAll() ([]*User, error) { + query := ` + SELECT id, username, email, created_at, updated_at + FROM users ORDER BY created_at DESC` + + rows, err := ur.db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var users []*User + for rows.Next() { + user := &User{} + err := rows.Scan( + &user.ID, &user.Username, &user.Email, &user.CreatedAt, &user.UpdatedAt, + ) + if err != nil { + return nil, err + } + users = append(users, user) + } + + return users, nil +} + +func (ur *UserRepository) Update(user *User) error { + query := ` + UPDATE users + SET username = ?, email = ?, updated_at = ? + WHERE id = ?` + + user.UpdatedAt = time.Now() + _, err := ur.db.Exec(query, user.Username, user.Email, user.UpdatedAt, user.ID) + return err +} + +func (ur *UserRepository) Delete(id int) error { + query := `DELETE FROM users WHERE id = ?` + _, err := ur.db.Exec(query, id) + return err +} + +func main() { + app := zoox.New() + + // Database configuration + config := DatabaseConfig{ + Driver: "sqlite3", + Database: "./users.db", + } + + // Initialize database + dbManager := NewDatabaseManager(config) + if err := dbManager.Connect(); err != nil { + log.Fatal("Failed to connect to database:", err) + } + defer dbManager.Close() + + // Initialize repository + userRepo := NewUserRepository(dbManager.GetDB()) + if err := userRepo.CreateTable(); err != nil { + log.Fatal("Failed to create table:", err) + } + + // API endpoints + app.Post("/users", func(ctx *zoox.Context) { + var user User + if err := ctx.BindJSON(&user); err != nil { + ctx.JSON(400, map[string]string{"error": "Invalid request"}) + return + } + + if err := userRepo.Create(&user); err != nil { + ctx.JSON(500, map[string]string{"error": err.Error()}) + return + } + + ctx.JSON(201, user) + }) + + app.Get("/users", func(ctx *zoox.Context) { + users, err := userRepo.GetAll() + if err != nil { + ctx.JSON(500, map[string]string{"error": err.Error()}) + return + } + + ctx.JSON(200, users) + }) + + app.Get("/users/:id", func(ctx *zoox.Context) { + id := ctx.ParamInt("id") + user, err := userRepo.GetByID(id) + if err != nil { + ctx.JSON(404, map[string]string{"error": err.Error()}) + return + } + + ctx.JSON(200, user) + }) + + app.Put("/users/:id", func(ctx *zoox.Context) { + id := ctx.ParamInt("id") + + var user User + if err := ctx.BindJSON(&user); err != nil { + ctx.JSON(400, map[string]string{"error": "Invalid request"}) + return + } + + user.ID = id + if err := userRepo.Update(&user); err != nil { + ctx.JSON(500, map[string]string{"error": err.Error()}) + return + } + + ctx.JSON(200, user) + }) + + app.Delete("/users/:id", func(ctx *zoox.Context) { + id := ctx.ParamInt("id") + + if err := userRepo.Delete(id); err != nil { + ctx.JSON(500, map[string]string{"error": err.Error()}) + return + } + + ctx.JSON(200, map[string]string{"message": "User deleted successfully"}) + }) + + // Web interface + app.Get("/", func(ctx *zoox.Context) { + html := ` + + + + User Management + + + +

    User Management

    + +
    +

    Add User

    + + + +
    + +
    +

    Users

    + +
    +
    + + + + + ` + ctx.HTML(200, html, nil) + }) + + log.Println("Server starting on :8080") + log.Println("Database: SQLite (users.db)") + log.Println("Interface: http://localhost:8080") + + app.Listen(":8080") +} +``` + +## 📚 Key Takeaways + +1. **Database Connections**: Manage database connections efficiently +2. **Repository Pattern**: Separate data access logic +3. **Connection Pooling**: Optimize database performance +4. **Error Handling**: Handle database errors gracefully +5. **Migrations**: Manage database schema changes + +## 🎯 Next Steps + +- Learn [Tutorial 12: Caching Strategies](./12-caching-strategies.md) +- Explore [Tutorial 13: Monitoring & Logging](./13-monitoring-logging.md) +- Study [Tutorial 14: Testing Strategies](./14-testing-strategies.md) + +--- + +**Congratulations!** You've mastered database integration in Zoox! \ No newline at end of file diff --git a/tutorials/12-caching-strategies.md b/tutorials/12-caching-strategies.md new file mode 100644 index 0000000..95516cf --- /dev/null +++ b/tutorials/12-caching-strategies.md @@ -0,0 +1,572 @@ +# Tutorial 12: Caching Strategies + +## 📖 Overview + +Learn to implement effective caching strategies in Zoox applications for improved performance. This tutorial covers memory caching, Redis integration, cache invalidation, and performance optimization techniques. + +## 🎯 Learning Objectives + +- Implement memory caching +- Integrate Redis for distributed caching +- Design cache invalidation strategies +- Optimize application performance +- Handle cache-related patterns + +## 📋 Prerequisites + +- Completed [Tutorial 01: Getting Started](./01-getting-started.md) +- Understanding of caching concepts +- Basic knowledge of Redis (optional) + +## 🚀 Getting Started + +### Multi-Level Caching System + +```go +package main + +import ( + "encoding/json" + "fmt" + "log" + "sync" + "time" + + "github.com/go-redis/redis/v8" + "github.com/go-zoox/zoox" + "golang.org/x/net/context" +) + +type CacheItem struct { + Data interface{} + ExpiresAt time.Time + Hits int64 +} + +type MemoryCache struct { + items map[string]CacheItem + mutex sync.RWMutex + ttl time.Duration +} + +func NewMemoryCache(ttl time.Duration) *MemoryCache { + cache := &MemoryCache{ + items: make(map[string]CacheItem), + ttl: ttl, + } + + // Start cleanup goroutine + go cache.cleanup() + + return cache +} + +func (mc *MemoryCache) Set(key string, value interface{}, ttl time.Duration) { + mc.mutex.Lock() + defer mc.mutex.Unlock() + + if ttl == 0 { + ttl = mc.ttl + } + + mc.items[key] = CacheItem{ + Data: value, + ExpiresAt: time.Now().Add(ttl), + Hits: 0, + } +} + +func (mc *MemoryCache) Get(key string) (interface{}, bool) { + mc.mutex.Lock() + defer mc.mutex.Unlock() + + item, exists := mc.items[key] + if !exists || time.Now().After(item.ExpiresAt) { + delete(mc.items, key) + return nil, false + } + + // Update hit count + item.Hits++ + mc.items[key] = item + + return item.Data, true +} + +func (mc *MemoryCache) Delete(key string) { + mc.mutex.Lock() + defer mc.mutex.Unlock() + delete(mc.items, key) +} + +func (mc *MemoryCache) Clear() { + mc.mutex.Lock() + defer mc.mutex.Unlock() + mc.items = make(map[string]CacheItem) +} + +func (mc *MemoryCache) Stats() map[string]interface{} { + mc.mutex.RLock() + defer mc.mutex.RUnlock() + + totalHits := int64(0) + for _, item := range mc.items { + totalHits += item.Hits + } + + return map[string]interface{}{ + "entries": len(mc.items), + "total_hits": totalHits, + "ttl_seconds": int(mc.ttl.Seconds()), + } +} + +func (mc *MemoryCache) cleanup() { + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + for range ticker.C { + mc.mutex.Lock() + now := time.Now() + for key, item := range mc.items { + if now.After(item.ExpiresAt) { + delete(mc.items, key) + } + } + mc.mutex.Unlock() + } +} + +// Redis Cache +type RedisCache struct { + client *redis.Client + ttl time.Duration +} + +func NewRedisCache(addr, password string, db int, ttl time.Duration) *RedisCache { + client := redis.NewClient(&redis.Options{ + Addr: addr, + Password: password, + DB: db, + }) + + return &RedisCache{ + client: client, + ttl: ttl, + } +} + +func (rc *RedisCache) Set(key string, value interface{}, ttl time.Duration) error { + if ttl == 0 { + ttl = rc.ttl + } + + data, err := json.Marshal(value) + if err != nil { + return err + } + + return rc.client.Set(context.Background(), key, data, ttl).Err() +} + +func (rc *RedisCache) Get(key string) (interface{}, bool) { + data, err := rc.client.Get(context.Background(), key).Result() + if err != nil { + return nil, false + } + + var value interface{} + if err := json.Unmarshal([]byte(data), &value); err != nil { + return nil, false + } + + return value, true +} + +func (rc *RedisCache) Delete(key string) error { + return rc.client.Del(context.Background(), key).Err() +} + +func (rc *RedisCache) Clear() error { + return rc.client.FlushDB(context.Background()).Err() +} + +// Multi-level cache manager +type CacheManager struct { + l1Cache *MemoryCache + l2Cache *RedisCache + enabled bool +} + +func NewCacheManager(l1TTL, l2TTL time.Duration) *CacheManager { + return &CacheManager{ + l1Cache: NewMemoryCache(l1TTL), + l2Cache: NewRedisCache("localhost:6379", "", 0, l2TTL), + enabled: true, + } +} + +func (cm *CacheManager) Get(key string) (interface{}, bool) { + if !cm.enabled { + return nil, false + } + + // Try L1 cache first + if value, found := cm.l1Cache.Get(key); found { + return value, true + } + + // Try L2 cache + if cm.l2Cache != nil { + if value, found := cm.l2Cache.Get(key); found { + // Store in L1 cache for faster access + cm.l1Cache.Set(key, value, 0) + return value, true + } + } + + return nil, false +} + +func (cm *CacheManager) Set(key string, value interface{}, ttl time.Duration) { + if !cm.enabled { + return + } + + // Store in L1 cache + cm.l1Cache.Set(key, value, ttl) + + // Store in L2 cache + if cm.l2Cache != nil { + cm.l2Cache.Set(key, value, ttl) + } +} + +func (cm *CacheManager) Delete(key string) { + cm.l1Cache.Delete(key) + if cm.l2Cache != nil { + cm.l2Cache.Delete(key) + } +} + +func (cm *CacheManager) Stats() map[string]interface{} { + return map[string]interface{}{ + "l1_cache": cm.l1Cache.Stats(), + "enabled": cm.enabled, + } +} + +// Cache middleware +func (cm *CacheManager) CacheMiddleware(ttl time.Duration) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + // Only cache GET requests + if ctx.Method() != "GET" { + ctx.Next() + return + } + + // Generate cache key + key := fmt.Sprintf("http:%s:%s", ctx.Method(), ctx.Request.URL.Path) + if ctx.Request.URL.RawQuery != "" { + key += "?" + ctx.Request.URL.RawQuery + } + + // Check cache + if data, found := cm.Get(key); found { + ctx.JSON(200, data) + ctx.Header("X-Cache", "HIT") + return + } + + // Capture response + recorder := &ResponseRecorder{ + original: ctx.Writer, + } + ctx.Writer = recorder + + ctx.Next() + + // Cache successful responses + if recorder.statusCode >= 200 && recorder.statusCode < 300 && recorder.data != nil { + cm.Set(key, recorder.data, ttl) + ctx.Header("X-Cache", "MISS") + } + + // Restore original writer + ctx.Writer = recorder.original + } +} + +type ResponseRecorder struct { + original zoox.ResponseWriter + data interface{} + statusCode int +} + +func (rr *ResponseRecorder) Header() map[string][]string { + return rr.original.Header() +} + +func (rr *ResponseRecorder) Write(data []byte) (int, error) { + // Try to parse JSON data for caching + var jsonData interface{} + if err := json.Unmarshal(data, &jsonData); err == nil { + rr.data = jsonData + } + + return rr.original.Write(data) +} + +func (rr *ResponseRecorder) WriteHeader(statusCode int) { + rr.statusCode = statusCode + rr.original.WriteHeader(statusCode) +} + +func main() { + app := zoox.New() + + // Create cache manager + cacheManager := NewCacheManager(5*time.Minute, 30*time.Minute) + + // Apply cache middleware to specific routes + app.Use(cacheManager.CacheMiddleware(10 * time.Minute)) + + // Sample data + products := []map[string]interface{}{ + {"id": 1, "name": "Laptop", "price": 999.99, "category": "Electronics"}, + {"id": 2, "name": "Mouse", "price": 29.99, "category": "Electronics"}, + {"id": 3, "name": "Keyboard", "price": 79.99, "category": "Electronics"}, + {"id": 4, "name": "Monitor", "price": 299.99, "category": "Electronics"}, + } + + // Cached endpoints + app.Get("/products", func(ctx *zoox.Context) { + // Simulate database query delay + time.Sleep(100 * time.Millisecond) + + category := ctx.Query("category") + if category != "" { + filtered := make([]map[string]interface{}, 0) + for _, product := range products { + if product["category"] == category { + filtered = append(filtered, product) + } + } + ctx.JSON(200, map[string]interface{}{ + "products": filtered, + "count": len(filtered), + "cached": false, + }) + return + } + + ctx.JSON(200, map[string]interface{}{ + "products": products, + "count": len(products), + "cached": false, + }) + }) + + app.Get("/products/:id", func(ctx *zoox.Context) { + id := ctx.ParamInt("id") + + // Simulate database query delay + time.Sleep(50 * time.Millisecond) + + for _, product := range products { + if product["id"] == id { + ctx.JSON(200, map[string]interface{}{ + "product": product, + "cached": false, + }) + return + } + } + + ctx.JSON(404, map[string]string{"error": "Product not found"}) + }) + + // Cache management endpoints + app.Get("/cache/stats", func(ctx *zoox.Context) { + ctx.JSON(200, cacheManager.Stats()) + }) + + app.Delete("/cache", func(ctx *zoox.Context) { + cacheManager.l1Cache.Clear() + if cacheManager.l2Cache != nil { + cacheManager.l2Cache.Clear() + } + + ctx.JSON(200, map[string]string{"message": "Cache cleared"}) + }) + + app.Delete("/cache/:key", func(ctx *zoox.Context) { + key := ctx.Param("key") + cacheManager.Delete(key) + + ctx.JSON(200, map[string]string{"message": "Cache key deleted"}) + }) + + // Demo interface + app.Get("/", func(ctx *zoox.Context) { + html := ` + + + + Caching Demo + + + +

    Caching Strategies Demo

    + +
    +

    Test Cached Endpoints

    + + + + +
    +
    + +
    +

    Cache Management

    + + +
    +
    + +
    +

    Performance Test

    + +
    +
    + + + + + ` + ctx.HTML(200, html, nil) + }) + + log.Println("Caching demo server starting on :8080") + log.Println("Demo: http://localhost:8080") + log.Println("Note: Redis cache disabled in this demo (L1 cache only)") + + app.Listen(":8080") +} +``` + +## 📚 Key Takeaways + +1. **Multi-level Caching**: Combine memory and distributed caching +2. **Cache Invalidation**: Implement proper cache expiration +3. **Performance Monitoring**: Track cache hit rates and performance +4. **Strategic Caching**: Cache expensive operations and frequent requests +5. **Cache Management**: Provide tools for cache administration + +## 🎯 Next Steps + +- Learn [Tutorial 13: Monitoring & Logging](./13-monitoring-logging.md) +- Explore [Tutorial 14: Testing Strategies](./14-testing-strategies.md) +- Study [Tutorial 16: Performance Optimization](./16-performance-optimization.md) + +--- + +**Congratulations!** You've mastered caching strategies in Zoox! \ No newline at end of file diff --git a/tutorials/13-monitoring-logging.md b/tutorials/13-monitoring-logging.md new file mode 100644 index 0000000..cf70f3e --- /dev/null +++ b/tutorials/13-monitoring-logging.md @@ -0,0 +1,613 @@ +# Tutorial 13: Monitoring & Logging + +## 📖 Overview + +Learn to implement comprehensive monitoring and logging for Zoox applications. This tutorial covers structured logging, metrics collection, health checks, and performance monitoring for production-ready applications. + +## 🎯 Learning Objectives + +- Implement structured logging +- Collect application metrics +- Build health check systems +- Monitor application performance +- Set up alerting and dashboards + +## 📋 Prerequisites + +- Completed [Tutorial 01: Getting Started](./01-getting-started.md) +- Understanding of logging and monitoring concepts +- Basic knowledge of metrics and observability + +## 🚀 Getting Started + +### Comprehensive Monitoring System + +```go +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + "runtime" + "sync" + "time" + + "github.com/go-zoox/zoox" + "github.com/sirupsen/logrus" +) + +// Structured Logger +type Logger struct { + *logrus.Logger +} + +func NewLogger() *Logger { + logger := logrus.New() + logger.SetFormatter(&logrus.JSONFormatter{ + TimestampFormat: time.RFC3339, + }) + logger.SetLevel(logrus.InfoLevel) + + return &Logger{Logger: logger} +} + +func (l *Logger) LogRequest(method, path string, statusCode int, duration time.Duration, userID string) { + l.WithFields(logrus.Fields{ + "type": "request", + "method": method, + "path": path, + "status_code": statusCode, + "duration_ms": duration.Milliseconds(), + "user_id": userID, + }).Info("HTTP request processed") +} + +func (l *Logger) LogError(err error, context map[string]interface{}) { + fields := logrus.Fields{ + "type": "error", + "error": err.Error(), + } + + for k, v := range context { + fields[k] = v + } + + l.WithFields(fields).Error("Application error occurred") +} + +func (l *Logger) LogMetric(name string, value float64, tags map[string]string) { + fields := logrus.Fields{ + "type": "metric", + "metric_name": name, + "value": value, + } + + for k, v := range tags { + fields["tag_"+k] = v + } + + l.WithFields(fields).Info("Metric recorded") +} + +// Metrics Collector +type Metrics struct { + counters map[string]int64 + gauges map[string]float64 + timers map[string][]time.Duration + mutex sync.RWMutex +} + +func NewMetrics() *Metrics { + return &Metrics{ + counters: make(map[string]int64), + gauges: make(map[string]float64), + timers: make(map[string][]time.Duration), + } +} + +func (m *Metrics) IncrementCounter(name string) { + m.mutex.Lock() + defer m.mutex.Unlock() + m.counters[name]++ +} + +func (m *Metrics) SetGauge(name string, value float64) { + m.mutex.Lock() + defer m.mutex.Unlock() + m.gauges[name] = value +} + +func (m *Metrics) RecordTimer(name string, duration time.Duration) { + m.mutex.Lock() + defer m.mutex.Unlock() + m.timers[name] = append(m.timers[name], duration) + + // Keep only last 100 measurements + if len(m.timers[name]) > 100 { + m.timers[name] = m.timers[name][1:] + } +} + +func (m *Metrics) GetStats() map[string]interface{} { + m.mutex.RLock() + defer m.mutex.RUnlock() + + stats := map[string]interface{}{ + "counters": make(map[string]int64), + "gauges": make(map[string]float64), + "timers": make(map[string]map[string]float64), + } + + // Copy counters + for k, v := range m.counters { + stats["counters"].(map[string]int64)[k] = v + } + + // Copy gauges + for k, v := range m.gauges { + stats["gauges"].(map[string]float64)[k] = v + } + + // Process timers + timers := make(map[string]map[string]float64) + for name, durations := range m.timers { + if len(durations) == 0 { + continue + } + + var total time.Duration + min := durations[0] + max := durations[0] + + for _, d := range durations { + total += d + if d < min { + min = d + } + if d > max { + max = d + } + } + + avg := total / time.Duration(len(durations)) + + timers[name] = map[string]float64{ + "count": float64(len(durations)), + "avg_ms": float64(avg.Milliseconds()), + "min_ms": float64(min.Milliseconds()), + "max_ms": float64(max.Milliseconds()), + "total_ms": float64(total.Milliseconds()), + } + } + + stats["timers"] = timers + return stats +} + +// Health Check System +type HealthChecker struct { + checks map[string]HealthCheck + mutex sync.RWMutex +} + +type HealthCheck func() error + +type HealthStatus struct { + Status string `json:"status"` + Checks map[string]CheckResult `json:"checks"` +} + +type CheckResult struct { + Status string `json:"status"` + Message string `json:"message,omitempty"` +} + +func NewHealthChecker() *HealthChecker { + return &HealthChecker{ + checks: make(map[string]HealthCheck), + } +} + +func (hc *HealthChecker) AddCheck(name string, check HealthCheck) { + hc.mutex.Lock() + defer hc.mutex.Unlock() + hc.checks[name] = check +} + +func (hc *HealthChecker) CheckHealth() HealthStatus { + hc.mutex.RLock() + defer hc.mutex.RUnlock() + + status := HealthStatus{ + Status: "healthy", + Checks: make(map[string]CheckResult), + } + + for name, check := range hc.checks { + if err := check(); err != nil { + status.Checks[name] = CheckResult{ + Status: "unhealthy", + Message: err.Error(), + } + status.Status = "unhealthy" + } else { + status.Checks[name] = CheckResult{ + Status: "healthy", + } + } + } + + return status +} + +// Monitoring Middleware +func MonitoringMiddleware(logger *Logger, metrics *Metrics) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + start := time.Now() + + // Process request + ctx.Next() + + // Record metrics + duration := time.Since(start) + path := ctx.Request.URL.Path + method := ctx.Method() + statusCode := ctx.Writer.Status() + + // Log request + userID := "" + if user := ctx.Get("user"); user != nil { + userID = fmt.Sprintf("%v", user) + } + + logger.LogRequest(method, path, statusCode, duration, userID) + + // Update metrics + metrics.IncrementCounter("http_requests_total") + metrics.IncrementCounter(fmt.Sprintf("http_requests_%s", method)) + metrics.IncrementCounter(fmt.Sprintf("http_status_%d", statusCode)) + metrics.RecordTimer("http_request_duration", duration) + + // Update response time gauge + metrics.SetGauge("http_response_time_ms", float64(duration.Milliseconds())) + } +} + +// System Metrics Collector +func CollectSystemMetrics(metrics *Metrics) { + ticker := time.NewTicker(30 * time.Second) + go func() { + for range ticker.C { + var m runtime.MemStats + runtime.ReadMemStats(&m) + + // Memory metrics + metrics.SetGauge("memory_alloc_bytes", float64(m.Alloc)) + metrics.SetGauge("memory_total_alloc_bytes", float64(m.TotalAlloc)) + metrics.SetGauge("memory_sys_bytes", float64(m.Sys)) + metrics.SetGauge("memory_heap_alloc_bytes", float64(m.HeapAlloc)) + metrics.SetGauge("memory_heap_sys_bytes", float64(m.HeapSys)) + + // GC metrics + metrics.SetGauge("gc_num", float64(m.NumGC)) + metrics.SetGauge("gc_pause_total_ns", float64(m.PauseTotalNs)) + + // Goroutine count + metrics.SetGauge("goroutines", float64(runtime.NumGoroutine())) + } + }() +} + +func main() { + app := zoox.New() + + // Initialize monitoring components + logger := NewLogger() + metrics := NewMetrics() + healthChecker := NewHealthChecker() + + // Start system metrics collection + CollectSystemMetrics(metrics) + + // Add health checks + healthChecker.AddCheck("database", func() error { + // Simulate database check + time.Sleep(10 * time.Millisecond) + return nil // or return error if unhealthy + }) + + healthChecker.AddCheck("external_service", func() error { + // Simulate external service check + return nil + }) + + // Apply monitoring middleware + app.Use(MonitoringMiddleware(logger, metrics)) + + // Health check endpoint + app.Get("/health", func(ctx *zoox.Context) { + health := healthChecker.CheckHealth() + + if health.Status == "healthy" { + ctx.JSON(200, health) + } else { + ctx.JSON(503, health) + } + }) + + // Metrics endpoint + app.Get("/metrics", func(ctx *zoox.Context) { + ctx.JSON(200, metrics.GetStats()) + }) + + // Sample application endpoints + app.Get("/api/users", func(ctx *zoox.Context) { + // Simulate processing time + time.Sleep(time.Duration(50+rand.Intn(100)) * time.Millisecond) + + users := []map[string]interface{}{ + {"id": 1, "name": "John Doe", "email": "john@example.com"}, + {"id": 2, "name": "Jane Smith", "email": "jane@example.com"}, + } + + ctx.JSON(200, users) + }) + + app.Get("/api/users/:id", func(ctx *zoox.Context) { + id := ctx.Param("id") + + // Simulate processing time + time.Sleep(time.Duration(20+rand.Intn(80)) * time.Millisecond) + + // Simulate occasional errors + if rand.Float32() < 0.1 { + logger.LogError(fmt.Errorf("user not found"), map[string]interface{}{ + "user_id": id, + "endpoint": "/api/users/:id", + }) + ctx.JSON(404, map[string]string{"error": "User not found"}) + return + } + + ctx.JSON(200, map[string]interface{}{ + "id": id, + "name": "User " + id, + "email": "user" + id + "@example.com", + }) + }) + + // Error endpoint for testing + app.Get("/api/error", func(ctx *zoox.Context) { + err := fmt.Errorf("simulated error") + logger.LogError(err, map[string]interface{}{ + "endpoint": "/api/error", + "severity": "high", + }) + ctx.JSON(500, map[string]string{"error": "Internal server error"}) + }) + + // Monitoring dashboard + app.Get("/", func(ctx *zoox.Context) { + html := ` + + + + Monitoring Dashboard + + + +
    +

    Monitoring Dashboard

    + +
    +

    Health Status

    +
    Loading...
    + +
    + +
    +

    Metrics

    +
    Loading...
    + +
    + +
    +

    Test Endpoints

    + + + +
    + +
    +

    Load Test

    + +
    +
    +
    + + + + + ` + ctx.HTML(200, html, nil) + }) + + log.Println("Monitoring server starting on :8080") + log.Println("Dashboard: http://localhost:8080") + log.Println("Health: http://localhost:8080/health") + log.Println("Metrics: http://localhost:8080/metrics") + + app.Listen(":8080") +} +``` + +## 📚 Key Takeaways + +1. **Structured Logging**: Use structured logs for better analysis +2. **Metrics Collection**: Track key performance indicators +3. **Health Checks**: Monitor system health and dependencies +4. **Real-time Monitoring**: Build dashboards for operational visibility +5. **Alerting**: Set up alerts for critical issues + +## 🎯 Next Steps + +- Learn [Tutorial 14: Testing Strategies](./14-testing-strategies.md) +- Explore [Tutorial 15: Security Best Practices](./15-security-best-practices.md) +- Study [Tutorial 16: Performance Optimization](./16-performance-optimization.md) + +--- + +**Congratulations!** You've mastered monitoring and logging in Zoox! \ No newline at end of file diff --git a/tutorials/14-testing-strategies.md b/tutorials/14-testing-strategies.md new file mode 100644 index 0000000..130b24c --- /dev/null +++ b/tutorials/14-testing-strategies.md @@ -0,0 +1,641 @@ +# Tutorial 14: Testing Strategies + +## Overview +Learn comprehensive testing strategies for Zoox applications, including unit tests, integration tests, and end-to-end testing approaches. + +## Learning Objectives +- Write unit tests for handlers and middleware +- Create integration tests for API endpoints +- Mock external dependencies +- Test WebSocket connections +- Performance and load testing +- Test coverage analysis + +## Prerequisites +- Complete Tutorial 13: Monitoring & Logging +- Basic understanding of Go testing +- Familiarity with testing frameworks + +## Testing Fundamentals + +### Basic Unit Testing + +```go +package main + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/go-zoox/zoox" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// User represents a user in our system +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Created time.Time `json:"created"` +} + +// UserService handles user operations +type UserService struct { + users map[int]*User + nextID int +} + +func NewUserService() *UserService { + return &UserService{ + users: make(map[int]*User), + nextID: 1, + } +} + +func (s *UserService) CreateUser(name, email string) *User { + user := &User{ + ID: s.nextID, + Name: name, + Email: email, + Created: time.Now(), + } + s.users[s.nextID] = user + s.nextID++ + return user +} + +func (s *UserService) GetUser(id int) *User { + return s.users[id] +} + +func (s *UserService) GetAllUsers() []*User { + var users []*User + for _, user := range s.users { + users = append(users, user) + } + return users +} + +// Handlers +func createUserHandler(service *UserService) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + var req struct { + Name string `json:"name"` + Email string `json:"email"` + } + + if err := ctx.BindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, map[string]string{ + "error": "Invalid JSON", + }) + return + } + + if req.Name == "" || req.Email == "" { + ctx.JSON(http.StatusBadRequest, map[string]string{ + "error": "Name and email are required", + }) + return + } + + user := service.CreateUser(req.Name, req.Email) + ctx.JSON(http.StatusCreated, user) + } +} + +func getUserHandler(service *UserService) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + id := ctx.Param("id") + userID := 0 + if _, err := fmt.Sscanf(id, "%d", &userID); err != nil { + ctx.JSON(http.StatusBadRequest, map[string]string{ + "error": "Invalid user ID", + }) + return + } + + user := service.GetUser(userID) + if user == nil { + ctx.JSON(http.StatusNotFound, map[string]string{ + "error": "User not found", + }) + return + } + + ctx.JSON(http.StatusOK, user) + } +} + +// Unit Tests +func TestUserService_CreateUser(t *testing.T) { + service := NewUserService() + + user := service.CreateUser("John Doe", "john@example.com") + + assert.NotNil(t, user) + assert.Equal(t, 1, user.ID) + assert.Equal(t, "John Doe", user.Name) + assert.Equal(t, "john@example.com", user.Email) + assert.False(t, user.Created.IsZero()) +} + +func TestUserService_GetUser(t *testing.T) { + service := NewUserService() + + // Create a user + created := service.CreateUser("Jane Doe", "jane@example.com") + + // Get the user + retrieved := service.GetUser(created.ID) + + assert.NotNil(t, retrieved) + assert.Equal(t, created.ID, retrieved.ID) + assert.Equal(t, created.Name, retrieved.Name) + assert.Equal(t, created.Email, retrieved.Email) +} + +func TestUserService_GetUser_NotFound(t *testing.T) { + service := NewUserService() + + user := service.GetUser(999) + + assert.Nil(t, user) +} + +// Integration Tests +func TestCreateUserHandler(t *testing.T) { + service := NewUserService() + app := zoox.New() + app.Post("/users", createUserHandler(service)) + + tests := []struct { + name string + payload interface{} + expectedStatus int + expectedError string + }{ + { + name: "Valid user creation", + payload: map[string]string{ + "name": "John Doe", + "email": "john@example.com", + }, + expectedStatus: http.StatusCreated, + }, + { + name: "Missing name", + payload: map[string]string{"email": "john@example.com"}, + expectedStatus: http.StatusBadRequest, + expectedError: "Name and email are required", + }, + { + name: "Missing email", + payload: map[string]string{"name": "John Doe"}, + expectedStatus: http.StatusBadRequest, + expectedError: "Name and email are required", + }, + { + name: "Invalid JSON", + payload: "invalid json", + expectedStatus: http.StatusBadRequest, + expectedError: "Invalid JSON", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var body bytes.Buffer + json.NewEncoder(&body).Encode(tt.payload) + + req := httptest.NewRequest(http.MethodPost, "/users", &body) + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + app.ServeHTTP(w, req) + + assert.Equal(t, tt.expectedStatus, w.Code) + + if tt.expectedError != "" { + var response map[string]string + err := json.NewDecoder(w.Body).Decode(&response) + require.NoError(t, err) + assert.Equal(t, tt.expectedError, response["error"]) + } else { + var user User + err := json.NewDecoder(w.Body).Decode(&user) + require.NoError(t, err) + assert.NotZero(t, user.ID) + assert.NotEmpty(t, user.Name) + assert.NotEmpty(t, user.Email) + } + }) + } +} + +func TestGetUserHandler(t *testing.T) { + service := NewUserService() + created := service.CreateUser("John Doe", "john@example.com") + + app := zoox.New() + app.Get("/users/:id", getUserHandler(service)) + + tests := []struct { + name string + userID string + expectedStatus int + expectedError string + }{ + { + name: "Valid user ID", + userID: fmt.Sprintf("%d", created.ID), + expectedStatus: http.StatusOK, + }, + { + name: "User not found", + userID: "999", + expectedStatus: http.StatusNotFound, + expectedError: "User not found", + }, + { + name: "Invalid user ID", + userID: "abc", + expectedStatus: http.StatusBadRequest, + expectedError: "Invalid user ID", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/users/"+tt.userID, nil) + w := httptest.NewRecorder() + + app.ServeHTTP(w, req) + + assert.Equal(t, tt.expectedStatus, w.Code) + + if tt.expectedError != "" { + var response map[string]string + err := json.NewDecoder(w.Body).Decode(&response) + require.NoError(t, err) + assert.Equal(t, tt.expectedError, response["error"]) + } else { + var user User + err := json.NewDecoder(w.Body).Decode(&user) + require.NoError(t, err) + assert.Equal(t, created.ID, user.ID) + assert.Equal(t, created.Name, user.Name) + assert.Equal(t, created.Email, user.Email) + } + }) + } +} +``` + +## Middleware Testing + +```go +// Custom middleware for testing +func authMiddleware(validToken string) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + token := ctx.Header("Authorization") + if token != "Bearer "+validToken { + ctx.JSON(http.StatusUnauthorized, map[string]string{ + "error": "Unauthorized", + }) + ctx.Abort() + return + } + ctx.Next() + } +} + +func TestAuthMiddleware(t *testing.T) { + app := zoox.New() + validToken := "test-token" + + app.Use(authMiddleware(validToken)) + app.Get("/protected", func(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, map[string]string{ + "message": "Access granted", + }) + }) + + tests := []struct { + name string + token string + expectedStatus int + expectedError string + }{ + { + name: "Valid token", + token: "Bearer " + validToken, + expectedStatus: http.StatusOK, + }, + { + name: "Invalid token", + token: "Bearer invalid-token", + expectedStatus: http.StatusUnauthorized, + expectedError: "Unauthorized", + }, + { + name: "Missing token", + token: "", + expectedStatus: http.StatusUnauthorized, + expectedError: "Unauthorized", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/protected", nil) + if tt.token != "" { + req.Header.Set("Authorization", tt.token) + } + + w := httptest.NewRecorder() + app.ServeHTTP(w, req) + + assert.Equal(t, tt.expectedStatus, w.Code) + + if tt.expectedError != "" { + var response map[string]string + err := json.NewDecoder(w.Body).Decode(&response) + require.NoError(t, err) + assert.Equal(t, tt.expectedError, response["error"]) + } + }) + } +} +``` + +## WebSocket Testing + +```go +func TestWebSocketConnection(t *testing.T) { + app := zoox.New() + + app.Get("/ws", func(ctx *zoox.Context) { + ws, err := ctx.WebSocket() + if err != nil { + t.Errorf("WebSocket upgrade failed: %v", err) + return + } + defer ws.Close() + + for { + messageType, message, err := ws.ReadMessage() + if err != nil { + break + } + + // Echo the message back + err = ws.WriteMessage(messageType, message) + if err != nil { + break + } + } + }) + + server := httptest.NewServer(app) + defer server.Close() + + // Convert HTTP URL to WebSocket URL + wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws" + + // Connect to WebSocket + conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) + require.NoError(t, err) + defer conn.Close() + + // Send a message + testMessage := "Hello WebSocket" + err = conn.WriteMessage(websocket.TextMessage, []byte(testMessage)) + require.NoError(t, err) + + // Read the echoed message + messageType, message, err := conn.ReadMessage() + require.NoError(t, err) + assert.Equal(t, websocket.TextMessage, messageType) + assert.Equal(t, testMessage, string(message)) +} +``` + +## Mock Testing + +```go +// Mock external service +type EmailService interface { + SendEmail(to, subject, body string) error +} + +type MockEmailService struct { + SentEmails []struct { + To string + Subject string + Body string + } + ShouldFail bool +} + +func (m *MockEmailService) SendEmail(to, subject, body string) error { + if m.ShouldFail { + return errors.New("email service unavailable") + } + + m.SentEmails = append(m.SentEmails, struct { + To string + Subject string + Body string + }{ + To: to, + Subject: subject, + Body: body, + }) + + return nil +} + +func notifyUserHandler(emailService EmailService) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + var req struct { + Email string `json:"email"` + Message string `json:"message"` + } + + if err := ctx.BindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, map[string]string{ + "error": "Invalid JSON", + }) + return + } + + err := emailService.SendEmail(req.Email, "Notification", req.Message) + if err != nil { + ctx.JSON(http.StatusInternalServerError, map[string]string{ + "error": "Failed to send email", + }) + return + } + + ctx.JSON(http.StatusOK, map[string]string{ + "message": "Email sent successfully", + }) + } +} + +func TestNotifyUserHandler(t *testing.T) { + mockEmailService := &MockEmailService{} + app := zoox.New() + app.Post("/notify", notifyUserHandler(mockEmailService)) + + payload := map[string]string{ + "email": "user@example.com", + "message": "Test notification", + } + + var body bytes.Buffer + json.NewEncoder(&body).Encode(payload) + + req := httptest.NewRequest(http.MethodPost, "/notify", &body) + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + app.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Len(t, mockEmailService.SentEmails, 1) + assert.Equal(t, "user@example.com", mockEmailService.SentEmails[0].To) + assert.Equal(t, "Notification", mockEmailService.SentEmails[0].Subject) + assert.Equal(t, "Test notification", mockEmailService.SentEmails[0].Body) +} + +func TestNotifyUserHandler_EmailServiceFailure(t *testing.T) { + mockEmailService := &MockEmailService{ShouldFail: true} + app := zoox.New() + app.Post("/notify", notifyUserHandler(mockEmailService)) + + payload := map[string]string{ + "email": "user@example.com", + "message": "Test notification", + } + + var body bytes.Buffer + json.NewEncoder(&body).Encode(payload) + + req := httptest.NewRequest(http.MethodPost, "/notify", &body) + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + app.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Len(t, mockEmailService.SentEmails, 0) +} +``` + +## Performance Testing + +```go +func BenchmarkCreateUser(b *testing.B) { + service := NewUserService() + app := zoox.New() + app.Post("/users", createUserHandler(service)) + + payload := map[string]string{ + "name": "John Doe", + "email": "john@example.com", + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + var body bytes.Buffer + json.NewEncoder(&body).Encode(payload) + + req := httptest.NewRequest(http.MethodPost, "/users", &body) + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + app.ServeHTTP(w, req) + + if w.Code != http.StatusCreated { + b.Errorf("Expected status %d, got %d", http.StatusCreated, w.Code) + } + } +} + +func BenchmarkGetUser(b *testing.B) { + service := NewUserService() + user := service.CreateUser("John Doe", "john@example.com") + + app := zoox.New() + app.Get("/users/:id", getUserHandler(service)) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/users/%d", user.ID), nil) + w := httptest.NewRecorder() + + app.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + b.Errorf("Expected status %d, got %d", http.StatusOK, w.Code) + } + } +} +``` + +## Test Coverage + +```bash +# Run tests with coverage +go test -cover ./... + +# Generate detailed coverage report +go test -coverprofile=coverage.out ./... +go tool cover -html=coverage.out -o coverage.html + +# Set coverage threshold +go test -cover ./... | grep -E "coverage: [0-9]+\.[0-9]+% of statements" +``` + +## Hands-on Exercise: Complete Test Suite + +Create a comprehensive test suite for a blog API with the following requirements: + +1. **Unit Tests**: Test all business logic functions +2. **Integration Tests**: Test all API endpoints +3. **Middleware Tests**: Test authentication and authorization +4. **Mock Tests**: Mock external dependencies (database, email service) +5. **Performance Tests**: Benchmark critical operations +6. **Coverage**: Achieve >80% test coverage + +## Key Testing Principles + +1. **Test Pyramid**: More unit tests, fewer integration tests, minimal E2E tests +2. **Test Independence**: Each test should be independent and repeatable +3. **Clear Naming**: Test names should clearly describe what is being tested +4. **Arrange-Act-Assert**: Structure tests with clear setup, execution, and verification +5. **Mock External Dependencies**: Use mocks to isolate units under test +6. **Test Edge Cases**: Include tests for error conditions and boundary values + +## Next Steps + +- Tutorial 15: Performance Optimization - Learn optimization techniques +- Tutorial 16: Security Best Practices - Implement security measures +- Explore advanced testing patterns +- Learn about contract testing +- Practice with test-driven development (TDD) + +## Additional Resources + +- [Go Testing Package](https://golang.org/pkg/testing/) +- [Testify Framework](https://github.com/stretchr/testify) +- [Go Test Coverage](https://golang.org/doc/tutorial/add-a-test) +- [Testing Best Practices](https://golang.org/doc/effective_go.html#testing) \ No newline at end of file diff --git a/tutorials/15-performance-optimization.md b/tutorials/15-performance-optimization.md new file mode 100644 index 0000000..a7c5ce9 --- /dev/null +++ b/tutorials/15-performance-optimization.md @@ -0,0 +1,765 @@ +# Tutorial 15: Performance Optimization + +## Overview +Learn essential performance optimization techniques for Zoox applications, including caching, connection pooling, middleware optimization, and monitoring performance metrics. + +## Learning Objectives +- Implement effective caching strategies +- Optimize database connections and queries +- Use connection pooling and resource management +- Apply middleware optimization techniques +- Monitor and measure performance +- Implement rate limiting and throttling + +## Prerequisites +- Complete Tutorial 14: Testing Strategies +- Understanding of Go performance concepts +- Basic knowledge of caching systems + +## Caching Strategies + +### Memory Caching + +```go +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "strconv" + "sync" + "time" + + "github.com/go-zoox/zoox" +) + +// CacheItem represents a cached item with expiration +type CacheItem struct { + Data interface{} + ExpiresAt time.Time +} + +// MemoryCache provides in-memory caching +type MemoryCache struct { + items map[string]CacheItem + mutex sync.RWMutex +} + +func NewMemoryCache() *MemoryCache { + cache := &MemoryCache{ + items: make(map[string]CacheItem), + } + + // Start cleanup goroutine + go cache.cleanup() + + return cache +} + +func (c *MemoryCache) Set(key string, value interface{}, ttl time.Duration) { + c.mutex.Lock() + defer c.mutex.Unlock() + + c.items[key] = CacheItem{ + Data: value, + ExpiresAt: time.Now().Add(ttl), + } +} + +func (c *MemoryCache) Get(key string) (interface{}, bool) { + c.mutex.RLock() + defer c.mutex.RUnlock() + + item, exists := c.items[key] + if !exists || time.Now().After(item.ExpiresAt) { + return nil, false + } + + return item.Data, true +} + +func (c *MemoryCache) Delete(key string) { + c.mutex.Lock() + defer c.mutex.Unlock() + + delete(c.items, key) +} + +func (c *MemoryCache) cleanup() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + for range ticker.C { + c.mutex.Lock() + now := time.Now() + for key, item := range c.items { + if now.After(item.ExpiresAt) { + delete(c.items, key) + } + } + c.mutex.Unlock() + } +} + +// Response caching middleware +func cacheMiddleware(cache *MemoryCache, ttl time.Duration) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + // Only cache GET requests + if ctx.Method() != http.MethodGet { + ctx.Next() + return + } + + cacheKey := ctx.Request().URL.Path + "?" + ctx.Request().URL.RawQuery + + // Try to get from cache + if cached, found := cache.Get(cacheKey); found { + ctx.JSON(http.StatusOK, cached) + return + } + + // Capture response + originalWriter := ctx.Writer + responseCapture := &responseWriter{ + ResponseWriter: originalWriter, + body: make([]byte, 0), + } + ctx.Writer = responseCapture + + ctx.Next() + + // Cache successful responses + if responseCapture.statusCode == http.StatusOK && len(responseCapture.body) > 0 { + var data interface{} + if err := json.Unmarshal(responseCapture.body, &data); err == nil { + cache.Set(cacheKey, data, ttl) + } + } + + // Write the actual response + originalWriter.WriteHeader(responseCapture.statusCode) + originalWriter.Write(responseCapture.body) + } +} + +type responseWriter struct { + http.ResponseWriter + body []byte + statusCode int +} + +func (rw *responseWriter) Write(b []byte) (int, error) { + rw.body = append(rw.body, b...) + return len(b), nil +} + +func (rw *responseWriter) WriteHeader(statusCode int) { + rw.statusCode = statusCode +} + +// User service with caching +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Created time.Time `json:"created"` +} + +type UserService struct { + users map[int]*User + cache *MemoryCache + mutex sync.RWMutex +} + +func NewUserService(cache *MemoryCache) *UserService { + return &UserService{ + users: make(map[int]*User), + cache: cache, + } +} + +func (s *UserService) GetUser(id int) *User { + // Try cache first + cacheKey := fmt.Sprintf("user:%d", id) + if cached, found := s.cache.Get(cacheKey); found { + return cached.(*User) + } + + // Get from "database" + s.mutex.RLock() + user := s.users[id] + s.mutex.RUnlock() + + // Cache the result + if user != nil { + s.cache.Set(cacheKey, user, 10*time.Minute) + } + + return user +} + +func (s *UserService) CreateUser(name, email string) *User { + s.mutex.Lock() + defer s.mutex.Unlock() + + id := len(s.users) + 1 + user := &User{ + ID: id, + Name: name, + Email: email, + Created: time.Now(), + } + + s.users[id] = user + + // Cache the new user + cacheKey := fmt.Sprintf("user:%d", id) + s.cache.Set(cacheKey, user, 10*time.Minute) + + return user +} + +func (s *UserService) UpdateUser(id int, name, email string) *User { + s.mutex.Lock() + defer s.mutex.Unlock() + + user := s.users[id] + if user == nil { + return nil + } + + user.Name = name + user.Email = email + + // Invalidate cache + cacheKey := fmt.Sprintf("user:%d", id) + s.cache.Delete(cacheKey) + + // Cache the updated user + s.cache.Set(cacheKey, user, 10*time.Minute) + + return user +} +``` + +## Connection Pooling + +```go +// Database connection pool simulation +type DatabasePool struct { + connections chan *Connection + maxConns int +} + +type Connection struct { + ID int + InUse bool + LastUsed time.Time +} + +func NewDatabasePool(maxConns int) *DatabasePool { + pool := &DatabasePool{ + connections: make(chan *Connection, maxConns), + maxConns: maxConns, + } + + // Initialize connections + for i := 0; i < maxConns; i++ { + pool.connections <- &Connection{ + ID: i + 1, + InUse: false, + LastUsed: time.Now(), + } + } + + return pool +} + +func (p *DatabasePool) GetConnection() (*Connection, error) { + select { + case conn := <-p.connections: + conn.InUse = true + conn.LastUsed = time.Now() + return conn, nil + case <-time.After(5 * time.Second): + return nil, fmt.Errorf("connection pool timeout") + } +} + +func (p *DatabasePool) ReleaseConnection(conn *Connection) { + conn.InUse = false + conn.LastUsed = time.Now() + + select { + case p.connections <- conn: + // Connection returned to pool + default: + // Pool is full, connection will be discarded + } +} + +// Database service with connection pooling +type DatabaseService struct { + pool *DatabasePool +} + +func NewDatabaseService(maxConns int) *DatabaseService { + return &DatabaseService{ + pool: NewDatabasePool(maxConns), + } +} + +func (db *DatabaseService) QueryUser(id int) (*User, error) { + conn, err := db.pool.GetConnection() + if err != nil { + return nil, err + } + defer db.pool.ReleaseConnection(conn) + + // Simulate database query + time.Sleep(10 * time.Millisecond) + + return &User{ + ID: id, + Name: fmt.Sprintf("User %d", id), + Email: fmt.Sprintf("user%d@example.com", id), + Created: time.Now(), + }, nil +} +``` + +## Request Rate Limiting + +```go +// Rate limiter implementation +type RateLimiter struct { + requests map[string][]time.Time + mutex sync.Mutex + limit int + window time.Duration +} + +func NewRateLimiter(limit int, window time.Duration) *RateLimiter { + rl := &RateLimiter{ + requests: make(map[string][]time.Time), + limit: limit, + window: window, + } + + // Cleanup old entries + go rl.cleanup() + + return rl +} + +func (rl *RateLimiter) Allow(clientID string) bool { + rl.mutex.Lock() + defer rl.mutex.Unlock() + + now := time.Now() + cutoff := now.Add(-rl.window) + + // Clean old requests + requests := rl.requests[clientID] + var validRequests []time.Time + for _, req := range requests { + if req.After(cutoff) { + validRequests = append(validRequests, req) + } + } + + // Check if limit exceeded + if len(validRequests) >= rl.limit { + rl.requests[clientID] = validRequests + return false + } + + // Add current request + validRequests = append(validRequests, now) + rl.requests[clientID] = validRequests + + return true +} + +func (rl *RateLimiter) cleanup() { + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + for range ticker.C { + rl.mutex.Lock() + now := time.Now() + cutoff := now.Add(-rl.window) + + for clientID, requests := range rl.requests { + var validRequests []time.Time + for _, req := range requests { + if req.After(cutoff) { + validRequests = append(validRequests, req) + } + } + + if len(validRequests) == 0 { + delete(rl.requests, clientID) + } else { + rl.requests[clientID] = validRequests + } + } + rl.mutex.Unlock() + } +} + +// Rate limiting middleware +func rateLimitMiddleware(limiter *RateLimiter) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + clientID := ctx.ClientIP() + + if !limiter.Allow(clientID) { + ctx.JSON(http.StatusTooManyRequests, map[string]string{ + "error": "Rate limit exceeded", + }) + ctx.Abort() + return + } + + ctx.Next() + } +} +``` + +## Response Compression + +```go +// Compression middleware +func compressionMiddleware() zoox.HandlerFunc { + return func(ctx *zoox.Context) { + // Check if client accepts gzip + if !strings.Contains(ctx.Header("Accept-Encoding"), "gzip") { + ctx.Next() + return + } + + // Create gzip writer + ctx.Set("Content-Encoding", "gzip") + + originalWriter := ctx.Writer + gzipWriter := gzip.NewWriter(originalWriter) + defer gzipWriter.Close() + + // Replace the writer + ctx.Writer = &gzipResponseWriter{ + ResponseWriter: originalWriter, + gzipWriter: gzipWriter, + } + + ctx.Next() + } +} + +type gzipResponseWriter struct { + http.ResponseWriter + gzipWriter *gzip.Writer +} + +func (w *gzipResponseWriter) Write(b []byte) (int, error) { + return w.gzipWriter.Write(b) +} +``` + +## Performance Monitoring + +```go +// Performance monitoring middleware +type PerformanceMetrics struct { + RequestCount int64 + TotalDuration time.Duration + AverageDuration time.Duration + MaxDuration time.Duration + MinDuration time.Duration + mutex sync.Mutex +} + +func NewPerformanceMetrics() *PerformanceMetrics { + return &PerformanceMetrics{ + MinDuration: time.Hour, // Initialize with high value + } +} + +func (pm *PerformanceMetrics) Record(duration time.Duration) { + pm.mutex.Lock() + defer pm.mutex.Unlock() + + pm.RequestCount++ + pm.TotalDuration += duration + pm.AverageDuration = pm.TotalDuration / time.Duration(pm.RequestCount) + + if duration > pm.MaxDuration { + pm.MaxDuration = duration + } + + if duration < pm.MinDuration { + pm.MinDuration = duration + } +} + +func (pm *PerformanceMetrics) GetStats() map[string]interface{} { + pm.mutex.Lock() + defer pm.mutex.Unlock() + + return map[string]interface{}{ + "request_count": pm.RequestCount, + "total_duration": pm.TotalDuration.String(), + "average_duration": pm.AverageDuration.String(), + "max_duration": pm.MaxDuration.String(), + "min_duration": pm.MinDuration.String(), + } +} + +func performanceMiddleware(metrics *PerformanceMetrics) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + start := time.Now() + + ctx.Next() + + duration := time.Since(start) + metrics.Record(duration) + + // Add performance headers + ctx.Set("X-Response-Time", duration.String()) + } +} +``` + +## Complete Example Application + +```go +func main() { + // Initialize components + cache := NewMemoryCache() + userService := NewUserService(cache) + dbService := NewDatabaseService(10) + rateLimiter := NewRateLimiter(100, time.Minute) + metrics := NewPerformanceMetrics() + + app := zoox.New() + + // Apply middleware in order + app.Use(performanceMiddleware(metrics)) + app.Use(rateLimitMiddleware(rateLimiter)) + app.Use(compressionMiddleware()) + app.Use(cacheMiddleware(cache, 5*time.Minute)) + + // Routes + app.Post("/users", func(ctx *zoox.Context) { + var req struct { + Name string `json:"name"` + Email string `json:"email"` + } + + if err := ctx.BindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, map[string]string{ + "error": "Invalid JSON", + }) + return + } + + user := userService.CreateUser(req.Name, req.Email) + ctx.JSON(http.StatusCreated, user) + }) + + app.Get("/users/:id", func(ctx *zoox.Context) { + id, err := strconv.Atoi(ctx.Param("id")) + if err != nil { + ctx.JSON(http.StatusBadRequest, map[string]string{ + "error": "Invalid user ID", + }) + return + } + + user := userService.GetUser(id) + if user == nil { + ctx.JSON(http.StatusNotFound, map[string]string{ + "error": "User not found", + }) + return + } + + ctx.JSON(http.StatusOK, user) + }) + + app.Get("/users/:id/db", func(ctx *zoox.Context) { + id, err := strconv.Atoi(ctx.Param("id")) + if err != nil { + ctx.JSON(http.StatusBadRequest, map[string]string{ + "error": "Invalid user ID", + }) + return + } + + user, err := dbService.QueryUser(id) + if err != nil { + ctx.JSON(http.StatusInternalServerError, map[string]string{ + "error": err.Error(), + }) + return + } + + ctx.JSON(http.StatusOK, user) + }) + + app.Get("/metrics", func(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, metrics.GetStats()) + }) + + // Health check endpoint + app.Get("/health", func(ctx *zoox.Context) { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "status": "healthy", + "timestamp": time.Now(), + "uptime": time.Since(startTime).String(), + }) + }) + + fmt.Println("High-performance server starting on :8080") + log.Fatal(app.Listen(":8080")) +} + +var startTime = time.Now() +``` + +## Optimization Best Practices + +### 1. Memory Management +```go +// Use object pools for frequently allocated objects +var userPool = sync.Pool{ + New: func() interface{} { + return &User{} + }, +} + +func getUser() *User { + return userPool.Get().(*User) +} + +func putUser(user *User) { + // Reset user fields + user.ID = 0 + user.Name = "" + user.Email = "" + userPool.Put(user) +} +``` + +### 2. JSON Optimization +```go +// Pre-allocate JSON encoders +var jsonEncoderPool = sync.Pool{ + New: func() interface{} { + return json.NewEncoder(nil) + }, +} + +func writeJSON(w http.ResponseWriter, data interface{}) error { + encoder := jsonEncoderPool.Get().(*json.Encoder) + defer jsonEncoderPool.Put(encoder) + + encoder.Reset(w) + return encoder.Encode(data) +} +``` + +### 3. Database Query Optimization +```go +// Use prepared statements +type PreparedQueries struct { + getUserByID *sql.Stmt + createUser *sql.Stmt + updateUser *sql.Stmt +} + +func NewPreparedQueries(db *sql.DB) (*PreparedQueries, error) { + getUserByID, err := db.Prepare("SELECT id, name, email FROM users WHERE id = ?") + if err != nil { + return nil, err + } + + createUser, err := db.Prepare("INSERT INTO users (name, email) VALUES (?, ?)") + if err != nil { + return nil, err + } + + updateUser, err := db.Prepare("UPDATE users SET name = ?, email = ? WHERE id = ?") + if err != nil { + return nil, err + } + + return &PreparedQueries{ + getUserByID: getUserByID, + createUser: createUser, + updateUser: updateUser, + }, nil +} +``` + +## Performance Testing + +```go +// Load testing helper +func loadTest(url string, concurrent int, requests int) { + var wg sync.WaitGroup + start := time.Now() + + for i := 0; i < concurrent; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + client := &http.Client{ + Timeout: 10 * time.Second, + } + + for j := 0; j < requests/concurrent; j++ { + resp, err := client.Get(url) + if err != nil { + fmt.Printf("Error: %v\n", err) + continue + } + resp.Body.Close() + } + }() + } + + wg.Wait() + duration := time.Since(start) + + fmt.Printf("Completed %d requests in %v\n", requests, duration) + fmt.Printf("Requests per second: %.2f\n", float64(requests)/duration.Seconds()) +} +``` + +## Key Takeaways + +1. **Caching**: Implement multi-layer caching (memory, Redis, CDN) +2. **Connection Pooling**: Reuse database connections effectively +3. **Rate Limiting**: Protect against abuse and ensure fair usage +4. **Compression**: Reduce bandwidth usage with gzip compression +5. **Monitoring**: Track performance metrics continuously +6. **Memory Management**: Use object pools and avoid memory leaks +7. **Database Optimization**: Use prepared statements and query optimization + +## Next Steps + +- Tutorial 16: Security Best Practices - Implement security measures +- Tutorial 17: Deployment Strategies - Deploy optimized applications +- Explore profiling tools (pprof) +- Learn about microservices optimization +- Study CDN and edge computing strategies + +## Additional Resources + +- [Go Performance Tips](https://golang.org/doc/effective_go.html#performance) +- [pprof Profiling](https://golang.org/pkg/runtime/pprof/) +- [Benchmarking in Go](https://golang.org/pkg/testing/#hdr-Benchmarks) +- [Memory Management](https://golang.org/doc/gc-guide) \ No newline at end of file diff --git a/tutorials/16-security-best-practices.md b/tutorials/16-security-best-practices.md new file mode 100644 index 0000000..f0a87e8 --- /dev/null +++ b/tutorials/16-security-best-practices.md @@ -0,0 +1,880 @@ +# Tutorial 16: Security Best Practices + +## Overview +Learn essential security practices for Zoox applications, including authentication, authorization, input validation, and protection against common vulnerabilities. + +## Learning Objectives +- Implement secure authentication mechanisms +- Apply proper authorization controls +- Validate and sanitize user inputs +- Protect against OWASP Top 10 vulnerabilities +- Secure API endpoints and data transmission +- Monitor security events and incidents + +## Prerequisites +- Complete Tutorial 15: Performance Optimization +- Understanding of web security concepts +- Knowledge of common attack vectors + +## Authentication Security + +### JWT Implementation with Security + +```go +package main + +import ( + "crypto/rand" + "encoding/hex" + "errors" + "log" + "net/http" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/go-zoox/zoox" + "golang.org/x/crypto/bcrypt" +) + +// Security configuration +type SecurityConfig struct { + JWTSecret []byte + TokenExpiry time.Duration + RefreshTokenExpiry time.Duration + BcryptCost int + MaxLoginAttempts int + LockoutDuration time.Duration +} + +// User with security fields +type SecureUser struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + PasswordHash string `json:"-"` + Role string `json:"role"` + LoginAttempts int `json:"-"` + LockedUntil time.Time `json:"-"` + LastLogin time.Time `json:"last_login"` + TwoFactorSecret string `json:"-"` + TwoFactorEnabled bool `json:"two_factor_enabled"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` +} + +// JWT Claims +type JWTClaims struct { + UserID int `json:"user_id"` + Username string `json:"username"` + Role string `json:"role"` + jwt.RegisteredClaims +} + +// Secure authentication service +type AuthService struct { + users map[string]*SecureUser + config SecurityConfig +} + +func NewAuthService(config SecurityConfig) *AuthService { + return &AuthService{ + users: make(map[string]*SecureUser), + config: config, + } +} + +func (s *AuthService) HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), s.config.BcryptCost) + return string(bytes), err +} + +func (s *AuthService) CheckPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +func (s *AuthService) GenerateToken(user *SecureUser) (string, error) { + claims := JWTClaims{ + UserID: user.ID, + Username: user.Username, + Role: user.Role, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.config.TokenExpiry)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "zoox-app", + Subject: user.Username, + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(s.config.JWTSecret) +} + +func (s *AuthService) ValidateToken(tokenString string) (*JWTClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + return s.config.JWTSecret, nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid { + return claims, nil + } + + return nil, errors.New("invalid token") +} + +func (s *AuthService) Register(username, email, password string) (*SecureUser, error) { + // Check if user exists + if _, exists := s.users[username]; exists { + return nil, errors.New("username already exists") + } + + // Validate password strength + if err := s.validatePasswordStrength(password); err != nil { + return nil, err + } + + // Hash password + hashedPassword, err := s.HashPassword(password) + if err != nil { + return nil, err + } + + user := &SecureUser{ + ID: len(s.users) + 1, + Username: username, + Email: email, + PasswordHash: hashedPassword, + Role: "user", + Created: time.Now(), + Updated: time.Now(), + } + + s.users[username] = user + return user, nil +} + +func (s *AuthService) Login(username, password string) (*SecureUser, string, error) { + user, exists := s.users[username] + if !exists { + return nil, "", errors.New("invalid credentials") + } + + // Check if account is locked + if time.Now().Before(user.LockedUntil) { + return nil, "", errors.New("account locked due to too many failed attempts") + } + + // Validate password + if !s.CheckPassword(password, user.PasswordHash) { + user.LoginAttempts++ + if user.LoginAttempts >= s.config.MaxLoginAttempts { + user.LockedUntil = time.Now().Add(s.config.LockoutDuration) + } + return nil, "", errors.New("invalid credentials") + } + + // Reset login attempts on successful login + user.LoginAttempts = 0 + user.LastLogin = time.Now() + + // Generate token + token, err := s.GenerateToken(user) + if err != nil { + return nil, "", err + } + + return user, token, nil +} + +func (s *AuthService) validatePasswordStrength(password string) error { + if len(password) < 8 { + return errors.New("password must be at least 8 characters long") + } + + hasUpper := false + hasLower := false + hasDigit := false + hasSpecial := false + + for _, char := range password { + switch { + case 'A' <= char && char <= 'Z': + hasUpper = true + case 'a' <= char && char <= 'z': + hasLower = true + case '0' <= char && char <= '9': + hasDigit = true + default: + hasSpecial = true + } + } + + if !hasUpper || !hasLower || !hasDigit || !hasSpecial { + return errors.New("password must contain uppercase, lowercase, digit, and special character") + } + + return nil +} + +// Security middleware +func authMiddleware(authService *AuthService) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + authHeader := ctx.Header("Authorization") + if authHeader == "" { + ctx.JSON(http.StatusUnauthorized, map[string]string{ + "error": "Authorization header required", + }) + ctx.Abort() + return + } + + tokenParts := strings.Split(authHeader, " ") + if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { + ctx.JSON(http.StatusUnauthorized, map[string]string{ + "error": "Invalid authorization header format", + }) + ctx.Abort() + return + } + + claims, err := authService.ValidateToken(tokenParts[1]) + if err != nil { + ctx.JSON(http.StatusUnauthorized, map[string]string{ + "error": "Invalid token", + }) + ctx.Abort() + return + } + + // Set user context + ctx.Set("user_id", claims.UserID) + ctx.Set("username", claims.Username) + ctx.Set("role", claims.Role) + + ctx.Next() + } +} + +// Role-based authorization middleware +func requireRole(role string) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + userRole, exists := ctx.Get("role") + if !exists { + ctx.JSON(http.StatusForbidden, map[string]string{ + "error": "No role information found", + }) + ctx.Abort() + return + } + + if userRole != role && userRole != "admin" { + ctx.JSON(http.StatusForbidden, map[string]string{ + "error": "Insufficient permissions", + }) + ctx.Abort() + return + } + + ctx.Next() + } +} +``` + +## Input Validation and Sanitization + +```go +import ( + "html" + "regexp" + "strings" + "unicode" +) + +// Input validator +type InputValidator struct { + emailRegex *regexp.Regexp + phoneRegex *regexp.Regexp + usernameRegex *regexp.Regexp +} + +func NewInputValidator() *InputValidator { + return &InputValidator{ + emailRegex: regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`), + phoneRegex: regexp.MustCompile(`^\+?[1-9]\d{1,14}$`), + usernameRegex: regexp.MustCompile(`^[a-zA-Z0-9_]{3,20}$`), + } +} + +func (v *InputValidator) ValidateEmail(email string) error { + if !v.emailRegex.MatchString(email) { + return errors.New("invalid email format") + } + return nil +} + +func (v *InputValidator) ValidateUsername(username string) error { + if !v.usernameRegex.MatchString(username) { + return errors.New("username must be 3-20 characters, alphanumeric or underscore only") + } + return nil +} + +func (v *InputValidator) SanitizeHTML(input string) string { + return html.EscapeString(input) +} + +func (v *InputValidator) SanitizeString(input string) string { + // Remove non-printable characters + result := strings.Map(func(r rune) rune { + if unicode.IsPrint(r) { + return r + } + return -1 + }, input) + + // Trim whitespace + return strings.TrimSpace(result) +} + +func (v *InputValidator) ValidateStringLength(input string, minLen, maxLen int) error { + length := len(strings.TrimSpace(input)) + if length < minLen { + return errors.New(fmt.Sprintf("input too short, minimum %d characters", minLen)) + } + if length > maxLen { + return errors.New(fmt.Sprintf("input too long, maximum %d characters", maxLen)) + } + return nil +} + +// Input validation middleware +func validationMiddleware(validator *InputValidator) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + // Get content type + contentType := ctx.Header("Content-Type") + + // Validate JSON content type for POST/PUT requests + if (ctx.Method() == http.MethodPost || ctx.Method() == http.MethodPut) && + !strings.Contains(contentType, "application/json") { + ctx.JSON(http.StatusBadRequest, map[string]string{ + "error": "Content-Type must be application/json", + }) + ctx.Abort() + return + } + + ctx.Next() + } +} +``` + +## SQL Injection Prevention + +```go +import ( + "database/sql" + "fmt" +) + +// Safe database operations +type SafeDB struct { + db *sql.DB +} + +func NewSafeDB(db *sql.DB) *SafeDB { + return &SafeDB{db: db} +} + +// Always use prepared statements +func (sdb *SafeDB) GetUserByID(userID int) (*SecureUser, error) { + query := "SELECT id, username, email, role, created FROM users WHERE id = ?" + + var user SecureUser + err := sdb.db.QueryRow(query, userID).Scan( + &user.ID, + &user.Username, + &user.Email, + &user.Role, + &user.Created, + ) + + if err != nil { + if err == sql.ErrNoRows { + return nil, errors.New("user not found") + } + return nil, err + } + + return &user, nil +} + +func (sdb *SafeDB) SearchUsers(searchTerm string, limit int) ([]*SecureUser, error) { + // Parameterized query to prevent SQL injection + query := ` + SELECT id, username, email, role, created + FROM users + WHERE username LIKE ? OR email LIKE ? + ORDER BY username + LIMIT ? + ` + + searchPattern := "%" + searchTerm + "%" + + rows, err := sdb.db.Query(query, searchPattern, searchPattern, limit) + if err != nil { + return nil, err + } + defer rows.Close() + + var users []*SecureUser + for rows.Next() { + var user SecureUser + err := rows.Scan( + &user.ID, + &user.Username, + &user.Email, + &user.Role, + &user.Created, + ) + if err != nil { + return nil, err + } + users = append(users, &user) + } + + return users, rows.Err() +} +``` + +## CORS and Security Headers + +```go +// Security headers middleware +func securityHeadersMiddleware() zoox.HandlerFunc { + return func(ctx *zoox.Context) { + // Prevent XSS attacks + ctx.Set("X-Content-Type-Options", "nosniff") + ctx.Set("X-Frame-Options", "DENY") + ctx.Set("X-XSS-Protection", "1; mode=block") + + // HSTS (HTTPS only) + ctx.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") + + // Content Security Policy + ctx.Set("Content-Security-Policy", + "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'") + + // Referrer Policy + ctx.Set("Referrer-Policy", "strict-origin-when-cross-origin") + + // Remove server information + ctx.Set("Server", "") + + ctx.Next() + } +} + +// CORS middleware with security +func corsMiddleware(allowedOrigins []string) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + origin := ctx.Header("Origin") + + // Check if origin is allowed + allowed := false + for _, allowedOrigin := range allowedOrigins { + if origin == allowedOrigin { + allowed = true + break + } + } + + if allowed { + ctx.Set("Access-Control-Allow-Origin", origin) + } + + ctx.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + ctx.Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + ctx.Set("Access-Control-Allow-Credentials", "true") + ctx.Set("Access-Control-Max-Age", "86400") + + // Handle preflight requests + if ctx.Method() == http.MethodOptions { + ctx.Status(http.StatusNoContent) + ctx.Abort() + return + } + + ctx.Next() + } +} +``` + +## Rate Limiting for Security + +```go +// Advanced rate limiter with different limits per endpoint +type EndpointRateLimiter struct { + limiters map[string]*RateLimiter + mutex sync.RWMutex +} + +func NewEndpointRateLimiter() *EndpointRateLimiter { + return &EndpointRateLimiter{ + limiters: make(map[string]*RateLimiter), + } +} + +func (erl *EndpointRateLimiter) AddEndpoint(endpoint string, limit int, window time.Duration) { + erl.mutex.Lock() + defer erl.mutex.Unlock() + + erl.limiters[endpoint] = NewRateLimiter(limit, window) +} + +func (erl *EndpointRateLimiter) Allow(endpoint, clientID string) bool { + erl.mutex.RLock() + limiter, exists := erl.limiters[endpoint] + erl.mutex.RUnlock() + + if !exists { + return true // No rate limit for this endpoint + } + + return limiter.Allow(clientID) +} + +// Security-focused rate limiting middleware +func securityRateLimitMiddleware(limiter *EndpointRateLimiter) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + clientID := ctx.ClientIP() + endpoint := ctx.Request().URL.Path + + // Apply stricter limits to sensitive endpoints + if strings.Contains(endpoint, "/login") || strings.Contains(endpoint, "/register") { + if !limiter.Allow(endpoint, clientID) { + // Log potential brute force attack + log.Printf("Rate limit exceeded for sensitive endpoint %s from IP %s", endpoint, clientID) + + ctx.JSON(http.StatusTooManyRequests, map[string]string{ + "error": "Too many attempts, please try again later", + }) + ctx.Abort() + return + } + } + + ctx.Next() + } +} +``` + +## Secure File Upload + +```go +import ( + "crypto/sha256" + "io" + "mime/multipart" + "path/filepath" +) + +type FileUploadConfig struct { + MaxFileSize int64 + AllowedTypes []string + UploadDirectory string + ScanForViruses bool +} + +type SecureFileUpload struct { + config FileUploadConfig +} + +func NewSecureFileUpload(config FileUploadConfig) *SecureFileUpload { + return &SecureFileUpload{config: config} +} + +func (sfu *SecureFileUpload) ValidateFile(header *multipart.FileHeader) error { + // Check file size + if header.Size > sfu.config.MaxFileSize { + return errors.New("file too large") + } + + // Check file extension + ext := strings.ToLower(filepath.Ext(header.Filename)) + allowed := false + for _, allowedType := range sfu.config.AllowedTypes { + if ext == allowedType { + allowed = true + break + } + } + + if !allowed { + return errors.New("file type not allowed") + } + + return nil +} + +func (sfu *SecureFileUpload) SaveFile(file multipart.File, header *multipart.FileHeader) (string, error) { + // Generate secure filename + hash := sha256.New() + io.Copy(hash, file) + file.Seek(0, 0) // Reset file pointer + + hashString := hex.EncodeToString(hash.Sum(nil)) + ext := filepath.Ext(header.Filename) + filename := hashString + ext + + // Create secure file path + filepath := filepath.Join(sfu.config.UploadDirectory, filename) + + // Save file with restricted permissions + dest, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return "", err + } + defer dest.Close() + + _, err = io.Copy(dest, file) + if err != nil { + return "", err + } + + return filename, nil +} + +// Secure file upload handler +func secureUploadHandler(uploader *SecureFileUpload) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + // Parse multipart form + err := ctx.Request().ParseMultipartForm(uploader.config.MaxFileSize) + if err != nil { + ctx.JSON(http.StatusBadRequest, map[string]string{ + "error": "Invalid multipart form", + }) + return + } + + file, header, err := ctx.Request().FormFile("file") + if err != nil { + ctx.JSON(http.StatusBadRequest, map[string]string{ + "error": "No file provided", + }) + return + } + defer file.Close() + + // Validate file + if err := uploader.ValidateFile(header); err != nil { + ctx.JSON(http.StatusBadRequest, map[string]string{ + "error": err.Error(), + }) + return + } + + // Save file + filename, err := uploader.SaveFile(file, header) + if err != nil { + ctx.JSON(http.StatusInternalServerError, map[string]string{ + "error": "Failed to save file", + }) + return + } + + ctx.JSON(http.StatusOK, map[string]string{ + "message": "File uploaded successfully", + "filename": filename, + }) + } +} +``` + +## Security Logging and Monitoring + +```go +// Security event types +type SecurityEventType string + +const ( + LoginAttempt SecurityEventType = "login_attempt" + LoginSuccess SecurityEventType = "login_success" + LoginFailure SecurityEventType = "login_failure" + UnauthorizedAccess SecurityEventType = "unauthorized_access" + RateLimitExceeded SecurityEventType = "rate_limit_exceeded" + SuspiciousActivity SecurityEventType = "suspicious_activity" +) + +// Security event +type SecurityEvent struct { + Type SecurityEventType `json:"type"` + UserID int `json:"user_id,omitempty"` + Username string `json:"username,omitempty"` + IP string `json:"ip"` + UserAgent string `json:"user_agent"` + Endpoint string `json:"endpoint"` + Timestamp time.Time `json:"timestamp"` + Details map[string]interface{} `json:"details,omitempty"` + Severity string `json:"severity"` +} + +// Security logger +type SecurityLogger struct { + events []SecurityEvent + mutex sync.Mutex +} + +func NewSecurityLogger() *SecurityLogger { + return &SecurityLogger{ + events: make([]SecurityEvent, 0), + } +} + +func (sl *SecurityLogger) LogEvent(event SecurityEvent) { + sl.mutex.Lock() + defer sl.mutex.Unlock() + + event.Timestamp = time.Now() + sl.events = append(sl.events, event) + + // Log to console/file + log.Printf("SECURITY EVENT: %s - %s from %s", event.Type, event.Username, event.IP) + + // Keep only last 1000 events + if len(sl.events) > 1000 { + sl.events = sl.events[len(sl.events)-1000:] + } +} + +func (sl *SecurityLogger) GetEvents(limit int) []SecurityEvent { + sl.mutex.Lock() + defer sl.mutex.Unlock() + + if limit > len(sl.events) { + limit = len(sl.events) + } + + start := len(sl.events) - limit + return sl.events[start:] +} + +// Security monitoring middleware +func securityMonitoringMiddleware(logger *SecurityLogger) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + start := time.Now() + + ctx.Next() + + // Log security events based on response + status := ctx.Writer.Status() + if status == http.StatusUnauthorized || status == http.StatusForbidden { + logger.LogEvent(SecurityEvent{ + Type: UnauthorizedAccess, + IP: ctx.ClientIP(), + UserAgent: ctx.Header("User-Agent"), + Endpoint: ctx.Request().URL.Path, + Severity: "medium", + Details: map[string]interface{}{ + "status_code": status, + "duration": time.Since(start).String(), + }, + }) + } + } +} +``` + +## Complete Secure Application Example + +```go +func main() { + // Security configuration + config := SecurityConfig{ + JWTSecret: []byte("your-super-secret-jwt-key-change-this"), + TokenExpiry: 24 * time.Hour, + RefreshTokenExpiry: 7 * 24 * time.Hour, + BcryptCost: 12, + MaxLoginAttempts: 5, + LockoutDuration: 15 * time.Minute, + } + + // Initialize services + authService := NewAuthService(config) + validator := NewInputValidator() + secLogger := NewSecurityLogger() + rateLimiter := NewEndpointRateLimiter() + + // Configure rate limits + rateLimiter.AddEndpoint("/login", 5, time.Minute) + rateLimiter.AddEndpoint("/register", 3, time.Minute) + + app := zoox.New() + + // Security middleware stack + app.Use(securityHeadersMiddleware()) + app.Use(corsMiddleware([]string{"https://yourdomain.com"})) + app.Use(securityRateLimitMiddleware(rateLimiter)) + app.Use(validationMiddleware(validator)) + app.Use(securityMonitoringMiddleware(secLogger)) + + // Public routes + app.Post("/register", registerHandler(authService, validator)) + app.Post("/login", loginHandler(authService, secLogger)) + + // Protected routes + protected := app.Group("/api") + protected.Use(authMiddleware(authService)) + + protected.Get("/profile", getProfileHandler()) + protected.Put("/profile", updateProfileHandler(validator)) + + // Admin routes + admin := protected.Group("/admin") + admin.Use(requireRole("admin")) + + admin.Get("/users", listUsersHandler()) + admin.Get("/security-events", getSecurityEventsHandler(secLogger)) + + fmt.Println("Secure server starting on :8443 (HTTPS)") + log.Fatal(app.ListenTLS(":8443", "server.crt", "server.key")) +} + +// Secure handlers implementation would go here... +``` + +## Key Security Takeaways + +1. **Authentication**: Use strong password policies and secure token management +2. **Authorization**: Implement role-based access control +3. **Input Validation**: Validate and sanitize all user inputs +4. **SQL Injection**: Always use parameterized queries +5. **XSS Protection**: Escape output and use security headers +6. **CSRF Protection**: Implement CSRF tokens for state-changing operations +7. **HTTPS**: Always use HTTPS in production +8. **Rate Limiting**: Protect against brute force and DoS attacks +9. **Security Monitoring**: Log and monitor security events +10. **Regular Updates**: Keep dependencies updated and scan for vulnerabilities + +## Next Steps + +- Tutorial 17: Deployment Strategies - Deploy secure applications +- Tutorial 18: Production Monitoring - Monitor production systems +- Implement security scanning in CI/CD +- Learn about penetration testing +- Study OWASP guidelines and best practices + +## Additional Resources + +- [OWASP Top 10](https://owasp.org/www-project-top-ten/) +- [Go Security Guidelines](https://golang.org/doc/security) +- [JWT Best Practices](https://tools.ietf.org/html/draft-ietf-oauth-jwt-bcp-07) +- [Secure Coding Practices](https://owasp.org/www-project-secure-coding-practices-quick-reference-guide/) \ No newline at end of file diff --git a/tutorials/17-deployment-strategies.md b/tutorials/17-deployment-strategies.md new file mode 100644 index 0000000..a44e063 --- /dev/null +++ b/tutorials/17-deployment-strategies.md @@ -0,0 +1,830 @@ +# Tutorial 17: Deployment Strategies + +## Overview +Learn comprehensive deployment strategies for Zoox applications, including containerization, cloud deployment, CI/CD pipelines, and production best practices. + +## Learning Objectives +- Containerize Zoox applications with Docker +- Deploy to various cloud platforms +- Set up CI/CD pipelines +- Implement blue-green deployments +- Configure production monitoring +- Handle scaling and load balancing + +## Prerequisites +- Complete Tutorial 16: Security Best Practices +- Basic understanding of Docker and containers +- Familiarity with cloud platforms + +## Docker Containerization + +### Dockerfile for Zoox Application + +```dockerfile +# Multi-stage build for optimized production image +FROM golang:1.21-alpine AS builder + +# Install necessary packages +RUN apk add --no-cache git ca-certificates tzdata + +# Set working directory +WORKDIR /app + +# Copy go mod files +COPY go.mod go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy source code +COPY . . + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . + +# Production stage +FROM alpine:latest + +# Install ca-certificates for HTTPS requests +RUN apk --no-cache add ca-certificates tzdata + +WORKDIR /root/ + +# Copy the binary from builder stage +COPY --from=builder /app/main . + +# Copy configuration files if needed +COPY --from=builder /app/config ./config + +# Create non-root user for security +RUN adduser -D -s /bin/sh appuser +USER appuser + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 + +# Run the application +CMD ["./main"] +``` + +### Docker Compose for Development + +```yaml +# docker-compose.yml +version: '3.8' + +services: + app: + build: . + ports: + - "8080:8080" + environment: + - ENV=development + - DB_HOST=postgres + - REDIS_HOST=redis + depends_on: + - postgres + - redis + volumes: + - ./logs:/app/logs + restart: unless-stopped + + postgres: + image: postgres:15-alpine + environment: + - POSTGRES_DB=zoox_app + - POSTGRES_USER=user + - POSTGRES_PASSWORD=password + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + restart: unless-stopped + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + restart: unless-stopped + + nginx: + image: nginx:alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - ./ssl:/etc/nginx/ssl + depends_on: + - app + restart: unless-stopped + +volumes: + postgres_data: + redis_data: +``` + +## Kubernetes Deployment + +### Kubernetes Manifests + +```yaml +# k8s/namespace.yaml +apiVersion: v1 +kind: Namespace +metadata: + name: zoox-app + +--- +# k8s/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config + namespace: zoox-app +data: + app.env: | + ENV=production + PORT=8080 + LOG_LEVEL=info + +--- +# k8s/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: app-secrets + namespace: zoox-app +type: Opaque +data: + # Base64 encoded values + DB_PASSWORD: + JWT_SECRET: + +--- +# k8s/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: zoox-app + namespace: zoox-app + labels: + app: zoox-app +spec: + replicas: 3 + selector: + matchLabels: + app: zoox-app + template: + metadata: + labels: + app: zoox-app + spec: + containers: + - name: zoox-app + image: your-registry/zoox-app:latest + ports: + - containerPort: 8080 + env: + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: app-secrets + key: DB_PASSWORD + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: app-secrets + key: JWT_SECRET + envFrom: + - configMapRef: + name: app-config + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /ready + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + +--- +# k8s/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: zoox-app-service + namespace: zoox-app +spec: + selector: + app: zoox-app + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + type: ClusterIP + +--- +# k8s/ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: zoox-app-ingress + namespace: zoox-app + annotations: + kubernetes.io/ingress.class: "nginx" + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/ssl-redirect: "true" +spec: + tls: + - hosts: + - your-domain.com + secretName: app-tls + rules: + - host: your-domain.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: zoox-app-service + port: + number: 80 + +--- +# k8s/hpa.yaml +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: zoox-app-hpa + namespace: zoox-app +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: zoox-app + minReplicas: 3 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 +``` + +## CI/CD Pipeline + +### GitHub Actions Workflow + +```yaml +# .github/workflows/deploy.yml +name: Build and Deploy + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.21 + + - name: Run tests + run: | + go mod download + go test -v ./... + go test -race -coverprofile=coverage.out ./... + + - name: Upload coverage + uses: codecov/codecov-action@v3 + with: + file: ./coverage.out + + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Run security scan + uses: securecodewarrior/github-action-add-sarif@v1 + with: + sarif-file: 'gosec-report.sarif' + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + + build: + needs: [test, security] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha,prefix=commit- + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + deploy-staging: + needs: build + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/develop' + environment: staging + + steps: + - name: Deploy to staging + run: | + echo "Deploying to staging environment" + # Add deployment commands here + + deploy-production: + needs: build + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + environment: production + + steps: + - name: Deploy to production + run: | + echo "Deploying to production environment" + # Add deployment commands here +``` + +## Configuration Management + +### Environment-based Configuration + +```go +// config/config.go +package config + +import ( + "os" + "strconv" + "time" +) + +type Config struct { + Server ServerConfig + Database DatabaseConfig + Redis RedisConfig + JWT JWTConfig + Logging LoggingConfig +} + +type ServerConfig struct { + Port string + ReadTimeout time.Duration + WriteTimeout time.Duration + IdleTimeout time.Duration +} + +type DatabaseConfig struct { + Host string + Port string + User string + Password string + Name string + SSLMode string + MaxConns int +} + +type RedisConfig struct { + Host string + Port string + Password string + DB int +} + +type JWTConfig struct { + Secret string + Expiry time.Duration +} + +type LoggingConfig struct { + Level string + Format string +} + +func Load() *Config { + return &Config{ + Server: ServerConfig{ + Port: getEnv("PORT", "8080"), + ReadTimeout: getDuration("READ_TIMEOUT", 15*time.Second), + WriteTimeout: getDuration("WRITE_TIMEOUT", 15*time.Second), + IdleTimeout: getDuration("IDLE_TIMEOUT", 60*time.Second), + }, + Database: DatabaseConfig{ + Host: getEnv("DB_HOST", "localhost"), + Port: getEnv("DB_PORT", "5432"), + User: getEnv("DB_USER", "user"), + Password: getEnv("DB_PASSWORD", "password"), + Name: getEnv("DB_NAME", "zoox_app"), + SSLMode: getEnv("DB_SSL_MODE", "disable"), + MaxConns: getInt("DB_MAX_CONNS", 25), + }, + Redis: RedisConfig{ + Host: getEnv("REDIS_HOST", "localhost"), + Port: getEnv("REDIS_PORT", "6379"), + Password: getEnv("REDIS_PASSWORD", ""), + DB: getInt("REDIS_DB", 0), + }, + JWT: JWTConfig{ + Secret: getEnv("JWT_SECRET", "change-this-secret"), + Expiry: getDuration("JWT_EXPIRY", 24*time.Hour), + }, + Logging: LoggingConfig{ + Level: getEnv("LOG_LEVEL", "info"), + Format: getEnv("LOG_FORMAT", "json"), + }, + } +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +func getInt(key string, defaultValue int) int { + if value := os.Getenv(key); value != "" { + if intValue, err := strconv.Atoi(value); err == nil { + return intValue + } + } + return defaultValue +} + +func getDuration(key string, defaultValue time.Duration) time.Duration { + if value := os.Getenv(key); value != "" { + if duration, err := time.ParseDuration(value); err == nil { + return duration + } + } + return defaultValue +} +``` + +## Health Checks and Monitoring + +```go +// health/health.go +package health + +import ( + "context" + "database/sql" + "net/http" + "time" + + "github.com/go-redis/redis/v8" + "github.com/go-zoox/zoox" +) + +type HealthChecker struct { + db *sql.DB + redis *redis.Client +} + +func NewHealthChecker(db *sql.DB, redis *redis.Client) *HealthChecker { + return &HealthChecker{ + db: db, + redis: redis, + } +} + +func (h *HealthChecker) HealthHandler() zoox.HandlerFunc { + return func(ctx *zoox.Context) { + status := h.checkHealth() + + if status["status"] == "healthy" { + ctx.JSON(http.StatusOK, status) + } else { + ctx.JSON(http.StatusServiceUnavailable, status) + } + } +} + +func (h *HealthChecker) ReadinessHandler() zoox.HandlerFunc { + return func(ctx *zoox.Context) { + ready := h.checkReadiness() + + if ready["ready"] == true { + ctx.JSON(http.StatusOK, ready) + } else { + ctx.JSON(http.StatusServiceUnavailable, ready) + } + } +} + +func (h *HealthChecker) checkHealth() map[string]interface{} { + checks := make(map[string]interface{}) + + // Database health check + if h.db != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := h.db.PingContext(ctx); err != nil { + checks["database"] = map[string]interface{}{ + "status": "unhealthy", + "error": err.Error(), + } + } else { + checks["database"] = map[string]interface{}{ + "status": "healthy", + } + } + } + + // Redis health check + if h.redis != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := h.redis.Ping(ctx).Err(); err != nil { + checks["redis"] = map[string]interface{}{ + "status": "unhealthy", + "error": err.Error(), + } + } else { + checks["redis"] = map[string]interface{}{ + "status": "healthy", + } + } + } + + // Overall status + overallStatus := "healthy" + for _, check := range checks { + if checkMap, ok := check.(map[string]interface{}); ok { + if checkMap["status"] == "unhealthy" { + overallStatus = "unhealthy" + break + } + } + } + + return map[string]interface{}{ + "status": overallStatus, + "timestamp": time.Now(), + "checks": checks, + } +} + +func (h *HealthChecker) checkReadiness() map[string]interface{} { + health := h.checkHealth() + + return map[string]interface{}{ + "ready": health["status"] == "healthy", + "timestamp": time.Now(), + "checks": health["checks"], + } +} +``` + +## Blue-Green Deployment + +```bash +#!/bin/bash +# scripts/blue-green-deploy.sh + +set -e + +NAMESPACE="zoox-app" +NEW_VERSION=$1 +CURRENT_SERVICE="zoox-app-service" + +if [ -z "$NEW_VERSION" ]; then + echo "Usage: $0 " + exit 1 +fi + +echo "Starting blue-green deployment for version $NEW_VERSION" + +# Check current deployment +CURRENT_DEPLOYMENT=$(kubectl get service $CURRENT_SERVICE -n $NAMESPACE -o jsonpath='{.spec.selector.version}') +echo "Current deployment: $CURRENT_DEPLOYMENT" + +# Determine new deployment name +if [ "$CURRENT_DEPLOYMENT" = "blue" ]; then + NEW_DEPLOYMENT="green" +else + NEW_DEPLOYMENT="blue" +fi + +echo "Deploying to: $NEW_DEPLOYMENT" + +# Update deployment manifest with new version +sed "s/{{VERSION}}/$NEW_VERSION/g; s/{{COLOR}}/$NEW_DEPLOYMENT/g" k8s/deployment-template.yaml > k8s/deployment-$NEW_DEPLOYMENT.yaml + +# Deploy new version +kubectl apply -f k8s/deployment-$NEW_DEPLOYMENT.yaml + +# Wait for deployment to be ready +echo "Waiting for deployment to be ready..." +kubectl wait --for=condition=available --timeout=300s deployment/zoox-app-$NEW_DEPLOYMENT -n $NAMESPACE + +# Run health checks +echo "Running health checks..." +POD_NAME=$(kubectl get pods -n $NAMESPACE -l version=$NEW_DEPLOYMENT -o jsonpath='{.items[0].metadata.name}') +kubectl port-forward -n $NAMESPACE $POD_NAME 8080:8080 & +PF_PID=$! + +sleep 5 + +# Check health endpoint +if curl -f http://localhost:8080/health; then + echo "Health check passed" + kill $PF_PID +else + echo "Health check failed" + kill $PF_PID + kubectl delete deployment zoox-app-$NEW_DEPLOYMENT -n $NAMESPACE + exit 1 +fi + +# Switch traffic to new deployment +echo "Switching traffic to new deployment..." +kubectl patch service $CURRENT_SERVICE -n $NAMESPACE -p '{"spec":{"selector":{"version":"'$NEW_DEPLOYMENT'"}}}' + +echo "Deployment completed successfully" + +# Clean up old deployment (optional) +read -p "Do you want to delete the old deployment? (y/n): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + kubectl delete deployment zoox-app-$CURRENT_DEPLOYMENT -n $NAMESPACE + echo "Old deployment deleted" +fi +``` + +## Monitoring and Observability + +### Prometheus Metrics + +```go +// monitoring/metrics.go +package monitoring + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + RequestsTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Total number of HTTP requests", + }, + []string{"method", "endpoint", "status"}, + ) + + RequestDuration = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "http_request_duration_seconds", + Help: "HTTP request duration in seconds", + Buckets: prometheus.DefBuckets, + }, + []string{"method", "endpoint"}, + ) + + ActiveConnections = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "http_active_connections", + Help: "Number of active HTTP connections", + }, + ) +) + +func MetricsMiddleware() zoox.HandlerFunc { + return func(ctx *zoox.Context) { + timer := prometheus.NewTimer(RequestDuration.WithLabelValues( + ctx.Method(), + ctx.Request().URL.Path, + )) + defer timer.ObserveDuration() + + ctx.Next() + + RequestsTotal.WithLabelValues( + ctx.Method(), + ctx.Request().URL.Path, + fmt.Sprintf("%d", ctx.Writer.Status()), + ).Inc() + } +} +``` + +## Production Checklist + +### Pre-deployment Checklist +- [ ] All tests passing +- [ ] Security scan completed +- [ ] Performance benchmarks met +- [ ] Configuration validated +- [ ] Database migrations tested +- [ ] Monitoring setup verified +- [ ] Backup procedures tested +- [ ] Rollback plan prepared + +### Post-deployment Checklist +- [ ] Health checks passing +- [ ] Metrics being collected +- [ ] Logs being generated +- [ ] Performance within expected range +- [ ] Error rates acceptable +- [ ] User traffic flowing correctly +- [ ] Backup systems functional + +## Key Takeaways + +1. **Containerization**: Use multi-stage Docker builds for optimized images +2. **Orchestration**: Leverage Kubernetes for scalable deployments +3. **CI/CD**: Implement automated testing and deployment pipelines +4. **Configuration**: Use environment-based configuration management +5. **Health Checks**: Implement comprehensive health and readiness checks +6. **Monitoring**: Set up proper metrics and observability +7. **Deployment Strategies**: Use blue-green or canary deployments for zero downtime + +## Next Steps + +- Tutorial 18: Production Monitoring - Advanced monitoring techniques +- Learn about service mesh (Istio, Linkerd) +- Explore GitOps deployment strategies +- Study disaster recovery planning +- Implement advanced security scanning + +## Additional Resources + +- [Docker Best Practices](https://docs.docker.com/develop/dev-best-practices/) +- [Kubernetes Documentation](https://kubernetes.io/docs/) +- [GitHub Actions](https://docs.github.com/en/actions) +- [Prometheus Monitoring](https://prometheus.io/docs/) \ No newline at end of file diff --git a/tutorials/18-production-monitoring.md b/tutorials/18-production-monitoring.md new file mode 100644 index 0000000..43a0f83 --- /dev/null +++ b/tutorials/18-production-monitoring.md @@ -0,0 +1,1022 @@ +# Tutorial 18: Production Monitoring + +## Overview +Master comprehensive production monitoring for Zoox applications, including observability, alerting, logging, metrics collection, and incident response. + +## Learning Objectives +- Implement comprehensive monitoring strategies +- Set up metrics collection and visualization +- Configure alerting and notification systems +- Master structured logging and log analysis +- Monitor application performance and health +- Implement distributed tracing +- Set up incident response procedures + +## Prerequisites +- Complete Tutorial 17: Deployment Strategies +- Understanding of monitoring concepts +- Experience with production systems + +## Metrics and Observability + +### Prometheus Integration + +```go +// monitoring/prometheus.go +package monitoring + +import ( + "net/http" + "strconv" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/go-zoox/zoox" +) + +// Application metrics +var ( + // Request metrics + HTTPRequestsTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Total number of HTTP requests", + }, + []string{"method", "endpoint", "status_code"}, + ) + + HTTPRequestDuration = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "http_request_duration_seconds", + Help: "HTTP request duration in seconds", + Buckets: []float64{0.001, 0.01, 0.1, 0.5, 1, 2.5, 5, 10}, + }, + []string{"method", "endpoint"}, + ) + + HTTPActiveConnections = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "http_active_connections", + Help: "Number of active HTTP connections", + }, + ) + + // Application metrics + DatabaseConnections = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "database_connections", + Help: "Number of database connections", + }, + []string{"database", "state"}, + ) + + CacheOperations = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "cache_operations_total", + Help: "Total number of cache operations", + }, + []string{"operation", "result"}, + ) + + BusinessMetrics = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "business_events_total", + Help: "Total number of business events", + }, + []string{"event_type", "status"}, + ) + + ErrorRate = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "application_errors_total", + Help: "Total number of application errors", + }, + []string{"error_type", "severity"}, + ) + + // Resource metrics + MemoryUsage = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "memory_usage_bytes", + Help: "Current memory usage in bytes", + }, + ) + + GoroutineCount = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "goroutines_count", + Help: "Number of goroutines", + }, + ) +) + +// Metrics middleware +func MetricsMiddleware() zoox.HandlerFunc { + return func(ctx *zoox.Context) { + start := time.Now() + + // Increment active connections + HTTPActiveConnections.Inc() + defer HTTPActiveConnections.Dec() + + ctx.Next() + + // Record metrics + duration := time.Since(start).Seconds() + method := ctx.Method() + endpoint := sanitizeEndpoint(ctx.Request().URL.Path) + statusCode := strconv.Itoa(ctx.Writer.Status()) + + HTTPRequestsTotal.WithLabelValues(method, endpoint, statusCode).Inc() + HTTPRequestDuration.WithLabelValues(method, endpoint).Observe(duration) + + // Record error metrics + if ctx.Writer.Status() >= 400 { + severity := "warning" + if ctx.Writer.Status() >= 500 { + severity = "error" + } + ErrorRate.WithLabelValues("http_error", severity).Inc() + } + } +} + +// Sanitize endpoint to reduce cardinality +func sanitizeEndpoint(path string) string { + // Replace dynamic segments with placeholders + // e.g., /users/123 becomes /users/:id + // This is a simple implementation - use a proper router for complex cases + if matches := regexp.MustCompile(`/\d+`).FindAllString(path, -1); len(matches) > 0 { + for _, match := range matches { + path = strings.Replace(path, match, "/:id", 1) + } + } + return path +} + +// Metrics endpoint handler +func MetricsHandler() http.Handler { + return promhttp.Handler() +} + +// Custom business metrics +func RecordUserRegistration(success bool) { + status := "success" + if !success { + status = "failure" + } + BusinessMetrics.WithLabelValues("user_registration", status).Inc() +} + +func RecordCacheHit(hit bool) { + result := "hit" + if !hit { + result = "miss" + } + CacheOperations.WithLabelValues("get", result).Inc() +} + +func RecordDatabaseConnectionState(database, state string, count float64) { + DatabaseConnections.WithLabelValues(database, state).Set(count) +} +``` + +## Structured Logging + +```go +// logging/logger.go +package logging + +import ( + "context" + "os" + "time" + + "github.com/sirupsen/logrus" + "github.com/go-zoox/zoox" +) + +// Logger configuration +type Config struct { + Level string + Format string + Output string + ServiceName string + Version string +} + +// Custom logger +type Logger struct { + *logrus.Logger + config Config +} + +// Log entry with context +type Entry struct { + *logrus.Entry +} + +func NewLogger(config Config) *Logger { + log := logrus.New() + + // Set log level + level, err := logrus.ParseLevel(config.Level) + if err != nil { + level = logrus.InfoLevel + } + log.SetLevel(level) + + // Set formatter + if config.Format == "json" { + log.SetFormatter(&logrus.JSONFormatter{ + TimestampFormat: time.RFC3339, + FieldMap: logrus.FieldMap{ + logrus.FieldKeyTime: "timestamp", + logrus.FieldKeyLevel: "level", + logrus.FieldKeyMsg: "message", + }, + }) + } else { + log.SetFormatter(&logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: time.RFC3339, + }) + } + + // Set output + if config.Output == "stdout" { + log.SetOutput(os.Stdout) + } else { + file, err := os.OpenFile(config.Output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err == nil { + log.SetOutput(file) + } + } + + return &Logger{ + Logger: log, + config: config, + } +} + +// Create entry with default fields +func (l *Logger) WithContext(ctx context.Context) *Entry { + entry := l.Logger.WithFields(logrus.Fields{ + "service": l.config.ServiceName, + "version": l.config.Version, + }) + + // Add request context if available + if requestID := ctx.Value("request_id"); requestID != nil { + entry = entry.WithField("request_id", requestID) + } + if userID := ctx.Value("user_id"); userID != nil { + entry = entry.WithField("user_id", userID) + } + + return &Entry{Entry: entry} +} + +// Logging middleware +func LoggingMiddleware(logger *Logger) zoox.HandlerFunc { + return func(ctx *zoox.Context) { + start := time.Now() + + // Generate request ID + requestID := generateRequestID() + ctx.Set("request_id", requestID) + + // Create context with request info + logCtx := context.WithValue(context.Background(), "request_id", requestID) + if userID, exists := ctx.Get("user_id"); exists { + logCtx = context.WithValue(logCtx, "user_id", userID) + } + + // Log request start + logger.WithContext(logCtx).WithFields(logrus.Fields{ + "method": ctx.Method(), + "path": ctx.Request().URL.Path, + "query": ctx.Request().URL.RawQuery, + "ip": ctx.ClientIP(), + "user_agent": ctx.Header("User-Agent"), + }).Info("Request started") + + ctx.Next() + + // Log request completion + duration := time.Since(start) + entry := logger.WithContext(logCtx).WithFields(logrus.Fields{ + "method": ctx.Method(), + "path": ctx.Request().URL.Path, + "status_code": ctx.Writer.Status(), + "duration_ms": duration.Milliseconds(), + "bytes_sent": ctx.Writer.Size(), + }) + + if ctx.Writer.Status() >= 400 { + if ctx.Writer.Status() >= 500 { + entry.Error("Request completed with server error") + } else { + entry.Warn("Request completed with client error") + } + } else { + entry.Info("Request completed successfully") + } + } +} + +// Generate unique request ID +func generateRequestID() string { + return fmt.Sprintf("%d-%s", time.Now().UnixNano(), + strings.Replace(uuid.New().String(), "-", "", -1)[:8]) +} + +// Structured error logging +func (e *Entry) LogError(err error, msg string, fields map[string]interface{}) { + entry := e.WithError(err).WithField("error_type", fmt.Sprintf("%T", err)) + for k, v := range fields { + entry = entry.WithField(k, v) + } + entry.Error(msg) +} + +// Business event logging +func (e *Entry) LogBusinessEvent(eventType string, fields map[string]interface{}) { + entry := e.WithField("event_type", eventType) + for k, v := range fields { + entry = entry.WithField(k, v) + } + entry.Info("Business event recorded") +} + +// Security event logging +func (e *Entry) LogSecurityEvent(eventType string, severity string, fields map[string]interface{}) { + entry := e.WithFields(logrus.Fields{ + "security_event": eventType, + "severity": severity, + }) + for k, v := range fields { + entry = entry.WithField(k, v) + } + entry.Warn("Security event detected") +} +``` + +## Health Monitoring + +```go +// health/comprehensive.go +package health + +import ( + "context" + "database/sql" + "fmt" + "net/http" + "time" + + "github.com/go-redis/redis/v8" + "github.com/go-zoox/zoox" +) + +type HealthStatus string + +const ( + StatusHealthy HealthStatus = "healthy" + StatusUnhealthy HealthStatus = "unhealthy" + StatusDegraded HealthStatus = "degraded" +) + +type HealthCheck struct { + Name string `json:"name"` + Status HealthStatus `json:"status"` + Message string `json:"message,omitempty"` + LastChecked time.Time `json:"last_checked"` + Duration time.Duration `json:"duration"` + Details map[string]interface{} `json:"details,omitempty"` +} + +type HealthResponse struct { + Status HealthStatus `json:"status"` + Timestamp time.Time `json:"timestamp"` + Uptime time.Duration `json:"uptime"` + Version string `json:"version"` + Checks map[string]HealthCheck `json:"checks"` +} + +type HealthMonitor struct { + startTime time.Time + version string + checks map[string]func() HealthCheck +} + +func NewHealthMonitor(version string) *HealthMonitor { + return &HealthMonitor{ + startTime: time.Now(), + version: version, + checks: make(map[string]func() HealthCheck), + } +} + +// Add health checks +func (hm *HealthMonitor) AddDatabaseCheck(name string, db *sql.DB) { + hm.checks[name] = func() HealthCheck { + start := time.Now() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := db.PingContext(ctx); err != nil { + return HealthCheck{ + Name: name, + Status: StatusUnhealthy, + Message: err.Error(), + LastChecked: time.Now(), + Duration: time.Since(start), + } + } + + // Check connection pool stats + stats := db.Stats() + details := map[string]interface{}{ + "open_connections": stats.OpenConnections, + "in_use": stats.InUse, + "idle": stats.Idle, + "max_open": stats.MaxOpenConnections, + } + + status := StatusHealthy + if stats.OpenConnections > int(float64(stats.MaxOpenConnections)*0.8) { + status = StatusDegraded + } + + return HealthCheck{ + Name: name, + Status: status, + LastChecked: time.Now(), + Duration: time.Since(start), + Details: details, + } + } +} + +func (hm *HealthMonitor) AddRedisCheck(name string, client *redis.Client) { + hm.checks[name] = func() HealthCheck { + start := time.Now() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Test basic connectivity + if err := client.Ping(ctx).Err(); err != nil { + return HealthCheck{ + Name: name, + Status: StatusUnhealthy, + Message: err.Error(), + LastChecked: time.Now(), + Duration: time.Since(start), + } + } + + // Get Redis info + info, err := client.Info(ctx, "memory").Result() + if err != nil { + return HealthCheck{ + Name: name, + Status: StatusDegraded, + Message: fmt.Sprintf("Could not get Redis info: %v", err), + LastChecked: time.Now(), + Duration: time.Since(start), + } + } + + return HealthCheck{ + Name: name, + Status: StatusHealthy, + LastChecked: time.Now(), + Duration: time.Since(start), + Details: map[string]interface{}{ + "info": info, + }, + } + } +} + +func (hm *HealthMonitor) AddCustomCheck(name string, checkFn func() HealthCheck) { + hm.checks[name] = checkFn +} + +// HTTP handlers +func (hm *HealthMonitor) HealthHandler() zoox.HandlerFunc { + return func(ctx *zoox.Context) { + response := hm.runHealthChecks() + + status := http.StatusOK + if response.Status == StatusUnhealthy { + status = http.StatusServiceUnavailable + } else if response.Status == StatusDegraded { + status = http.StatusOK // Still return 200 for degraded + } + + ctx.JSON(status, response) + } +} + +func (hm *HealthMonitor) ReadinessHandler() zoox.HandlerFunc { + return func(ctx *zoox.Context) { + response := hm.runHealthChecks() + + // For readiness, only healthy is acceptable + if response.Status == StatusHealthy { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "ready": true, + "timestamp": time.Now(), + }) + } else { + ctx.JSON(http.StatusServiceUnavailable, map[string]interface{}{ + "ready": false, + "timestamp": time.Now(), + "checks": response.Checks, + }) + } + } +} + +func (hm *HealthMonitor) LivenessHandler() zoox.HandlerFunc { + return func(ctx *zoox.Context) { + // Simple liveness check - just return OK if server is running + ctx.JSON(http.StatusOK, map[string]interface{}{ + "alive": true, + "timestamp": time.Now(), + "uptime": time.Since(hm.startTime).String(), + }) + } +} + +func (hm *HealthMonitor) runHealthChecks() HealthResponse { + checks := make(map[string]HealthCheck) + overallStatus := StatusHealthy + + for name, checkFn := range hm.checks { + check := checkFn() + checks[name] = check + + if check.Status == StatusUnhealthy { + overallStatus = StatusUnhealthy + } else if check.Status == StatusDegraded && overallStatus == StatusHealthy { + overallStatus = StatusDegraded + } + } + + return HealthResponse{ + Status: overallStatus, + Timestamp: time.Now(), + Uptime: time.Since(hm.startTime), + Version: hm.version, + Checks: checks, + } +} +``` + +## Alerting System + +```go +// alerting/manager.go +package alerting + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "sync" + "time" +) + +type AlertLevel string + +const ( + AlertInfo AlertLevel = "info" + AlertWarning AlertLevel = "warning" + AlertCritical AlertLevel = "critical" +) + +type Alert struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Level AlertLevel `json:"level"` + Service string `json:"service"` + Timestamp time.Time `json:"timestamp"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Resolved bool `json:"resolved"` +} + +type AlertManager struct { + alerts map[string]*Alert + mutex sync.RWMutex + webhookURL string + slackToken string + emailConfig EmailConfig +} + +type EmailConfig struct { + SMTPHost string + SMTPPort int + Username string + Password string + FromAddress string + ToAddresses []string +} + +func NewAlertManager(webhookURL, slackToken string, emailConfig EmailConfig) *AlertManager { + return &AlertManager{ + alerts: make(map[string]*Alert), + webhookURL: webhookURL, + slackToken: slackToken, + emailConfig: emailConfig, + } +} + +func (am *AlertManager) TriggerAlert(alert Alert) { + alert.ID = generateAlertID() + alert.Timestamp = time.Now() + + am.mutex.Lock() + am.alerts[alert.ID] = &alert + am.mutex.Unlock() + + // Send notifications + go am.sendNotifications(alert) +} + +func (am *AlertManager) ResolveAlert(alertID string) { + am.mutex.Lock() + if alert, exists := am.alerts[alertID]; exists { + alert.Resolved = true + alert.Timestamp = time.Now() + } + am.mutex.Unlock() +} + +func (am *AlertManager) GetActiveAlerts() []*Alert { + am.mutex.RLock() + defer am.mutex.RUnlock() + + var active []*Alert + for _, alert := range am.alerts { + if !alert.Resolved { + active = append(active, alert) + } + } + return active +} + +func (am *AlertManager) sendNotifications(alert Alert) { + // Send to webhook + if am.webhookURL != "" { + am.sendWebhook(alert) + } + + // Send to Slack + if am.slackToken != "" { + am.sendSlack(alert) + } + + // Send email for critical alerts + if alert.Level == AlertCritical && am.emailConfig.SMTPHost != "" { + am.sendEmail(alert) + } +} + +func (am *AlertManager) sendWebhook(alert Alert) { + payload, _ := json.Marshal(alert) + + client := &http.Client{Timeout: 10 * time.Second} + _, err := client.Post(am.webhookURL, "application/json", bytes.NewBuffer(payload)) + if err != nil { + fmt.Printf("Failed to send webhook: %v\n", err) + } +} + +func (am *AlertManager) sendSlack(alert Alert) { + color := "good" + if alert.Level == AlertWarning { + color = "warning" + } else if alert.Level == AlertCritical { + color = "danger" + } + + payload := map[string]interface{}{ + "channel": "#alerts", + "attachments": []map[string]interface{}{ + { + "color": color, + "title": alert.Title, + "text": alert.Description, + "fields": []map[string]interface{}{ + {"title": "Service", "value": alert.Service, "short": true}, + {"title": "Level", "value": string(alert.Level), "short": true}, + {"title": "Time", "value": alert.Timestamp.Format(time.RFC3339), "short": true}, + }, + }, + }, + } + + jsonPayload, _ := json.Marshal(payload) + + req, _ := http.NewRequest("POST", "https://slack.com/api/chat.postMessage", bytes.NewBuffer(jsonPayload)) + req.Header.Set("Authorization", "Bearer "+am.slackToken) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 10 * time.Second} + _, err := client.Do(req) + if err != nil { + fmt.Printf("Failed to send Slack message: %v\n", err) + } +} + +// Monitoring-based alerting +func (am *AlertManager) StartMonitoring() { + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + + for range ticker.C { + am.checkMetrics() + } +} + +func (am *AlertManager) checkMetrics() { + // Check error rate + if errorRate := am.getErrorRate(); errorRate > 5.0 { + am.TriggerAlert(Alert{ + Title: "High Error Rate", + Description: fmt.Sprintf("Error rate is %.2f%%, above threshold of 5%%", errorRate), + Level: AlertCritical, + Service: "api", + Metadata: map[string]interface{}{ + "error_rate": errorRate, + "threshold": 5.0, + }, + }) + } + + // Check response time + if avgResponseTime := am.getAverageResponseTime(); avgResponseTime > 2000 { + am.TriggerAlert(Alert{ + Title: "High Response Time", + Description: fmt.Sprintf("Average response time is %dms, above threshold of 2000ms", avgResponseTime), + Level: AlertWarning, + Service: "api", + Metadata: map[string]interface{}{ + "response_time": avgResponseTime, + "threshold": 2000, + }, + }) + } + + // Check memory usage + if memoryUsage := am.getMemoryUsage(); memoryUsage > 85.0 { + am.TriggerAlert(Alert{ + Title: "High Memory Usage", + Description: fmt.Sprintf("Memory usage is %.2f%%, above threshold of 85%%", memoryUsage), + Level: AlertWarning, + Service: "system", + Metadata: map[string]interface{}{ + "memory_usage": memoryUsage, + "threshold": 85.0, + }, + }) + } +} + +// Metric collection methods (implement based on your metrics system) +func (am *AlertManager) getErrorRate() float64 { + // Implementation would query Prometheus or your metrics system + return 0.0 +} + +func (am *AlertManager) getAverageResponseTime() int { + // Implementation would query Prometheus or your metrics system + return 0 +} + +func (am *AlertManager) getMemoryUsage() float64 { + // Implementation would query system metrics + return 0.0 +} + +func generateAlertID() string { + return fmt.Sprintf("%d-%s", time.Now().UnixNano(), + strings.Replace(uuid.New().String(), "-", "", -1)[:8]) +} +``` + +## Complete Monitoring Setup + +```go +// main.go with comprehensive monitoring +func main() { + // Configuration + config := loadConfig() + + // Initialize monitoring components + logger := logging.NewLogger(logging.Config{ + Level: config.LogLevel, + Format: "json", + Output: "stdout", + ServiceName: "zoox-app", + Version: config.Version, + }) + + healthMonitor := health.NewHealthMonitor(config.Version) + alertManager := alerting.NewAlertManager( + config.WebhookURL, + config.SlackToken, + config.EmailConfig, + ) + + // Initialize database and add health check + db := initDatabase(config.DatabaseURL) + healthMonitor.AddDatabaseCheck("primary_db", db) + + // Initialize Redis and add health check + redisClient := initRedis(config.RedisURL) + healthMonitor.AddRedisCheck("redis_cache", redisClient) + + // Add custom health checks + healthMonitor.AddCustomCheck("external_api", func() health.HealthCheck { + start := time.Now() + resp, err := http.Get("https://api.external-service.com/health") + if err != nil { + return health.HealthCheck{ + Name: "external_api", + Status: health.StatusUnhealthy, + Message: err.Error(), + LastChecked: time.Now(), + Duration: time.Since(start), + } + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return health.HealthCheck{ + Name: "external_api", + Status: health.StatusDegraded, + Message: fmt.Sprintf("HTTP %d", resp.StatusCode), + LastChecked: time.Now(), + Duration: time.Since(start), + } + } + + return health.HealthCheck{ + Name: "external_api", + Status: health.StatusHealthy, + LastChecked: time.Now(), + Duration: time.Since(start), + } + }) + + // Start alert monitoring + go alertManager.StartMonitoring() + + // Initialize Zoox app + app := zoox.New() + + // Add monitoring middleware + app.Use(monitoring.MetricsMiddleware()) + app.Use(logging.LoggingMiddleware(logger)) + + // Health endpoints + app.Get("/health", healthMonitor.HealthHandler()) + app.Get("/health/ready", healthMonitor.ReadinessHandler()) + app.Get("/health/live", healthMonitor.LivenessHandler()) + + // Metrics endpoint for Prometheus + app.Get("/metrics", func(ctx *zoox.Context) { + monitoring.MetricsHandler().ServeHTTP(ctx.Writer, ctx.Request()) + }) + + // Alert management endpoints + app.Get("/alerts", func(ctx *zoox.Context) { + alerts := alertManager.GetActiveAlerts() + ctx.JSON(http.StatusOK, alerts) + }) + + // Your application routes here... + setupApplicationRoutes(app, logger, alertManager) + + // Graceful shutdown with monitoring + setupGracefulShutdown(app, logger, db, redisClient) + + logger.WithContext(context.Background()).Info("Starting server with comprehensive monitoring") + log.Fatal(app.Listen(":" + config.Port)) +} + +func setupGracefulShutdown(app *zoox.Engine, logger *logging.Logger, db *sql.DB, redis *redis.Client) { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + go func() { + <-c + ctx := context.Background() + logger.WithContext(ctx).Info("Shutting down gracefully...") + + // Close database connections + if err := db.Close(); err != nil { + logger.WithContext(ctx).WithError(err).Error("Error closing database") + } + + // Close Redis connections + if err := redis.Close(); err != nil { + logger.WithContext(ctx).WithError(err).Error("Error closing Redis") + } + + logger.WithContext(ctx).Info("Shutdown complete") + os.Exit(0) + }() +} +``` + +## Monitoring Dashboard Configuration + +### Grafana Dashboard JSON + +```json +{ + "dashboard": { + "title": "Zoox Application Monitoring", + "panels": [ + { + "title": "Request Rate", + "type": "graph", + "targets": [ + { + "expr": "rate(http_requests_total[5m])", + "legendFormat": "{{method}} {{endpoint}}" + } + ] + }, + { + "title": "Response Time", + "type": "graph", + "targets": [ + { + "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))", + "legendFormat": "95th percentile" + }, + { + "expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))", + "legendFormat": "50th percentile" + } + ] + }, + { + "title": "Error Rate", + "type": "singlestat", + "targets": [ + { + "expr": "rate(http_requests_total{status_code=~\"5..\"}[5m]) / rate(http_requests_total[5m]) * 100" + } + ] + }, + { + "title": "Active Connections", + "type": "singlestat", + "targets": [ + { + "expr": "http_active_connections" + } + ] + } + ] + } +} +``` + +## Key Monitoring Takeaways + +1. **Metrics**: Collect comprehensive application and business metrics +2. **Logging**: Implement structured logging with correlation IDs +3. **Health Checks**: Monitor application and dependency health +4. **Alerting**: Set up proactive alerting based on SLOs +5. **Observability**: Ensure you can understand system behavior +6. **Dashboards**: Create actionable monitoring dashboards +7. **Incident Response**: Have clear procedures for incident handling + +## Next Steps + +- Implement distributed tracing with Jaeger/Zipkin +- Set up log aggregation with ELK stack +- Learn about SRE practices and SLOs +- Explore chaos engineering +- Study advanced monitoring patterns + +## Additional Resources + +- [Prometheus Documentation](https://prometheus.io/docs/) +- [Grafana Documentation](https://grafana.com/docs/) +- [The Twelve-Factor App](https://12factor.net/) +- [SRE Book](https://sre.google/books/) \ No newline at end of file diff --git a/tutorials/README.md b/tutorials/README.md new file mode 100644 index 0000000..ba0d85a --- /dev/null +++ b/tutorials/README.md @@ -0,0 +1,245 @@ +# Zoox Framework Tutorials + +This directory contains comprehensive step-by-step tutorials for learning the Zoox Go web framework. Each tutorial builds upon previous concepts and provides hands-on experience with real-world examples. + +## 📚 Tutorial Series Overview + +### 🟢 Beginner Level (Tutorials 01-06) +- **01-getting-started** - First steps with Zoox +- **02-routing-fundamentals** - HTTP routing and parameters +- **03-request-response-handling** - Data handling and validation +- **04-middleware-basics** - Understanding middleware concepts +- **05-working-with-json** - JSON APIs and data binding +- **06-template-engine** - Server-side rendering + +### 🟡 Intermediate Level (Tutorials 07-12) +- **07-static-files-assets** - Serving static content +- **08-websocket-development** - Real-time applications +- **09-json-rpc-services** - RPC service architecture +- **10-authentication-authorization** - Security implementation +- **11-database-integration** - Database operations +- **12-caching-strategies** - Performance optimization + +### 🔴 Advanced Level (Tutorials 13-18) +- **13-monitoring-logging** - Observability and debugging +- **14-testing-strategies** - Comprehensive testing +- **15-performance-optimization** - Advanced performance +- **16-security-best-practices** - Production security +- **17-deployment-strategies** - Production deployment +- **18-production-monitoring** - Enterprise monitoring + +## 🎯 Learning Paths + +### Path 1: Web Development Beginner +**Duration: 2-3 weeks** +``` +01 → 02 → 03 → 06 → 07 → 10 +Getting Started → Routing → Request/Response → Templates → Static Files → Auth +``` + +### Path 2: API Development Focus +**Duration: 3-4 weeks** +``` +01 → 02 → 03 → 05 → 09 → 11 → 16 +Getting Started → Routing → Request/Response → JSON → JSON-RPC → Database → Security +``` + +### Path 3: Real-time Applications +**Duration: 2-3 weeks** +``` +01 → 02 → 04 → 08 → 12 → 13 +Getting Started → Routing → Middleware → WebSocket → Caching → Monitoring +``` + +### Path 4: Production Deployment +**Duration: 4-5 weeks** +``` +01 → 02 → 04 → 10 → 13 → 14 → 15 → 16 → 17 → 18 +Complete production-ready development cycle +``` + +## 📖 How to Use These Tutorials + +### Prerequisites +- Go 1.19 or higher +- Basic understanding of Go programming +- Text editor or IDE +- Terminal/command line access + +### Tutorial Structure +Each tutorial follows this format: +``` +tutorials/XX-tutorial-name/ +├── README.md # Tutorial content and instructions +├── starter/ # Starting code template +├── solution/ # Complete solution +├── exercises/ # Practice exercises +└── resources/ # Additional resources +``` + +### Getting Started +1. **Clone the repository:** + ```bash + git clone https://github.com/go-zoox/zoox.git + cd zoox/tutorials + ``` + +2. **Choose a tutorial:** + ```bash + cd 01-getting-started + ``` + +3. **Follow the README:** + Each tutorial README contains step-by-step instructions + +4. **Practice with exercises:** + Complete the exercises to reinforce learning + +## 🌟 Featured Tutorials + +### 🚀 Tutorial 01: Getting Started +**Estimated time: 30 minutes** + +Learn the basics of creating a Zoox application, handling routes, and serving your first web page. + +**What you'll build:** A simple "Hello World" web server +**Key concepts:** Application setup, basic routing, server startup + +### 🛣️ Tutorial 02: Routing Fundamentals +**Estimated time: 45 minutes** + +Master HTTP routing including path parameters, query strings, and route groups. + +**What you'll build:** A REST API with multiple endpoints +**Key concepts:** HTTP methods, URL parameters, route organization + +### 📊 Tutorial 05: Working with JSON +**Estimated time: 1 hour** + +Build robust JSON APIs with data validation and error handling. + +**What you'll build:** A complete CRUD API for a todo application +**Key concepts:** JSON binding, validation, structured responses + +### 🔌 Tutorial 08: WebSocket Development +**Estimated time: 1.5 hours** + +Create real-time applications using WebSocket connections. + +**What you'll build:** A real-time chat application +**Key concepts:** WebSocket handling, connection management, broadcasting + +### 🔐 Tutorial 10: Authentication & Authorization +**Estimated time: 2 hours** + +Implement secure authentication and role-based access control. + +**What you'll build:** A secure API with JWT authentication +**Key concepts:** JWT tokens, middleware, permissions + +### 🚀 Tutorial 17: Deployment Strategies +**Estimated time: 2 hours** + +Deploy your application to production with Docker and Kubernetes. + +**What you'll build:** Complete deployment pipeline +**Key concepts:** Containerization, orchestration, CI/CD + +## 📋 Tutorial Status + +| Tutorial | Status | Difficulty | Duration | Prerequisites | +|----------|--------|------------|----------|---------------| +| 01-getting-started | ✅ Complete | ⭐ | 30 min | Go basics | +| 02-routing-fundamentals | ✅ Complete | ⭐ | 45 min | Tutorial 01 | +| 03-request-response-handling | ✅ Complete | ⭐⭐ | 1 hour | Tutorial 02 | +| 04-middleware-basics | ✅ Complete | ⭐⭐ | 45 min | Tutorial 03 | +| 05-working-with-json | ✅ Complete | ⭐⭐ | 1 hour | Tutorial 03 | +| 06-template-engine | ✅ Complete | ⭐⭐ | 1 hour | Tutorial 05 | +| 07-static-files-assets | ✅ Complete | ⭐⭐ | 45 min | Tutorial 06 | +| 08-websocket-development | ✅ Complete | ⭐⭐⭐ | 1.5 hours | Tutorial 04 | +| 09-json-rpc-services | ✅ Complete | ⭐⭐⭐ | 1 hour | Tutorial 05 | +| 10-authentication-authorization | ✅ Complete | ⭐⭐⭐ | 2 hours | Tutorial 05 | +| 11-database-integration | ✅ Complete | ⭐⭐⭐ | 1.5 hours | Tutorial 05 | +| 12-caching-strategies | ✅ Complete | ⭐⭐⭐ | 1 hour | Tutorial 11 | +| 13-monitoring-logging | ✅ Complete | ⭐⭐⭐⭐ | 1.5 hours | Tutorial 10 | +| 14-testing-strategies | ✅ Complete | ⭐⭐⭐⭐ | 2 hours | Tutorial 05 | +| 15-performance-optimization | ✅ Complete | ⭐⭐⭐⭐ | 2 hours | Tutorial 12 | +| 16-security-best-practices | ✅ Complete | ⭐⭐⭐⭐ | 2 hours | Tutorial 10 | +| 17-deployment-strategies | ✅ Complete | ⭐⭐⭐⭐⭐ | 2 hours | Tutorial 16 | +| 18-production-monitoring | ✅ Complete | ⭐⭐⭐⭐⭐ | 2 hours | Tutorial 17 | + +## 🎓 Completion Certificates + +Complete learning paths to earn certificates: +- **🥉 Zoox Beginner** - Complete tutorials 1-6 +- **🥈 Zoox Developer** - Complete tutorials 1-12 +- **🥇 Zoox Expert** - Complete all tutorials 1-18 + +## 🤝 Contributing to Tutorials + +We welcome contributions to improve these tutorials: + +### Adding New Tutorials +1. Follow the existing tutorial structure +2. Include comprehensive examples +3. Provide clear step-by-step instructions +4. Add practice exercises +5. Test all code examples + +### Improving Existing Tutorials +1. Fix typos and errors +2. Add more examples +3. Improve explanations +4. Update outdated information + +### Tutorial Guidelines +- **Clear objectives** - State what students will learn +- **Step-by-step approach** - Break complex concepts into steps +- **Hands-on examples** - Provide working code +- **Practice exercises** - Reinforce learning +- **Real-world relevance** - Use practical scenarios + +## 📞 Getting Help + +### Community Support +- **GitHub Discussions** - Ask questions and share knowledge +- **Discord Channel** - Real-time community help +- **Stack Overflow** - Tag questions with `zoox-framework` + +### Tutorial Issues +- **Bug Reports** - Report errors in tutorial content +- **Feature Requests** - Suggest new tutorial topics +- **Improvements** - Propose enhancements + +## 📚 Additional Resources + +### Documentation +- [Main Documentation](../DOCUMENTATION.md) - Complete API reference +- [Examples](../examples/) - Working code examples +- [Contributing Guide](../CONTRIBUTING.md) - How to contribute + +### External Resources +- [Go Documentation](https://golang.org/doc/) - Official Go documentation +- [HTTP Specification](https://tools.ietf.org/html/rfc7231) - HTTP/1.1 standard +- [WebSocket Specification](https://tools.ietf.org/html/rfc6455) - WebSocket standard + +### Video Tutorials +- Coming soon: Video walkthroughs for each tutorial +- YouTube playlist with practical examples +- Live coding sessions and Q&A + +## 🔄 Updates and Maintenance + +These tutorials are regularly updated to: +- Reflect latest Zoox framework features +- Improve clarity and examples +- Fix bugs and issues +- Add new content based on community feedback + +Check the git history for recent updates to each tutorial. + +--- + +**Start your Zoox journey today!** 🚀 + +Choose a learning path above or jump directly to [Tutorial 01: Getting Started](./01-getting-started/) to begin. \ No newline at end of file