From 46122d8b9120db9c3cdf57b30185a56637a0d2fc Mon Sep 17 00:00:00 2001 From: Jeferson Siqueira Date: Fri, 4 Sep 2026 10:32:05 +0100 Subject: [PATCH] refactor!: rename to brdoc, delete the cache, version the API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes that belong together because each is a break, and one break is cheaper than three. The module is github.com/jeferson0306/brdoc. api-data-validator described a service; what this mostly is now is a library, and .../api-data-validator/validate reads badly in an import block. GitHub redirects the old URL, and nothing depends on the old module path yet — which is precisely why this is the moment. The cache is gone. #10's benchmarks put a CPF check at 522ns against a Redis round trip measured at 150-220ms in production, and building the cache key required the normalised value, which is most of the validation — the answer was already computed before the cache was consulted. It was never an optimisation. from_cache leaves the response with it, and /health stops reporting a subsystem that no longer exists: validation is arithmetic with no dependencies, so if the process answers, it works. Enumerating subsystems a service does not have is theatre. /v1 is mounted, and the unversioned paths stay permanently. They were published, things point at them, and breaking a URL to tidy a prefix is a cost paid by other people. /v1 exists so the next change has somewhere to go rather than to move anyone today. Smoke tested against a running binary: both route generations answer, from_cache is absent from the response, and health reports one field. --- README.md | 4 +- cmd/brdoc/main.go | 2 +- go.mod | 2 +- handlers/batch.go | 16 +----- handlers/batch_test.go | 2 +- handlers/health.go | 29 ++-------- handlers/validate.go | 23 ++------ handlers/validate_test.go | 2 +- internal/cache/cache.go | 109 ------------------------------------- main.go | 31 +++++++---- models/response.go | 2 - validate/benchmark_test.go | 2 +- validate/example_test.go | 2 +- 13 files changed, 40 insertions(+), 186 deletions(-) delete mode 100644 internal/cache/cache.go diff --git a/README.md b/README.md index e95e5c5..0b25885 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Data Validator API A Go library for validating Brazilian documents, and an HTTP service built on it. ```bash -go get github.com/jeferson0306/api-data-validator/validate +go get github.com/jeferson0306/brdoc/validate ``` ```go @@ -47,7 +47,7 @@ is wrong — see `validate/inscricao_estadual_test.go`. ## Command line ```bash -go install github.com/jeferson0306/api-data-validator/cmd/brdoc@latest +go install github.com/jeferson0306/brdoc/cmd/brdoc@latest brdoc cpf 529.982.247-25 && echo accepted brdoc ie 0100482300112 AC diff --git a/cmd/brdoc/main.go b/cmd/brdoc/main.go index a86465f..cf74239 100644 --- a/cmd/brdoc/main.go +++ b/cmd/brdoc/main.go @@ -18,7 +18,7 @@ import ( "os" "strings" - "github.com/jeferson0306/api-data-validator/validate" + "github.com/jeferson0306/brdoc/validate" ) const ( diff --git a/go.mod b/go.mod index 550a7c6..b0ab013 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/jeferson0306/api-data-validator +module github.com/jeferson0306/brdoc go 1.22.5 diff --git a/handlers/batch.go b/handlers/batch.go index 3c77b15..62cd162 100644 --- a/handlers/batch.go +++ b/handlers/batch.go @@ -7,9 +7,8 @@ import ( "strings" "time" - "github.com/jeferson0306/api-data-validator/internal/cache" - "github.com/jeferson0306/api-data-validator/models" - "github.com/jeferson0306/api-data-validator/validate" + "github.com/jeferson0306/brdoc/models" + "github.com/jeferson0306/brdoc/validate" "github.com/google/uuid" ) @@ -102,17 +101,6 @@ func validateItem(item models.BatchItem) models.BatchResult { return result } - // CPF is the one check routed through the cache, so that from_cache means - // the same thing on both endpoints. - if key == "cpf" { - outcome, fromCache := cache.CPF(item.Value) - result.Value = outcome.Normalized - result.IsValid = outcome.Valid - result.Message = outcome.Reason - result.FromCache = fromCache - return result - } - outcome, known := validate.ByKey(key, item.Value, item.Qualifier) if !known { // Distinguished from a failed validation on purpose: an unknown key is diff --git a/handlers/batch_test.go b/handlers/batch_test.go index 92d8f21..af0ff6b 100644 --- a/handlers/batch_test.go +++ b/handlers/batch_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "github.com/jeferson0306/api-data-validator/models" + "github.com/jeferson0306/brdoc/models" ) func postBatch(t *testing.T, body string) (*httptest.ResponseRecorder, models.BatchResponse) { diff --git a/handlers/health.go b/handlers/health.go index 9e90466..6c3398d 100644 --- a/handlers/health.go +++ b/handlers/health.go @@ -3,33 +3,14 @@ package handlers import ( "net/http" - "github.com/jeferson0306/api-data-validator/internal/cache" - "github.com/gin-gonic/gin" ) -// HealthHandler reports whether the service is up and whether its cache is -// reachable. +// HealthHandler reports that the service is up. // -// The cache is reported but never gates the status: validation is pure -// computation and stays correct with Redis down, so a degraded cache must not -// take the service out of a load balancer. It exists to make the degradation -// visible — a silently unreachable Redis is how this service spent its first -// deployment paying latency for a cache that never returned a hit. +// There is nothing else to report. Validation is arithmetic with no +// dependencies: if the process is answering, it is working. A health check that +// enumerates subsystems the service does not have is theatre. func HealthHandler(c *gin.Context) { - // A local named "cache" would shadow the package; more importantly, there - // are now three states rather than two. "disabled" is not a fault. - status := "disabled" - switch { - case !cache.Enabled(): - case cache.Healthy(): - status = "ok" - default: - status = "unreachable" - } - - c.JSON(http.StatusOK, gin.H{ - "status": "ok", - "cache": status, - }) + c.JSON(http.StatusOK, gin.H{"status": "ok"}) } diff --git a/handlers/validate.go b/handlers/validate.go index ea1bb1b..a1dea9f 100644 --- a/handlers/validate.go +++ b/handlers/validate.go @@ -2,9 +2,8 @@ package handlers import ( "encoding/json" - "github.com/jeferson0306/api-data-validator/internal/cache" - "github.com/jeferson0306/api-data-validator/models" - "github.com/jeferson0306/api-data-validator/validate" + "github.com/jeferson0306/brdoc/models" + "github.com/jeferson0306/brdoc/validate" "net/http" "strings" "time" @@ -73,19 +72,8 @@ func ValidateHandler(w http.ResponseWriter, r *http.Request) { // The qualifier is only read for the validations that take one; passing it // unconditionally keeps this loop from growing a special case per document. - // CPF is the one check routed through the cache, because from_cache is part - // of the published response. Everything else goes straight to the library. - var ( - result validate.Result - fromCache bool - ) - if key == "cpf" { - result, fromCache = cache.CPF(value) - } else { - result, _ = validate.ByKey(key, value, r.URL.Query().Get("uf")) - } - - response = createResponse(key, value, result.Normalized, result.Valid, result.Reason, start, fromCache) + result, _ := validate.ByKey(key, value, r.URL.Query().Get("uf")) + response = createResponse(key, value, result.Normalized, result.Valid, result.Reason, start) writeResponse(w, response) } @@ -138,7 +126,7 @@ func writeResponse(w http.ResponseWriter, response models.ValidationResponse) { } // createResponse creates a response with the provided data. -func createResponse(key, rawValue, sanitizedValue string, isValid bool, message string, start time.Time, fromCache bool) models.ValidationResponse { +func createResponse(key, rawValue, sanitizedValue string, isValid bool, message string, start time.Time) models.ValidationResponse { statusCode := http.StatusOK errorCode := "" if !isValid { @@ -157,6 +145,5 @@ func createResponse(key, rawValue, sanitizedValue string, isValid bool, message RequestID: uuid.New().String(), Timestamp: time.Now(), ExecutionTimeMs: int(time.Since(start).Milliseconds()), - FromCache: fromCache, } } diff --git a/handlers/validate_test.go b/handlers/validate_test.go index 6494219..fc1f7e5 100644 --- a/handlers/validate_test.go +++ b/handlers/validate_test.go @@ -6,7 +6,7 @@ import ( "net/http/httptest" "testing" - "github.com/jeferson0306/api-data-validator/models" + "github.com/jeferson0306/brdoc/models" ) func TestValidateHandlerMissingParameter(t *testing.T) { diff --git a/internal/cache/cache.go b/internal/cache/cache.go deleted file mode 100644 index 53bc27a..0000000 --- a/internal/cache/cache.go +++ /dev/null @@ -1,109 +0,0 @@ -// Package cache remembers CPF results between requests. -// -// It lives here rather than in the validate package on purpose: validation is -// arithmetic and must work with nothing else running, while a cache is -// infrastructure with a connection, a timeout and a failure mode. Keeping them -// apart is what lets the library be imported without opening a socket. -package cache - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "log/slog" - "os" - "time" - - "github.com/go-redis/redis/v8" - - "github.com/jeferson0306/api-data-validator/validate" -) - -const ( - // timeout bounds every call. Validation itself is measured in microseconds, - // so an unreachable cache must never hold a request open — the answer is - // recomputed instead, which costs less than waiting. - timeout = 150 * time.Millisecond - // retention is generous because the answer cannot change: a CPF that is - // valid today was valid yesterday. - retention = 24 * time.Hour -) - -var client = redis.NewClient(&redis.Options{Addr: address()}) - -func address() string { - if addr := os.Getenv("REDIS_ADDR"); addr != "" { - return addr - } - return "localhost:6379" -} - -// keyFor hashes the value before it becomes a key. -// -// The key used to be "cpf:" followed by the CPF itself, which put every CPF -// ever checked into the cache in the clear. A digest keys just as well — equal -// inputs still collide onto one entry — while storing nothing that identifies -// a person. -func keyFor(value string) string { - digest := sha256.Sum256([]byte(value)) - return "cpf:" + hex.EncodeToString(digest[:]) -} - -// CPF validates through the cache, reporting whether the answer was remembered. -// -// Note what this costs. Building the key needs the normalised value, and -// normalising is most of the validation, so the answer is already computed -// before the cache is consulted — see the benchmarks in the validate package, -// where a CPF check runs in about 500 nanoseconds against a Redis round trip -// measured at 150 to 220 milliseconds in production. The cache is retained -// because from_cache is part of the published response; the numbers argue for -// removing both. -func CPF(value string) (validate.Result, bool) { - result := validate.CPF(value) - - if !Enabled() { - return result, false - } - - key := keyFor(result.Normalized) - - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - if cached, err := client.Get(ctx, key).Result(); err == nil { - result.Valid = cached == "true" - return result, true - } - - stored := "false" - if result.Valid { - stored = "true" - } - - if err := client.Set(ctx, key, stored, retention).Err(); err != nil { - // Debug, not error: a missing cache is a degraded mode, not a failure, - // and at request rate an unreachable Redis would drown the log. - // Healthy is what surfaces the condition. - slog.Debug("cache write failed", slog.String("error", err.Error())) - } - - return result, false -} - -// Enabled reports whether a cache was configured at all. Unset, the service -// skips the round trip rather than attempting one and timing out per request. -func Enabled() bool { - return os.Getenv("REDIS_ADDR") != "" -} - -// Healthy reports whether the cache is reachable, so a health endpoint can say -// so rather than the service degrading in silence. -func Healthy() bool { - if !Enabled() { - return false - } - - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - return client.Ping(ctx).Err() == nil -} diff --git a/main.go b/main.go index c61cd93..47c29c1 100644 --- a/main.go +++ b/main.go @@ -11,10 +11,10 @@ import ( "syscall" "time" - "github.com/jeferson0306/api-data-validator/docs" - "github.com/jeferson0306/api-data-validator/handlers" - "github.com/jeferson0306/api-data-validator/middleware" - "github.com/jeferson0306/api-data-validator/observability" + "github.com/jeferson0306/brdoc/docs" + "github.com/jeferson0306/brdoc/handlers" + "github.com/jeferson0306/brdoc/middleware" + "github.com/jeferson0306/brdoc/observability" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" @@ -39,14 +39,23 @@ func main() { configureClientAddressing(router, logger) - router.GET("/health", handlers.HealthHandler) router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) - router.GET("/validate", func(c *gin.Context) { - handlers.ValidateHandler(c.Writer, c.Request) - }) - router.POST("/validate/batch", func(c *gin.Context) { - handlers.BatchHandler(c.Writer, c.Request) - }) + + validate := func(c *gin.Context) { handlers.ValidateHandler(c.Writer, c.Request) } + batch := func(c *gin.Context) { handlers.BatchHandler(c.Writer, c.Request) } + + // Versioned routes are where new work goes. + v1 := router.Group("/v1") + v1.GET("/health", handlers.HealthHandler) + v1.GET("/validate", validate) + v1.POST("/validate/batch", batch) + + // The unversioned paths stay, permanently. They were published, things + // point at them, and breaking a URL to tidy a prefix is a cost paid by + // other people. /v1 exists so the *next* change has somewhere to go. + router.GET("/health", handlers.HealthHandler) + router.GET("/validate", validate) + router.POST("/validate/batch", batch) server := &http.Server{ Addr: ":" + port(), diff --git a/models/response.go b/models/response.go index c074306..d2d3018 100644 --- a/models/response.go +++ b/models/response.go @@ -14,7 +14,6 @@ type ValidationResponse struct { Timestamp time.Time `json:"timestamp"` ExecutionTimeMs int `json:"execution_time_ms"` LocationData map[string]interface{} `json:"location_data,omitempty"` - FromCache bool `json:"from_cache"` } // BatchRequest is one form's worth of values, checked in a single call. @@ -66,5 +65,4 @@ type BatchResult struct { Value string `json:"value"` IsValid bool `json:"is_valid"` Message string `json:"message"` - FromCache bool `json:"from_cache"` } diff --git a/validate/benchmark_test.go b/validate/benchmark_test.go index 8afc53b..f3b849d 100644 --- a/validate/benchmark_test.go +++ b/validate/benchmark_test.go @@ -3,7 +3,7 @@ package validate_test import ( "testing" - "github.com/jeferson0306/api-data-validator/validate" + "github.com/jeferson0306/brdoc/validate" ) // These exist to answer one question the service's design depends on: is any of diff --git a/validate/example_test.go b/validate/example_test.go index fb08e71..68f735f 100644 --- a/validate/example_test.go +++ b/validate/example_test.go @@ -3,7 +3,7 @@ package validate_test import ( "fmt" - "github.com/jeferson0306/api-data-validator/validate" + "github.com/jeferson0306/brdoc/validate" ) func ExampleCPF() {