Welcome back! So far, you've learned how to manipulate new and existing ToDo items with GET, POST, PUT and DELETE requests. Now, let's focus on another useful feature — tracking the creation time of each ToDo item. This helps keep your ToDo list organized and offers valuable insights into task management over time.
In addition to tracking time, ensuring that data being added to your application is valid and accurate is crucial. For instance, it doesn't make sense to add a ToDo item with a creation date in the past. We'll employ validation rules to avoid such scenarios, providing an organized and reliable user experience.
We'll start by introducing a new field, CreationTime
, in our Todo
struct. This field will automatically capture the current time when a new ToDo item is created. To ensure this field is always captured and valid, we'll use some validation tags.
Here's how the Todo
struct looks now:
Go1package models 2 3import ( 4 "time" 5) 6 7// Todo models a single task item with essential details 8type Todo struct { 9 ID int `json:"id"` 10 Title string `json:"title" binding:"required"` 11 Completed bool `json:"completed"` 12 CreationTime time.Time `json:"creationTime" binding:"notpast"` 13}
In this code snippet we simply added the CreationTime
field of type time.Time
to record the creation time of each ToDo item. The binding
tag on the CreationTime
field contains our custom notpast
rule, which we will define shortly. The binding
tag in Gin is used to apply specific validation rules to struct fields during the binding process, ensuring the data conforms to the specified constraints. Here, it makes the Title
field required and it ensures that the CreationTime
isn't set to a past date by enforcing this rule when new ToDo items are added.
Validation in web applications helps maintain data integrity. In this lesson, we will implement a custom validator to ensure CreationTime
is never set to a past date. This validator will be registered with the Gin framework.
Here's the code snippet implementing the custom validator, which can be placed in a utils
package:
Go1package utils 2 3import ( 4 "time" 5 6 "github.com/go-playground/validator/v10" // Provides validation methods 7) 8 9// NotPastDate is a custom validator that ensures the date is not in the past 10var NotPastDate validator.Func = func(fl validator.FieldLevel) bool { 11 // Attempt to convert the field to a time.Time type 12 date, ok := fl.Field().Interface().(time.Time) 13 if !ok { 14 // If the conversion fails, it's an invalid date, return false 15 return false 16 } 17 // Return true if date is not in the past, false otherwise 18 return !date.Before(time.Now().Add(-1 * time.Second)) 19}
In this code snippet:
- The
notPastDate
function is defined with thevalidator.Func
type, ensuring integration with the Gin validator system as a custom function. - It takes a parameter of type
validator.FieldLevel
, providing context about the field being validated, which is essential for checking input data. - The line
fl.Field().Interface().(time.Time)
extracts and asserts the field's value to thetime.Time
type, allowing for time-specific operations. - The validation logic checks
!date.Before(time.Now().Add(-1 * time.Second))
to ensure thedate
is not set in the past, with a one-second buffer. This buffer accounts for minor discrepancies in time calculation or processing delays, enhancing the reliability of theCreationTime
field validation.
By following this structure, the function effectively integrates with Gin's validation engine, allowing us to enforce the notpast
validation rule on the CreationTime
field.
Now, let's set up the necessary routes to handle the creation and retrieval of ToDo items. We'll include validation to ensure CreationTime
is correctly applied during item creation.
Here's how we set up the router definition and handler functions:
/router/router.go
:
Go1package router 2 3import ( 4 "github.com/gin-gonic/gin" 5 "github.com/gin-gonic/gin/binding" 6 "github.com/go-playground/validator/v10" 7 8 "codesignal.com/todoapp/controllers" 9) 10 11func RegisterRoutes(router *gin.Engine) { 12 controllers.RegisterValidators() 13 14 // POST route to add a new todo 15 router.POST("/api/todos", controllers.AddTodo) 16 17 // GET route to retrieve all todos 18 router.GET("/api/todos", controllers.GetTodos) 19}
/controllers/controllers.go
:
Go1package controllers 2 3import ( 4 "net/http" 5 6 "github.com/gin-gonic/gin" 7 8 "codesignal.com/todoapp/models" 9 "codesignal.com/todoapp/services" 10 "codesignal.com/todoapp/utils" 11) 12 13func AddTodo(c *gin.Context) { 14 var newTodo models.Todo 15 if err := c.ShouldBindJSON(&newTodo); err != nil { 16 c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON: " + err.Error()}) 17 return 18 } 19 20 newTodo = services.CreateTodo(newTodo) 21 c.JSON(http.StatusCreated, newTodo) 22} 23 24func GetTodos(c *gin.Context) { 25 todos := services.RetrieveTodos() 26 c.JSON(http.StatusOK, todos) 27} 28 29// RegisterValidators registers custom validators 30func RegisterValidators() { 31 if v, ok := binding.Validator.Engine().(*validator.Validate); ok { 32 v.RegisterValidation("notpast", utils.NotPastDate) 33 } 34}
/services/services.go
:
Go1package services 2 3import ( 4 "time" 5 6 "codesignal.com/todoapp/models" 7) 8 9var todos []models.Todo 10 11func CreateTodo(todo models.Todo) models.Todo { 12 todo.ID = len(todos) + 1 13 todo.CreationTime = time.Now() 14 todos = append(todos, todo) 15 return todo 16} 17 18func RetrieveTodos() []models.Todo { 19 return todos 20}
- The
RegisterRoutes
function first adds our customnotPastDate
validator. - We define a POST route using the
AddTodo
handler function, which binds JSON data to a new ToDo item, setting theCreationTime
to the current time. - The
CreateTodo
function in theservices
package manages the business logic for adding a new ToDo item. - Validation occurs upon binding, ensuring the
CreationTime
adheres to our rules. - The GET route is provided by the
GetTodos
handler function, which retrieves all ToDo items, displaying their creation times.
In this lesson, you enhanced your ToDo app by adding a CreationTime
field and ensured its validity using custom validators. You've set up routes to add and retrieve these items while maintaining data integrity. Take some time to practice, and explore how these concepts improve the overall functionality and reliability of your application.
As you move forward with your journey, these building blocks will serve as a solid foundation for more complex features. Keep up the great work, and prepare to tackle new challenges!