feat: impl requests handllers for contacts and healthcheck

This commit is contained in:
2025-11-02 22:03:10 -03:00
parent 4a28259dc8
commit 3b1b05d5a6
5 changed files with 342 additions and 0 deletions

View 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
}
}
}

View 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)
}

View 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))
}

View 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"})
}

View 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,
})
}