diff --git a/README.md b/README.md index e38f2be..c536f16 100644 --- a/README.md +++ b/README.md @@ -12,69 +12,25 @@ cleaner, more maintainable code with reduced boilerplate. - **Modular Design:** Each component (Request, Validation, Response) can be used independently, enhancing testability and flexibility. -> **Note:** Currently it only supports Chi. +### Supported routers + +- Gorilla MUX +- Chi +- Go Standard +- ...maybe more? Submit a PR with an example. ## Installation To install **httpsuite**, run: ``` -go get github.com/rluders/httpsuite +go get github.com/rluders/httpsuite/v2 ``` ## Usage ### Request Parsing with URL Parameters -Easily parse incoming requests and set URL parameters: - -```go -package main - -import ( - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "github.com/rluders/httpsuite" - "log" - "net/http" -) - -type SampleRequest struct { - Name string `json:"name" validate:"required,min=3"` - Age int `json:"age" validate:"required,min=1"` -} - -func (r *SampleRequest) SetParam(fieldName, value string) error { - switch fieldName { - case "name": - r.Name = value - } - return nil -} - -func main() { - r := chi.NewRouter() - r.Use(middleware.Logger) - r.Use(middleware.Recoverer) - - r.Post("/submit/{name}", func(w http.ResponseWriter, r *http.Request) { - // Step 1: Parse the request and validate it - req, err := httpsuite.ParseRequest[*SampleRequest](w, r, "name") - if err != nil { - log.Printf("Error parsing or validating request: %v", err) - return - } - - // Step 2: Send a success response - httpsuite.SendResponse[SampleRequest](w, http.StatusOK, *req, nil, nil) - }) - - log.Println("Starting server on :8080") - http.ListenAndServe(":8080", r) -} - -``` - Check out the [example folder for a complete project](./examples) demonstrating how to integrate **httpsuite** into your Go microservices. diff --git a/examples/chi/go.mod b/examples/chi/go.mod new file mode 100644 index 0000000..7a0328c --- /dev/null +++ b/examples/chi/go.mod @@ -0,0 +1,20 @@ +module chi_example + +go 1.23 + +require ( + github.com/go-chi/chi/v5 v5.2.0 + github.com/rluders/httpsuite/v2 v2.0.0 +) + +require ( + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.24.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect +) diff --git a/examples/chi/go.sum b/examples/chi/go.sum new file mode 100644 index 0000000..280d726 --- /dev/null +++ b/examples/chi/go.sum @@ -0,0 +1,30 @@ +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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= +github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= +github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +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/chi/main.go b/examples/chi/main.go new file mode 100644 index 0000000..1b9208d --- /dev/null +++ b/examples/chi/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/rluders/httpsuite/v2" + "log" + "net/http" + "strconv" +) + +type SampleRequest struct { + ID int `json:"id" validate:"required"` + Name string `json:"name" validate:"required,min=3"` + Age int `json:"age" validate:"required,min=1"` +} + +type SampleResponse struct { + ID int `json:"id"` + Name string `json:"name"` + Age int `json:"age"` +} + +func (r *SampleRequest) SetParam(fieldName, value string) error { + switch fieldName { + case "id": + id, err := strconv.Atoi(value) + if err != nil { + return err + } + r.ID = id + } + return nil +} + +func ChiParamExtractor(r *http.Request, key string) string { + return chi.URLParam(r, key) +} + +// You can test it using: +// +// curl -X POST http://localhost:8080/submit/123 \ +// -H "Content-Type: application/json" \ +// -d '{"name": "John Doe", "age": 30}' +// +// And you should get: +// +// {"data":{"id":123,"name":"John Doe","age":30}} +func main() { + // Creating the router with Chi + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + // Define the endpoint POST + r.Post("/submit/{id}", func(w http.ResponseWriter, r *http.Request) { + // Using the function for parameter extraction to the ParseRequest + req, err := httpsuite.ParseRequest[*SampleRequest](w, r, ChiParamExtractor, "id") + if err != nil { + log.Printf("Error parsing or validating request: %v", err) + return + } + + resp := &SampleResponse{ + ID: req.ID, + Name: req.Name, + Age: req.Age, + } + + // Sending success response + httpsuite.SendResponse[SampleResponse](w, http.StatusOK, *resp, nil, nil) + }) + + // Starting the server + log.Println("Starting server on :8080") + http.ListenAndServe(":8080", r) +} diff --git a/examples/gorillamux/go.mod b/examples/gorillamux/go.mod new file mode 100644 index 0000000..3c23cfd --- /dev/null +++ b/examples/gorillamux/go.mod @@ -0,0 +1,20 @@ +module gorillamux_example + +go 1.23 + +require ( + github.com/gorilla/mux v1.8.1 + github.com/rluders/httpsuite/v2 v2.0.0 +) + +require ( + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.24.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect +) diff --git a/examples/gorillamux/go.sum b/examples/gorillamux/go.sum new file mode 100644 index 0000000..fb00659 --- /dev/null +++ b/examples/gorillamux/go.sum @@ -0,0 +1,30 @@ +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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= +github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +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/gorillamux/main.go b/examples/gorillamux/main.go new file mode 100644 index 0000000..4217465 --- /dev/null +++ b/examples/gorillamux/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "github.com/gorilla/mux" + "github.com/rluders/httpsuite/v2" + "log" + "net/http" + "strconv" +) + +type SampleRequest struct { + ID int `json:"id" validate:"required"` + Name string `json:"name" validate:"required,min=3"` + Age int `json:"age" validate:"required,min=1"` +} + +type SampleResponse struct { + ID int `json:"id"` + Name string `json:"name"` + Age int `json:"age"` +} + +func (r *SampleRequest) SetParam(fieldName, value string) error { + switch fieldName { + case "id": + id, err := strconv.Atoi(value) + if err != nil { + return err + } + r.ID = id + } + return nil +} + +func GorillaMuxParamExtractor(r *http.Request, key string) string { + return mux.Vars(r)[key] // Extracts parameter using Gorilla Mux +} + +// Test the server using: +// curl -X POST http://localhost:8080/submit/123 -H "Content-Type: application/json" -d '{"name": "John Doe", "age": 30}' +func main() { + // Creating the router with Gorilla Mux + r := mux.NewRouter() + + r.HandleFunc("/submit/{id}", func(w http.ResponseWriter, r *http.Request) { + // Using the function for parameter extraction to the ParseRequest + req, err := httpsuite.ParseRequest[*SampleRequest](w, r, GorillaMuxParamExtractor, "id") + if err != nil { + log.Printf("Error parsing or validating request: %v", err) + return + } + + resp := &SampleResponse{ + ID: req.ID, + Name: req.Name, + Age: req.Age, + } + + // Sending success response + httpsuite.SendResponse[SampleResponse](w, http.StatusOK, *resp, nil, nil) + }).Methods("POST") + + // Starting the server + log.Println("Starting server on :8080") + http.ListenAndServe(":8080", r) +} diff --git a/examples/main.go b/examples/main.go deleted file mode 100644 index 22b8b94..0000000 --- a/examples/main.go +++ /dev/null @@ -1,43 +0,0 @@ -package main - -import ( - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "github.com/rluders/httpsuite" - "log" - "net/http" -) - -type SampleRequest struct { - Name string `json:"name" validate:"required,min=3"` - Age int `json:"age" validate:"required,min=1"` -} - -func (r *SampleRequest) SetParam(fieldName, value string) error { - switch fieldName { - case "name": - r.Name = value - } - return nil -} - -func main() { - r := chi.NewRouter() - r.Use(middleware.Logger) - r.Use(middleware.Recoverer) - - r.Post("/submit/{name}", func(w http.ResponseWriter, r *http.Request) { - // Step 1: Parse the request and validate it - req, err := httpsuite.ParseRequest[*SampleRequest](w, r, "name") - if err != nil { - log.Printf("Error parsing or validating request: %v", err) - return - } - - // Step 2: Send a success response - httpsuite.SendResponse[SampleRequest](w, http.StatusOK, *req, nil, nil) - }) - - log.Println("Starting server on :8080") - http.ListenAndServe(":8080", r) -} diff --git a/examples/stdmux/go.mod b/examples/stdmux/go.mod new file mode 100644 index 0000000..3bd464c --- /dev/null +++ b/examples/stdmux/go.mod @@ -0,0 +1,17 @@ +module stdmux_example + +go 1.23 + +require github.com/rluders/httpsuite/v2 v2.0.0 + +require ( + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.24.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect +) diff --git a/examples/stdmux/go.sum b/examples/stdmux/go.sum new file mode 100644 index 0000000..03f59a4 --- /dev/null +++ b/examples/stdmux/go.sum @@ -0,0 +1,28 @@ +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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= +github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +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/stdmux/main.go b/examples/stdmux/main.go new file mode 100644 index 0000000..dc7dd19 --- /dev/null +++ b/examples/stdmux/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "github.com/rluders/httpsuite/v2" + "log" + "net/http" + "strconv" +) + +type SampleRequest struct { + ID int `json:"id" validate:"required"` + Name string `json:"name" validate:"required,min=3"` + Age int `json:"age" validate:"required,min=1"` +} + +type SampleResponse struct { + ID int `json:"id"` + Name string `json:"name"` + Age int `json:"age"` +} + +func (r *SampleRequest) SetParam(fieldName, value string) error { + switch fieldName { + case "id": + id, err := strconv.Atoi(value) + if err != nil { + return err + } + r.ID = id + } + return nil +} + +func StdMuxParamExtractor(r *http.Request, key string) string { + // Remove "/submit/" (7 characters) from the URL path to get just the "id" + // Example: /submit/123 -> 123 + return r.URL.Path[len("/submit/"):] // Skip the "/submit/" part +} + +// You can test it using: +// +// curl -X POST http://localhost:8080/submit/123 \ +// -H "Content-Type: application/json" \ +// -d '{"name": "John Doe", "age": 30}' +func main() { + // Creating the router using the Go standard mux + mux := http.NewServeMux() + + // Define the endpoint POST + mux.HandleFunc("/submit/", func(w http.ResponseWriter, r *http.Request) { + // Using the function for parameter extraction to the ParseRequest + req, err := httpsuite.ParseRequest[*SampleRequest](w, r, StdMuxParamExtractor, "id") + if err != nil { + log.Printf("Error parsing or validating request: %v", err) + return + } + + resp := &SampleResponse{ + ID: req.ID, + Name: req.Name, + Age: req.Age, + } + + // Sending success response + httpsuite.SendResponse[SampleResponse](w, http.StatusOK, *resp, nil, nil) + }) + + // Starting the server + log.Println("Starting server on :8080") + http.ListenAndServe(":8080", mux) +} diff --git a/go.mod b/go.mod index ab1af3c..5ce4ce4 100644 --- a/go.mod +++ b/go.mod @@ -1,23 +1,22 @@ -module github.com/rluders/httpsuite +module github.com/rluders/httpsuite/v2 -go 1.23.1 +go 1.23 require ( - github.com/go-chi/chi/v5 v5.1.0 - github.com/go-playground/validator/v10 v10.22.1 - github.com/stretchr/testify v1.9.0 + github.com/go-playground/validator/v10 v10.24.0 + github.com/stretchr/testify v1.10.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.29.0 // indirect - golang.org/x/net v0.31.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ee7f13b..91d9c8e 100644 --- a/go.sum +++ b/go.sum @@ -1,39 +1,29 @@ 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/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= -github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= -github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= -github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= -github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= +github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 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/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/request.go b/request.go index 72c3e2b..04e222b 100644 --- a/request.go +++ b/request.go @@ -3,7 +3,6 @@ package httpsuite import ( "encoding/json" "errors" - "github.com/go-chi/chi/v5" "net/http" "reflect" ) @@ -16,51 +15,77 @@ type RequestParamSetter interface { SetParam(fieldName, value string) error } -// ParseRequest parses the incoming HTTP request into a specified struct type, handling JSON decoding and URL parameters. -// It validates the parsed request and returns it along with any potential errors. -// The pathParams variadic argument allows specifying URL parameters to be extracted. -// If an error occurs during parsing, validation, or parameter setting, it responds with an appropriate HTTP status. -func ParseRequest[T RequestParamSetter](w http.ResponseWriter, r *http.Request, pathParams ...string) (T, error) { +// ParamExtractor is a function type that extracts a URL parameter from the incoming HTTP request. +// It takes the `http.Request` and a `key` as arguments, and returns the value of the URL parameter +// as a string. This function allows flexibility for extracting parameters from different routers, +// such as Chi, Echo, Gorilla Mux, or the default Go router. +// +// Example usage: +// +// paramExtractor := func(r *http.Request, key string) string { +// return r.URL.Query().Get(key) +// } +type ParamExtractor func(r *http.Request, key string) string + +// ParseRequest parses the incoming HTTP request into a specified struct type, +// handling JSON decoding and extracting URL parameters using the provided `paramExtractor` function. +// The `paramExtractor` allows flexibility to integrate with various routers (e.g., Chi, Echo, Gorilla Mux). +// It extracts the specified parameters from the URL and sets them on the struct. +// +// The `pathParams` variadic argument is used to specify which URL parameters to extract and set on the struct. +// +// The function also validates the parsed request. If the request fails validation or if any error occurs during +// JSON parsing or parameter extraction, it responds with an appropriate HTTP status and error message. +// +// Parameters: +// - `w`: The `http.ResponseWriter` used to send the response to the client. +// - `r`: The incoming HTTP request to be parsed. +// - `paramExtractor`: A function that extracts URL parameters from the request. This function allows custom handling +// of parameters based on the router being used. +// - `pathParams`: A variadic argument specifying which URL parameters to extract and set on the struct. +// +// Returns: +// - A parsed struct of the specified type `T`, if successful. +// - An error, if parsing, validation, or parameter extraction fails. +// +// Example usage: +// +// request, err := ParseRequest[MyRequestType](w, r, MyParamExtractor, "id", "name") +// if err != nil { +// // Handle error +// } +// +// // Continue processing the valid request... +func ParseRequest[T RequestParamSetter](w http.ResponseWriter, r *http.Request, paramExtractor ParamExtractor, pathParams ...string) (T, error) { var request T var empty T - - defer func() { - _ = r.Body.Close() - }() + defer func() { _ = r.Body.Close() }() if r.Body != http.NoBody { if err := json.NewDecoder(r.Body).Decode(&request); err != nil { - SendResponse[any](w, http.StatusBadRequest, nil, - []Error{{Code: http.StatusBadRequest, Message: "Invalid JSON format"}}, nil) + SendResponse[any](w, http.StatusBadRequest, nil, []Error{{Code: http.StatusBadRequest, Message: "Invalid JSON format"}}, nil) return empty, err } } - // If body wasn't parsed request may be nil and cause problems ahead if isRequestNil(request) { request = reflect.New(reflect.TypeOf(request).Elem()).Interface().(T) } - // Parse URL parameters for _, key := range pathParams { - value := chi.URLParam(r, key) + value := paramExtractor(r, key) if value == "" { - SendResponse[any](w, http.StatusBadRequest, nil, - []Error{{Code: http.StatusBadRequest, Message: "Parameter " + key + " not found in request"}}, nil) + SendResponse[any](w, http.StatusBadRequest, nil, []Error{{Code: http.StatusBadRequest, Message: "Parameter " + key + " not found in request"}}, nil) return empty, errors.New("missing parameter: " + key) } - if err := request.SetParam(key, value); err != nil { - SendResponse[any](w, http.StatusInternalServerError, nil, - []Error{{Code: http.StatusInternalServerError, Message: "Failed to set field " + key, Details: err.Error()}}, nil) + SendResponse[any](w, http.StatusInternalServerError, nil, []Error{{Code: http.StatusInternalServerError, Message: "Failed to set field " + key, Details: err.Error()}}, nil) return empty, err } } - // Validate the combined request struct if validationErr := IsRequestValid(request); validationErr != nil { - SendResponse[any](w, http.StatusBadRequest, nil, - []Error{{Code: http.StatusBadRequest, Message: "Validation error", Details: validationErr}}, nil) + SendResponse[any](w, http.StatusBadRequest, nil, []Error{{Code: http.StatusBadRequest, Message: "Validation error", Details: validationErr}}, nil) return empty, errors.New("validation error") } diff --git a/request_test.go b/request_test.go index 40b0aff..eca1e05 100644 --- a/request_test.go +++ b/request_test.go @@ -2,13 +2,10 @@ package httpsuite import ( "bytes" - "context" "encoding/json" "errors" "fmt" - "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" - "log" "net/http" "net/http/httptest" "strconv" @@ -16,7 +13,7 @@ import ( "testing" ) -// TestRequest includes custom type annotation for UUID +// TestRequest includes custom type annotation for UUID type type TestRequest struct { ID int `json:"id" validate:"required"` Name string `json:"name" validate:"required"` @@ -31,19 +28,26 @@ func (r *TestRequest) SetParam(fieldName, value string) error { } r.ID = id default: - log.Printf("Parameter %s cannot be set", fieldName) + fmt.Printf("Parameter %s cannot be set", fieldName) } - return nil } -func Test_ParseRequest(t *testing.T) { - testSetURLParam := func(r *http.Request, fieldName, value string) *http.Request { - ctx := chi.NewRouteContext() - ctx.URLParams.Add(fieldName, value) - return r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx)) +// This implementation extracts parameters from the path, assuming the request URL follows a pattern +// like "/test/{id}", where "id" is a path parameter. +func MyParamExtractor(r *http.Request, key string) string { + // Here, we can extract parameters directly from the URL path for simplicity. + // Example: for "/test/123", if key is "ID", we want to capture "123". + pathSegments := strings.Split(r.URL.Path, "/") + + // You should know how the path is structured; in this case, we expect the ID to be the second segment. + if len(pathSegments) > 2 && key == "ID" { + return pathSegments[2] } + return "" +} +func Test_ParseRequest(t *testing.T) { type args struct { w http.ResponseWriter r *http.Request @@ -55,6 +59,7 @@ func Test_ParseRequest(t *testing.T) { want *TestRequest wantErr assert.ErrorAssertionFunc } + tests := []testCase[TestRequest]{ { name: "Successful Request", @@ -63,8 +68,7 @@ func Test_ParseRequest(t *testing.T) { r: func() *http.Request { body, _ := json.Marshal(TestRequest{Name: "Test"}) req := httptest.NewRequest("POST", "/test/123", bytes.NewBuffer(body)) - req = testSetURLParam(req, "ID", "123") - req.Header.Set("Content-Type", "application/json") + req.URL.Path = "/test/123" return req }(), pathParams: []string{"ID"}, @@ -75,27 +79,8 @@ func Test_ParseRequest(t *testing.T) { { name: "Missing body", args: args{ - w: httptest.NewRecorder(), - r: func() *http.Request { - req := httptest.NewRequest("POST", "/test/123", nil) - req = testSetURLParam(req, "ID", "123") - req.Header.Set("Content-Type", "application/json") - return req - }(), - pathParams: []string{"ID"}, - }, - want: nil, - wantErr: assert.Error, - }, - { - name: "Missing Path Parameter", - args: args{ - w: httptest.NewRecorder(), - r: func() *http.Request { - req := httptest.NewRequest("POST", "/test", nil) - req.Header.Set("Content-Type", "application/json") - return req - }(), + w: httptest.NewRecorder(), + r: httptest.NewRequest("POST", "/test/123", nil), pathParams: []string{"ID"}, }, want: nil, @@ -107,40 +92,7 @@ func Test_ParseRequest(t *testing.T) { w: httptest.NewRecorder(), r: func() *http.Request { req := httptest.NewRequest("POST", "/test/123", bytes.NewBufferString("{invalid-json}")) - req = testSetURLParam(req, "ID", "123") - req.Header.Set("Content-Type", "application/json") - return req - }(), - pathParams: []string{"ID"}, - }, - want: nil, - wantErr: assert.Error, - }, - { - name: "Validation Error for body", - args: args{ - w: httptest.NewRecorder(), - r: func() *http.Request { - body, _ := json.Marshal(TestRequest{}) - req := httptest.NewRequest("POST", "/test/123", bytes.NewBuffer(body)) - req = testSetURLParam(req, "ID", "123") - req.Header.Set("Content-Type", "application/json") - return req - }(), - pathParams: []string{"ID"}, - }, - want: nil, - wantErr: assert.Error, - }, - { - name: "Validation Error for zero ID", - args: args{ - w: httptest.NewRecorder(), - r: func() *http.Request { - body, _ := json.Marshal(TestRequest{Name: "Test"}) - req := httptest.NewRequest("POST", "/test/0", bytes.NewBuffer(body)) - req = testSetURLParam(req, "ID", "0") - req.Header.Set("Content-Type", "application/json") + req.URL.Path = "/test/123" return req }(), pathParams: []string{"ID"}, @@ -152,7 +104,7 @@ func Test_ParseRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := ParseRequest[*TestRequest](tt.args.w, tt.args.r, tt.args.pathParams...) + got, err := ParseRequest[*TestRequest](tt.args.w, tt.args.r, MyParamExtractor, tt.args.pathParams...) if !tt.wantErr(t, err, fmt.Sprintf("parseRequest(%v, %v, %v)", tt.args.w, tt.args.r, tt.args.pathParams)) { return }