Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,19 @@ name: Tests
# - Minimal dependencies (core routing: stdlib only, middleware: jwt + x/time)
# - GOEXPERIMENT=jsonv2 required for encoding/json/v2 support
#
# Branch Strategy (Git Flow):
# - feature/** branches: Development work
# - release/** branches: Pre-release testing (test here BEFORE merging to main)
# - develop branch: Integration branch
# Branch Strategy (GitHub Flow):
# - feature/*, release/*, hotfix/* branches: Development work
# - main branch: Production-ready code only (protected)
# - Pull requests: Must pass all tests before merge
# - Push to main triggers tests as final validation after merge

on:
push:
branches:
- main
- develop
- 'feature/**'
- 'release/**'
- 'hotfix/**'
pull_request:
branches:
- main
- develop

jobs:
# Unit tests - Cross-platform
Expand Down
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Planned
- Future features and enhancements (Phase 4: Ecosystem)

## [0.3.4] - 2026-03-05

### Changed
- **Test Coverage**: Comprehensive test coverage improvements for awesome-go submission
- Total coverage: 90.1% → **93.8%** (+3.7%)
- Core (`fursy`): 93.1% → **94.3%** (+1.2%)
- Binding (`internal/binding`): 64.4% → **97.7%** (+33.3%)
- Middleware: 91.7% → **95.6%** (+3.9%)

### Added
- **Binding Tests**: Full coverage for `setField` (all Go types: int8-64, uint8-64, float32/64, bool, string), `mapForm` edge cases (non-pointer, non-struct, unexported fields, missing values), XML/multipart binder error paths
- **Box Tests**: Tests for `Unauthorized()` and `Forbidden()` response methods
- **Generic Router Tests**: Tests for `HEAD[Req,Res]()` and `OPTIONS[Req,Res]()` (new file `router_generic_test.go`)
- **OpenAPI Tests**: Tests for `WriteJSON()` (Content-Type, body validation) and `WriteYAML()` (not-implemented error)
- **CircuitBreaker Tests**: Tests for `CircuitBreaker()` default constructor, `CircuitBreakerWithName()`, `GetState()`, `GetCounts()`, `Reset()`, `FormatState()`
- **Middleware Tests**: Tests for `Logger()` and `Recovery()` default constructors

### Fixed
- **README.md**: Fixed 5 critical API mismatches between documentation and actual code
- `JWT()` signature: was showing config struct, corrected to `JWT(signingKey)` / `JWTWithConfig(config)`
- `RateLimit()` signature: was showing config struct, corrected to `RateLimit(rate, burst)` / `RateLimitWithConfig(config)`
- `CircuitBreaker()` signature: removed non-existent fields (`ResetTimeout`, `FailureRatio`), added helper functions
- `router.Run()`: replaced with `http.ListenAndServe()` (Run method does not exist)
- Package godoc: fixed `*fursy.Box` without type params → `*fursy.Context`

### Testing
- All tests passing, 0 linter issues (golangci-lint clean)
- No performance regressions

## [0.3.3] - 2025-11-24

### Fixed
Expand Down Expand Up @@ -514,4 +543,10 @@ See [ROADMAP.md](ROADMAP.md) for detailed plans and timelines.

---

[0.3.4]: https://github.com/coregx/fursy/releases/tag/v0.3.4
[0.3.3]: https://github.com/coregx/fursy/releases/tag/v0.3.3
[0.3.2]: https://github.com/coregx/fursy/releases/tag/v0.3.2
[0.3.1]: https://github.com/coregx/fursy/releases/tag/v0.3.1
[0.3.0]: https://github.com/coregx/fursy/releases/tag/v0.3.0
[0.2.0]: https://github.com/coregx/fursy/releases/tag/v0.2.0
[0.1.0]: https://github.com/coregx/fursy/releases/tag/v0.1.0
73 changes: 36 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,20 +322,22 @@ router.Use(middleware.SecureWithConfig(middleware.SecureConfig{
JWT token validation with algorithm confusion prevention.

```go
import "github.com/golang-jwt/jwt/v5"
// Simple usage with signing key
router.Use(middleware.JWT([]byte("your-secret-key")))

router.Use(middleware.JWT(middleware.JWTConfig{
// With full configuration
router.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte("your-secret-key"),
SigningMethod: jwt.SigningMethodHS256,
TokenLookup: "header:Authorization",
SigningMethod: "HS256",
TokenLookup: "header:Authorization",
}))

// With custom validation
router.Use(middleware.JWT(middleware.JWTConfig{
SigningKey: []byte("secret"),
SigningMethod: jwt.SigningMethodHS256,
Issuer: "my-app",
Audience: []string{"api"},
// With issuer and audience validation
router.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte("secret"),
SigningMethod: "HS256",
ValidateIssuer: "my-app",
ValidateAudience: "api",
}))
```

Expand All @@ -356,20 +358,16 @@ router.Use(middleware.JWT(middleware.JWTConfig{
Token bucket rate limiting with RFC-compliant headers.

```go
router.Use(middleware.RateLimit(middleware.RateLimitConfig{
Rate: 100, // 100 requests per second
Burst: 200, // burst of 200
KeyFunc: middleware.RateLimitByIP,
}))
// Simple usage (100 req/s, burst of 200)
router.Use(middleware.RateLimit(100, 200))

// Custom key function
router.Use(middleware.RateLimit(middleware.RateLimitConfig{
// With full configuration and custom key function
router.Use(middleware.RateLimitWithConfig(middleware.RateLimitConfig{
Rate: 10,
Burst: 20,
KeyFunc: func(c *fursy.Context) string {
// Rate limit by user ID
userID := c.Get("user_id").(string)
return userID
return c.GetString("user_id")
},
}))
```
Expand All @@ -393,19 +391,20 @@ router.Use(middleware.RateLimit(middleware.RateLimitConfig{
Zero-dependency circuit breaker for fault tolerance.

```go
router.Use(middleware.CircuitBreaker(middleware.CircuitBreakerConfig{
MaxRequests: 100,
// Simple usage with defaults (5 consecutive failures, 60s timeout)
router.Use(middleware.CircuitBreaker())

// With consecutive failures threshold
router.Use(middleware.CircuitBreakerConsecutive(5, 30*time.Second))

// With ratio-based threshold (3 failures out of 10 requests)
router.Use(middleware.CircuitBreakerRatio(3, 10, 30*time.Second))

// With full configuration
router.Use(middleware.CircuitBreakerWithConfig(middleware.CircuitBreakerConfig{
ConsecutiveFailures: 5,
Timeout: 30 * time.Second,
ResetTimeout: 60 * time.Second,
}))

// With ratio-based threshold
router.Use(middleware.CircuitBreaker(middleware.CircuitBreakerConfig{
MaxRequests: 1000,
FailureRatio: 0.25, // Open circuit when 25% of requests fail
Timeout: 30 * time.Second,
ResetTimeout: 60 * time.Second,
MaxRequests: 2, // Allow 2 requests in half-open state
}))
```

Expand All @@ -419,7 +418,7 @@ router.Use(middleware.CircuitBreaker(middleware.CircuitBreakerConfig{
- ✅ Custom error handler
- ✅ Thread-safe (concurrent request handling)

**Coverage**: 95.5%
**Coverage**: 95.6%
**Dependencies**: Zero (stdlib only)

---
Expand All @@ -436,7 +435,7 @@ router.Use(middleware.CircuitBreaker(middleware.CircuitBreakerConfig{
| **Rate Limit** | ✅ Built-in (RFC headers) | 🔧 Plugin | 🔧 Plugin | ✅ Built-in |
| **Security Headers** | ✅ OWASP 2025 | ❌ | 🔧 Plugin | ✅ Basic |
| **Circuit Breaker** | ✅ Zero deps | ❌ | ❌ | ❌ |
| **Test Coverage** | **93.1%** | ? | ? | ? |
| **Test Coverage** | **93.8%** | ? | ? | ? |
| **Dependencies** | **Core: 0, JWT: 1, RateLimit: 1** | Multiple | Multiple | Multiple |

**Legend**:
Expand Down Expand Up @@ -748,7 +747,7 @@ func main() {
return c.Created("/users/"+user.ID, user)
})

router.Run(":8080")
log.Fatal(http.ListenAndServe(":8080", router))
}
```

Expand Down Expand Up @@ -1149,11 +1148,11 @@ Your fursy application will automatically send traces to Jaeger. No configuratio

## 📈 Status

**Current Version**: v0.3.0 (Production Ready)
**Current Version**: v0.3.3 (Production Ready)

**Status**: Production Ready - Complete ecosystem with real-time, database, and production examples

**Coverage**: 93.1% test coverage (core), 650+ tests total
**Coverage**: 93.8% test coverage (total), 94.3% core, 97.7% binding, 95.6% middleware

**Performance**: 256 ns/op (static), 326 ns/op (parametric), 1 alloc/op

Expand All @@ -1171,7 +1170,7 @@ Production Validation 2 Plugins (NOT Rushing!)
Features OpenAPI DDD Boilerplate
```

**Current Status**: v0.3.0 Production Ready ✅
**Current Status**: v0.3.3 Production Ready ✅
**Ecosystem**: stream v0.1.0 (SSE + WebSocket), 2 production plugins, 10 examples
**Next**: v0.x.x feature releases as needed (Cache, more plugins, community tools)
**v1.0.0 LTS**: After 6-12 months of production usage and full API stabilization
Expand Down Expand Up @@ -1251,7 +1250,7 @@ FURSY stands on the shoulders of giants:

*Built with ❤️ by the coregx team*

**Version**: v0.3.0 - Production Ready
**Version**: v0.3.3 - Production Ready
**Ecosystem**: stream v0.1.0 + 2 plugins + 10 examples + DDD boilerplate
**Next**: v1.0.0 LTS (after full API stabilization)

Expand Down
10 changes: 5 additions & 5 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
> **Strategic Advantage**: Modern Go 1.25+ features + proven routing patterns!
> **Approach**: Combine best practices from httprouter, Gin, Echo with type-safe generics

**Last Updated**: 2025-11-24 | **Current Version**: v0.3.3 (Production Ready + Real-Time + Database + Hotfix) | **Phase**: Phase 4 In Progress (Ecosystem) | **Target**: v1.0.0 LTS (TBD, after full API stabilization)
**Last Updated**: 2026-03-05 | **Current Version**: v0.3.4 (Production Ready + 93.8% Coverage) | **Phase**: Phase 4 In Progress (Ecosystem + Awesome-Go Submission) | **Target**: v1.0.0 LTS (TBD, after full API stabilization)

---

Expand Down Expand Up @@ -127,11 +127,11 @@ v1.0.0 LTS → Long-term support (Q3 2026)

---

## 📊 Current Status (v0.2.0 - Production Ready + Documentation & Examples)
## 📊 Current Status (v0.3.4 - Production Ready + Awesome-Go Ready)

**Phase**: 🚀 Phase 4 In Progress (Ecosystem Building)
**Phase**: 🚀 Phase 4 In Progress (Ecosystem Building + Awesome-Go Submission)
**Performance**: Production-ready! (256-326 ns/op, 1 alloc/op)
**Coverage**: 91.7% overall, 94.3% validator plugin
**Coverage**: 93.8% total (94.3% core, 97.7% binding, 95.6% middleware)

**What Works**:
- ✅ **Radix tree routing** (zero-allocation lookup, <500ns parametric routes)
Expand All @@ -153,7 +153,7 @@ v1.0.0 LTS → Long-term support (Q3 2026)
- ✅ Memory efficient: context pooling prevents leaks

**Validation**:
- ✅ 91.7% test coverage (exceeded Phase 2 target of 88%)
- ✅ 93.8% test coverage (exceeded all phase targets)
- ✅ 0 linter issues (34+ linters via golangci-lint)
- ✅ Race detector clean
- ✅ Cross-platform (Linux, macOS, Windows)
Expand Down
44 changes: 44 additions & 0 deletions box_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,3 +455,47 @@ func TestBox_ConvenienceMethods_RESTWorkflow(t *testing.T) {
t.Errorf("DELETE: expected status 204, got %d", w.Code)
}
}

// TestBox_Unauthorized tests the Unauthorized convenience method.
func TestBox_Unauthorized(t *testing.T) {
r := New()

GET[Empty, TestResponse](r, "/protected", func(c *Box[Empty, TestResponse]) error {
return c.Unauthorized(TestResponse{Message: "Invalid credentials"})
})

req := httptest.NewRequest(http.MethodGet, "/protected", http.NoBody)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)

if w.Code != http.StatusUnauthorized {
t.Errorf("expected status 401, got %d", w.Code)
}

expectedBody := `{"id":0,"message":"Invalid credentials"}` + "\n"
if w.Body.String() != expectedBody {
t.Errorf("expected body %q, got %q", expectedBody, w.Body.String())
}
}

// TestBox_Forbidden tests the Forbidden convenience method.
func TestBox_Forbidden(t *testing.T) {
r := New()

GET[Empty, TestResponse](r, "/admin", func(c *Box[Empty, TestResponse]) error {
return c.Forbidden(TestResponse{Message: "Insufficient permissions"})
})

req := httptest.NewRequest(http.MethodGet, "/admin", http.NoBody)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)

if w.Code != http.StatusForbidden {
t.Errorf("expected status 403, got %d", w.Code)
}

expectedBody := `{"id":0,"message":"Insufficient permissions"}` + "\n"
if w.Body.String() != expectedBody {
t.Errorf("expected body %q, got %q", expectedBody, w.Body.String())
}
}
Loading