Best Practices for Building a Validation Layer in Go

best-practices-for-building-a-validation-layer-in-go

Validation is an essential part of any software system. It helps ensure that the data being processed or stored in the system is correct and meets the required constraints.

In this article, we will discuss how to implement a generic validation layer in Go that can be used to validate any object using a list of validations.

The Problem
Consider a scenario where we have a user registration system that accepts user details such as name, email, and password. Before storing the user data in the database, we need to validate it to ensure that the data meets the required constraints. For example, the name should not be empty, the email should be in a valid format, and the password should meet the minimum strength requirements.

One way to implement the validation layer is to define a validation function for each constraint and call them one by one for each object. However, this approach can be tedious and error-prone, especially when dealing with a large number of objects.

The Solution
To solve this problem, we can implement a generic validation layer that takes a list of validations and applies them to any object.

package validation

import (
    "errors"
    "fmt"
)

type Rule func(key string, value interface{}) error

type Rules []Rule

type Validator struct {
    rules Rules
}

func (v *Validator) Add(rule Rule) {
    v.rules = append(v.rules, rule)
}

func (v *Validator) Validate(data map[string]interface{}) []error {
    var errors []error
    for _, rule := range v.rules {
        for key, value := range data {
            if err := rule(key, value); err != nil {
                errors = append(errors, err)
            }
        }
    }
    return errors
}

func ValidateLength(maxLength int) Rule {
    return func(key string, value interface{}) error {
        str, ok := value.(string)
        if !ok {
            return fmt.Errorf("%s is not a string", key)
        }
        if len(str) > maxLength {
            return fmt.Errorf("%s must be less than or equal to %d characters", key, maxLength)
        }
        return nil
    }
}

func ValidatePresence(key string, value interface{}) error {
    if value == "" {
        return fmt.Errorf("%s can't be blank", key)
    }
    return nil
}

func ValidateRange(min, max int) Rule {
    return func(key string, value interface{}) error {
        num, ok := value.(int)
        if !ok {
            return fmt.Errorf("%s is not a number", key)
        }
        if num < min || num > max {
            return fmt.Errorf("%s must be between %d and %d", key, min, max)
        }
        return nil
    }
}

func ValidateEmail(key string, value interface{}) error {
    str, ok := value.(string)
    if !ok {
        return fmt.Errorf("%s is not a string", key)
    }
    // simplified email validation for example purposes
    if str == "" || str[:1] == "@" || str[len(str)-1:] == "@" {
        return fmt.Errorf("%s is not a valid email address", key)
    }
    return nil
}

The Rule type represents a single validation rule, which takes a key-value pair and returns an error if the value fails to meet the rule’s criteria.

The Rules type is a slice of Rule types, representing a collection of validation rules.

The Validator type contains a list of rules, and has methods for adding new rules and validating data against all the rules in the list.

The Add method allows you to add a new validation rule to the validator.

The Validate method takes a map of key-value pairs and returns a list of errors found during the validation process.

The ValidateLength, ValidatePresence, ValidateRange, and ValidateEmail functions are examples of predefined rules that you can use. These functions return a Rule type, which you can add to a Validator instance using the Add method.

Now that we have our validation layer in place, let’s see how we can use it to validate the request body of an API endpoint. We’ll create a simple HTTP handler that accepts a JSON request body and validates it against a set of validation rules.

func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
    var user User
    err := json.NewDecoder(r.Body).Decode(&user)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        fmt.Fprintf(w, "failed to decode request body: %v", err)
        return
    }

    // Define validation rules
    rules := []validation.Rule{
        validation.ValidateRequired("Name", user.Name),
        validation.ValidateLength("Name", user.Name, 3, 50),
        validation.ValidateEmail("Email", user.Email),
        validation.ValidateRequired("Password", user.Password),
        validation.ValidateLength("Password", user.Password, 8, 50),
    }

    // Execute validation rules and get errors
    errors := validation.Execute(rules)

    if len(errors) > 0 {
        w.WriteHeader(http.StatusBadRequest)
        for _, err := range errors {
            fmt.Fprintf(w, "%sn", err.Error())
        }
        return
    }

    // Do something with the validated user object
    // ...
}

Here, we’re defining the validation rules as a slice of validation.Rule objects. We’re then passing this slice to the validation.Execute function, which executes the validation rules and returns a slice of errors.

If any validation errors occur, we return a 400 Bad Request status code with the list of errors. Otherwise, we do something with the validated user object.

Conclusion
In this article, we’ve seen how to create a flexible and reusable validation layer in Go using functional options and closures. This approach allows us to define complex validation rules and reuse them across multiple parts of our application.

By separating the validation logic from the business logic of our application, we can make our code more modular and easier to test. We can also provide better feedback to the user when validation errors occur.

I hope this article has been helpful and provides you with a good starting point for creating your own validation layer in Go.

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post
methods-to-install-wi-fi-boosters

Methods to Install Wi fi Boosters

Next Post
how-come-businesses-ought-to-develop-android-apps

How come Businesses Ought to Develop Android Apps

Related Posts
arkui-x平台差异化

ArkUI-X平台差异化

跨平台使用场景是一套ArkTS代码运行在多个终端设备上,如Android、iOS、OpenHarmony(含基于OpenHarmony发行的商业版,如HarmonyOS Next)。当不同平台业务逻辑不同,或使用了不支持跨平台的API,就需要根据平台不同进行一定代码差异化适配。当前仅支持在代码运行态进行差异化,接下来详细介绍场景及如何差异化适配。 使用场景 平台差异化适用于以下两种典型场景: 1.自身业务逻辑不同平台本来就有差异; 2.在OpenHarmony上调用了不支持跨平台的API,这就需要在OpenHarmony上仍然调用对应API,其他平台通过Bridge桥接机制进行差异化处理; 判断平台类型 可以通过let osName: string = deviceInfo.osFullName;获取对应OS名字,该接口已支持跨平台,不同平台上其返回值如下: OpenHarmony上,osName等于OpenHarmony-XXX Android上,osName等于Android XXX iOS上,osName等于iOS XXX 示例如下:…
Read More