371 lines
8.7 KiB
Go
371 lines
8.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
// User represents a user in the system
|
|
type User struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
Email string `json:"email"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// UserService handles user-related operations
|
|
type UserService struct {
|
|
users map[int]*User
|
|
nextID int
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
// NewUserService creates a new instance of UserService
|
|
func NewUserService() *UserService {
|
|
return &UserService{
|
|
users: make(map[int]*User),
|
|
nextID: 1,
|
|
}
|
|
}
|
|
|
|
// CreateUser adds a new user to the service
|
|
func (us *UserService) CreateUser(name, email string) (*User, error) {
|
|
us.mutex.Lock()
|
|
defer us.mutex.Unlock()
|
|
|
|
if name == "" || email == "" {
|
|
return nil, fmt.Errorf("name and email are required")
|
|
}
|
|
|
|
// Check for duplicate email
|
|
for _, user := range us.users {
|
|
if user.Email == email {
|
|
return nil, fmt.Errorf("user with email %s already exists", email)
|
|
}
|
|
}
|
|
|
|
user := &User{
|
|
ID: us.nextID,
|
|
Name: name,
|
|
Email: email,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
us.users[us.nextID] = user
|
|
us.nextID++
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// GetUser retrieves a user by ID
|
|
func (us *UserService) GetUser(id int) (*User, error) {
|
|
us.mutex.RLock()
|
|
defer us.mutex.RUnlock()
|
|
|
|
user, exists := us.users[id]
|
|
if !exists {
|
|
return nil, fmt.Errorf("user with ID %d not found", id)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// GetAllUsers returns all users
|
|
func (us *UserService) GetAllUsers() []*User {
|
|
us.mutex.RLock()
|
|
defer us.mutex.RUnlock()
|
|
|
|
users := make([]*User, 0, len(us.users))
|
|
for _, user := range us.users {
|
|
users = append(users, user)
|
|
}
|
|
|
|
return users
|
|
}
|
|
|
|
// UpdateUser modifies an existing user
|
|
func (us *UserService) UpdateUser(id int, name, email string) (*User, error) {
|
|
us.mutex.Lock()
|
|
defer us.mutex.Unlock()
|
|
|
|
user, exists := us.users[id]
|
|
if !exists {
|
|
return nil, fmt.Errorf("user with ID %d not found", id)
|
|
}
|
|
|
|
// Check for duplicate email (excluding current user)
|
|
if email != user.Email {
|
|
for _, u := range us.users {
|
|
if u.Email == email && u.ID != id {
|
|
return nil, fmt.Errorf("user with email %s already exists", email)
|
|
}
|
|
}
|
|
}
|
|
|
|
if name != "" {
|
|
user.Name = name
|
|
}
|
|
if email != "" {
|
|
user.Email = email
|
|
}
|
|
user.UpdatedAt = time.Now()
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// DeleteUser removes a user from the service
|
|
func (us *UserService) DeleteUser(id int) error {
|
|
us.mutex.Lock()
|
|
defer us.mutex.Unlock()
|
|
|
|
if _, exists := us.users[id]; !exists {
|
|
return fmt.Errorf("user with ID %d not found", id)
|
|
}
|
|
|
|
delete(us.users, id)
|
|
return nil
|
|
}
|
|
|
|
// APIServer represents the HTTP server
|
|
type APIServer struct {
|
|
userService *UserService
|
|
router *mux.Router
|
|
}
|
|
|
|
// NewAPIServer creates a new API server instance
|
|
func NewAPIServer(userService *UserService) *APIServer {
|
|
server := &APIServer{
|
|
userService: userService,
|
|
router: mux.NewRouter(),
|
|
}
|
|
server.setupRoutes()
|
|
return server
|
|
}
|
|
|
|
// setupRoutes configures the API routes
|
|
func (s *APIServer) setupRoutes() {
|
|
api := s.router.PathPrefix("/api/v1").Subrouter()
|
|
|
|
// User routes
|
|
api.HandleFunc("/users", s.handleGetUsers).Methods("GET")
|
|
api.HandleFunc("/users", s.handleCreateUser).Methods("POST")
|
|
api.HandleFunc("/users/{id:[0-9]+}", s.handleGetUser).Methods("GET")
|
|
api.HandleFunc("/users/{id:[0-9]+}", s.handleUpdateUser).Methods("PUT")
|
|
api.HandleFunc("/users/{id:[0-9]+}", s.handleDeleteUser).Methods("DELETE")
|
|
|
|
// Health check
|
|
api.HandleFunc("/health", s.handleHealthCheck).Methods("GET")
|
|
|
|
// Add CORS middleware
|
|
s.router.Use(s.corsMiddleware)
|
|
s.router.Use(s.loggingMiddleware)
|
|
}
|
|
|
|
// HTTP Handlers
|
|
|
|
func (s *APIServer) handleGetUsers(w http.ResponseWriter, r *http.Request) {
|
|
users := s.userService.GetAllUsers()
|
|
s.writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"users": users,
|
|
"count": len(users),
|
|
})
|
|
}
|
|
|
|
func (s *APIServer) handleCreateUser(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
s.writeError(w, http.StatusBadRequest, "Invalid JSON payload")
|
|
return
|
|
}
|
|
|
|
user, err := s.userService.CreateUser(req.Name, req.Email)
|
|
if err != nil {
|
|
s.writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
s.writeJSON(w, http.StatusCreated, map[string]*User{"user": user})
|
|
}
|
|
|
|
func (s *APIServer) handleGetUser(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
id, err := strconv.Atoi(vars["id"])
|
|
if err != nil {
|
|
s.writeError(w, http.StatusBadRequest, "Invalid user ID")
|
|
return
|
|
}
|
|
|
|
user, err := s.userService.GetUser(id)
|
|
if err != nil {
|
|
s.writeError(w, http.StatusNotFound, err.Error())
|
|
return
|
|
}
|
|
|
|
s.writeJSON(w, http.StatusOK, map[string]*User{"user": user})
|
|
}
|
|
|
|
func (s *APIServer) handleUpdateUser(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
id, err := strconv.Atoi(vars["id"])
|
|
if err != nil {
|
|
s.writeError(w, http.StatusBadRequest, "Invalid user ID")
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
s.writeError(w, http.StatusBadRequest, "Invalid JSON payload")
|
|
return
|
|
}
|
|
|
|
user, err := s.userService.UpdateUser(id, req.Name, req.Email)
|
|
if err != nil {
|
|
status := http.StatusBadRequest
|
|
if strings.Contains(err.Error(), "not found") {
|
|
status = http.StatusNotFound
|
|
}
|
|
s.writeError(w, status, err.Error())
|
|
return
|
|
}
|
|
|
|
s.writeJSON(w, http.StatusOK, map[string]*User{"user": user})
|
|
}
|
|
|
|
func (s *APIServer) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
id, err := strconv.Atoi(vars["id"])
|
|
if err != nil {
|
|
s.writeError(w, http.StatusBadRequest, "Invalid user ID")
|
|
return
|
|
}
|
|
|
|
if err := s.userService.DeleteUser(id); err != nil {
|
|
s.writeError(w, http.StatusNotFound, err.Error())
|
|
return
|
|
}
|
|
|
|
s.writeJSON(w, http.StatusOK, map[string]string{"message": "User deleted successfully"})
|
|
}
|
|
|
|
func (s *APIServer) handleHealthCheck(w http.ResponseWriter, r *http.Request) {
|
|
s.writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"status": "healthy",
|
|
"timestamp": time.Now(),
|
|
"service": "user-api",
|
|
})
|
|
}
|
|
|
|
// Middleware
|
|
|
|
func (s *APIServer) corsMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
|
|
if r.Method == "OPTIONS" {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func (s *APIServer) loggingMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
start := time.Now()
|
|
|
|
// Wrap ResponseWriter to capture status code
|
|
ww := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
|
|
|
|
next.ServeHTTP(ww, r)
|
|
|
|
log.Printf("%s %s %d %v", r.Method, r.URL.Path, ww.statusCode, time.Since(start))
|
|
})
|
|
}
|
|
|
|
// Helper methods
|
|
|
|
func (s *APIServer) writeJSON(w http.ResponseWriter, status int, data interface{}) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(data)
|
|
}
|
|
|
|
func (s *APIServer) writeError(w http.ResponseWriter, status int, message string) {
|
|
s.writeJSON(w, status, map[string]string{"error": message})
|
|
}
|
|
|
|
// responseWriter wraps http.ResponseWriter to capture status code
|
|
type responseWriter struct {
|
|
http.ResponseWriter
|
|
statusCode int
|
|
}
|
|
|
|
func (rw *responseWriter) WriteHeader(code int) {
|
|
rw.statusCode = code
|
|
rw.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
// Start starts the HTTP server
|
|
func (s *APIServer) Start(ctx context.Context, addr string) error {
|
|
server := &http.Server{
|
|
Addr: addr,
|
|
Handler: s.router,
|
|
ReadTimeout: 15 * time.Second,
|
|
WriteTimeout: 15 * time.Second,
|
|
IdleTimeout: 60 * time.Second,
|
|
}
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
log.Println("Shutting down server...")
|
|
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
if err := server.Shutdown(shutdownCtx); err != nil {
|
|
log.Printf("Server shutdown error: %v", err)
|
|
}
|
|
}()
|
|
|
|
log.Printf("Server starting on %s", addr)
|
|
return server.ListenAndServe()
|
|
}
|
|
|
|
func main() {
|
|
userService := NewUserService()
|
|
|
|
// Add some sample data
|
|
userService.CreateUser("John Doe", "john@example.com")
|
|
userService.CreateUser("Jane Smith", "jane@example.com")
|
|
|
|
server := NewAPIServer(userService)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
if err := server.Start(ctx, ":8080"); err != nil && err != http.ErrServerClosed {
|
|
log.Fatalf("Server failed to start: %v", err)
|
|
}
|
|
} |