package main import ( "context" "embed" "errors" "log" "net/http" "os" "strconv" "github.com/gin-gonic/gin" ) //go:embed migrations/*.sql var migrations embed.FS var db *Db const ( ErrInternalServerError = "Internal Server Error" ErrInvalidUserID = "Invalid user ID" ErrUserNotFound = "User not found" ErrCheckingUserExist = "Error checking user existence: %v" ) // ServerConfig holds configuration for the server. type ServerConfig struct { // The datasource to the SQLite database. // Use ":memory:" for an in-memory database. Datasource string // The port to listen on. // Use an empty string to listen on a random port. Port string // The channel that gets signaled when the server has started. Started chan bool } func getUsers(c *gin.Context) { users, err := db.GetUsers() if err != nil { log.Printf("Error getting users: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } c.IndentedJSON(http.StatusOK, users) } func getUser(c *gin.Context) { userIdStr := c.Param("userId") userId, err := strconv.Atoi(userIdStr) if err != nil { log.Printf(ErrInvalidUserID+": %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID}) return } user, err := db.GetUser(userId) if err != nil { log.Printf("Error getting user: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } if user == nil { c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound}) return } c.IndentedJSON(http.StatusOK, user) } func getUserGoals(c *gin.Context) { userIdStr := c.Param("userId") userId, err := strconv.Atoi(userIdStr) if err != nil { log.Printf(ErrInvalidUserID+": %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID}) return } exists, err := db.UserExists(userId) if err != nil { log.Printf(ErrCheckingUserExist, err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } if !exists { log.Printf(ErrCheckingUserExist, err) c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound}) return } goals, err := db.GetUserGoals(userId) if err != nil { log.Printf("Error getting user goals: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } c.IndentedJSON(http.StatusOK, goals) } func createUserGoal(c *gin.Context) { userIdStr := c.Param("userId") userId, err := strconv.Atoi(userIdStr) if err != nil { log.Printf(ErrInvalidUserID+": %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID}) return } // Parse request body var goalRequest CreateGoalRequest if err := c.ShouldBindJSON(&goalRequest); err != nil { log.Printf("Error parsing request body: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } // Validate request if goalRequest.Name == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Goal name cannot be empty"}) return } // Create goal in database goalId, err := db.CreateGoal(userId, &goalRequest) if err != nil { log.Printf("Error creating goal: %v", err) if err.Error() == "user does not exist" { c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound}) } else { c.JSON(http.StatusBadRequest, gin.H{"error": "Could not create goal"}) } return } // Return created goal ID response := CreateGoalResponse{ID: goalId} c.IndentedJSON(http.StatusCreated, response) } func deleteUserGoal(c *gin.Context) { userIdStr := c.Param("userId") goalIdStr := c.Param("goalId") userId, err := strconv.Atoi(userIdStr) if err != nil { log.Printf(ErrInvalidUserID+": %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID}) return } goalId, err := strconv.Atoi(goalIdStr) if err != nil { log.Printf("Invalid goal ID: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"}) return } exists, err := db.UserExists(userId) if err != nil { log.Printf(ErrCheckingUserExist, err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } if !exists { c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound}) return } err = db.DeleteGoal(userId, goalId) if err != nil { if err.Error() == "goal not found" { c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"}) } else { log.Printf("Error deleting goal: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) } return } c.JSON(http.StatusOK, gin.H{"message": "Goal deleted successfully"}) } /* * Initialises the database, and then starts the server. If the context gets cancelled, the server is shutdown and the database is closed. */ func start(ctx context.Context, config *ServerConfig) { db = NewDb(config.Datasource) defer db.db.MustClose() router := gin.Default() router.GET("/api/users", getUsers) router.GET("/api/user/:userId", getUser) router.GET("/api/user/:userId/goals", getUserGoals) router.POST("/api/user/:userId/goals", createUserGoal) router.DELETE("/api/user/:userId/goal/:goalId", deleteUserGoal) srv := &http.Server{ Addr: ":" + config.Port, Handler: router.Handler(), } go func() { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("listen: %s\n", err) } }() log.Printf("Running server on port %s\n", config.Port) if config.Started != nil { config.Started <- true } <-ctx.Done() log.Println("Shutting down") if err := srv.Shutdown(context.Background()); err != nil { log.Fatalf("Server forced to shutdown: %v", err) } } func main() { config := ServerConfig{ Datasource: os.Getenv("DB_PATH"), } start(context.Background(), &config) }