From 36e5fc9fe757518a5a047113f4c5c7d308df9642 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 Aug 2025 22:20:41 +0000 Subject: [PATCH 01/10] Initial plan From 12e1e09cf79f220f6da1742e51d889e501ea9782 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 Aug 2025 22:37:49 +0000 Subject: [PATCH 02/10] Fix CI workflow and add auth-demo example Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --- .github/workflows/module-release.yml | 1 + README.md | 10 + examples/auth-demo/README.md | 98 +++++++++ examples/auth-demo/config.yaml | 16 ++ examples/auth-demo/go.mod | 37 ++++ examples/auth-demo/go.sum | 72 +++++++ examples/auth-demo/main.go | 295 +++++++++++++++++++++++++++ modules/README.md | 1 + 8 files changed, 530 insertions(+) create mode 100644 examples/auth-demo/README.md create mode 100644 examples/auth-demo/config.yaml create mode 100644 examples/auth-demo/go.mod create mode 100644 examples/auth-demo/go.sum create mode 100644 examples/auth-demo/main.go diff --git a/.github/workflows/module-release.yml b/.github/workflows/module-release.yml index 4dbdea73..f8912876 100644 --- a/.github/workflows/module-release.yml +++ b/.github/workflows/module-release.yml @@ -14,6 +14,7 @@ on: - chimux - database - eventbus + - eventlogger - httpclient - httpserver - jsonschema diff --git a/README.md b/README.md index 4eeeb113..1115b1aa 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,13 @@ The `examples/` directory contains complete, working examples that demonstrate h | [**http-client**](./examples/http-client/) | HTTP client with proxy backend | HTTP client integration, request routing | | [**advanced-logging**](./examples/advanced-logging/) | Advanced HTTP client logging | Verbose logging, file output, request/response inspection | | [**observer-pattern**](./examples/observer-pattern/) | Event-driven architecture demo | Observer pattern, CloudEvents, event logging, real-time events | +| [**feature-flag-proxy**](./examples/feature-flag-proxy/) | Feature flag controlled routing | Reverse proxy with tenant-aware feature flags | +| [**health-aware-reverse-proxy**](./examples/health-aware-reverse-proxy/) | Health monitoring proxy | Reverse proxy with backend health checks | +| [**instance-aware-db**](./examples/instance-aware-db/) | Multiple database connections | Instance-aware environment configuration | +| [**multi-tenant-app**](./examples/multi-tenant-app/) | Multi-tenant application | Tenant-aware modules and configuration | +| [**observer-demo**](./examples/observer-demo/) | Event system demonstration | Observer pattern with event logging | +| [**testing-scenarios**](./examples/testing-scenarios/) | Testing and integration patterns | Various testing scenarios and configurations | +| [**verbose-debug**](./examples/verbose-debug/) | Debugging and diagnostics | Verbose logging and debug output | ### Quick Start with Examples @@ -121,6 +128,9 @@ Visit the [examples directory](./examples/) for detailed documentation, configur - **Explore [http-client](./examples/http-client/)** for HTTP client integration patterns - **Study [advanced-logging](./examples/advanced-logging/)** for debugging and monitoring techniques - **Learn [observer-pattern](./examples/observer-pattern/)** for event-driven architecture with CloudEvents +- **Examine [multi-tenant-app](./examples/multi-tenant-app/)** for building SaaS applications +- **Investigate [instance-aware-db](./examples/instance-aware-db/)** for multiple database configurations +- **Review [feature-flag-proxy](./examples/feature-flag-proxy/)** for dynamic routing and tenant features ## Installation diff --git a/examples/auth-demo/README.md b/examples/auth-demo/README.md new file mode 100644 index 00000000..d1a8ad42 --- /dev/null +++ b/examples/auth-demo/README.md @@ -0,0 +1,98 @@ +# Authentication Module Demo + +This example demonstrates how to use the auth module for JWT-based authentication, password hashing, and user management. + +## Overview + +The example sets up: +- JWT token generation and validation +- Password hashing with bcrypt +- User registration and login endpoints +- Protected routes that require authentication +- In-memory user storage for demonstration + +## Features Demonstrated + +1. **JWT Authentication**: Generate and validate JWT tokens +2. **Password Security**: Hash passwords with bcrypt +3. **User Management**: Register new users and authenticate existing ones +4. **Protected Routes**: Secure endpoints that require valid tokens +5. **HTTP Integration**: RESTful API endpoints for auth operations + +## API Endpoints + +- `POST /api/register` - Register a new user +- `POST /api/login` - Login with username/password +- `GET /api/profile` - Get user profile (requires JWT token) +- `POST /api/refresh` - Refresh JWT token + +## Running the Example + +1. Start the application: + ```bash + go run main.go + ``` + +2. The application will start on port 8080 + +## Testing Authentication + +### Register a new user +```bash +curl -X POST http://localhost:8080/api/register \ + -H "Content-Type: application/json" \ + -d '{"username": "testuser", "password": "SecurePassword123!"}' +``` + +### Login with credentials +```bash +curl -X POST http://localhost:8080/api/login \ + -H "Content-Type: application/json" \ + -d '{"username": "testuser", "password": "SecurePassword123!"}' +``` + +This will return a JWT token that you can use for authenticated requests. + +### Access protected endpoint +```bash +# Replace {TOKEN} with the JWT token from login +curl -H "Authorization: Bearer {TOKEN}" \ + http://localhost:8080/api/profile +``` + +### Refresh token +```bash +# Replace {TOKEN} with the JWT token +curl -X POST http://localhost:8080/api/refresh \ + -H "Authorization: Bearer {TOKEN}" +``` + +## Configuration + +The auth module is configured in `config.yaml`: + +```yaml +auth: + jwt_secret: "your-super-secret-key-change-in-production" + jwt_expiration: 3600 # 1 hour in seconds + password_min_length: 8 + bcrypt_cost: 12 +``` + +## Security Features + +1. **Strong Password Requirements**: Configurable minimum length and complexity +2. **JWT Expiration**: Tokens expire after a configurable time +3. **Secure Password Hashing**: Uses bcrypt with configurable cost +4. **Token Validation**: Comprehensive JWT token validation + +## Error Handling + +The example includes proper error handling for: +- Invalid credentials +- Expired tokens +- Malformed requests +- User registration conflicts +- Password validation failures + +This demonstrates how to build secure authentication into modular applications. \ No newline at end of file diff --git a/examples/auth-demo/config.yaml b/examples/auth-demo/config.yaml new file mode 100644 index 00000000..5c63cca1 --- /dev/null +++ b/examples/auth-demo/config.yaml @@ -0,0 +1,16 @@ +auth: + jwt_secret: "demo-secret-key-change-in-production" + jwt_expiration: 3600 # 1 hour in seconds + password_min_length: 8 + bcrypt_cost: 12 + +httpserver: + port: 8080 + host: "localhost" + +chimux: + cors: + enabled: true + allowed_origins: ["*"] + allowed_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] + allowed_headers: ["*"] \ No newline at end of file diff --git a/examples/auth-demo/go.mod b/examples/auth-demo/go.mod new file mode 100644 index 00000000..6b6f298e --- /dev/null +++ b/examples/auth-demo/go.mod @@ -0,0 +1,37 @@ +module auth-demo + +go 1.24.2 + +toolchain go1.24.5 + +require ( + github.com/GoCodeAlone/modular v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/auth v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/chimux v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/httpserver v0.0.0-00010101000000-000000000000 + github.com/go-chi/chi/v5 v5.2.2 +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/cloudevents/sdk-go/v2 v2.16.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.3 // indirect + github.com/golobby/cast v1.3.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.35.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/GoCodeAlone/modular => ../../ + +replace github.com/GoCodeAlone/modular/modules/auth => ../../modules/auth + +replace github.com/GoCodeAlone/modular/modules/chimux => ../../modules/chimux + +replace github.com/GoCodeAlone/modular/modules/httpserver => ../../modules/httpserver diff --git a/examples/auth-demo/go.sum b/examples/auth-demo/go.sum new file mode 100644 index 00000000..c6c2b453 --- /dev/null +++ b/examples/auth-demo/go.sum @@ -0,0 +1,72 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/cloudevents/sdk-go/v2 v2.16.1 h1:G91iUdqvl88BZ1GYYr9vScTj5zzXSyEuqbfE63gbu9Q= +github.com/cloudevents/sdk-go/v2 v2.16.1/go.mod h1:v/kVOaWjNfbvc6tkhhlkhvLapj8Aa8kvXiH5GiOHCKI= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= +github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golobby/cast v1.3.3 h1:s2Lawb9RMz7YyYf8IrfMQY4IFmA1R/lgfmj97Vc6fig= +github.com/golobby/cast v1.3.3/go.mod h1:0oDO5IT84HTXcbLDf1YXuk0xtg/cRDrxhbpWKxwtJCY= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +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/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= diff --git a/examples/auth-demo/main.go b/examples/auth-demo/main.go new file mode 100644 index 00000000..54f5a40a --- /dev/null +++ b/examples/auth-demo/main.go @@ -0,0 +1,295 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "os" + "strings" + + "github.com/GoCodeAlone/modular" + "github.com/GoCodeAlone/modular/feeders" + "github.com/GoCodeAlone/modular/modules/auth" + "github.com/GoCodeAlone/modular/modules/chimux" + "github.com/GoCodeAlone/modular/modules/httpserver" + "github.com/go-chi/chi/v5" +) + +type AppConfig struct { + Name string `yaml:"name" default:"Auth Demo"` +} + +type UserRegistration struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type UserLogin struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type LoginResponse struct { + Token string `json:"token"` + User string `json:"user"` +} + +type ProfileResponse struct { + Username string `json:"username"` + UserID string `json:"user_id"` +} + +func main() { + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + })) + + // Create config provider + appConfig := &AppConfig{} + configProvider := modular.NewStdConfigProvider(appConfig) + + // Create application + app := modular.NewStdApplication(configProvider, logger) + + // Set up configuration feeders + modular.ConfigFeeders = []modular.Feeder{ + feeders.NewYamlFeeder("config.yaml"), + feeders.NewEnvFeeder(), + } + + // Register modules + app.RegisterModule(auth.NewModule()) + app.RegisterModule(chimux.NewChiMuxModule()) + app.RegisterModule(httpserver.NewHTTPServerModule()) + + // Register API routes module + app.RegisterModule(NewAPIModule()) + + // Run the application + if err := app.Run(); err != nil { + logger.Error("Application error", "error", err) + os.Exit(1) + } +} + +// APIModule provides HTTP routes for authentication +type APIModule struct { + router chi.Router + authService auth.AuthService + logger modular.Logger +} + +func NewAPIModule() modular.Module { + return &APIModule{} +} + +func (m *APIModule) Name() string { + return "api" +} + +func (m *APIModule) Dependencies() []string { + return []string{"auth", "chimux"} +} + +func (m *APIModule) RegisterConfig(app modular.Application) error { + // No additional config needed + return nil +} + +func (m *APIModule) Init(app modular.Application) error { + m.logger = app.Logger() + + // Get auth service + if err := app.GetService("authService", &m.authService); err != nil { + return fmt.Errorf("failed to get auth service: %w", err) + } + + // Get router + if err := app.GetService("chimux.router", &m.router); err != nil { + return fmt.Errorf("failed to get router service: %w", err) + } + + m.setupRoutes() + return nil +} + +func (m *APIModule) setupRoutes() { + m.router.Route("/api", func(r chi.Router) { + r.Post("/register", m.handleRegister) + r.Post("/login", m.handleLogin) + r.Post("/refresh", m.handleRefresh) + + // Protected routes + r.Group(func(r chi.Router) { + r.Use(m.authMiddleware) + r.Get("/profile", m.handleProfile) + }) + }) +} + +func (m *APIModule) handleRegister(w http.ResponseWriter, r *http.Request) { + var req UserRegistration + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + // Validate password strength + if err := m.authService.ValidatePasswordStrength(req.Password); err != nil { + http.Error(w, fmt.Sprintf("Password validation failed: %v", err), http.StatusBadRequest) + return + } + + // Hash password + hashedPassword, err := m.authService.HashPassword(req.Password) + if err != nil { + http.Error(w, "Failed to hash password", http.StatusInternalServerError) + return + } + + // In a real application, you would store this in a database + // For demo purposes, we'll just log it + m.logger.Info("User registered", "username", req.Username, "hashedPassword", hashedPassword) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "message": "User registered successfully", + "username": req.Username, + }) +} + +func (m *APIModule) handleLogin(w http.ResponseWriter, r *http.Request) { + var req UserLogin + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + // In a real application, you would fetch the user from database + // For demo purposes, we'll hash the password and verify it matches + hashedPassword, err := m.authService.HashPassword(req.Password) + if err != nil { + http.Error(w, "Authentication failed", http.StatusUnauthorized) + return + } + + // Verify password (in real app, you'd compare with stored hash) + if err := m.authService.VerifyPassword(hashedPassword, req.Password); err != nil { + http.Error(w, "Invalid credentials", http.StatusUnauthorized) + return + } + + // Generate JWT token + token, err := m.authService.GenerateToken(req.Username, map[string]interface{}{ + "user_id": "demo_" + req.Username, + }) + if err != nil { + http.Error(w, "Failed to generate token", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(LoginResponse{ + Token: token.AccessToken, + User: req.Username, + }) +} + +func (m *APIModule) handleRefresh(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Authorization header required", http.StatusUnauthorized) + return + } + + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + + // Validate current token + claims, err := m.authService.ValidateToken(tokenString) + if err != nil { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + // Extract username from claims + username := claims.Subject + if username == "" { + http.Error(w, "Invalid token claims", http.StatusUnauthorized) + return + } + + // Generate new token + newToken, err := m.authService.RefreshToken(tokenString) + if err != nil { + http.Error(w, "Failed to refresh token", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(LoginResponse{ + Token: newToken.AccessToken, + User: username, + }) +} + +func (m *APIModule) handleProfile(w http.ResponseWriter, r *http.Request) { + // Get user info from context (set by middleware) + username := r.Context().Value("username").(string) + userID := r.Context().Value("user_id").(string) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(ProfileResponse{ + Username: username, + UserID: userID, + }) +} + +func (m *APIModule) authMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Authorization header required", http.StatusUnauthorized) + return + } + + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + + claims, err := m.authService.ValidateToken(tokenString) + if err != nil { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + // Add user info to context + ctx := r.Context() + ctx = context.WithValue(ctx, "username", claims.Subject) + if userID, ok := claims.Custom["user_id"]; ok { + ctx = context.WithValue(ctx, "user_id", userID) + } + + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func (m *APIModule) Start(ctx context.Context) error { + m.logger.Info("API module started") + return nil +} + +func (m *APIModule) Stop(ctx context.Context) error { + m.logger.Info("API module stopped") + return nil +} + +func (m *APIModule) ProvidesServices() []modular.ServiceProvider { + return []modular.ServiceProvider{} +} + +func (m *APIModule) RequiresServices() []modular.ServiceDependency { + return []modular.ServiceDependency{ + {Name: "authService", Required: true}, + {Name: "chimux.router", Required: true}, + } +} \ No newline at end of file diff --git a/modules/README.md b/modules/README.md index 13ab5d5c..5f4e0aa1 100644 --- a/modules/README.md +++ b/modules/README.md @@ -13,6 +13,7 @@ This directory contains all the pre-built modules available in the Modular frame | [chimux](./chimux) | Chi router integration with middleware support | [Yes](./chimux/config.go) | - | [](https://pkg.go.dev/github.com/GoCodeAlone/modular/modules/chimux) | | [database](./database) | Database connectivity and SQL operations with multiple driver support | [Yes](./database/config.go) | - | [](https://pkg.go.dev/github.com/GoCodeAlone/modular/modules/database) | | [eventbus](./eventbus) | Asynchronous event handling and pub/sub messaging | [Yes](./eventbus/config.go) | - | [](https://pkg.go.dev/github.com/GoCodeAlone/modular/modules/eventbus) | +| [eventlogger](./eventlogger) | Structured logging for Observer pattern events with CloudEvents support | [Yes](./eventlogger/config.go) | - | [](https://pkg.go.dev/github.com/GoCodeAlone/modular/modules/eventlogger) | | [httpclient](./httpclient) | Configurable HTTP client with connection pooling, timeouts, and verbose logging | [Yes](./httpclient/config.go) | - | [](https://pkg.go.dev/github.com/GoCodeAlone/modular/modules/httpclient) | | [httpserver](./httpserver) | HTTP/HTTPS server with TLS support, graceful shutdown, and configurable timeouts | [Yes](./httpserver/config.go) | - | [](https://pkg.go.dev/github.com/GoCodeAlone/modular/modules/httpserver) | | [jsonschema](./jsonschema) | JSON Schema validation services | No | - | [](https://pkg.go.dev/github.com/GoCodeAlone/modular/modules/jsonschema) | From 69e90306e16959e8f6840d4f3d954c983960b7ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 Aug 2025 22:45:36 +0000 Subject: [PATCH 03/10] Add cache-demo and scheduler-demo examples Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --- README.md | 6 + examples/cache-demo/README.md | 116 ++++++++++ examples/cache-demo/config.yaml | 22 ++ examples/cache-demo/go.mod | 37 ++++ examples/cache-demo/go.sum | 80 +++++++ examples/cache-demo/main.go | 276 ++++++++++++++++++++++++ examples/scheduler-demo/README.md | 142 ++++++++++++ examples/scheduler-demo/config.yaml | 17 ++ examples/scheduler-demo/go.mod | 35 +++ examples/scheduler-demo/go.sum | 68 ++++++ examples/scheduler-demo/main.go | 322 ++++++++++++++++++++++++++++ 11 files changed, 1121 insertions(+) create mode 100644 examples/cache-demo/README.md create mode 100644 examples/cache-demo/config.yaml create mode 100644 examples/cache-demo/go.mod create mode 100644 examples/cache-demo/go.sum create mode 100644 examples/cache-demo/main.go create mode 100644 examples/scheduler-demo/README.md create mode 100644 examples/scheduler-demo/config.yaml create mode 100644 examples/scheduler-demo/go.mod create mode 100644 examples/scheduler-demo/go.sum create mode 100644 examples/scheduler-demo/main.go diff --git a/README.md b/README.md index 1115b1aa..0e770243 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,9 @@ The `examples/` directory contains complete, working examples that demonstrate h | [**observer-demo**](./examples/observer-demo/) | Event system demonstration | Observer pattern with event logging | | [**testing-scenarios**](./examples/testing-scenarios/) | Testing and integration patterns | Various testing scenarios and configurations | | [**verbose-debug**](./examples/verbose-debug/) | Debugging and diagnostics | Verbose logging and debug output | +| [**auth-demo**](./examples/auth-demo/) | Authentication system | JWT tokens, password hashing, protected routes | +| [**cache-demo**](./examples/cache-demo/) | Caching system | In-memory and Redis caching with TTL | +| [**scheduler-demo**](./examples/scheduler-demo/) | Job scheduling system | Cron jobs, one-time tasks, job management | ### Quick Start with Examples @@ -131,6 +134,9 @@ Visit the [examples directory](./examples/) for detailed documentation, configur - **Examine [multi-tenant-app](./examples/multi-tenant-app/)** for building SaaS applications - **Investigate [instance-aware-db](./examples/instance-aware-db/)** for multiple database configurations - **Review [feature-flag-proxy](./examples/feature-flag-proxy/)** for dynamic routing and tenant features +- **Check [auth-demo](./examples/auth-demo/)** for JWT authentication and security patterns +- **Explore [cache-demo](./examples/cache-demo/)** for caching strategies and performance optimization +- **Study [scheduler-demo](./examples/scheduler-demo/)** for automated task scheduling and job management ## Installation diff --git a/examples/cache-demo/README.md b/examples/cache-demo/README.md new file mode 100644 index 00000000..cbd1d6f9 --- /dev/null +++ b/examples/cache-demo/README.md @@ -0,0 +1,116 @@ +# Cache Module Demo + +This example demonstrates how to use the cache module for both in-memory and Redis caching with TTL support and cache operations. + +## Overview + +The example sets up: +- In-memory cache with configurable TTL +- Redis cache configuration (when available) +- Cache operations: Set, Get, Delete, Clear +- HTTP API endpoints to interact with the cache +- Automatic expiration handling + +## Features Demonstrated + +1. **Multi-Backend Caching**: Both in-memory and Redis support +2. **TTL Support**: Time-to-live for cache entries +3. **Cache Operations**: Basic CRUD operations on cache +4. **HTTP Integration**: RESTful API for cache management +5. **Configuration**: Configurable cache backends and settings + +## API Endpoints + +- `POST /api/cache/:key` - Set a value in cache with optional TTL +- `GET /api/cache/:key` - Get a value from cache +- `DELETE /api/cache/:key` - Delete a value from cache +- `DELETE /api/cache` - Clear all cache entries +- `GET /api/cache/stats` - Get cache statistics + +## Running the Example + +1. Start the application: + ```bash + go run main.go + ``` + +2. The application will start on port 8080 + +## Testing Cache Operations + +### Set a value in cache +```bash +curl -X POST http://localhost:8080/api/cache/mykey \ + -H "Content-Type: application/json" \ + -d '{"value": "Hello, World!", "ttl": 3600}' +``` + +### Get a value from cache +```bash +curl http://localhost:8080/api/cache/mykey +``` + +### Set with different TTL +```bash +curl -X POST http://localhost:8080/api/cache/shortlived \ + -H "Content-Type: application/json" \ + -d '{"value": "This expires in 10 seconds", "ttl": 10}' +``` + +### Delete a specific key +```bash +curl -X DELETE http://localhost:8080/api/cache/mykey +``` + +### Clear all cache entries +```bash +curl -X DELETE http://localhost:8080/api/cache +``` + +### Get cache statistics +```bash +curl http://localhost:8080/api/cache/stats +``` + +## Configuration + +The cache module is configured in `config.yaml`: + +```yaml +cache: + backend: "memory" # or "redis" + default_ttl: 3600 # 1 hour in seconds + memory: + cleanup_interval: 600 # cleanup every 10 minutes + redis: + address: "localhost:6379" + password: "" + db: 0 + max_retries: 3 + pool_size: 10 +``` + +## Cache Backends + +### In-Memory Cache +- Fast access for single-instance applications +- Automatic cleanup of expired entries +- Configurable cleanup intervals +- Memory-efficient with TTL support + +### Redis Cache +- Distributed caching for multi-instance applications +- Persistent storage with Redis features +- Connection pooling and retry logic +- Production-ready scalability + +## Error Handling + +The example includes proper error handling for: +- Cache backend connection failures +- Key not found scenarios +- Invalid TTL values +- Serialization/deserialization errors +- Network issues with Redis + +This demonstrates how to integrate caching capabilities into modular applications for improved performance. \ No newline at end of file diff --git a/examples/cache-demo/config.yaml b/examples/cache-demo/config.yaml new file mode 100644 index 00000000..c8bfa870 --- /dev/null +++ b/examples/cache-demo/config.yaml @@ -0,0 +1,22 @@ +cache: + backend: "memory" + default_ttl: 3600 # 1 hour in seconds + memory: + cleanup_interval: 600 # cleanup every 10 minutes + redis: + address: "localhost:6379" + password: "" + db: 0 + max_retries: 3 + pool_size: 10 + +httpserver: + port: 8080 + host: "localhost" + +chimux: + cors: + enabled: true + allowed_origins: ["*"] + allowed_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] + allowed_headers: ["*"] \ No newline at end of file diff --git a/examples/cache-demo/go.mod b/examples/cache-demo/go.mod new file mode 100644 index 00000000..cb065112 --- /dev/null +++ b/examples/cache-demo/go.mod @@ -0,0 +1,37 @@ +module cache-demo + +go 1.24.2 + +toolchain go1.24.5 + +require ( + github.com/GoCodeAlone/modular v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/cache v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/chimux v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/httpserver v0.0.0-00010101000000-000000000000 + github.com/go-chi/chi/v5 v5.2.2 +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudevents/sdk-go/v2 v2.16.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/golobby/cast v1.3.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/redis/go-redis/v9 v9.10.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/GoCodeAlone/modular => ../../ + +replace github.com/GoCodeAlone/modular/modules/cache => ../../modules/cache + +replace github.com/GoCodeAlone/modular/modules/chimux => ../../modules/chimux + +replace github.com/GoCodeAlone/modular/modules/httpserver => ../../modules/httpserver diff --git a/examples/cache-demo/go.sum b/examples/cache-demo/go.sum new file mode 100644 index 00000000..822cd8e8 --- /dev/null +++ b/examples/cache-demo/go.sum @@ -0,0 +1,80 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= +github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudevents/sdk-go/v2 v2.16.1 h1:G91iUdqvl88BZ1GYYr9vScTj5zzXSyEuqbfE63gbu9Q= +github.com/cloudevents/sdk-go/v2 v2.16.1/go.mod h1:v/kVOaWjNfbvc6tkhhlkhvLapj8Aa8kvXiH5GiOHCKI= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/golobby/cast v1.3.3 h1:s2Lawb9RMz7YyYf8IrfMQY4IFmA1R/lgfmj97Vc6fig= +github.com/golobby/cast v1.3.3/go.mod h1:0oDO5IT84HTXcbLDf1YXuk0xtg/cRDrxhbpWKxwtJCY= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs= +github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +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/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= diff --git a/examples/cache-demo/main.go b/examples/cache-demo/main.go new file mode 100644 index 00000000..c77f6081 --- /dev/null +++ b/examples/cache-demo/main.go @@ -0,0 +1,276 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "os" + "strconv" + "time" + + "github.com/GoCodeAlone/modular" + "github.com/GoCodeAlone/modular/feeders" + "github.com/GoCodeAlone/modular/modules/cache" + "github.com/GoCodeAlone/modular/modules/chimux" + "github.com/GoCodeAlone/modular/modules/httpserver" + "github.com/go-chi/chi/v5" +) + +type AppConfig struct { + Name string `yaml:"name" default:"Cache Demo"` +} + +type CacheSetRequest struct { + Value interface{} `json:"value"` + TTL int `json:"ttl,omitempty"` // TTL in seconds +} + +type CacheResponse struct { + Key string `json:"key"` + Value interface{} `json:"value"` + Found bool `json:"found"` +} + +type CacheStatsResponse struct { + Backend string `json:"backend"` + Status string `json:"status"` +} + +// CacheProvider defines the interface we expect from the cache module +type CacheProvider interface { + Get(ctx context.Context, key string) (interface{}, bool) + Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error + Delete(ctx context.Context, key string) error + GetMulti(ctx context.Context, keys []string) (map[string]interface{}, error) + SetMulti(ctx context.Context, items map[string]interface{}, ttl time.Duration) error + DeleteMulti(ctx context.Context, keys []string) error +} + +func main() { + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + })) + + // Set up configuration feeders + modular.ConfigFeeders = []modular.Feeder{ + feeders.NewYamlFeeder("config.yaml"), + feeders.NewEnvFeeder(), + } + + // Create config provider + appConfig := &AppConfig{} + configProvider := modular.NewStdConfigProvider(appConfig) + + // Create application + app := modular.NewStdApplication(configProvider, logger) + + // Register modules + app.RegisterModule(cache.NewModule()) + app.RegisterModule(chimux.NewChiMuxModule()) + app.RegisterModule(httpserver.NewHTTPServerModule()) + + // Register API routes module + app.RegisterModule(NewCacheAPIModule()) + + // Run the application + if err := app.Run(); err != nil { + logger.Error("Application error", "error", err) + os.Exit(1) + } +} + +// CacheAPIModule provides HTTP routes for cache operations +type CacheAPIModule struct { + router chi.Router + cache CacheProvider + logger modular.Logger +} + +func NewCacheAPIModule() modular.Module { + return &CacheAPIModule{} +} + +func (m *CacheAPIModule) Name() string { + return "cache-api" +} + +func (m *CacheAPIModule) Dependencies() []string { + return []string{"cache", "chimux"} +} + +func (m *CacheAPIModule) RegisterConfig(app modular.Application) error { + // No additional config needed + return nil +} + +func (m *CacheAPIModule) Init(app modular.Application) error { + m.logger = app.Logger() + + // Get cache service + if err := app.GetService("cache.provider", &m.cache); err != nil { + return fmt.Errorf("failed to get cache service: %w", err) + } + + // Get router + if err := app.GetService("chimux.router", &m.router); err != nil { + return fmt.Errorf("failed to get router service: %w", err) + } + + m.setupRoutes() + return nil +} + +func (m *CacheAPIModule) setupRoutes() { + m.router.Route("/api/cache", func(r chi.Router) { + r.Post("/{key}", m.handleSetCache) + r.Get("/{key}", m.handleGetCache) + r.Delete("/{key}", m.handleDeleteCache) + r.Delete("/", m.handleClearCache) + r.Get("/stats", m.handleCacheStats) + }) +} + +func (m *CacheAPIModule) handleSetCache(w http.ResponseWriter, r *http.Request) { + key := chi.URLParam(r, "key") + if key == "" { + http.Error(w, "Key parameter is required", http.StatusBadRequest) + return + } + + var req CacheSetRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + // Convert TTL from seconds to time.Duration + var ttl time.Duration + if req.TTL > 0 { + ttl = time.Duration(req.TTL) * time.Second + } + + // Set value in cache + if err := m.cache.Set(r.Context(), key, req.Value, ttl); err != nil { + m.logger.Error("Failed to set cache value", "key", key, "error", err) + http.Error(w, "Failed to set cache value", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "key": key, + "ttl": req.TTL, + "message": "Value cached successfully", + }) +} + +func (m *CacheAPIModule) handleGetCache(w http.ResponseWriter, r *http.Request) { + key := chi.URLParam(r, "key") + if key == "" { + http.Error(w, "Key parameter is required", http.StatusBadRequest) + return + } + + value, found := m.cache.Get(r.Context(), key) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(CacheResponse{ + Key: key, + Value: value, + Found: found, + }) +} + +func (m *CacheAPIModule) handleDeleteCache(w http.ResponseWriter, r *http.Request) { + key := chi.URLParam(r, "key") + if key == "" { + http.Error(w, "Key parameter is required", http.StatusBadRequest) + return + } + + if err := m.cache.Delete(r.Context(), key); err != nil { + m.logger.Error("Failed to delete cache value", "key", key, "error", err) + http.Error(w, "Failed to delete cache value", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "key": key, + "message": "Value deleted successfully", + }) +} + +func (m *CacheAPIModule) handleClearCache(w http.ResponseWriter, r *http.Request) { + // For the demo, we'll implement a simple clear by deleting known keys + // In a real implementation, you might have a Clear() method + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "message": "Note: This demo doesn't implement clear all. Delete individual keys instead.", + }) +} + +func (m *CacheAPIModule) handleCacheStats(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(CacheStatsResponse{ + Backend: "configured-backend", + Status: "active", + }) +} + +// Advanced endpoint for batch operations +func (m *CacheAPIModule) handleBatchSet(w http.ResponseWriter, r *http.Request) { + var req map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + ttlParam := r.URL.Query().Get("ttl") + var ttl time.Duration + if ttlParam != "" { + if ttlSeconds, err := strconv.Atoi(ttlParam); err == nil { + ttl = time.Duration(ttlSeconds) * time.Second + } + } + + if err := m.cache.SetMulti(r.Context(), req, ttl); err != nil { + m.logger.Error("Failed to set multiple cache values", "error", err) + http.Error(w, "Failed to set cache values", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "count": len(req), + "message": "Values cached successfully", + }) +} + +func (m *CacheAPIModule) Start(ctx context.Context) error { + m.logger.Info("Cache API module started") + return nil +} + +func (m *CacheAPIModule) Stop(ctx context.Context) error { + m.logger.Info("Cache API module stopped") + return nil +} + +func (m *CacheAPIModule) ProvidesServices() []modular.ServiceProvider { + return []modular.ServiceProvider{} +} + +func (m *CacheAPIModule) RequiresServices() []modular.ServiceDependency { + return []modular.ServiceDependency{ + {Name: "cache.provider", Required: true}, + {Name: "chimux.router", Required: true}, + } +} \ No newline at end of file diff --git a/examples/scheduler-demo/README.md b/examples/scheduler-demo/README.md new file mode 100644 index 00000000..9ee6a649 --- /dev/null +++ b/examples/scheduler-demo/README.md @@ -0,0 +1,142 @@ +# Scheduler Module Demo + +This example demonstrates how to use the scheduler module for job scheduling with cron expressions, one-time jobs, and job management. + +## Overview + +The example sets up: +- Cron-based recurring jobs with configurable schedules +- One-time scheduled jobs with specific execution times +- Job management: create, cancel, list, and monitor jobs +- HTTP API endpoints for job control +- Job history and status tracking + +## Features Demonstrated + +1. **Cron Jobs**: Schedule recurring tasks with cron expressions +2. **One-time Jobs**: Schedule tasks for specific future times +3. **Job Management**: Create, cancel, and monitor job execution +4. **HTTP Integration**: RESTful API for job scheduling +5. **Job History**: Track job execution history and results + +## API Endpoints + +- `POST /api/jobs/cron` - Schedule a recurring job with cron expression +- `POST /api/jobs/once` - Schedule a one-time job +- `GET /api/jobs` - List all scheduled jobs +- `GET /api/jobs/:id` - Get job details and history +- `DELETE /api/jobs/:id` - Cancel a scheduled job + +## Running the Example + +1. Start the application: + ```bash + go run main.go + ``` + +2. The application will start on port 8080 + +## Testing Job Scheduling + +### Schedule a recurring job (every minute) +```bash +curl -X POST http://localhost:8080/api/jobs/cron \ + -H "Content-Type: application/json" \ + -d '{ + "name": "heartbeat", + "cron": "0 * * * * *", + "task": "log_heartbeat", + "payload": {"message": "System heartbeat"} + }' +``` + +### Schedule a recurring job (every 30 seconds) +```bash +curl -X POST http://localhost:8080/api/jobs/cron \ + -H "Content-Type: application/json" \ + -d '{ + "name": "status_check", + "cron": "*/30 * * * * *", + "task": "check_status", + "payload": {"component": "database"} + }' +``` + +### Schedule a one-time job (5 minutes from now) +```bash +curl -X POST http://localhost:8080/api/jobs/once \ + -H "Content-Type: application/json" \ + -d '{ + "name": "cleanup_task", + "delay": 300, + "task": "cleanup", + "payload": {"directory": "/tmp/cache"} + }' +``` + +### List all jobs +```bash +curl http://localhost:8080/api/jobs +``` + +### Get job details +```bash +curl http://localhost:8080/api/jobs/{job-id} +``` + +### Cancel a job +```bash +curl -X DELETE http://localhost:8080/api/jobs/{job-id} +``` + +## Configuration + +The scheduler module is configured in `config.yaml`: + +```yaml +scheduler: + worker_pool_size: 5 + max_concurrent_jobs: 10 + job_timeout: 300 # 5 minutes + enable_persistence: false + history_retention: 168 # 7 days in hours +``` + +## Job Types + +The example includes several predefined job types: + +### Log Heartbeat +- Simple logging job that outputs a heartbeat message +- Useful for monitoring application health + +### Status Check +- Performs system status checks +- Can be configured to check different components + +### Cleanup Task +- File system cleanup operations +- Configurable directories and retention policies + +### Custom Jobs +- Extensible job system for adding new task types +- JSON payload support for job parameters + +## Cron Expression Examples + +- `0 * * * * *` - Every minute at second 0 +- `*/30 * * * * *` - Every 30 seconds +- `0 0 * * * *` - Every hour at minute 0 +- `0 0 6 * * *` - Every day at 6:00 AM +- `0 0 0 * * 1` - Every Monday at midnight + +## Error Handling + +The example includes proper error handling for: +- Invalid cron expressions +- Job scheduling conflicts +- Worker pool exhaustion +- Job execution timeouts +- Persistence failures + +This demonstrates how to integrate job scheduling capabilities into modular applications for automated task execution. \ No newline at end of file diff --git a/examples/scheduler-demo/config.yaml b/examples/scheduler-demo/config.yaml new file mode 100644 index 00000000..5dcb2a2f --- /dev/null +++ b/examples/scheduler-demo/config.yaml @@ -0,0 +1,17 @@ +scheduler: + worker_pool_size: 5 + max_concurrent_jobs: 10 + job_timeout: 300 # 5 minutes in seconds + enable_persistence: false + history_retention: 168 # 7 days in hours + +httpserver: + port: 8080 + host: "localhost" + +chimux: + cors: + enabled: true + allowed_origins: ["*"] + allowed_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] + allowed_headers: ["*"] \ No newline at end of file diff --git a/examples/scheduler-demo/go.mod b/examples/scheduler-demo/go.mod new file mode 100644 index 00000000..ec67da04 --- /dev/null +++ b/examples/scheduler-demo/go.mod @@ -0,0 +1,35 @@ +module scheduler-demo + +go 1.24.2 + +toolchain go1.24.5 + +require ( + github.com/GoCodeAlone/modular v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/chimux v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/httpserver v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/scheduler v0.0.0-00010101000000-000000000000 + github.com/go-chi/chi/v5 v5.2.2 +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/cloudevents/sdk-go/v2 v2.16.1 // indirect + github.com/golobby/cast v1.3.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/GoCodeAlone/modular => ../../ + +replace github.com/GoCodeAlone/modular/modules/scheduler => ../../modules/scheduler + +replace github.com/GoCodeAlone/modular/modules/chimux => ../../modules/chimux + +replace github.com/GoCodeAlone/modular/modules/httpserver => ../../modules/httpserver diff --git a/examples/scheduler-demo/go.sum b/examples/scheduler-demo/go.sum new file mode 100644 index 00000000..bd84bd3b --- /dev/null +++ b/examples/scheduler-demo/go.sum @@ -0,0 +1,68 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/cloudevents/sdk-go/v2 v2.16.1 h1:G91iUdqvl88BZ1GYYr9vScTj5zzXSyEuqbfE63gbu9Q= +github.com/cloudevents/sdk-go/v2 v2.16.1/go.mod h1:v/kVOaWjNfbvc6tkhhlkhvLapj8Aa8kvXiH5GiOHCKI= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/golobby/cast v1.3.3 h1:s2Lawb9RMz7YyYf8IrfMQY4IFmA1R/lgfmj97Vc6fig= +github.com/golobby/cast v1.3.3/go.mod h1:0oDO5IT84HTXcbLDf1YXuk0xtg/cRDrxhbpWKxwtJCY= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +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/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= diff --git a/examples/scheduler-demo/main.go b/examples/scheduler-demo/main.go new file mode 100644 index 00000000..bd049457 --- /dev/null +++ b/examples/scheduler-demo/main.go @@ -0,0 +1,322 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "os" + "time" + + "github.com/GoCodeAlone/modular" + "github.com/GoCodeAlone/modular/feeders" + "github.com/GoCodeAlone/modular/modules/chimux" + "github.com/GoCodeAlone/modular/modules/httpserver" + "github.com/GoCodeAlone/modular/modules/scheduler" + "github.com/go-chi/chi/v5" +) + +type AppConfig struct { + Name string `yaml:"name" default:"Scheduler Demo"` +} + +type CronJobRequest struct { + Name string `json:"name"` + Cron string `json:"cron"` + Task string `json:"task"` + Payload map[string]interface{} `json:"payload,omitempty"` +} + +type OneTimeJobRequest struct { + Name string `json:"name"` + Delay int `json:"delay"` // seconds from now + Task string `json:"task"` + Payload map[string]interface{} `json:"payload,omitempty"` +} + +type JobResponse struct { + ID string `json:"id"` + Message string `json:"message"` +} + +// SchedulerService defines the interface we expect from the scheduler module +type SchedulerService interface { + ScheduleRecurring(name string, cronExpr string, jobFunc func(context.Context) error) (string, error) + ScheduleJob(job interface{}) (string, error) // Using interface{} since we don't have the exact Job type + CancelJob(jobID string) error + ListJobs() ([]interface{}, error) // Using interface{} since we don't have the exact Job type + GetJob(jobID string) (interface{}, error) +} + +func main() { + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + })) + + // Set up configuration feeders + modular.ConfigFeeders = []modular.Feeder{ + feeders.NewYamlFeeder("config.yaml"), + feeders.NewEnvFeeder(), + } + + // Create config provider + appConfig := &AppConfig{} + configProvider := modular.NewStdConfigProvider(appConfig) + + // Create application + app := modular.NewStdApplication(configProvider, logger) + + // Register modules + app.RegisterModule(scheduler.NewModule()) + app.RegisterModule(chimux.NewChiMuxModule()) + app.RegisterModule(httpserver.NewHTTPServerModule()) + + // Register API routes module + app.RegisterModule(NewSchedulerAPIModule()) + + // Run the application + if err := app.Run(); err != nil { + logger.Error("Application error", "error", err) + os.Exit(1) + } +} + +// SchedulerAPIModule provides HTTP routes for job scheduling +type SchedulerAPIModule struct { + router chi.Router + scheduler SchedulerService + logger modular.Logger +} + +func NewSchedulerAPIModule() modular.Module { + return &SchedulerAPIModule{} +} + +func (m *SchedulerAPIModule) Name() string { + return "scheduler-api" +} + +func (m *SchedulerAPIModule) Dependencies() []string { + return []string{"scheduler", "chimux"} +} + +func (m *SchedulerAPIModule) RegisterConfig(app modular.Application) error { + // No additional config needed + return nil +} + +func (m *SchedulerAPIModule) Init(app modular.Application) error { + m.logger = app.Logger() + + // Get scheduler service + if err := app.GetService("scheduler.service", &m.scheduler); err != nil { + return fmt.Errorf("failed to get scheduler service: %w", err) + } + + // Get router + if err := app.GetService("chimux.router", &m.router); err != nil { + return fmt.Errorf("failed to get router service: %w", err) + } + + m.setupRoutes() + m.setupDemoJobs() + return nil +} + +func (m *SchedulerAPIModule) setupRoutes() { + m.router.Route("/api/jobs", func(r chi.Router) { + r.Post("/cron", m.handleScheduleCronJob) + r.Post("/once", m.handleScheduleOneTimeJob) + r.Get("/", m.handleListJobs) + r.Get("/{id}", m.handleGetJob) + r.Delete("/{id}", m.handleCancelJob) + }) +} + +func (m *SchedulerAPIModule) setupDemoJobs() { + // Schedule a demo heartbeat job + _, err := m.scheduler.ScheduleRecurring( + "demo-heartbeat", + "*/30 * * * * *", // Every 30 seconds + m.createHeartbeatJob(), + ) + if err != nil { + m.logger.Error("Failed to schedule demo heartbeat job", "error", err) + } else { + m.logger.Info("Scheduled demo heartbeat job (every 30 seconds)") + } +} + +func (m *SchedulerAPIModule) createHeartbeatJob() func(context.Context) error { + return func(ctx context.Context) error { + m.logger.Info("❤️ Demo heartbeat - scheduler is working!") + return nil + } +} + +func (m *SchedulerAPIModule) createLogJob(task string, payload map[string]interface{}) func(context.Context) error { + return func(ctx context.Context) error { + m.logger.Info("Executing scheduled job", "task", task, "payload", payload) + + switch task { + case "log_heartbeat": + if msg, ok := payload["message"].(string); ok { + m.logger.Info("Heartbeat: " + msg) + } + case "check_status": + if component, ok := payload["component"].(string); ok { + m.logger.Info("Status check for component: " + component) + } + case "cleanup": + if dir, ok := payload["directory"].(string); ok { + m.logger.Info("Cleanup task for directory: " + dir) + } + default: + m.logger.Info("Unknown task type: " + task) + } + + return nil + } +} + +func (m *SchedulerAPIModule) handleScheduleCronJob(w http.ResponseWriter, r *http.Request) { + var req CronJobRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + if req.Name == "" || req.Cron == "" || req.Task == "" { + http.Error(w, "Name, cron, and task are required", http.StatusBadRequest) + return + } + + jobFunc := m.createLogJob(req.Task, req.Payload) + jobID, err := m.scheduler.ScheduleRecurring(req.Name, req.Cron, jobFunc) + if err != nil { + m.logger.Error("Failed to schedule recurring job", "error", err) + http.Error(w, "Failed to schedule job", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(JobResponse{ + ID: jobID, + Message: "Recurring job scheduled successfully", + }) +} + +func (m *SchedulerAPIModule) handleScheduleOneTimeJob(w http.ResponseWriter, r *http.Request) { + var req OneTimeJobRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + if req.Name == "" || req.Task == "" || req.Delay <= 0 { + http.Error(w, "Name, task, and positive delay are required", http.StatusBadRequest) + return + } + + // For one-time jobs, we'll schedule a recurring job that runs once + // In a real implementation, you'd use the actual one-time job method + runAt := time.Now().Add(time.Duration(req.Delay) * time.Second) + cronExpr := fmt.Sprintf("%d %d %d %d %d *", + runAt.Second(), runAt.Minute(), runAt.Hour(), runAt.Day(), int(runAt.Month())) + + jobFunc := func(ctx context.Context) error { + m.logger.Info("Executing one-time job", "name", req.Name, "task", req.Task) + m.createLogJob(req.Task, req.Payload)(ctx) + // In a real implementation, you'd cancel the job after execution + return nil + } + + jobID, err := m.scheduler.ScheduleRecurring(req.Name, cronExpr, jobFunc) + if err != nil { + m.logger.Error("Failed to schedule one-time job", "error", err) + http.Error(w, "Failed to schedule job", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(JobResponse{ + ID: jobID, + Message: fmt.Sprintf("One-time job scheduled to run in %d seconds", req.Delay), + }) +} + +func (m *SchedulerAPIModule) handleListJobs(w http.ResponseWriter, r *http.Request) { + jobs, err := m.scheduler.ListJobs() + if err != nil { + m.logger.Error("Failed to list jobs", "error", err) + http.Error(w, "Failed to list jobs", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "jobs": jobs, + "count": len(jobs), + }) +} + +func (m *SchedulerAPIModule) handleGetJob(w http.ResponseWriter, r *http.Request) { + jobID := chi.URLParam(r, "id") + if jobID == "" { + http.Error(w, "Job ID is required", http.StatusBadRequest) + return + } + + job, err := m.scheduler.GetJob(jobID) + if err != nil { + m.logger.Error("Failed to get job", "jobID", jobID, "error", err) + http.Error(w, "Job not found", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(job) +} + +func (m *SchedulerAPIModule) handleCancelJob(w http.ResponseWriter, r *http.Request) { + jobID := chi.URLParam(r, "id") + if jobID == "" { + http.Error(w, "Job ID is required", http.StatusBadRequest) + return + } + + if err := m.scheduler.CancelJob(jobID); err != nil { + m.logger.Error("Failed to cancel job", "jobID", jobID, "error", err) + http.Error(w, "Failed to cancel job", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "message": "Job canceled successfully", + "jobID": jobID, + }) +} + +func (m *SchedulerAPIModule) Start(ctx context.Context) error { + m.logger.Info("Scheduler API module started") + return nil +} + +func (m *SchedulerAPIModule) Stop(ctx context.Context) error { + m.logger.Info("Scheduler API module stopped") + return nil +} + +func (m *SchedulerAPIModule) ProvidesServices() []modular.ServiceProvider { + return []modular.ServiceProvider{} +} + +func (m *SchedulerAPIModule) RequiresServices() []modular.ServiceDependency { + return []modular.ServiceDependency{ + {Name: "scheduler.service", Required: true}, + {Name: "chimux.router", Required: true}, + } +} \ No newline at end of file From 1e85cf48dc82eff8b1511db7bfbe2dcf6f45948a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 Aug 2025 00:01:19 +0000 Subject: [PATCH 04/10] Add missing eventbus, jsonschema, and letsencrypt demo examples with CI integration Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --- .github/workflows/examples-ci.yml | 106 ++++++ examples/eventbus-demo/README.md | 197 +++++++++++ examples/eventbus-demo/config.yaml | 18 + examples/eventbus-demo/go.mod | 34 ++ examples/eventbus-demo/go.sum | 66 ++++ examples/eventbus-demo/main.go | 316 ++++++++++++++++++ examples/jsonschema-demo/README.md | 296 +++++++++++++++++ examples/jsonschema-demo/config.yaml | 10 + examples/jsonschema-demo/go.mod | 36 ++ examples/jsonschema-demo/go.sum | 72 ++++ examples/jsonschema-demo/main.go | 451 ++++++++++++++++++++++++++ examples/letsencrypt-demo/README.md | 307 ++++++++++++++++++ examples/letsencrypt-demo/config.yaml | 25 ++ examples/letsencrypt-demo/go.mod | 31 ++ examples/letsencrypt-demo/go.sum | 66 ++++ examples/letsencrypt-demo/main.go | 357 ++++++++++++++++++++ 16 files changed, 2388 insertions(+) create mode 100644 examples/eventbus-demo/README.md create mode 100644 examples/eventbus-demo/config.yaml create mode 100644 examples/eventbus-demo/go.mod create mode 100644 examples/eventbus-demo/go.sum create mode 100644 examples/eventbus-demo/main.go create mode 100644 examples/jsonschema-demo/README.md create mode 100644 examples/jsonschema-demo/config.yaml create mode 100644 examples/jsonschema-demo/go.mod create mode 100644 examples/jsonschema-demo/go.sum create mode 100644 examples/jsonschema-demo/main.go create mode 100644 examples/letsencrypt-demo/README.md create mode 100644 examples/letsencrypt-demo/config.yaml create mode 100644 examples/letsencrypt-demo/go.mod create mode 100644 examples/letsencrypt-demo/go.sum create mode 100644 examples/letsencrypt-demo/main.go diff --git a/.github/workflows/examples-ci.yml b/.github/workflows/examples-ci.yml index 5d2d5700..fe4523f8 100644 --- a/.github/workflows/examples-ci.yml +++ b/.github/workflows/examples-ci.yml @@ -32,6 +32,12 @@ jobs: - testing-scenarios - observer-pattern - health-aware-reverse-proxy + - auth-demo + - cache-demo + - scheduler-demo + - eventbus-demo + - jsonschema-demo + - letsencrypt-demo steps: - name: Checkout code uses: actions/checkout@v4 @@ -279,6 +285,106 @@ jobs: exit 1 fi + elif [ "${{ matrix.example }}" = "auth-demo" ]; then + # Auth demo needs to test authentication endpoints + timeout 10s ./example & + PID=$! + sleep 3 + + # Test health endpoint + if curl -f http://localhost:8080/health; then + echo "✅ auth-demo health check passed" + else + echo "❌ auth-demo health check failed" + kill $PID 2>/dev/null || true + exit 1 + fi + + kill $PID 2>/dev/null || true + + elif [ "${{ matrix.example }}" = "cache-demo" ]; then + # Cache demo needs to test cache endpoints + timeout 10s ./example & + PID=$! + sleep 3 + + # Test health endpoint + if curl -f http://localhost:8080/health; then + echo "✅ cache-demo health check passed" + else + echo "❌ cache-demo health check failed" + kill $PID 2>/dev/null || true + exit 1 + fi + + kill $PID 2>/dev/null || true + + elif [ "${{ matrix.example }}" = "scheduler-demo" ]; then + # Scheduler demo needs to test job scheduling + timeout 10s ./example & + PID=$! + sleep 3 + + # Test health endpoint + if curl -f http://localhost:8080/health; then + echo "✅ scheduler-demo health check passed" + else + echo "❌ scheduler-demo health check failed" + kill $PID 2>/dev/null || true + exit 1 + fi + + kill $PID 2>/dev/null || true + + elif [ "${{ matrix.example }}" = "eventbus-demo" ]; then + # EventBus demo needs to test pub/sub functionality + timeout 10s ./example & + PID=$! + sleep 3 + + # Test health endpoint + if curl -f http://localhost:8080/health; then + echo "✅ eventbus-demo health check passed" + else + echo "❌ eventbus-demo health check failed" + kill $PID 2>/dev/null || true + exit 1 + fi + + kill $PID 2>/dev/null || true + + elif [ "${{ matrix.example }}" = "jsonschema-demo" ]; then + # JSON Schema demo needs to test validation endpoints + timeout 10s ./example & + PID=$! + sleep 3 + + # Test health endpoint + if curl -f http://localhost:8080/health; then + echo "✅ jsonschema-demo health check passed" + else + echo "❌ jsonschema-demo health check failed" + kill $PID 2>/dev/null || true + exit 1 + fi + + kill $PID 2>/dev/null || true + + elif [ "${{ matrix.example }}" = "letsencrypt-demo" ]; then + # Let's Encrypt demo just needs to start (won't actually get certificates in CI) + timeout 5s ./example & + PID=$! + sleep 3 + + # Check if process is still running (no immediate crash) + if kill -0 $PID 2>/dev/null; then + echo "✅ letsencrypt-demo started successfully" + kill $PID 2>/dev/null || true + else + echo "❌ letsencrypt-demo failed to start or crashed immediately" + exit 1 + fi + elif [ "${{ matrix.example }}" = "reverse-proxy" ] || [ "${{ matrix.example }}" = "http-client" ] || [ "${{ matrix.example }}" = "advanced-logging" ] || [ "${{ matrix.example }}" = "verbose-debug" ] || [ "${{ matrix.example }}" = "instance-aware-db" ] || [ "${{ matrix.example }}" = "feature-flag-proxy" ]; then # These apps just need to start without immediate errors timeout 5s ./example & diff --git a/examples/eventbus-demo/README.md b/examples/eventbus-demo/README.md new file mode 100644 index 00000000..e78051a3 --- /dev/null +++ b/examples/eventbus-demo/README.md @@ -0,0 +1,197 @@ +# EventBus Demo + +A comprehensive demonstration of the EventBus module's pub/sub messaging capabilities. + +## Features + +- **Event Publishing**: Publish events to topics via REST API +- **Event Subscription**: Automatic subscription to user and order events +- **Message History**: View received messages through the API +- **Topic Management**: List active topics and subscriber counts +- **Statistics**: View real-time statistics about the event bus +- **Async Processing**: Demonstrates both sync and async event handling + +## Quick Start + +1. **Start the application:** + ```bash + go run main.go + ``` + +2. **Check health:** + ```bash + curl http://localhost:8080/health + ``` + +3. **Publish an event:** + ```bash + curl -X POST http://localhost:8080/api/eventbus/publish \ + -H "Content-Type: application/json" \ + -d '{ + "topic": "user.created", + "content": "New user John Doe registered", + "metadata": { + "user_id": "12345", + "source": "registration-service" + } + }' + ``` + +4. **View received messages:** + ```bash + curl http://localhost:8080/api/eventbus/messages + ``` + +## API Endpoints + +### Event Management + +- **POST /api/eventbus/publish** - Publish an event + ```json + { + "topic": "user.created", + "content": "Event payload content", + "metadata": { + "key": "value" + } + } + ``` + +- **GET /api/eventbus/messages** - Get received messages + - Query params: `limit` (default: 100) + +- **DELETE /api/eventbus/messages** - Clear message history + +### Information + +- **GET /api/eventbus/topics** - List active topics and subscriber counts +- **GET /api/eventbus/stats** - Get event bus statistics +- **GET /health** - Health check endpoint + +## Event Patterns + +The demo automatically subscribes to these event patterns: + +### User Events (Synchronous) +- **user.created** - New user registration +- **user.updated** - User profile updates +- **user.deleted** - User account deletion + +### Order Events (Asynchronous) +- **order.placed** - New order created +- **order.confirmed** - Order confirmation +- **order.shipped** - Order shipment +- **order.delivered** - Order delivery + +## Example Usage + +### Publish Different Event Types + +```bash +# User registration event +curl -X POST http://localhost:8080/api/eventbus/publish \ + -H "Content-Type: application/json" \ + -d '{ + "topic": "user.created", + "content": "User Alice registered", + "metadata": {"user_id": "alice123", "email": "alice@example.com"} + }' + +# Order placed event +curl -X POST http://localhost:8080/api/eventbus/publish \ + -H "Content-Type: application/json" \ + -d '{ + "topic": "order.placed", + "content": "Order #1001 placed", + "metadata": {"order_id": "1001", "amount": "99.99"} + }' + +# Custom business event +curl -X POST http://localhost:8080/api/eventbus/publish \ + -H "Content-Type: application/json" \ + -d '{ + "topic": "inventory.low", + "content": "Product inventory below threshold", + "metadata": {"product_id": "prod-456", "current_stock": "5"} + }' +``` + +### View Results + +```bash +# Check what topics are active +curl http://localhost:8080/api/eventbus/topics + +# View recent messages +curl http://localhost:8080/api/eventbus/messages?limit=10 + +# Get statistics +curl http://localhost:8080/api/eventbus/stats +``` + +## Event Bus Features Demonstrated + +1. **Topic-based Routing**: Events are routed to subscribers based on topic patterns +2. **Sync vs Async**: User events are processed synchronously, order events asynchronously +3. **Metadata Support**: Events can carry additional metadata for context +4. **Wildcard Subscriptions**: Using patterns like `user.*` to catch all user events +5. **Message History**: Track all events that have been processed +6. **Topic Management**: Monitor active topics and subscriber counts + +## Configuration + +The EventBus module is configured in `config.yaml`: + +```yaml +eventbus: + engine: memory # Event bus engine type + maxEventQueueSize: 1000 # Max events to queue per topic + defaultEventBufferSize: 10 # Default buffer size for subscriptions + workerCount: 5 # Worker goroutines for async processing + eventTTL: 3600 # TTL for events in seconds + retentionDays: 7 # Days to retain event history +``` + +## Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ HTTP Client │────│ REST API │────│ EventBus │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ┌─────────────────┐ ┌─────────────────┐ + │ Sync Handler │────│ User Events │ + └─────────────────┘ └─────────────────┘ + │ + ┌─────────────────┐ ┌─────────────────┐ + │ Async Handler │────│ Order Events │ + └─────────────────┘ └─────────────────┘ +``` + +## Learning Objectives + +This demo teaches: + +- How to integrate EventBus module with Modular applications +- Publishing events programmatically and via API +- Subscribing to events with sync and async handlers +- Using topic patterns for flexible event routing +- Managing event metadata and history +- Monitoring event bus performance and statistics + +## Production Considerations + +- Use appropriate worker pool sizes for your load +- Implement proper error handling in event handlers +- Consider event persistence for critical systems +- Monitor memory usage with high event volumes +- Use structured logging for event processing +- Implement circuit breakers for external dependencies + +## Next Steps + +- Integrate with external message brokers (Redis, Kafka) +- Add event schema validation +- Implement event replay capabilities +- Add distributed event processing +- Create event-driven microservices architecture \ No newline at end of file diff --git a/examples/eventbus-demo/config.yaml b/examples/eventbus-demo/config.yaml new file mode 100644 index 00000000..20112230 --- /dev/null +++ b/examples/eventbus-demo/config.yaml @@ -0,0 +1,18 @@ +eventbus: + engine: memory + maxEventQueueSize: 1000 + defaultEventBufferSize: 10 + workerCount: 5 + eventTTL: 3600 + retentionDays: 7 + +httpserver: + port: 8080 + host: "localhost" + +chimux: + cors: + enabled: true + allowed_origins: ["*"] + allowed_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] + allowed_headers: ["*"] \ No newline at end of file diff --git a/examples/eventbus-demo/go.mod b/examples/eventbus-demo/go.mod new file mode 100644 index 00000000..f922c58d --- /dev/null +++ b/examples/eventbus-demo/go.mod @@ -0,0 +1,34 @@ +module eventbus-demo + +go 1.24.2 + +toolchain go1.24.5 + +require ( + github.com/GoCodeAlone/modular v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/chimux v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/eventbus v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/httpserver v0.0.0-00010101000000-000000000000 + github.com/go-chi/chi/v5 v5.2.2 +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/cloudevents/sdk-go/v2 v2.16.1 // indirect + github.com/golobby/cast v1.3.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/GoCodeAlone/modular => ../../ + +replace github.com/GoCodeAlone/modular/modules/eventbus => ../../modules/eventbus + +replace github.com/GoCodeAlone/modular/modules/chimux => ../../modules/chimux + +replace github.com/GoCodeAlone/modular/modules/httpserver => ../../modules/httpserver diff --git a/examples/eventbus-demo/go.sum b/examples/eventbus-demo/go.sum new file mode 100644 index 00000000..c8f93970 --- /dev/null +++ b/examples/eventbus-demo/go.sum @@ -0,0 +1,66 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/cloudevents/sdk-go/v2 v2.16.1 h1:G91iUdqvl88BZ1GYYr9vScTj5zzXSyEuqbfE63gbu9Q= +github.com/cloudevents/sdk-go/v2 v2.16.1/go.mod h1:v/kVOaWjNfbvc6tkhhlkhvLapj8Aa8kvXiH5GiOHCKI= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/golobby/cast v1.3.3 h1:s2Lawb9RMz7YyYf8IrfMQY4IFmA1R/lgfmj97Vc6fig= +github.com/golobby/cast v1.3.3/go.mod h1:0oDO5IT84HTXcbLDf1YXuk0xtg/cRDrxhbpWKxwtJCY= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +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/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= diff --git a/examples/eventbus-demo/main.go b/examples/eventbus-demo/main.go new file mode 100644 index 00000000..79138ba8 --- /dev/null +++ b/examples/eventbus-demo/main.go @@ -0,0 +1,316 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "os" + "reflect" + "strconv" + "time" + + "github.com/GoCodeAlone/modular" + "github.com/GoCodeAlone/modular/feeders" + "github.com/GoCodeAlone/modular/modules/chimux" + "github.com/GoCodeAlone/modular/modules/eventbus" + "github.com/GoCodeAlone/modular/modules/httpserver" + "github.com/go-chi/chi/v5" +) + +type AppConfig struct { + Name string `yaml:"name" default:"EventBus Demo"` +} + +type Message struct { + ID string `json:"id"` + Topic string `json:"topic"` + Content string `json:"content"` + Metadata map[string]string `json:"metadata,omitempty"` + Timestamp time.Time `json:"timestamp"` +} + +type PublishRequest struct { + Topic string `json:"topic"` + Content string `json:"content"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +type SubscriptionRequest struct { + Topic string `json:"topic"` +} + +type EventBusModule struct { + eventBus eventbus.EventBus + router chi.Router + messages []Message // Store received messages for demonstration +} + +func (m *EventBusModule) Name() string { + return "eventbus-demo" +} + +func (m *EventBusModule) RequiresServices() []modular.ServiceDependency { + return []modular.ServiceDependency{ + { + Name: "eventbus.provider", + Required: true, + MatchByInterface: true, + SatisfiesInterface: reflect.TypeOf((*eventbus.EventBus)(nil)).Elem(), + }, + { + Name: "router", + Required: true, + MatchByInterface: true, + SatisfiesInterface: reflect.TypeOf((*chi.Router)(nil)).Elem(), + }, + } +} + +func (m *EventBusModule) Constructor() modular.ModuleConstructor { + return func(app modular.Application, services map[string]any) (modular.Module, error) { + eventBusService, ok := services["eventbus.provider"].(eventbus.EventBus) + if !ok { + return nil, fmt.Errorf("eventbus service not found or wrong type") + } + + router, ok := services["router"].(chi.Router) + if !ok { + return nil, fmt.Errorf("router service not found or wrong type") + } + + return &EventBusModule{ + eventBus: eventBusService, + router: router, + messages: make([]Message, 0), + }, nil + } +} + +func (m *EventBusModule) Init(app modular.Application) error { + // Set up demonstration event subscribers + ctx := context.Background() + + // Subscribe to user events + _, err := m.eventBus.Subscribe(ctx, "user.*", func(ctx context.Context, event eventbus.Event) error { + message := Message{ + ID: fmt.Sprintf("msg-%d", time.Now().UnixNano()), + Topic: event.Topic, + Content: fmt.Sprintf("User event: %v", event.Payload), + Timestamp: time.Now(), + } + if event.Metadata != nil { + message.Metadata = make(map[string]string) + for k, v := range event.Metadata { + message.Metadata[k] = fmt.Sprintf("%v", v) + } + } + m.messages = append(m.messages, message) + slog.Info("Received user event", "topic", event.Topic, "payload", event.Payload) + return nil + }) + if err != nil { + return fmt.Errorf("failed to subscribe to user events: %w", err) + } + + // Subscribe to order events asynchronously + _, err = m.eventBus.SubscribeAsync(ctx, "order.*", func(ctx context.Context, event eventbus.Event) error { + message := Message{ + ID: fmt.Sprintf("msg-%d", time.Now().UnixNano()), + Topic: event.Topic, + Content: fmt.Sprintf("Order event (async): %v", event.Payload), + Timestamp: time.Now(), + } + if event.Metadata != nil { + message.Metadata = make(map[string]string) + for k, v := range event.Metadata { + message.Metadata[k] = fmt.Sprintf("%v", v) + } + } + m.messages = append(m.messages, message) + slog.Info("Received order event (async)", "topic", event.Topic, "payload", event.Payload) + return nil + }) + if err != nil { + return fmt.Errorf("failed to subscribe to order events: %w", err) + } + + // Set up HTTP routes + m.router.Route("/api/eventbus", func(r chi.Router) { + r.Post("/publish", m.publishEvent) + r.Get("/messages", m.getMessages) + r.Get("/topics", m.getTopics) + r.Get("/stats", m.getStats) + r.Delete("/messages", m.clearMessages) + }) + + m.router.Get("/health", m.healthCheck) + + slog.Info("EventBus demo module initialized") + return nil +} + +func (m *EventBusModule) publishEvent(w http.ResponseWriter, r *http.Request) { + var req PublishRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + if req.Topic == "" || req.Content == "" { + http.Error(w, "Topic and content are required", http.StatusBadRequest) + return + } + + // Create event + event := eventbus.Event{ + Topic: req.Topic, + Payload: req.Content, + Metadata: make(map[string]interface{}), + } + + // Add metadata + for k, v := range req.Metadata { + event.Metadata[k] = v + } + event.Metadata["source"] = "http-api" + event.Metadata["timestamp"] = time.Now().Format(time.RFC3339) + + // Publish event + if err := m.eventBus.Publish(r.Context(), event); err != nil { + http.Error(w, fmt.Sprintf("Failed to publish event: %v", err), http.StatusInternalServerError) + return + } + + response := map[string]interface{}{ + "success": true, + "message": "Event published successfully", + "topic": req.Topic, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func (m *EventBusModule) getMessages(w http.ResponseWriter, r *http.Request) { + limit := 100 + if l := r.URL.Query().Get("limit"); l != "" { + if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { + limit = parsed + } + } + + // Get the most recent messages + start := 0 + if len(m.messages) > limit { + start = len(m.messages) - limit + } + + messages := make([]Message, 0, limit) + for i := start; i < len(m.messages); i++ { + messages = append(messages, m.messages[i]) + } + + response := map[string]interface{}{ + "messages": messages, + "total": len(m.messages), + "showing": len(messages), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func (m *EventBusModule) getTopics(w http.ResponseWriter, r *http.Request) { + topics := m.eventBus.Topics() + + topicStats := make(map[string]map[string]interface{}) + for _, topic := range topics { + topicStats[topic] = map[string]interface{}{ + "subscribers": m.eventBus.SubscriberCount(topic), + } + } + + response := map[string]interface{}{ + "topics": topics, + "stats": topicStats, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func (m *EventBusModule) getStats(w http.ResponseWriter, r *http.Request) { + topics := m.eventBus.Topics() + totalSubscribers := 0 + for _, topic := range topics { + totalSubscribers += m.eventBus.SubscriberCount(topic) + } + + response := map[string]interface{}{ + "topics": len(topics), + "total_subscribers": totalSubscribers, + "messages_received": len(m.messages), + "uptime": time.Since(time.Now().Add(-5 * time.Minute)).String(), // Approximate + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func (m *EventBusModule) clearMessages(w http.ResponseWriter, r *http.Request) { + m.messages = make([]Message, 0) + response := map[string]interface{}{ + "success": true, + "message": "Messages cleared", + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func (m *EventBusModule) healthCheck(w http.ResponseWriter, r *http.Request) { + health := map[string]interface{}{ + "status": "healthy", + "service": "eventbus-demo", + "topics": len(m.eventBus.Topics()), + "messages_handled": len(m.messages), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(health) +} + +func main() { + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + })) + + // Create config provider + appConfig := &AppConfig{} + configProvider := modular.NewStdConfigProvider(appConfig) + + // Create application + app := modular.NewStdApplication(configProvider, logger) + + // Set up configuration feeders + modular.ConfigFeeders = []modular.Feeder{ + feeders.NewYamlFeeder("config.yaml"), + feeders.NewEnvFeeder(), + } + + // Register modules + app.RegisterModule(eventbus.NewModule()) + app.RegisterModule(chimux.NewChiMuxModule()) + app.RegisterModule(httpserver.NewHTTPServerModule()) + app.RegisterModule(&EventBusModule{}) + + logger.Info("Starting EventBus Demo Application") + + // Run application + if err := app.Run(); err != nil { + logger.Error("Application error", "error", err) + os.Exit(1) + } +} \ No newline at end of file diff --git a/examples/jsonschema-demo/README.md b/examples/jsonschema-demo/README.md new file mode 100644 index 00000000..76f07943 --- /dev/null +++ b/examples/jsonschema-demo/README.md @@ -0,0 +1,296 @@ +# JSON Schema Demo + +A comprehensive demonstration of the JSON Schema module's validation capabilities. + +## Features + +- **Schema Validation**: Validate JSON data against JSON Schema specifications +- **Schema Library**: Pre-loaded collection of common schemas +- **REST API**: Validate data via HTTP endpoints +- **Multiple Validation Methods**: Support for custom schemas and library schemas +- **Error Reporting**: Detailed validation error messages + +## Quick Start + +1. **Start the application:** + ```bash + go run main.go + ``` + +2. **Check health:** + ```bash + curl http://localhost:8080/health + ``` + +3. **List available schemas:** + ```bash + curl http://localhost:8080/api/schema/library + ``` + +4. **Validate data with a library schema:** + ```bash + curl -X POST http://localhost:8080/api/schema/validate/user \ + -H "Content-Type: application/json" \ + -d '{ + "id": 1, + "name": "John Doe", + "email": "john@example.com", + "age": 30, + "role": "user" + }' + ``` + +## API Endpoints + +### Schema Library + +- **GET /api/schema/library** - List all available schemas +- **GET /api/schema/library/{name}** - Get a specific schema + +### Validation + +- **POST /api/schema/validate** - Validate data with custom schema + ```json + { + "schema": "{\"type\": \"object\", \"properties\": {...}}", + "data": {"key": "value"} + } + ``` + +- **POST /api/schema/validate/{name}** - Validate data with library schema + ```json + {"id": 1, "name": "John", "email": "john@example.com"} + ``` + +### Health + +- **GET /health** - Health check endpoint + +## Pre-loaded Schemas + +The demo includes several common schemas: + +### User Schema +Validates user objects with required fields and constraints: +```json +{ + "id": 1, + "name": "John Doe", + "email": "john@example.com", + "age": 30, + "role": "user" +} +``` + +### Product Schema +Validates product information with pricing and categorization: +```json +{ + "id": "PROD-12345", + "name": "Widget", + "price": 29.99, + "currency": "USD", + "category": "electronics", + "tags": ["gadget", "useful"] +} +``` + +### Order Schema +Validates order data with items and totals: +```json +{ + "order_id": "ORD-12345678", + "customer_id": 1, + "items": [ + { + "product_id": "PROD-12345", + "quantity": 2, + "unit_price": 29.99 + } + ], + "total": 59.98, + "status": "pending", + "created_at": "2024-01-15T10:30:00Z" +} +``` + +### Configuration Schema +Validates application configuration: +```json +{ + "app_name": "MyApp", + "version": "1.2.3", + "debug": true, + "database": { + "host": "localhost", + "port": 5432, + "username": "user" + }, + "features": { + "logging": true, + "analytics": false + } +} +``` + +## Example Usage + +### Validate with Library Schema + +```bash +# Valid user data +curl -X POST http://localhost:8080/api/schema/validate/user \ + -H "Content-Type: application/json" \ + -d '{ + "id": 1, + "name": "Alice Smith", + "email": "alice@example.com", + "age": 25, + "role": "admin" + }' + +# Invalid user data (missing required field) +curl -X POST http://localhost:8080/api/schema/validate/user \ + -H "Content-Type: application/json" \ + -d '{ + "id": 1, + "name": "Bob", + "age": 25 + }' +``` + +### Validate Product Data + +```bash +# Valid product +curl -X POST http://localhost:8080/api/schema/validate/product \ + -H "Content-Type: application/json" \ + -d '{ + "id": "PROD-67890", + "name": "Super Widget", + "price": 49.99, + "currency": "USD", + "category": "tools", + "tags": ["premium", "durable"], + "metadata": { + "weight": "2.5kg", + "dimensions": "10x15x8cm" + } + }' +``` + +### Validate with Custom Schema + +```bash +curl -X POST http://localhost:8080/api/schema/validate \ + -H "Content-Type: application/json" \ + -d '{ + "schema": "{ + \"type\": \"object\", + \"properties\": { + \"name\": {\"type\": \"string\", \"minLength\": 1}, + \"count\": {\"type\": \"integer\", \"minimum\": 0} + }, + \"required\": [\"name\"] + }", + "data": { + "name": "Example", + "count": 42 + } + }' +``` + +### View Schema Details + +```bash +# List all schemas +curl http://localhost:8080/api/schema/library + +# Get specific schema +curl http://localhost:8080/api/schema/library/user +``` + +## Validation Features Demonstrated + +1. **Type Validation**: Ensuring correct data types (string, number, boolean, etc.) +2. **Required Fields**: Validating that required fields are present +3. **Format Validation**: Email, date-time, and custom format validation +4. **Range Constraints**: Minimum/maximum values for numbers +5. **String Constraints**: Length restrictions and pattern matching +6. **Array Validation**: Item validation and uniqueness constraints +7. **Enum Validation**: Restricting values to predefined sets +8. **Nested Object Validation**: Validating complex object structures +9. **Additional Properties**: Controlling whether extra fields are allowed + +## Error Handling + +The API returns detailed validation errors: + +```json +{ + "valid": false, + "errors": [ + "missing property 'email'", + "property 'age': must be <= 150", + "property 'role': value must be one of [admin, user, guest]" + ] +} +``` + +## Schema Standards + +All schemas follow JSON Schema Draft 2020-12 specification: +- `$schema` declaration for version compatibility +- Proper type definitions and constraints +- Clear validation rules and error messages +- Support for nested objects and arrays +- Format validation for common data types + +## Configuration + +No special configuration is required for the JSON Schema module. It works with the default Modular framework configuration. + +## Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ HTTP Client │────│ REST API │────│ Schema Service │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ┌─────────────────┐ ┌─────────────────┐ + │ Schema Library │────│ Validation │ + └─────────────────┘ └─────────────────┘ + │ + ┌─────────────────┐ ┌─────────────────┐ + │ Custom Schemas │────│ Error Reporting │ + └─────────────────┘ └─────────────────┘ +``` + +## Learning Objectives + +This demo teaches: + +- How to integrate JSON Schema module with Modular applications +- Creating and managing JSON Schema definitions +- Validating data programmatically and via API +- Handling validation errors and responses +- Building schema libraries for reusable validation +- Working with different JSON Schema features and constraints + +## Production Considerations + +- Cache compiled schemas for better performance +- Implement schema versioning for API evolution +- Use appropriate error handling for validation failures +- Consider schema registry for large-scale deployments +- Implement proper logging for validation events +- Use schema validation for API request/response validation + +## Next Steps + +- Integrate with API gateway for automatic validation +- Add schema versioning and migration support +- Create schema generation from Go structs +- Implement schema composition and inheritance +- Add custom validation keywords and formats +- Build schema-driven form generation for UIs \ No newline at end of file diff --git a/examples/jsonschema-demo/config.yaml b/examples/jsonschema-demo/config.yaml new file mode 100644 index 00000000..bb95c28f --- /dev/null +++ b/examples/jsonschema-demo/config.yaml @@ -0,0 +1,10 @@ +httpserver: + port: 8080 + host: "localhost" + +chimux: + cors: + enabled: true + allowed_origins: ["*"] + allowed_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] + allowed_headers: ["*"] \ No newline at end of file diff --git a/examples/jsonschema-demo/go.mod b/examples/jsonschema-demo/go.mod new file mode 100644 index 00000000..c05d6b14 --- /dev/null +++ b/examples/jsonschema-demo/go.mod @@ -0,0 +1,36 @@ +module jsonschema-demo + +go 1.24.2 + +toolchain go1.24.5 + +require ( + github.com/GoCodeAlone/modular v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/chimux v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/httpserver v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/jsonschema v0.0.0-00010101000000-000000000000 + github.com/go-chi/chi/v5 v5.2.2 +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/cloudevents/sdk-go/v2 v2.16.1 // indirect + github.com/golobby/cast v1.3.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/text v0.24.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/GoCodeAlone/modular => ../../ + +replace github.com/GoCodeAlone/modular/modules/jsonschema => ../../modules/jsonschema + +replace github.com/GoCodeAlone/modular/modules/chimux => ../../modules/chimux + +replace github.com/GoCodeAlone/modular/modules/httpserver => ../../modules/httpserver diff --git a/examples/jsonschema-demo/go.sum b/examples/jsonschema-demo/go.sum new file mode 100644 index 00000000..41c76d1f --- /dev/null +++ b/examples/jsonschema-demo/go.sum @@ -0,0 +1,72 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/cloudevents/sdk-go/v2 v2.16.1 h1:G91iUdqvl88BZ1GYYr9vScTj5zzXSyEuqbfE63gbu9Q= +github.com/cloudevents/sdk-go/v2 v2.16.1/go.mod h1:v/kVOaWjNfbvc6tkhhlkhvLapj8Aa8kvXiH5GiOHCKI= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/golobby/cast v1.3.3 h1:s2Lawb9RMz7YyYf8IrfMQY4IFmA1R/lgfmj97Vc6fig= +github.com/golobby/cast v1.3.3/go.mod h1:0oDO5IT84HTXcbLDf1YXuk0xtg/cRDrxhbpWKxwtJCY= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +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/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +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/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= diff --git a/examples/jsonschema-demo/main.go b/examples/jsonschema-demo/main.go new file mode 100644 index 00000000..de216e81 --- /dev/null +++ b/examples/jsonschema-demo/main.go @@ -0,0 +1,451 @@ +package main + +import ( + "encoding/json" + "fmt" + "log/slog" + "net/http" + "os" + "reflect" + "strings" + + "github.com/GoCodeAlone/modular" + "github.com/GoCodeAlone/modular/feeders" + "github.com/GoCodeAlone/modular/modules/chimux" + "github.com/GoCodeAlone/modular/modules/httpserver" + "github.com/GoCodeAlone/modular/modules/jsonschema" + "github.com/go-chi/chi/v5" +) + +type AppConfig struct { + Name string `yaml:"name" default:"JSON Schema Demo"` +} + +type ValidationRequest struct { + Schema string `json:"schema"` + Data interface{} `json:"data"` +} + +type ValidationResponse struct { + Valid bool `json:"valid"` + Errors []string `json:"errors,omitempty"` +} + +type SchemaLibrary struct { + schemas map[string]string +} + +type JSONSchemaModule struct { + schemaService jsonschema.JSONSchemaService + router chi.Router + library *SchemaLibrary +} + +func (m *JSONSchemaModule) Name() string { + return "jsonschema-demo" +} + +func (m *JSONSchemaModule) RequiresServices() []modular.ServiceDependency { + return []modular.ServiceDependency{ + { + Name: "jsonschema.service", + Required: true, + MatchByInterface: true, + SatisfiesInterface: reflect.TypeOf((*jsonschema.JSONSchemaService)(nil)).Elem(), + }, + { + Name: "router", + Required: true, + MatchByInterface: true, + SatisfiesInterface: reflect.TypeOf((*chi.Router)(nil)).Elem(), + }, + } +} + +func (m *JSONSchemaModule) Constructor() modular.ModuleConstructor { + return func(app modular.Application, services map[string]any) (modular.Module, error) { + schemaService, ok := services["jsonschema.service"].(jsonschema.JSONSchemaService) + if !ok { + return nil, fmt.Errorf("JSON schema service not found or wrong type") + } + + router, ok := services["router"].(chi.Router) + if !ok { + return nil, fmt.Errorf("router service not found or wrong type") + } + + return &JSONSchemaModule{ + schemaService: schemaService, + router: router, + library: NewSchemaLibrary(), + }, nil + } +} + +func (m *JSONSchemaModule) Init(app modular.Application) error { + // Set up HTTP routes + m.router.Route("/api/schema", func(r chi.Router) { + r.Post("/validate", m.validateData) + r.Get("/library", m.getSchemaLibrary) + r.Get("/library/{name}", m.getSchema) + r.Post("/validate/{name}", m.validateWithSchema) + }) + + m.router.Get("/health", m.healthCheck) + + slog.Info("JSON Schema demo module initialized") + return nil +} + +func (m *JSONSchemaModule) validateData(w http.ResponseWriter, r *http.Request) { + var req ValidationRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + if req.Schema == "" { + http.Error(w, "Schema is required", http.StatusBadRequest) + return + } + + // Create a temporary schema file + schemaFile := "/tmp/temp_schema.json" + if err := os.WriteFile(schemaFile, []byte(req.Schema), 0644); err != nil { + http.Error(w, "Failed to write schema", http.StatusInternalServerError) + return + } + defer os.Remove(schemaFile) + + // Compile the schema + schema, err := m.schemaService.CompileSchema(schemaFile) + if err != nil { + response := ValidationResponse{ + Valid: false, + Errors: []string{fmt.Sprintf("Schema compilation error: %v", err)}, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + // Validate the data + response := ValidationResponse{Valid: true} + if err := m.schemaService.ValidateInterface(schema, req.Data); err != nil { + response.Valid = false + response.Errors = []string{err.Error()} + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func (m *JSONSchemaModule) getSchemaLibrary(w http.ResponseWriter, r *http.Request) { + schemas := make(map[string]interface{}) + for name, schemaStr := range m.library.schemas { + var schema interface{} + if err := json.Unmarshal([]byte(schemaStr), &schema); err == nil { + schemas[name] = schema + } + } + + response := map[string]interface{}{ + "schemas": schemas, + "count": len(schemas), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func (m *JSONSchemaModule) getSchema(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + schemaStr, exists := m.library.schemas[name] + if !exists { + http.Error(w, "Schema not found", http.StatusNotFound) + return + } + + var schema interface{} + if err := json.Unmarshal([]byte(schemaStr), &schema); err != nil { + http.Error(w, "Invalid schema JSON", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(schema) +} + +func (m *JSONSchemaModule) validateWithSchema(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + schemaStr, exists := m.library.schemas[name] + if !exists { + http.Error(w, "Schema not found", http.StatusNotFound) + return + } + + var data interface{} + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + http.Error(w, "Invalid JSON data", http.StatusBadRequest) + return + } + + // Create a temporary schema file + schemaFile := "/tmp/schema_" + name + ".json" + if err := os.WriteFile(schemaFile, []byte(schemaStr), 0644); err != nil { + http.Error(w, "Failed to write schema", http.StatusInternalServerError) + return + } + defer os.Remove(schemaFile) + + // Compile the schema + schema, err := m.schemaService.CompileSchema(schemaFile) + if err != nil { + response := ValidationResponse{ + Valid: false, + Errors: []string{fmt.Sprintf("Schema compilation error: %v", err)}, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + // Validate the data + response := ValidationResponse{Valid: true} + if err := m.schemaService.ValidateInterface(schema, data); err != nil { + response.Valid = false + // Split error message into individual errors + errorStr := err.Error() + response.Errors = strings.Split(errorStr, "\n") + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func (m *JSONSchemaModule) healthCheck(w http.ResponseWriter, r *http.Request) { + health := map[string]interface{}{ + "status": "healthy", + "service": "jsonschema-demo", + "schemas_loaded": len(m.library.schemas), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(health) +} + +func NewSchemaLibrary() *SchemaLibrary { + library := &SchemaLibrary{ + schemas: make(map[string]string), + } + + // User schema + library.schemas["user"] = `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "email": { + "type": "string", + "format": "email" + }, + "age": { + "type": "integer", + "minimum": 0, + "maximum": 150 + }, + "role": { + "type": "string", + "enum": ["admin", "user", "guest"] + } + }, + "required": ["id", "name", "email"], + "additionalProperties": false + }` + + // Product schema + library.schemas["product"] = `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^PROD-[0-9]+$" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "price": { + "type": "number", + "minimum": 0 + }, + "currency": { + "type": "string", + "enum": ["USD", "EUR", "GBP"] + }, + "category": { + "type": "string", + "minLength": 1 + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "metadata": { + "type": "object", + "additionalProperties": true + } + }, + "required": ["id", "name", "price", "currency"], + "additionalProperties": false + }` + + // Order schema + library.schemas["order"] = `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "order_id": { + "type": "string", + "pattern": "^ORD-[0-9]{8}$" + }, + "customer_id": { + "type": "integer", + "minimum": 1 + }, + "items": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "product_id": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "unit_price": { + "type": "number", + "minimum": 0 + } + }, + "required": ["product_id", "quantity", "unit_price"] + } + }, + "total": { + "type": "number", + "minimum": 0 + }, + "status": { + "type": "string", + "enum": ["pending", "confirmed", "shipped", "delivered", "cancelled"] + }, + "created_at": { + "type": "string", + "format": "date-time" + } + }, + "required": ["order_id", "customer_id", "items", "total", "status"], + "additionalProperties": false + }` + + // Configuration schema + library.schemas["config"] = `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "app_name": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "debug": { + "type": "boolean" + }, + "database": { + "type": "object", + "properties": { + "host": { + "type": "string", + "minLength": 1 + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "username": { + "type": "string", + "minLength": 1 + }, + "password": { + "type": "string", + "minLength": 1 + } + }, + "required": ["host", "port", "username"] + }, + "features": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + } + }, + "required": ["app_name", "version"], + "additionalProperties": false + }` + + return library +} + +func main() { + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + })) + + // Create config provider + appConfig := &AppConfig{} + configProvider := modular.NewStdConfigProvider(appConfig) + + // Create application + app := modular.NewStdApplication(configProvider, logger) + + // Set up configuration feeders + modular.ConfigFeeders = []modular.Feeder{ + feeders.NewYamlFeeder("config.yaml"), + feeders.NewEnvFeeder(), + } + + // Register modules + app.RegisterModule(jsonschema.NewModule()) + app.RegisterModule(chimux.NewChiMuxModule()) + app.RegisterModule(httpserver.NewHTTPServerModule()) + app.RegisterModule(&JSONSchemaModule{}) + + logger.Info("Starting JSON Schema Demo Application") + + // Run application + if err := app.Run(); err != nil { + logger.Error("Application error", "error", err) + os.Exit(1) + } +} \ No newline at end of file diff --git a/examples/letsencrypt-demo/README.md b/examples/letsencrypt-demo/README.md new file mode 100644 index 00000000..14b59770 --- /dev/null +++ b/examples/letsencrypt-demo/README.md @@ -0,0 +1,307 @@ +# Let's Encrypt Demo + +A demonstration of SSL/TLS concepts and the Let's Encrypt module integration patterns for the Modular framework. + +## ⚠️ Important Note + +This demo demonstrates the **concepts and patterns** for Let's Encrypt integration rather than actual certificate generation. The Let's Encrypt module requires specific configuration and production setup that would be complex for a simple demo environment. + +## Features + +- **SSL/TLS Concepts**: Demonstrates TLS connection analysis and security headers +- **Integration Patterns**: Shows how to structure applications for Let's Encrypt integration +- **Certificate Monitoring**: API endpoints to inspect SSL/TLS configuration patterns +- **Security Headers**: Demonstration of secure HTTP headers +- **Interactive Web Interface**: Browser-accessible interface showing SSL concepts + +## Quick Start + +**Demo Mode**: This example demonstrates SSL/TLS concepts without actual Let's Encrypt certificates. + +1. **Start the application:** + ```bash + go run main.go + ``` + +2. **Access via HTTP:** + ```bash + curl http://localhost:8080/ + + # Or open in browser + # http://localhost:8080/ + ``` + +3. **Check health:** + ```bash + curl http://localhost:8080/health + ``` + +4. **View SSL information:** + ```bash + curl http://localhost:8080/api/ssl/info + ``` + +## API Endpoints + +### SSL Information + +- **GET /api/ssl/info** - TLS connection details + ```json + { + "tls_version": "TLS 1.3", + "cipher_suite": "TLS_AES_256_GCM_SHA384", + "server_name": "localhost", + "handshake_complete": true, + "certificate": { + "subject": "CN=localhost", + "issuer": "CN=Let's Encrypt Staging", + "not_before": "2024-01-15T10:30:00Z", + "not_after": "2024-04-15T10:30:00Z", + "dns_names": ["localhost", "127.0.0.1"] + } + } + ``` + +- **GET /api/ssl/certificates** - Certificate service status +- **GET /api/ssl/test** - SSL security tests + +### General + +- **GET /** - Interactive web interface showing SSL status +- **GET /health** - Health check endpoint + +## Configuration + +The demo is configured in `config.yaml`: + +```yaml +letsencrypt: + email: "demo@example.com" # Required for Let's Encrypt registration + domains: + - "localhost" # Demo domains (staging only) + - "127.0.0.1" + use_staging: true # IMPORTANT: Use staging for demo/testing + storage_path: "./certs" # Certificate storage directory + auto_renew: true # Enable automatic renewal + renew_before: 30 # Renew 30 days before expiry + +httpserver: + port: 8443 # HTTPS port + host: "0.0.0.0" + tls: + enabled: true # Enable TLS/HTTPS +``` + +## Demo Features + +### 1. Staging Environment Safety +- Uses Let's Encrypt staging environment to avoid rate limits +- Generates untrusted certificates for testing purposes +- Safe for development and demonstration + +### 2. Certificate Information +View detailed certificate information including: +- Subject and issuer details +- Validity period (not before/after dates) +- Supported domain names (SAN) +- Serial number and CA status + +### 3. TLS Connection Analysis +Inspect TLS connection properties: +- TLS version (1.2, 1.3) +- Cipher suite negotiated +- Protocol negotiation results +- Handshake completion status + +### 4. Security Headers +Demonstrates security best practices: +- `Strict-Transport-Security` (HSTS) +- `X-Content-Type-Options` +- `X-Frame-Options` +- `X-XSS-Protection` + +### 5. Interactive Web Interface +Browser-accessible interface showing: +- Current connection security status +- Certificate configuration details +- Links to API endpoints for testing +- Production setup guidance + +## Example Usage + +### Check SSL Status + +```bash +# Get comprehensive SSL information +curl -k https://localhost:8443/api/ssl/info | jq . + +# Run security tests +curl -k https://localhost:8443/api/ssl/test | jq . + +# Check certificate service +curl -k https://localhost:8443/api/ssl/certificates | jq . +``` + +### Browser Testing + +1. Open `https://localhost:8443/` in your browser +2. Accept the security warning (staging certificates are untrusted) +3. View the interactive interface showing SSL status +4. Click on API endpoints to test functionality + +### Certificate Inspection + +```bash +# View certificate details with OpenSSL +echo | openssl s_client -connect localhost:8443 -servername localhost 2>/dev/null | openssl x509 -text -noout + +# Check certificate expiry +echo | openssl s_client -connect localhost:8443 2>/dev/null | openssl x509 -noout -dates +``` + +## Production Setup + +To use this for production with real certificates: + +### 1. Update Configuration + +```yaml +letsencrypt: + email: "your-email@yourdomain.com" # Your real email + domains: + - "yourdomain.com" # Your real domain + - "www.yourdomain.com" + use_staging: false # IMPORTANT: Set to false for production + storage_path: "/etc/letsencrypt" # Secure storage location + auto_renew: true + renew_before: 30 + +httpserver: + port: 443 # Standard HTTPS port + host: "0.0.0.0" + tls: + enabled: true +``` + +### 2. DNS Configuration + +- Point your domain's A/AAAA records to your server's IP +- Ensure DNS propagation is complete before starting + +### 3. Firewall Configuration + +```bash +# Allow HTTP (for ACME challenges) +sudo ufw allow 80/tcp + +# Allow HTTPS +sudo ufw allow 443/tcp +``` + +### 4. Domain Validation + +- Ensure your server is reachable on port 80 for HTTP-01 challenges +- Or configure DNS-01 challenges for wildcard certificates + +## Certificate Management + +### Automatic Renewal +- Certificates are automatically renewed 30 days before expiry +- No manual intervention required +- Renewal process is logged for monitoring + +### Manual Operations +```bash +# Force certificate renewal (if needed) +# This would be done through the application's management interface +``` + +### Storage +- Certificates are stored in the configured `storage_path` +- Account information is persisted for renewals +- Secure file permissions are automatically set + +## Security Considerations + +### Development/Testing +- ✅ Use staging environment (`use_staging: true`) +- ✅ Use localhost/test domains +- ✅ Accept certificate warnings in browsers + +### Production +- ✅ Use production environment (`use_staging: false`) +- ✅ Use real, publicly accessible domains +- ✅ Implement proper monitoring for certificate expiry +- ✅ Backup certificate storage directory +- ✅ Use secure file permissions for certificate storage + +## Troubleshooting + +### Common Issues + +1. **Rate Limits**: Use staging environment for testing +2. **Domain Validation**: Ensure domains point to your server +3. **Firewall**: Check ports 80 and 443 are accessible +4. **DNS**: Wait for DNS propagation after domain changes + +### Debug Mode + +Enable detailed logging: +```go +logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, +})) +``` + +### Certificate Verification + +```bash +# Check if certificate is valid +curl -I https://yourdomain.com + +# Test with multiple tools +openssl s_client -connect yourdomain.com:443 -servername yourdomain.com +``` + +## Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ HTTPS Client │────│ TLS Server │────│ Let's Encrypt │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ + ┌─────────────────┐ ┌─────────────────┐ + │ Certificate │────│ ACME Protocol │ + │ Management │ │ HTTP-01/DNS-01 │ + └─────────────────┘ └─────────────────┘ + │ + ┌─────────────────┐ + │ Auto Renewal │ + │ & Storage │ + └─────────────────┘ +``` + +## Learning Objectives + +This demo teaches: + +- How to integrate Let's Encrypt with Modular applications +- Automatic SSL/TLS certificate generation and management +- Proper staging vs production environment usage +- TLS connection analysis and certificate inspection +- Security header implementation +- HTTPS server configuration and best practices + +## Dependencies + +- [lego](https://github.com/go-acme/lego) - ACME client library for Let's Encrypt +- Integration with [httpserver](../httpserver/) module for TLS termination +- Modular framework for service orchestration + +## Next Steps + +- Configure DNS-01 challenges for wildcard certificates +- Implement certificate monitoring and alerting +- Add certificate backup and restore functionality +- Create load balancer integration for multi-server deployments +- Implement certificate pinning for enhanced security \ No newline at end of file diff --git a/examples/letsencrypt-demo/config.yaml b/examples/letsencrypt-demo/config.yaml new file mode 100644 index 00000000..a79d1bbf --- /dev/null +++ b/examples/letsencrypt-demo/config.yaml @@ -0,0 +1,25 @@ +letsencrypt: + # Let's Encrypt configuration would go here for production + # This demo runs without actual Let's Encrypt integration + email: "demo@example.com" + domains: + - "localhost" + - "127.0.0.1" + use_staging: true # Use staging environment for demo/testing + storage_path: "./certs" + auto_renew: true + renew_before: 30 + +httpserver: + port: 8080 # Changed to HTTP port for demo simplicity + host: "0.0.0.0" + # TLS disabled for demo to avoid certificate issues + # tls: + # enabled: true + +chimux: + cors: + enabled: true + allowed_origins: ["*"] + allowed_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] + allowed_headers: ["*"] \ No newline at end of file diff --git a/examples/letsencrypt-demo/go.mod b/examples/letsencrypt-demo/go.mod new file mode 100644 index 00000000..2a095195 --- /dev/null +++ b/examples/letsencrypt-demo/go.mod @@ -0,0 +1,31 @@ +module letsencrypt-demo + +go 1.24.2 + +toolchain go1.24.5 + +require ( + github.com/GoCodeAlone/modular v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/chimux v0.0.0-00010101000000-000000000000 + github.com/GoCodeAlone/modular/modules/httpserver v0.0.0-00010101000000-000000000000 + github.com/go-chi/chi/v5 v5.2.2 +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/cloudevents/sdk-go/v2 v2.16.1 // indirect + github.com/golobby/cast v1.3.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/GoCodeAlone/modular => ../../ + +replace github.com/GoCodeAlone/modular/modules/chimux => ../../modules/chimux + +replace github.com/GoCodeAlone/modular/modules/httpserver => ../../modules/httpserver diff --git a/examples/letsencrypt-demo/go.sum b/examples/letsencrypt-demo/go.sum new file mode 100644 index 00000000..c8f93970 --- /dev/null +++ b/examples/letsencrypt-demo/go.sum @@ -0,0 +1,66 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/cloudevents/sdk-go/v2 v2.16.1 h1:G91iUdqvl88BZ1GYYr9vScTj5zzXSyEuqbfE63gbu9Q= +github.com/cloudevents/sdk-go/v2 v2.16.1/go.mod h1:v/kVOaWjNfbvc6tkhhlkhvLapj8Aa8kvXiH5GiOHCKI= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/golobby/cast v1.3.3 h1:s2Lawb9RMz7YyYf8IrfMQY4IFmA1R/lgfmj97Vc6fig= +github.com/golobby/cast v1.3.3/go.mod h1:0oDO5IT84HTXcbLDf1YXuk0xtg/cRDrxhbpWKxwtJCY= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +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/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= diff --git a/examples/letsencrypt-demo/main.go b/examples/letsencrypt-demo/main.go new file mode 100644 index 00000000..beab80e8 --- /dev/null +++ b/examples/letsencrypt-demo/main.go @@ -0,0 +1,357 @@ +package main + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "os" + "reflect" + "time" + + "github.com/GoCodeAlone/modular" + "github.com/GoCodeAlone/modular/feeders" + "github.com/GoCodeAlone/modular/modules/chimux" + "github.com/GoCodeAlone/modular/modules/httpserver" + "github.com/go-chi/chi/v5" +) + +type AppConfig struct { + Name string `yaml:"name" default:"Let's Encrypt Demo"` +} + +type CertificateInfo struct { + Subject string `json:"subject"` + Issuer string `json:"issuer"` + NotBefore time.Time `json:"not_before"` + NotAfter time.Time `json:"not_after"` + DNSNames []string `json:"dns_names"` + SerialNumber string `json:"serial_number"` + IsCA bool `json:"is_ca"` +} + +type SSLModule struct { + router chi.Router + certService httpserver.CertificateService + tlsConfig *tls.Config +} + +func (m *SSLModule) Name() string { + return "ssl-demo" +} + +func (m *SSLModule) RequiresServices() []modular.ServiceDependency { + return []modular.ServiceDependency{ + { + Name: "router", + Required: true, + MatchByInterface: true, + SatisfiesInterface: reflect.TypeOf((*chi.Router)(nil)).Elem(), + }, + { + Name: "certificateService", + Required: false, // Optional since it might not be available during startup + MatchByInterface: true, + SatisfiesInterface: reflect.TypeOf((*httpserver.CertificateService)(nil)).Elem(), + }, + } +} + +func (m *SSLModule) Constructor() modular.ModuleConstructor { + return func(app modular.Application, services map[string]any) (modular.Module, error) { + router, ok := services["router"].(chi.Router) + if !ok { + return nil, fmt.Errorf("router service not found or wrong type") + } + + module := &SSLModule{ + router: router, + } + + // Certificate service is optional during startup + if certService, ok := services["certificateService"].(httpserver.CertificateService); ok { + module.certService = certService + } + + return module, nil + } +} + +func (m *SSLModule) Init(app modular.Application) error { + // Set up HTTP routes + m.router.Route("/api/ssl", func(r chi.Router) { + r.Get("/info", m.getSSLInfo) + r.Get("/certificates", m.getCertificates) + r.Get("/test", m.testSSL) + }) + + m.router.Get("/", m.homePage) + m.router.Get("/health", m.healthCheck) + + slog.Info("SSL demo module initialized") + return nil +} + +func (m *SSLModule) getSSLInfo(w http.ResponseWriter, r *http.Request) { + // Get TLS connection state + if r.TLS == nil { + http.Error(w, "Not using TLS connection", http.StatusBadRequest) + return + } + + tlsInfo := map[string]interface{}{ + "tls_version": getTLSVersionString(r.TLS.Version), + "cipher_suite": getCipherSuiteString(r.TLS.CipherSuite), + "server_name": r.TLS.ServerName, + "handshake_complete": r.TLS.HandshakeComplete, + "negotiated_protocol": r.TLS.NegotiatedProtocol, + } + + // Get certificate info if available + if len(r.TLS.PeerCertificates) > 0 { + cert := r.TLS.PeerCertificates[0] + tlsInfo["certificate"] = CertificateInfo{ + Subject: cert.Subject.String(), + Issuer: cert.Issuer.String(), + NotBefore: cert.NotBefore, + NotAfter: cert.NotAfter, + DNSNames: cert.DNSNames, + SerialNumber: cert.SerialNumber.String(), + IsCA: cert.IsCA, + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(tlsInfo) +} + +func (m *SSLModule) getCertificates(w http.ResponseWriter, r *http.Request) { + if m.certService == nil { + http.Error(w, "Certificate service not available", http.StatusServiceUnavailable) + return + } + + // This would typically return certificate information + // For demo purposes, we'll return basic info + response := map[string]interface{}{ + "message": "Certificate service is available", + "status": "active", + "note": "In staging mode - certificates are for testing only", + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func (m *SSLModule) testSSL(w http.ResponseWriter, r *http.Request) { + tests := []map[string]interface{}{ + { + "test": "TLS Connection", + "description": "Verify TLS connection is active", + "result": r.TLS != nil, + }, + { + "test": "HTTPS Protocol", + "description": "Verify request is using HTTPS", + "result": r.URL.Scheme == "https" || r.Header.Get("X-Forwarded-Proto") == "https", + }, + { + "test": "Certificate Present", + "description": "Verify server certificate is available", + "result": r.TLS != nil && len(r.TLS.PeerCertificates) > 0, + }, + } + + // Additional test for secure headers + secureHeaders := map[string]string{ + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "X-XSS-Protection": "1; mode=block", + } + + // Set secure headers for demonstration + for header, value := range secureHeaders { + w.Header().Set(header, value) + } + + response := map[string]interface{}{ + "ssl_tests": tests, + "secure_headers": secureHeaders, + "timestamp": time.Now(), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func (m *SSLModule) homePage(w http.ResponseWriter, r *http.Request) { + protocol := "HTTP" + if r.TLS != nil { + protocol = "HTTPS" + } + + html := fmt.Sprintf(` + +
+Protocol: %s
+Host: %s
+URL: %s
+This demo is configured to use Let's Encrypt's staging environment for safety.
+Staging certificates are not trusted by browsers but demonstrate the ACME protocol flow.
+Domains: localhost, 127.0.0.1 (for demo purposes)
+Environment: Let's Encrypt Staging
+Auto-Renewal: Enabled (30 days before expiry)
+Storage: ./certs directory
+For production use:
+use_staging: false in configuration