diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 7c3830c..0cfad2e 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -2,20 +2,20 @@ name: Go
on:
push:
- branches: [ "master" ]
+ branches: ["master"]
pull_request:
- branches: [ "master" ]
+ branches: ["master"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - name: Set up Go
- uses: actions/setup-go@v4
- with:
- go-version: '1.20'
- - name: Build
- run: go build -v ./...
- - name: Test
- run: go test -v ./...
+ - uses: actions/checkout@v4
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.24"
+ - name: Build
+ run: go build -v ./...
+ - name: Test
+ run: go test -v ./...
diff --git a/cmd/main.go b/cmd/main.go
new file mode 100644
index 0000000..ee0cf14
--- /dev/null
+++ b/cmd/main.go
@@ -0,0 +1,59 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "net/http"
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+ "time"
+
+ "github.com/felipefrizzo/brazilian-zipcode-api/internal/config"
+ "github.com/felipefrizzo/brazilian-zipcode-api/internal/server"
+)
+
+func main() {
+ logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
+ logger.Info("starting server")
+
+ cfg, err := config.New()
+ if err != nil {
+ logger.Error("failed to create config", slog.String("error", err.Error()))
+ os.Exit(1)
+ }
+
+ srvr, err := server.New(logger, cfg)
+ if err != nil {
+ logger.Error("failed to create server", slog.String("error", err.Error()))
+ os.Exit(1)
+ }
+
+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
+ defer stop()
+
+ var wg sync.WaitGroup
+ wg.Add(1)
+
+ go func() {
+ defer wg.Done()
+ logger.Info(fmt.Sprintf("server is running on port %s", cfg.ServerPort))
+ if err := srvr.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ logger.Error("server error", slog.String("error", err.Error()))
+ }
+ }()
+
+ <-ctx.Done()
+
+ logger.Info("shutting down server")
+ shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ if err := srvr.Server.Shutdown(shutdownCtx); err != nil {
+ logger.Error("server shutdown error", slog.String("error", err.Error()))
+ }
+
+ wg.Wait()
+}
diff --git a/cmd/server.go b/cmd/server.go
deleted file mode 100644
index 6317d64..0000000
--- a/cmd/server.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package server
-
-import (
- "context"
- "log"
- "net/http"
- "os"
-
- "github.com/felipefrizzo/brazilian-zipcode-api/internals/middleware"
- "github.com/felipefrizzo/brazilian-zipcode-api/internals/zipcode"
- "github.com/gorilla/handlers"
- "github.com/gorilla/mux"
- "go.mongodb.org/mongo-driver/mongo"
-)
-
-// Server struct has router and db instances
-type Server struct {
- Router *mux.Router
- Mongo *mongo.Client
- MongoContext context.Context
-}
-
-// Initialize initializer server with predefined configuration
-func (s *Server) Initialize() {
- s.Router = mux.NewRouter()
- s.Router.Use(mux.CORSMethodMiddleware(s.Router))
-
- s.InitializeMongo()
- s.InitializeZipcode()
-}
-
-// InitializeMongo initialize mongo db server
-func (s *Server) InitializeMongo() {
- var err error
-
- s.Mongo, s.MongoContext, err = middleware.MongoConnection()
- if err != nil {
- log.Printf("Error to close connection with MongoDB %v", err)
- panic("Error to close connection with MongoDB")
- }
-}
-
-// InitializeZipcode initialize zipcode service
-func (s *Server) InitializeZipcode() {
- service := zipcode.New(s.Mongo)
- handler := zipcode.Handlers{
- Service: service,
- }
- handler.AddHandlers(s.Router)
-}
-
-// Run the app on it's router
-func (s *Server) Run(host string) {
- router := handlers.LoggingHandler(os.Stdout, s.Router)
- log.Fatal(http.ListenAndServe(host, router))
-}
diff --git a/go.mod b/go.mod
index 5bc3680..d2d029f 100644
--- a/go.mod
+++ b/go.mod
@@ -1,31 +1,31 @@
module github.com/felipefrizzo/brazilian-zipcode-api
-go 1.17
+go 1.24.2
require (
- github.com/caarlos0/env/v6 v6.8.0
- github.com/gorilla/handlers v1.5.1
- github.com/gorilla/mux v1.8.0
- github.com/stretchr/testify v1.6.1
- go.mongodb.org/mongo-driver v1.8.1
+ github.com/julienschmidt/httprouter v1.3.0
+ github.com/mitchellh/mapstructure v1.5.0
+ github.com/redis/go-redis/v9 v9.8.0
+ github.com/richardwilkes/toolbox v1.123.1
+ github.com/spf13/viper v1.20.1
)
require (
- github.com/davecgh/go-spew v1.1.1 // indirect
- github.com/felixge/httpsnoop v1.0.1 // indirect
- github.com/go-stack/stack v1.8.0 // indirect
- github.com/golang/snappy v0.0.1 // indirect
- github.com/klauspost/compress v1.13.6 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/fsnotify/fsnotify v1.9.0 // indirect
+ github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
- github.com/pkg/errors v0.9.1 // indirect
- github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/xdg-go/pbkdf2 v1.0.0 // indirect
- github.com/xdg-go/scram v1.0.2 // indirect
- github.com/xdg-go/stringprep v1.0.2 // indirect
- github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
- golang.org/x/crypto v0.31.0 // indirect
- golang.org/x/sync v0.10.0 // indirect
- golang.org/x/text v0.21.0 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/sagikazarmark/locafero v0.9.0 // indirect
+ github.com/sourcegraph/conc v0.3.0 // indirect
+ github.com/spf13/afero v1.14.0 // indirect
+ github.com/spf13/cast v1.8.0 // indirect
+ github.com/spf13/pflag v1.0.6 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ golang.org/x/sys v0.32.0 // indirect
+ golang.org/x/text v0.24.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
- gopkg.in/yaml.v3 v3.0.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 9b98386..b8e8661 100644
--- a/go.sum
+++ b/go.sum
@@ -1,132 +1,67 @@
-github.com/caarlos0/env/v6 v6.8.0 h1:abF9JinEXaibthiOowf4uSnRBWN66aJOxSpHLH67jeI=
-github.com/caarlos0/env/v6 v6.8.0/go.mod h1:FE0jGiAnQqtv2TenJ4KTa8+/T2Ss8kdS5s1VEjasoN0=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
-github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
-github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
-github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
-github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
-github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
-github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
-github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
-github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
-github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
-github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
-github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
-github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
-github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
-github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
-github.com/xdg-go/scram v1.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w=
-github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
-github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
-github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
-github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
-github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-go.mongodb.org/mongo-driver v1.8.1 h1:OZE4Wni/SJlrcmSIBRYNzunX5TKxjrTS4jKSnA99oKU=
-go.mongodb.org/mongo-driver v1.8.1/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
-golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
-golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
-golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
+github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
+github.com/richardwilkes/toolbox v1.123.1 h1:D0iWoJvF9gx0WTiTTMriws2RiG2aRzCTFBx4habecfY=
+github.com/richardwilkes/toolbox v1.123.1/go.mod h1:W8T2nCknTllNBBi/NtbBLfLi+knf4kKxQEBXBXxBGp0=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
+github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
+github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
+github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
+github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
+github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
+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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
-gopkg.in/yaml.v3 v3.0.0/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/internal/address/address.go b/internal/address/address.go
new file mode 100644
index 0000000..7caa9d0
--- /dev/null
+++ b/internal/address/address.go
@@ -0,0 +1,24 @@
+package address
+
+import (
+ "context"
+ "time"
+)
+
+// Address struct has information from brazilian addresses
+type Address struct {
+ FederativeUnit string `json:"federative_unit" bson:"federative_unit" xml:"uf"`
+ City string `json:"city" bson:"city" xml:"cidade"`
+ Neighborhood string `json:"neighborhood" bson:"neighborhood" xml:"bairro"`
+ AddressName string `json:"address_name" bson:"address_name" xml:"end"`
+ Complement string `json:"complement" bson:"complement" xml:"complemento2"`
+ Zipcode string `json:"zipcode" bson:"zipcode" xml:"cep"`
+ CreatedAt time.Time `json:"created_at" bson:"created_at"`
+}
+
+// AddressRepository interface defines methods for interacting with address data.
+type AddressRepository interface {
+ Get(ctx context.Context, zipcode string) (*Address, error)
+ Save(ctx context.Context, address *Address, zipcode string) error
+ Delete(ctx context.Context, zipcode string) error
+}
diff --git a/internal/address/redis/redis.go b/internal/address/redis/redis.go
new file mode 100644
index 0000000..9b15761
--- /dev/null
+++ b/internal/address/redis/redis.go
@@ -0,0 +1,126 @@
+package redis
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+ "time"
+
+ "github.com/felipefrizzo/brazilian-zipcode-api/internal/address"
+ "github.com/felipefrizzo/brazilian-zipcode-api/internal/correios"
+ "github.com/redis/go-redis/v9"
+ "github.com/richardwilkes/toolbox/errs"
+)
+
+const (
+ prefix = "brazilian-zipcode-api:address:"
+)
+
+type client struct {
+ redis *redis.Client
+ ttl time.Duration
+
+ correiosService correios.Correios
+}
+
+// NewClient creates a new address redis client.
+func NewClient(addr, username, password string, db int, correiosService correios.Correios) address.AddressRepository {
+ fmt.Printf("about to connect on redis addr: %s on db: %d\n", addr, db)
+ return &client{
+ redis: redis.NewClient(&redis.Options{
+ Addr: addr,
+ Username: username,
+ Password: password,
+ DB: db,
+ }),
+ ttl: time.Hour,
+
+ correiosService: correiosService,
+ }
+}
+
+func (c *client) getAndCreate(ctx context.Context, zipcode string) (*address.Address, error) {
+ addr, err := c.correiosService.GetAddressByZipcode(ctx, zipcode)
+ if err != nil {
+ return nil, errs.Wrap(err)
+ }
+
+ if err := c.Save(ctx, addr, zipcode); err != nil {
+ return nil, errs.Wrap(err)
+ }
+
+ return addr, nil
+}
+
+// Get retrieves an address from redis.
+func (c *client) Get(ctx context.Context, zipcode string) (*address.Address, error) {
+ key := fmt.Sprintf("%s%s", prefix, zipcode)
+
+ exists, err := c.redis.Exists(ctx, key).Result()
+ if err != nil {
+ return nil, errs.Wrap(err)
+ }
+ if exists == 0 {
+ slog.Info("address not found in redis, searching in correios api and saving to redis", "zipcode", zipcode)
+ addr, err := c.getAndCreate(ctx, zipcode)
+ if err != nil {
+ return nil, errs.Wrap(err)
+ }
+
+ return addr, nil
+
+ }
+
+ ttl, err := c.redis.TTL(ctx, key).Result()
+ if err != nil {
+ return nil, errs.Wrap(err)
+ }
+
+ if ttl <= 0 {
+ slog.Info("address expired, searching in correios api and saving to redis", "zipcode", zipcode)
+ if err := c.Delete(ctx, zipcode); err != nil {
+ return nil, errs.Wrap(err)
+ }
+
+ addr, err := c.getAndCreate(ctx, zipcode)
+ if err != nil {
+ return nil, errs.Wrap(err)
+ }
+
+ return addr, nil
+ }
+
+ content, err := c.redis.Get(ctx, key).Result()
+ if err != nil {
+ if err == redis.Nil {
+ return nil, errs.Newf("address not found")
+ }
+ return nil, errs.Wrap(err)
+ }
+
+ var addr address.Address
+ if err := json.Unmarshal([]byte(content), &addr); err != nil {
+ return nil, errs.Wrap(err)
+ }
+
+ return &addr, nil
+}
+
+// Save saves an address to redis.
+func (c *client) Save(ctx context.Context, address *address.Address, zipcode string) error {
+ key := fmt.Sprintf("%s%s", prefix, zipcode)
+
+ content, err := json.Marshal(address)
+ if err != nil {
+ return err
+ }
+
+ return c.redis.Set(ctx, key, content, c.ttl).Err()
+}
+
+// Delete deletes an address from redis.
+func (c *client) Delete(ctx context.Context, zipcode string) error {
+ key := fmt.Sprintf("%s%s", prefix, zipcode)
+ return c.redis.Del(ctx, key).Err()
+}
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..ed68e9c
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,58 @@
+package config
+
+import (
+ "os"
+ "strings"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/richardwilkes/toolbox/errs"
+ "github.com/spf13/viper"
+)
+
+const (
+ correiosURl = "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente?wsdl"
+)
+
+// Config represents the application configuration.
+type Config struct {
+ ServerPort string `mapstructure:"port"`
+
+ CorreiosURL string `mapstructure:"correios_url"`
+
+ DatabaseDriver string `mapstructure:"database_driver"`
+ DatabaseURL string `mapstructure:"database_url"`
+ DatabasePort string `mapstructure:"database_port"`
+ DatabaseName string `mapstructure:"database_name"`
+ DatabaseUsername string `mapstructure:"database_username"`
+ DatabasePassword string `mapstructure:"database_password"`
+}
+
+// New creates a new config instance.
+func New() (*Config, error) {
+ viper.SetEnvPrefix("APP")
+ viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
+ viper.AutomaticEnv()
+
+ if _, err := os.Stat(".env"); err == nil {
+ viper.SetConfigFile(".env")
+ viper.SetConfigType("env")
+ if err := viper.ReadInConfig(); err != nil {
+ return nil, errs.Wrap(err)
+ }
+ }
+
+ viper.SetDefault("port", "8080")
+ viper.SetDefault("correios.url", correiosURl)
+
+ var result map[string]any
+ var config Config
+ if err := viper.Unmarshal(&result); err != nil {
+ return nil, errs.Wrap(err)
+ }
+
+ if err := mapstructure.Decode(result, &config); err != nil {
+ return nil, errs.Wrap(err)
+ }
+
+ return &config, nil
+}
diff --git a/internal/correios/correios.go b/internal/correios/correios.go
new file mode 100644
index 0000000..9fafa91
--- /dev/null
+++ b/internal/correios/correios.go
@@ -0,0 +1,120 @@
+package correios
+
+import (
+ "bytes"
+ "context"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/felipefrizzo/brazilian-zipcode-api/internal/address"
+ "github.com/felipefrizzo/brazilian-zipcode-api/internal/helpers"
+ "github.com/richardwilkes/toolbox/errs"
+)
+
+const (
+ requestBody = `
+
+
+
+
+ %s
+
+
+
+ `
+
+ zipcodeNotFound string = "CEP NAO ENCONTRADO"
+ invalidZipcode string = "CEP INVÁLIDO"
+)
+
+// Correios represents the Correios service interface.
+// api reference documentation: https://www.correios.com.br/atendimento/developers/arquivos/manual-para-integracao-correios-api
+type Correios interface {
+ GetAddressByZipcode(ctx context.Context, zipcode string) (*address.Address, error)
+}
+
+type correios struct {
+ url string
+ httpClient *http.Client
+}
+
+type soapResponseError struct {
+ FaultError string `xml:"Body>Fault>faultstring"`
+}
+
+type soapResponse struct {
+ Address address.Address `xml:"Body>consultaCEPResponse>return"`
+}
+
+// New creates a new instance of the Correios service.
+func New(url string) Correios {
+ return &correios{
+ url: url,
+ httpClient: &http.Client{},
+ }
+}
+
+// GetAddressByZipcode retrieves the address information for a given zip code.
+func (c *correios) GetAddressByZipcode(ctx context.Context, zipcode string) (*address.Address, error) {
+ var payload []byte
+ payload = fmt.Appendf(payload, requestBody, zipcode)
+ resp, err := c.sendHTTPRequest(ctx, http.MethodPost, c.url, bytes.NewReader(payload))
+ if err != nil {
+ return nil, errs.Wrap(err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ fmt.Println("Unexpected status code:", resp.StatusCode)
+ var soapError soapResponseError
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, errs.NewWithCause("failed to read response body", err)
+ }
+
+ if err := xml.Unmarshal(helpers.ISO8859ToUTF8(body), &soapError); err != nil {
+ return nil, errs.NewWithCause("failed to unmarshal SOAP error", err)
+ }
+
+ if soapError.FaultError == zipcodeNotFound {
+ return nil, errs.NewWithCause("zipcode not found", errors.New(soapError.FaultError))
+ }
+
+ if soapError.FaultError == invalidZipcode {
+ return nil, errs.NewWithCause("invalid zipcode", errors.New(soapError.FaultError))
+ }
+
+ return nil, errs.NewWithCause("unexpected status code", err)
+ }
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, errs.NewWithCause("failed to read response body", err)
+ }
+
+ var soap soapResponse
+ if err := xml.Unmarshal(helpers.ISO8859ToUTF8(body), &soap); err != nil {
+ return nil, errs.NewWithCause("failed to unmarshal SOAP error", err)
+ }
+
+ return &soap.Address, nil
+}
+
+func (c *correios) sendHTTPRequest(ctx context.Context, method, url string, payload io.Reader) (*http.Response, error) {
+ req, err := http.NewRequestWithContext(ctx, method, url, payload)
+ if err != nil {
+ return nil, errs.NewWithCause("failed to create a request", err)
+ }
+
+ req.Header.Add("Content-Type", "application/soap+xml; charset=iso-8859-1")
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ fmt.Println(err)
+ return nil, errs.NewWithCause("failed to send a request", err)
+ }
+
+ return resp, nil
+}
diff --git a/internals/helpers/decode.go b/internal/helpers/decode.go
similarity index 100%
rename from internals/helpers/decode.go
rename to internal/helpers/decode.go
diff --git a/internal/server/server.go b/internal/server/server.go
new file mode 100644
index 0000000..40af935
--- /dev/null
+++ b/internal/server/server.go
@@ -0,0 +1,100 @@
+package server
+
+import (
+ "fmt"
+ "log/slog"
+ "net/http"
+ "strconv"
+
+ "github.com/felipefrizzo/brazilian-zipcode-api/internal/address"
+ "github.com/felipefrizzo/brazilian-zipcode-api/internal/address/redis"
+ "github.com/felipefrizzo/brazilian-zipcode-api/internal/config"
+ "github.com/felipefrizzo/brazilian-zipcode-api/internal/correios"
+ "github.com/felipefrizzo/brazilian-zipcode-api/internal/zipcode"
+ "github.com/julienschmidt/httprouter"
+ "github.com/richardwilkes/toolbox/errs"
+)
+
+type Server struct {
+ Router *httprouter.Router
+ Server *http.Server
+}
+
+// Handler represents a gateway handler
+type Handler interface {
+ AddHandlers(*httprouter.Router)
+}
+
+type handlerServices struct {
+ address address.AddressRepository
+ correios correios.Correios
+}
+
+// New creates a new server instance
+func New(logger *slog.Logger, cfg *config.Config) (*Server, error) {
+ logger.Info("starting up server")
+
+ services, err := createServices(cfg)
+ if err != nil {
+ return nil, errs.Wrap(err)
+ }
+
+ router := httprouter.New()
+ router.GET("/health", healthcheckHandler)
+ router.PanicHandler = panicHandler
+ server := &http.Server{
+ Addr: ":" + cfg.ServerPort,
+ Handler: router,
+ }
+
+ srvr := &Server{
+ Router: router,
+ Server: server,
+ }
+
+ srvr.registerHandlers(services)
+ return srvr, nil
+}
+
+func (srvr *Server) AddHandlers(handlers ...Handler) {
+ for _, h := range handlers {
+ h.AddHandlers(srvr.Router)
+ }
+}
+
+func createServices(cfg *config.Config) (*handlerServices, error) {
+ var addrService address.AddressRepository
+
+ correiosService := correios.New(cfg.CorreiosURL)
+ switch cfg.DatabaseDriver {
+ case "redis":
+ db, err := strconv.ParseInt(cfg.DatabaseName, 10, 32) // database name is translated to int as database number in redis
+ if err != nil {
+ return nil, errs.Wrap(err)
+ }
+
+ addrService = redis.NewClient(fmt.Sprintf("%s:%s", cfg.DatabaseURL, cfg.DatabasePort), cfg.DatabaseUsername, cfg.DatabasePassword, int(db), correiosService)
+ default:
+ return nil, errs.Newf("unsupported database driver: %s", cfg.DatabaseDriver)
+ }
+
+ return &handlerServices{
+ address: addrService,
+ correios: correiosService,
+ }, nil
+}
+
+func (srvr *Server) registerHandlers(svcs *handlerServices) {
+ zipcodeHandler := zipcode.New(svcs.address)
+
+ srvr.AddHandlers(zipcodeHandler)
+}
+
+func healthcheckHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("OK"))
+}
+
+func panicHandler(w http.ResponseWriter, req *http.Request, rcv any) {
+ w.WriteHeader(http.StatusInternalServerError)
+}
diff --git a/internal/zipcode/handler.go b/internal/zipcode/handler.go
new file mode 100644
index 0000000..0c2fba4
--- /dev/null
+++ b/internal/zipcode/handler.go
@@ -0,0 +1,53 @@
+package zipcode
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/felipefrizzo/brazilian-zipcode-api/internal/address"
+ "github.com/julienschmidt/httprouter"
+)
+
+// Handler some description
+type Handler struct {
+ Address address.AddressRepository
+}
+
+// New creates a new instance of Handler
+func New(addr address.AddressRepository) *Handler {
+ return &Handler{
+ Address: addr,
+ }
+}
+
+// AddHandlers some description
+func (h *Handler) AddHandlers(r *httprouter.Router) {
+ r.GET("/zipcode/:zipcode", h.FetchAddressByZipcode())
+}
+
+// FetchAddressByZipcode fetches an address by zipcode
+func (h *Handler) FetchAddressByZipcode() httprouter.Handle {
+ return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
+ zipcode := params.ByName("zipcode")
+ if zipcode == "" {
+ http.Error(w, "missing zipcode", http.StatusBadRequest)
+ return
+ }
+
+ addr, err := h.Address.Get(r.Context(), zipcode)
+ if err != nil {
+ http.Error(w, "address not found", http.StatusNotFound)
+ return
+ }
+
+ response, err := json.Marshal(addr)
+ if err != nil {
+ http.Error(w, "internal server error", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ w.Write(response)
+ }
+}
diff --git a/internals/configs/config.go b/internals/configs/config.go
deleted file mode 100644
index d91f64a..0000000
--- a/internals/configs/config.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package configs
-
-import (
- "fmt"
-
- "github.com/caarlos0/env/v6"
-)
-
-type mongoConfig struct {
- MongoURI string `env:"MONGO_URI" envDefault:"mongodb://localhost:27017"`
-}
-
-type config struct {
- Mongo mongoConfig
- CorreiosURL string
-}
-
-// Config values collect from environment variables
-var Config config
-
-func init() {
- mongo := mongoConfig{}
- if err := env.Parse(&mongo); err != nil {
- panic(fmt.Errorf("Error to collect mongo environment values. Error: %s", err))
- }
-
- Config.Mongo = mongo
- Config.CorreiosURL = "https://apps.correios.com.br/SigepMasterJPA/AtendeClienteService/AtendeCliente?wsdl"
-}
diff --git a/internals/helpers/request.go b/internals/helpers/request.go
deleted file mode 100644
index 47d9704..0000000
--- a/internals/helpers/request.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package helpers
-
-import (
- "bytes"
- "fmt"
- "net/http"
-)
-
-// RequestXML abstraction function to make HTTP request in XML
-func RequestXML(url string, body string, fields []interface{}) (*http.Response, error) {
- client := &http.Client{}
- payload := []byte(fmt.Sprintf(body, fields...))
-
- request, err := http.NewRequest("POST", url, bytes.NewReader(payload))
- if err != nil {
- return nil, err
- }
-
- request.Header.Add("Content-Type", "application/soap+xml; charset=iso-8859-1")
- response, err := client.Do(request)
- if err != nil {
- return nil, err
- }
-
- return response, nil
-}
diff --git a/internals/middleware/mongo.go b/internals/middleware/mongo.go
deleted file mode 100644
index b81362c..0000000
--- a/internals/middleware/mongo.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package middleware
-
-import (
- "context"
- "time"
-
- "github.com/felipefrizzo/brazilian-zipcode-api/internals/configs"
- "go.mongodb.org/mongo-driver/mongo"
- "go.mongodb.org/mongo-driver/mongo/options"
-)
-
-// MongoConnection open connection with mongodb
-func MongoConnection() (*mongo.Client, context.Context, error) {
- config := configs.Config.Mongo
-
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
- client, err := mongo.Connect(ctx, options.Client().ApplyURI(config.MongoURI))
- if err != nil {
- return nil, nil, err
- }
-
- return client, ctx, nil
-}
diff --git a/internals/models/address.go b/internals/models/address.go
deleted file mode 100644
index c867fee..0000000
--- a/internals/models/address.go
+++ /dev/null
@@ -1,149 +0,0 @@
-package models
-
-import (
- "encoding/xml"
- "errors"
- "fmt"
- "io/ioutil"
- "net/http"
- "time"
-
- "github.com/felipefrizzo/brazilian-zipcode-api/internals/configs"
- "github.com/felipefrizzo/brazilian-zipcode-api/internals/helpers"
- bson "go.mongodb.org/mongo-driver/bson/primitive"
-)
-
-const (
- body string = `
-
-
-
-
- %s
-
-
-
- `
- zipcodeNotFound string = "CEP NAO ENCONTRADO"
- zipcodeInvalid string = "CEP INVÁLIDO"
-)
-
-// ErrAddressNotFound returns if address was not found
-// ErrAddressInvalid returns if zip code sent is invalid
-var (
- ErrAddressNotFound = errors.New("Address not found")
- ErrAddressInvalid = errors.New("The informed zip code is invalid")
-)
-
-type (
- // AddressInterface interface that exports the model methods
- AddressInterface interface {
- Create(zipcode string) error
- IsUpdated() bool
- Update(zipcode string) error
- }
-
- soapResponse struct {
- Address Address `xml:"Body>consultaCEPResponse>return"`
- }
-
- soapResponseError struct {
- FaultError string `xml:"Body>Fault>faultstring"`
- }
-
- // Address struct has information from brazilian addresses
- Address struct {
- ID bson.ObjectID `json:"-" bson:"_id,omitempty"`
- FederativeUnit string `json:"federative_unit" bson:"federative_unit" xml:"uf"`
- City string `json:"city" bson:"city" xml:"cidade"`
- Neighborhood string `json:"neighborhood" bson:"neighborhood" xml:"bairro"`
- AddressName string `json:"address_name" bson:"address_name" xml:"end"`
- Complement string `json:"complement" bson:"complement" xml:"complemento2"`
- Zipcode string `json:"zipcode" bson:"zipcode" xml:"cep"`
- CreatedAt time.Time `json:"created_at" bson:"created_at"`
- UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
- }
-)
-
-// Create this function create address if doesn't exists
-func (a *Address) Create(zipcode string) error {
- c, err := fetchCorreiosAddress(zipcode)
- if err != nil {
- return err
- }
-
- a.FederativeUnit = c.FederativeUnit
- a.City = c.City
- a.Neighborhood = c.Neighborhood
- a.AddressName = c.AddressName
- a.Complement = c.Complement
- a.Zipcode = c.Zipcode
- a.CreatedAt = time.Now().UTC()
- a.UpdatedAt = time.Now().UTC()
-
- return nil
-}
-
-// IsUpdated returns if the address is older than 7 days
-func (a *Address) IsUpdated() bool {
- var sevenDaysAgo time.Time = time.Now().UTC().AddDate(0, 0, -7)
- return a.UpdatedAt.After(sevenDaysAgo)
-}
-
-// Update this function update address if outdated
-func (a *Address) Update() error {
- c, err := fetchCorreiosAddress(a.Zipcode)
- if err != nil {
- return err
- }
-
- a.FederativeUnit = c.FederativeUnit
- a.City = c.City
- a.Neighborhood = c.Neighborhood
- a.AddressName = c.AddressName
- a.Complement = c.Complement
- a.Zipcode = c.Zipcode
- a.UpdatedAt = time.Now().UTC()
-
- return nil
-}
-
-func fetchCorreiosAddress(zipcode string) (*Address, error) {
- var soap soapResponse
- var URL string = configs.Config.CorreiosURL
-
- response, err := helpers.RequestXML(URL, body, []interface{}{zipcode})
- if err != nil {
- return nil, fmt.Errorf("Error for fetching data from correios API. %v", err)
- }
- defer response.Body.Close()
-
- if response.StatusCode != http.StatusOK {
- var soapError soapResponseError
-
- data, err := ioutil.ReadAll(response.Body)
- if err != nil {
- return nil, fmt.Errorf("Error to read request body. %v", err)
- }
-
- xml.Unmarshal(helpers.ISO8859ToUTF8(data), &soapError)
- if soapError.FaultError == zipcodeNotFound {
- return nil, ErrAddressNotFound
- }
-
- if soapError.FaultError == zipcodeInvalid {
- return nil, ErrAddressInvalid
- }
-
- return nil, fmt.Errorf("Status code: %v", response.StatusCode)
- }
-
- data, err := ioutil.ReadAll(response.Body)
- if err != nil {
- return nil, fmt.Errorf("Error to read request body. %v", err)
- }
-
- xml.Unmarshal(helpers.ISO8859ToUTF8(data), &soap)
-
- return &soap.Address, nil
-}
diff --git a/internals/models/address_test.go b/internals/models/address_test.go
deleted file mode 100644
index 5ce6308..0000000
--- a/internals/models/address_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package models
-
-import (
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestAddressIsUpdated(t *testing.T) {
- var a Address = Address{
- FederativeUnit: "PR",
- City: "Cascavel",
- Neighborhood: "Santa Felicidade",
- AddressName: "Rua Major João Ribeiro Pinheiro",
- Complement: "",
- Zipcode: "85803260",
- CreatedAt: time.Now().UTC(),
- UpdatedAt: time.Now().UTC(),
- }
-
- assert.True(t, a.IsUpdated())
-}
-
-func TestAddressIsNotUpdated(t *testing.T) {
- var a Address = Address{
- FederativeUnit: "PR",
- City: "Cascavel",
- Neighborhood: "Santa Felicidade",
- AddressName: "Rua Major João Ribeiro Pinheiro",
- Complement: "",
- Zipcode: "85803260",
- CreatedAt: time.Now().UTC().AddDate(0, 0, -8),
- UpdatedAt: time.Now().UTC().AddDate(0, 0, -8),
- }
-
- assert.False(t, a.IsUpdated())
-}
diff --git a/internals/zipcode/handler.go b/internals/zipcode/handler.go
deleted file mode 100644
index 496addd..0000000
--- a/internals/zipcode/handler.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package zipcode
-
-import (
- "encoding/json"
- "net/http"
-
- "github.com/felipefrizzo/brazilian-zipcode-api/internals/models"
- "github.com/gorilla/mux"
-)
-
-// Handlers some description
-type Handlers struct {
- Service Service
-}
-
-// AddHandlers some description
-func (h *Handlers) AddHandlers(r *mux.Router) {
- r.HandleFunc("/zipcode/{zipcode:[0-9]+}", h.FetchAddressByZipcode).Methods("GET")
-}
-
-// FetchAddressByZipcode function for returns the address corresponding to the zipcode
-func (h *Handlers) FetchAddressByZipcode(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- w.Header().Set("Access-Control-Allow-Origin", "*")
-
- var params map[string]string = mux.Vars(r)
- var zipcode string = params["zipcode"]
-
- address, err := h.Service.FetchAddressByZipcode(zipcode)
- if err != nil {
- if err == models.ErrAddressNotFound {
- http.Error(w, models.ErrAddressNotFound.Error(), http.StatusNotFound)
- return
- }
- if err == models.ErrAddressInvalid {
- http.Error(w, models.ErrAddressInvalid.Error(), http.StatusBadRequest)
- return
- }
-
- http.Error(w, "Internal server error", http.StatusInternalServerError)
- return
- }
-
- json.NewEncoder(w).Encode(address)
-}
diff --git a/internals/zipcode/service.go b/internals/zipcode/service.go
deleted file mode 100644
index db6d2b9..0000000
--- a/internals/zipcode/service.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package zipcode
-
-import (
- "fmt"
- "log"
-
- "github.com/felipefrizzo/brazilian-zipcode-api/internals/models"
- "go.mongodb.org/mongo-driver/bson"
- "go.mongodb.org/mongo-driver/bson/primitive"
- "go.mongodb.org/mongo-driver/mongo"
-)
-
-// Service represents zipcode application interface
-type Service interface {
- FetchAddressByZipcode(zipcode string) (*models.Address, error)
-}
-
-type zipcode struct {
- MongoSession *mongo.Client
-}
-
-// New initialize new zipcode service
-func New(session *mongo.Client) Service {
- return &zipcode{
- MongoSession: session,
- }
-}
-
-// FetchAddressByZipcode returns the address corresponding to the zipcode
-func (z *zipcode) FetchAddressByZipcode(zipcode string) (*models.Address, error) {
- var address models.Address
-
- if err := z.MongoSession.Database("zipcode").Collection("addresses").FindOne(nil, bson.M{"zipcode": zipcode}).Decode(&address); err != nil {
- if err == mongo.ErrNoDocuments {
- if err := address.Create(zipcode); err != nil {
- return nil, err
- }
- insert, err := z.MongoSession.Database("zipcode").Collection("addresses").InsertOne(nil, address)
- if err != nil {
- log.Printf("Error inserting address %v", err)
- return nil, err
- }
-
- address.ID = insert.InsertedID.(primitive.ObjectID)
- return &address, nil
- }
- return nil, models.ErrAddressNotFound
- }
-
- if address.IsUpdated() {
- return &address, nil
- }
-
- if err := address.Update(); err != nil {
- z.MongoSession.Database("zipcode").Collection("addresses").DeleteOne(nil, bson.M{"_id": address.ID})
- return nil, err
- }
-
- if _, err := z.MongoSession.Database("zipcode").Collection("addresses").ReplaceOne(nil, address, bson.M{"upsert": true}); err != nil {
- errMessage := fmt.Sprintf("Error updating address %v", err)
- log.Println(errMessage)
- return nil, fmt.Errorf(errMessage)
- }
-
- return &address, nil
-}
diff --git a/main.go b/main.go
deleted file mode 100644
index baa3afc..0000000
--- a/main.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package main
-
-import (
- server "github.com/felipefrizzo/brazilian-zipcode-api/cmd"
-)
-
-func main() {
- server := &server.Server{}
- server.Initialize()
- server.Run(":8000")
-}