From 9bd04f843a9083052255a1d366e2aab1857b4603 Mon Sep 17 00:00:00 2001 From: Hazem Krimi Date: Mon, 2 Jun 2025 17:35:07 +0100 Subject: [PATCH] chore: use the echo framework instead of plain go net/http --- cmd/root.go | 10 +--- go.mod | 10 ++++ go.sum | 28 +++++++++ internal/api/api.go | 29 ++++++--- internal/api/client.go | 120 +++++++++++++++++++++++++++----------- internal/api/routes.go | 17 +++--- internal/models/client.go | 48 ++++++++++++--- internal/models/db.go | 8 +-- 8 files changed, 199 insertions(+), 71 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 71539f7..6f60c0e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,8 +4,6 @@ Copyright © 2025 Hazem Krimi me@hazemkrimi.tech package cmd import ( - "fmt" - "net/http" "os" "github.com/hazemKrimi/crimson-vault/internal/api" @@ -25,13 +23,9 @@ to quickly create a Cobra application.`, // Uncomment the following line if your bare application // has an action associated with it: Run: func(cmd *cobra.Command, args []string) { - apiWrapper := api.APIWrapper{} - mux := http.NewServeMux() + server := api.API{} - apiWrapper.Initialize() - mux.Handle("/clients/", api.ClientRoutes(&apiWrapper)) - fmt.Println("Server listening on PORT 5000...") - http.ListenAndServe(":5000", mux) + server.Initialize() }, } diff --git a/go.mod b/go.mod index 4028ce5..15aa01e 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/hazemKrimi/crimson-vault go 1.24.3 require ( + github.com/labstack/echo/v4 v4.13.4 github.com/spf13/cobra v1.9.1 gorm.io/driver/sqlite v1.5.7 gorm.io/gorm v1.30.0 @@ -12,7 +13,16 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/spf13/pflag v1.0.6 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect + golang.org/x/time v0.11.0 // indirect ) diff --git a/go.sum b/go.sum index a26060d..143a250 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,48 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= +github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +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/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= diff --git a/internal/api/api.go b/internal/api/api.go index 8a20904..0a2e3c4 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -1,16 +1,29 @@ package api -import "github.com/hazemKrimi/crimson-vault/internal/models" +import ( + "github.com/hazemKrimi/crimson-vault/internal/models" + "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v4" +) -type APIWrapper struct { - dbWrapper *models.DBWrapper +type API struct { + instance *echo.Echo + db *models.DB } -func (api *APIWrapper) Initialize() { - wrapper := models.DBWrapper{} +func (api *API) Initialize() { + db := &models.DB{} + ech := echo.New() - wrapper.Connect() - wrapper.MigrateClients() + db.Connect() + db.MigrateClients() - api.dbWrapper = &wrapper; + api.db = db + api.instance = ech + + api.ClientRoutes() + api.instance.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"*"}, + })) + api.instance.Logger.Fatal(api.instance.Start(":5000")) } diff --git a/internal/api/client.go b/internal/api/client.go index 0ccb5dd..e257d32 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -1,60 +1,112 @@ package api import ( - "encoding/json" "fmt" "log" "net/http" "strconv" "github.com/hazemKrimi/crimson-vault/internal/models" + "github.com/labstack/echo/v4" ) -func (api *APIWrapper) CreateClientHandler(writer http.ResponseWriter, request *http.Request) { +func (api *API) CreateClientHandler(context echo.Context) error { var body models.CreateClientBody - if err := json.NewDecoder(request.Body).Decode(&body); err != nil { - http.Error(writer, "Invalid JSON", http.StatusBadRequest) + if err := context.Bind(&body); err != nil { log.Println(fmt.Sprintf("Error creating Client: %v.", err)) - return + return context.String(http.StatusBadRequest, "Invalid JSON!") } - client := api.dbWrapper.CreateClient(body) + client := api.db.CreateClient(body) log.Println(fmt.Sprintf("Client created with ID %d.", client.ID)) - json.NewEncoder(writer).Encode(client) + return context.JSON(http.StatusOK, client) } -func (api *APIWrapper) GetClientsHandler(writer http.ResponseWriter, request *http.Request) { - idString := request.URL.Query().Get("id") - - if idString != "" { - id, err := strconv.Atoi(idString) - - if err != nil { - http.Error(writer, "Unexpected error getting Client.", http.StatusInternalServerError) - return - } - - var client models.Client - - if err := api.dbWrapper.GetClient(id, &client); err != nil { - http.Error(writer, "Client not found.", http.StatusNotFound) - return - } - - log.Println(fmt.Sprintf("Got client with ID %d.", client.ID)) - json.NewEncoder(writer).Encode(client) - return - } - - clients, err := api.dbWrapper.GetClients() +func (api *API) GetAllClientsHandler(context echo.Context) error { + clients, err := api.db.GetClients() if err != nil { - http.Error(writer, "Unexpected error getting Clients.", http.StatusInternalServerError) - return + return context.String(http.StatusInternalServerError, "Unexpected error getting Clients!") } log.Println("Got all Clients.") - json.NewEncoder(writer).Encode(clients) + return context.JSON(http.StatusOK, clients) +} + +func (api *API) GetClientHandler(context echo.Context) error { + idString := context.Param("id") + + if idString == "" { + return context.String(http.StatusBadRequest, "ID is required to get a Client!") + } + + id, err := strconv.Atoi(idString) + + if err != nil { + return context.String(http.StatusInternalServerError, "Unexpected error getting Client!") + } + + var client models.Client + + if err := api.db.GetClient(id, &client); err != nil { + return context.String(http.StatusNotFound, "Client not found!") + } + + log.Println(fmt.Sprintf("Got client with ID %d.", client.ID)) + return context.JSON(http.StatusOK, client) +} + +func (api *API) UpdateClientHandler(context echo.Context) error { + idString := context.Param("id") + + if idString == "" { + return context.String(http.StatusBadRequest, "ID is required to update a Client!") + } + + id, err := strconv.Atoi(idString) + + if err != nil { + return context.String(http.StatusInternalServerError, "Unexpected error updating Client!") + } + + var body models.UpdateClientBody + + if err := context.Bind(&body); err != nil { + log.Println(fmt.Sprintf("Error updating Client: %v.", err)) + return context.String(http.StatusBadRequest, "Invalid JSON!") + } + + var client models.Client + + if err := api.db.UpdateClient(id, body, &client); err != nil { + return context.String(http.StatusNotFound, "Client not found!") + } + + log.Println(fmt.Sprintf("Updated client with ID %d.", client.ID)) + return context.JSON(http.StatusOK, client) +} + +func (api *API) DeleteClientHandler(context echo.Context) error { + idString := context.Param("id") + + if idString == "" { + return context.String(http.StatusBadRequest, "ID is required to delete a Client!") + } + + id, err := strconv.Atoi(idString) + + if err != nil { + return context.String(http.StatusInternalServerError, "Unexpected error deleting Client!") + } + + var client models.Client + + if err := api.db.DeleteClient(id); err != nil { + return context.String(http.StatusNotFound, "Client not found!") + } + + log.Println(fmt.Sprintf("Deleted client with ID %d.", client.ID)) + return context.String(http.StatusOK, "Client deleted successfully!") } diff --git a/internal/api/routes.go b/internal/api/routes.go index b87040e..7f26f37 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -1,12 +1,11 @@ package api -import "net/http" - -func ClientRoutes(api *APIWrapper) (*http.ServeMux) { - mux := http.NewServeMux() - - mux.HandleFunc("GET /", api.GetClientsHandler) - mux.HandleFunc("POST /", api.CreateClientHandler) - - return mux +func (api *API) ClientRoutes() { + group := api.instance.Group("/clients") + + group.GET("/", api.GetAllClientsHandler) + group.POST("/", api.CreateClientHandler) + group.GET("/:id", api.GetClientHandler) + group.PUT("/:id", api.UpdateClientHandler) + group.DELETE("/:id", api.DeleteClientHandler) } diff --git a/internal/models/client.go b/internal/models/client.go index b90a63e..b6d5381 100644 --- a/internal/models/client.go +++ b/internal/models/client.go @@ -22,21 +22,27 @@ type CreateClientBody struct { Phone string `json:"phone"` } -func (wrapper *DBWrapper) MigrateClients() { - wrapper.db.AutoMigrate(&Client{}) +type UpdateClientBody struct { + Name string `json:"name"` + Country string `json:"country"` + Phone string `json:"phone"` } -func (wrapper *DBWrapper) CreateClient(body CreateClientBody) Client { +func (db *DB) MigrateClients() { + db.instance.AutoMigrate(&Client{}) +} + +func (db *DB) CreateClient(body CreateClientBody) Client { client := Client{Name: body.Name, Country: body.Country, Phone: body.Phone} - wrapper.db.Create(&client) + db.instance.Create(&client) return client } -func (wrapper *DBWrapper) GetClients() ([]Client, error) { +func (db *DB) GetClients() ([]Client, error) { var clients []Client - result := wrapper.db.Find(&clients) + result := db.instance.Find(&clients) if result.Error != nil { return nil, result.Error @@ -45,8 +51,34 @@ func (wrapper *DBWrapper) GetClients() ([]Client, error) { return clients, nil } -func (wrapper *DBWrapper) GetClient(id int, client *Client) error { - result := wrapper.db.Where("id = ?", id).First(&client, id) +func (db *DB) GetClient(id int, client *Client) error { + result := db.instance.Where("id = ?", id).First(&client, id) + + if result.Error != nil { + return result.Error + } + + return nil +} + +func (db *DB) UpdateClient(id int, body UpdateClientBody, client *Client) error { + result := db.instance.Where("id = ?", id).First(&client, id) + + if result.Error != nil { + return result.Error + } + + result = db.instance.Model(&client).Updates(Client{Name: body.Name, Country: body.Country, Phone: body.Phone}) + + if result.Error != nil { + return result.Error + } + + return nil +} + +func (db *DB) DeleteClient(id int) error { + result := db.instance.Delete(&Client{}, id) if result.Error != nil { return result.Error diff --git a/internal/models/db.go b/internal/models/db.go index 9d3257a..3a99bb1 100644 --- a/internal/models/db.go +++ b/internal/models/db.go @@ -7,16 +7,16 @@ import ( "gorm.io/gorm" ) -type DBWrapper struct { - db *gorm.DB +type DB struct { + instance *gorm.DB } -func (wrapper *DBWrapper) Connect() { +func (wrapper *DB) Connect() { db, err := gorm.Open(sqlite.Open("crimson_vault.db"), &gorm.Config{}) if err != nil { log.Fatal(err) } - wrapper.db = db + wrapper.instance = db }