feat: impl requests handllers for contacts and healthcheck
This commit is contained in:
47
internal/handler/base_handler.go
Normal file
47
internal/handler/base_handler.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Route struct {
|
||||
pattern string
|
||||
handler func(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
type baseHandler struct {
|
||||
mux *http.ServeMux
|
||||
registeredRoutes map[string]bool
|
||||
Routes []Route
|
||||
}
|
||||
|
||||
var (
|
||||
bh *baseHandler
|
||||
bhOnce sync.Once
|
||||
bhMu sync.RWMutex
|
||||
)
|
||||
|
||||
func NewBaseHandler(mux *http.ServeMux, routes []Route) *baseHandler {
|
||||
bhOnce.Do(func() {
|
||||
bh = &baseHandler{
|
||||
mux: mux,
|
||||
registeredRoutes: make(map[string]bool),
|
||||
Routes: routes,
|
||||
}
|
||||
})
|
||||
|
||||
bhMu.Lock()
|
||||
defer bhMu.Unlock()
|
||||
bh.registerRoutes(routes)
|
||||
return bh
|
||||
}
|
||||
|
||||
func (b *baseHandler) registerRoutes(routes []Route) {
|
||||
for _, route := range routes {
|
||||
if !bh.registeredRoutes[route.pattern] {
|
||||
b.mux.HandleFunc(route.pattern, route.handler)
|
||||
bh.registeredRoutes[route.pattern] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
135
internal/handler/contact_handler.go
Normal file
135
internal/handler/contact_handler.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"gitea.gabilandia.com/gabdlr/agenda-web-go/internal/models"
|
||||
"gitea.gabilandia.com/gabdlr/agenda-web-go/internal/repository"
|
||||
)
|
||||
|
||||
type ContactHandler struct {
|
||||
repository repository.Repository[models.Contact]
|
||||
}
|
||||
|
||||
const required_field = "is required"
|
||||
const invalid_id = "Invalid ID"
|
||||
|
||||
func HandleContacts(mux *http.ServeMux, repo repository.Repository[models.Contact]) {
|
||||
ch := &ContactHandler{repository: repo}
|
||||
routes := []Route{
|
||||
{"GET /contacts", ch.getAll},
|
||||
{"GET /contacts/{id}", ch.getByID},
|
||||
{"POST /contacts", ch.create},
|
||||
{"PUT /contacts/{id}", ch.update},
|
||||
{"DELETE /contacts/{id}", ch.delete},
|
||||
}
|
||||
NewBaseHandler(mux, routes)
|
||||
}
|
||||
|
||||
func (h *ContactHandler) getAll(w http.ResponseWriter, r *http.Request) {
|
||||
contacts, err := h.repository.GetAll()
|
||||
if err != nil {
|
||||
InternalError(w, err)
|
||||
return
|
||||
}
|
||||
JSONSuccess(w, contacts, "Contact list retrieved successfully", http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *ContactHandler) getByID(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(r.PathValue("id"))
|
||||
if err != nil {
|
||||
BadRequest(w, ErrInvalidFormat, invalid_id)
|
||||
return
|
||||
}
|
||||
|
||||
contact, err := h.repository.GetByID(id)
|
||||
if err != nil {
|
||||
InternalError(w, err)
|
||||
return
|
||||
}
|
||||
if contact == nil {
|
||||
NotFound(w, ErrNotFound, "Contact not found")
|
||||
return
|
||||
}
|
||||
JSONSuccess(w, contact, "Contact retrieved successfully", http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *ContactHandler) create(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var contact models.Contact
|
||||
if err := json.NewDecoder(r.Body).Decode(&contact); err != nil {
|
||||
BadRequest(w, ErrInvalidJSON, "Error parsing the contact")
|
||||
return
|
||||
}
|
||||
|
||||
if contact.Name == "" {
|
||||
BadRequest(w, ErrMissingRequired, RequiredFieldErr("name"))
|
||||
return
|
||||
}
|
||||
|
||||
if contact.Company == "" {
|
||||
BadRequest(w, ErrMissingRequired, RequiredFieldErr("company"))
|
||||
return
|
||||
}
|
||||
|
||||
if contact.Phone == "" {
|
||||
BadRequest(w, ErrMissingRequired, RequiredFieldErr("phone"))
|
||||
return
|
||||
}
|
||||
|
||||
id, err := h.repository.Create(&contact)
|
||||
if err != nil {
|
||||
InternalError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
contact.ID = int(id)
|
||||
JSONSuccess(w, contact, "Contact created successfully", http.StatusCreated)
|
||||
}
|
||||
|
||||
func (h *ContactHandler) update(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(r.PathValue("id"))
|
||||
if err != nil {
|
||||
BadRequest(w, ErrInvalidFormat, invalid_id)
|
||||
return
|
||||
}
|
||||
|
||||
var contact models.Contact
|
||||
if err := json.NewDecoder(r.Body).Decode(&contact); err != nil {
|
||||
BadRequest(w, ErrInvalidJSON, "Error parsing the contact")
|
||||
return
|
||||
}
|
||||
|
||||
contact.ID = id
|
||||
|
||||
updateErr := h.repository.Update(&contact)
|
||||
|
||||
if updateErr != nil {
|
||||
InternalError(w, updateErr)
|
||||
return
|
||||
}
|
||||
|
||||
JSONSuccess(w, nil, "Contact updated successfully", http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *ContactHandler) delete(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(r.PathValue("id"))
|
||||
|
||||
if err != nil {
|
||||
BadRequest(w, ErrInvalidFormat, invalid_id)
|
||||
return
|
||||
}
|
||||
rowsAffected, err := h.repository.Delete(id)
|
||||
|
||||
if err != nil {
|
||||
InternalError(w, err)
|
||||
return
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
BadRequest(w, ErrNotFound, "Contact not found")
|
||||
return
|
||||
}
|
||||
JSONSuccess(w, nil, "Contact deleted successfully", http.StatusOK)
|
||||
}
|
||||
88
internal/handler/errors.go
Normal file
88
internal/handler/errors.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
/*
|
||||
Domain (First Digit)
|
||||
|
||||
1xxx - Request/Input Errors
|
||||
|
||||
2xxx - Authentication/Authorization
|
||||
|
||||
3xxx - Business Logic/Validation
|
||||
|
||||
4xxx - Data/Resource Errors
|
||||
|
||||
5xxx - System/External Service Errors
|
||||
|
||||
Category (Second Digit)
|
||||
|
||||
x0xx - General/Generic errors in that domain
|
||||
|
||||
x1xx - Format/Structure errors
|
||||
|
||||
x2xx - Validation/Constraint errors
|
||||
|
||||
x3xx - State/Workflow errors
|
||||
|
||||
x4xx - Permission/Access errors
|
||||
|
||||
Specific Error (Last Two Digits)
|
||||
|
||||
xx00-xx99 - Specific error instances
|
||||
*/
|
||||
|
||||
// Domains
|
||||
const (
|
||||
DomainRequest = 1000
|
||||
DomainAuth = 2000
|
||||
DomainData = 4000
|
||||
DomainSystem = 5000
|
||||
)
|
||||
|
||||
const (
|
||||
CategoryGeneral = 0
|
||||
CategoryFormat = 100
|
||||
CategoryValidation = 200
|
||||
CategoryState = 300
|
||||
CategoryPermission = 400
|
||||
)
|
||||
|
||||
// Err code Domain + Category + Specific
|
||||
const (
|
||||
CodeInvalidJSON = DomainRequest + CategoryFormat + 1
|
||||
InvalidParamFormat = DomainRequest + CategoryFormat + 0
|
||||
CodeValidationFailed = DomainRequest + CategoryValidation + 0
|
||||
CodeMissingRequired = DomainRequest + CategoryValidation + 1
|
||||
CodeNotFound = DomainData + CategoryPermission + 0
|
||||
CodeInternalError = DomainSystem + CategoryGeneral + 0
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidJSON = APIError{Code: CodeInvalidJSON, Message: "Invalid JSON in request body"}
|
||||
ErrValidation = APIError{Code: CodeValidationFailed, Message: "Validation failed"}
|
||||
ErrMissingRequired = APIError{Code: CodeMissingRequired, Message: "Required field is missing"}
|
||||
ErrNotFound = APIError{Code: CodeNotFound, Message: "Resource not found"}
|
||||
ErrInternalServer = APIError{Code: CodeInternalError, Message: "Internal server error"}
|
||||
ErrInvalidFormat = APIError{Code: InvalidParamFormat, Message: "The given param doesn't match the format expectations"}
|
||||
)
|
||||
|
||||
// Helper functions for common errors
|
||||
func BadRequest(w http.ResponseWriter, err APIError, details string) {
|
||||
JSONError(w, err.Message, details, err.Code, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func NotFound(w http.ResponseWriter, err APIError, details string) {
|
||||
JSONError(w, err.Message, details, err.Code, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func InternalError(w http.ResponseWriter, err error) {
|
||||
JSONError(w, ErrInternalServer.Message, err.Error(), ErrInternalServer.Code, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func RequiredFieldErr(field string) string {
|
||||
return (fmt.Sprintf("%s %s", field, required_field))
|
||||
}
|
||||
16
internal/handler/health_handler.go
Normal file
16
internal/handler/health_handler.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func HandlHealthChecks(mux *http.ServeMux) {
|
||||
routes := []Route{{"GET /health", healthCheck}}
|
||||
NewBaseHandler(mux, routes)
|
||||
}
|
||||
|
||||
func healthCheck(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
}
|
||||
56
internal/handler/response.go
Normal file
56
internal/handler/response.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type APIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data any `json:"data,omitempty"`
|
||||
Errors []APIError `json:"errors,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type APIError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
const content_type = "Content-Type"
|
||||
const application_json = "application/json"
|
||||
|
||||
func JSONSuccess(w http.ResponseWriter, data any, message string, statusCode int) {
|
||||
w.Header().Set(content_type, application_json)
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(APIResponse{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
func JSONError(w http.ResponseWriter, message, details string, code, statusCode int) {
|
||||
w.Header().Set(content_type, application_json)
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(APIResponse{
|
||||
Success: false,
|
||||
Errors: []APIError{
|
||||
{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Details: details,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func JSONErrors(w http.ResponseWriter, errors []APIError, statusCode int) {
|
||||
w.Header().Set(content_type, application_json)
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(APIResponse{
|
||||
Success: false,
|
||||
Errors: errors,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user