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