ToDoアプリの作成（バリデーション編）

<h2>はじめに</h2>
今回は，前回に続き外部パッケージのozzo-validationを使ってバリデーションの実装を行っていきます。

https://qiita.com/sawadashinji0526/items/816646ed12200ab76084

<h2>バリデーションの実装(タスク)</h2>
タスクのバリデーション処理を実装します。

```validator/task_validator.go
package validator

import (
	"go-rest-api/model"
    //外部パッケージなのでgo mod tidyコマンドを実行してダウンロードします。
	validation "github.com/go-ozzo/ozzo-validation/v4"
)

type ITaskValidator interface {
    //タスクのバリデーション処理を実行するメソッドです。
    //バリデーション対象のタスクです。
    //バリデーションエラーが発生した場合に返されるエラーです。
	TaskValidate(task model.Task) error
}

//タスクバリデータの具体的な実装です。
type taskValidator struct{}

//この関数は、taskValidator 構造体の新しいインスタンスを生成して返します。
func NewTaskValidator() ITaskValidator {
	return &taskValidator{}
}

//タスクのバリデーション処理を実行するメソッドの実装です。
func (tv *taskValidator) TaskValidate(task model.Task) error {
    //validation.ValidateStruct 関数を使って、task 構造体のバリデーションを実行します。
    //validation.Field は、バリデーション対象のフィールドを指定します。
	return validation.ValidateStruct(&task,
		validation.Field(
            //task 構造体の Title フィールドをバリデーション対象として指定します。
			&task.Title,
            //Title フィールドが必須であることを指定します。エラーメッセージは "title is required" です。
			validation.Required.Error("title is required"),
            //Title フィールドの長さが1文字以上10文字以下であることを指定します。エラーメッセージは "limited max 10 char" です。
			validation.RuneLength(1, 10).Error("limited max 10 char"),
		),
	)
}
```

<h2>バリデーションの実装(ユーザー)</h2>
ユーザーのバリデーション処理を実装します。

```validator/user_validator.go
package validator

import (
	"go-rest-api/model"
	validation "github.com/go-ozzo/ozzo-validation/v4"
    //外部パッケージなのでgo mod tidyコマンドを実行してダウンロードします。
	"github.com/go-ozzo/ozzo-validation/v4/is"
)

//ユーザーバリデータが実装すべきメソッドを定義するインターフェースです。
type IUserValidator interface {
    //ユーザーのバリデーション処理を実行するメソッドです。
    //user model.User: バリデーション対象のユーザーです。
    //error: バリデーションエラーが発生した場合に返されるエラーです。
	UserValidate(user model.User) error
}

//ユーザーバリデータの具体的な実装です。
type userValidator struct{}

//userValidator 構造体の新しいインスタンスを作成する関数です。
func NewUserValidator() IUserValidator {
    //userValidator 構造体の新しいインスタンスを生成して返します。
	return &userValidator{}
}

//ユーザーのバリデーション処理を実行するメソッドの実装です。
//validation.ValidateStruct 関数を使って、user 構造体のバリデーションを実行します。
func (uv *userValidator) UserValidate(user model.User) error {
	return validation.ValidateStruct(&user,
        //バリデーション対象のフィールドを指定します。
		validation.Field(
            //user 構造体の Email フィールドをバリデーション対象として指定します。
			&user.Email,
            //Email フィールドが必須であることを指定します。エラーメッセージは "email is required" です。
			validation.Required.Error("email is required"),
            //Email フィールドが有効なメールアドレス形式であることを指定します。エラーメッセージは "is not valid email format" です。
			is.Email.Error("is not valid email format"),
		),
		validation.Field(
            //user 構造体の Password フィールドをバリデーション対象として指定します。
			&user.Password,
            //Password フィールドが必須であることを指定します。エラーメッセージは "password is required" です。
			validation.Required.Error("password is required"),
            //Password フィールドの長さが6文字以上30文字以下であることを指定します。エラーメッセージは "limited min 6 max 30char" です。
			validation.RuneLength(6, 30).Error("limited min 6 max 30char"),
		),
	)
}
```


<h2>タスクユースケースの実装</h2>
タスクユースケースの方にもバリデーションの機能を実装していきます。

```usecase/task_usecase.go
package usecase

import (
	"go-rest-api/model"
	"go-rest-api/repository"
	"go-rest-api/validator"
)

type ITaskUsecase interface {
	GetAllTasks(userId uint) ([]model.TaskResponse, error)
	GetTaskById(userId uint, taskId uint) (model.TaskResponse, error)
	CreateTask(task model.Task) (model.TaskResponse, error)
	UpdateTask(task model.Task, userId uint, taskId uint) (model.TaskResponse, error)
	DeleteTask(userId uint, taskId uint) error
}

type taskUsecase struct {
	tr repository.ITaskRepository
    //タスクバリデータへの依存関係を保持します。（新たに追加したコード）
	tv validator.ITaskValidator
}

//タスクバリデーターが注入できるよう、引数に`tv validator.ITaskValidator`を追加
func NewTaskUsecase(tr repository.ITaskRepository, tv validator.ITaskValidator) ITaskUsecase {
    //タスクユースケースをインスタンス化するフィールドに`tv`を追加
	return &taskUsecase{tr, tv}
}

func (tu *taskUsecase) GetAllTasks(userId uint) ([]model.TaskResponse, error) {
	tasks := []model.Task{}
	if err := tu.tr.GetAllTasks(&tasks, userId); err != nil {
		return nil, err
	}
	resTasks := []model.TaskResponse{}
	for _, v := range tasks {
		t := model.TaskResponse{
			ID:        v.ID,
			Title:     v.Title,
			CreatedAt: v.CreatedAt,
			UpdatedAt: v.UpdatedAt,
		}
		resTasks = append(resTasks, t)
	}
	return resTasks, nil
}

func (tu *taskUsecase) GetTaskById(userId uint, taskId uint) (model.TaskResponse, error) {
	task := model.Task{}
	if err := tu.tr.GetTaskById(&task, userId, taskId); err != nil {
		return model.TaskResponse{}, err
	}
	resTask := model.TaskResponse{
		ID:        task.ID,
		Title:     task.Title,
		CreatedAt: task.CreatedAt,
		UpdatedAt: task.UpdatedAt,
	}
	return resTask, nil
}

func (tu *taskUsecase) CreateTask(task model.Task) (model.TaskResponse, error) {
    //タスクバリデータの TaskValidate メソッドを呼び出して、受け取ったタスクのデータが妥当かどうか検証します。
    //バリデーション処理中にエラーが発生した場合、エラーを返します。(新たに追加したコード)
	if err := tu.tv.TaskValidate(task); err != nil {
		return model.TaskResponse{}, err
	}

	if err := tu.tr.CreateTask(&task); err != nil {
		return model.TaskResponse{}, err
	}
	resTask := model.TaskResponse{
		ID:        task.ID,
		Title:     task.Title,
		CreatedAt: task.CreatedAt,
		UpdatedAt: task.UpdatedAt,
	}
	return resTask, nil
}

func (tu *taskUsecase) UpdateTask(task model.Task, userId uint, taskId uint) (model.TaskResponse, error) {
    //タスクバリデータの TaskValidate メソッドを呼び出して、受け取ったタスクのデータが妥当かどうか検証します。
    //バリデーション処理中にエラーが発生した場合、エラーを返します。
	if err := tu.tv.TaskValidate(task); err != nil {
		return model.TaskResponse{}, err
	}
	if err := tu.tr.UpdateTask(&task, userId, taskId); err != nil {
		return model.TaskResponse{}, err
	}
	resTask := model.TaskResponse{
		ID:        task.ID,
		Title:     task.Title,
		CreatedAt: task.CreatedAt,
		UpdatedAt: task.UpdatedAt,
	}
	return resTask, nil
}

func (tu *taskUsecase) DeleteTask(userId uint, taskId uint) error {
	if err := tu.tr.DeleteTask(userId, taskId); err != nil {
		return err
	}
	return nil
}
```

<h2>ユーザーユースケースの実装</h2>
ユーザーユースケースの方にもバリデーションの機能を実装していきます。

```usecase/user_usecase.go

package usecase

import (
	"go-rest-api/model"
	"go-rest-api/repository"
	"go-rest-api/validator"
	"os"
	"time"

	"github.com/golang-jwt/jwt/v4"
	"golang.org/x/crypto/bcrypt"
)

type IUserUsecase interface {
	SignUp(user model.User) (model.UserResponse, error)
	Login(user model.User) (string, error)
}

type userUsecase struct {
	ur repository.IUserRepository
     //ユーザーバリデータへの依存関係を保持します。（新たに追加したコード）
	uv validator.IUserValidator
}

    //ユーザーバリデーターが注入できるよう、引数に`uv validator.IUserValidator`を追加
func NewUserUsecase(ur repository.IUserRepository, uv validator.IUserValidator) IUserUsecase {
    //ユーザーユースケースをインスタンス化するフィールドに`uv`を追加
	return &userUsecase{ur, uv}
}

func (uu *userUsecase) SignUp(user model.User) (model.UserResponse, error) {
    //ユーザーバリデータの UserValidate メソッドを呼び出して、受け取ったタスクのデータが妥当かどうか検証します。（新たに追加したコード）
    //バリデーション処理中にエラーが発生した場合、エラーを返します。
	if err := uu.uv.UserValidate(user); err != nil {
		return model.UserResponse{}, err
	}
 
	hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 10)
	if err != nil {
		return model.UserResponse{}, err
	}
	newUser := model.User{Email: user.Email, Password: string(hash)}
	if err := uu.ur.CreateUser(&newUser); err != nil {
		return model.UserResponse{}, err
	}
	resUser := model.UserResponse{
		ID:    newUser.ID,
		Email: newUser.Email,
	}
	return resUser, nil
}

func (uu *userUsecase) Login(user model.User) (string, error) {
    //ユーザーバリデータの UserValidate メソッドを呼び出して、受け取ったタスクのデータが妥当かどうか検証します。（新たに追加したコード）
    //バリデーション処理中にエラーが発生した場合、エラーを返します。
	if err := uu.uv.UserValidate(user); err != nil {
		return "", err
	}
 
	storedUser := model.User{}
	if err := uu.ur.GetUserByEmail(&storedUser, user.Email); err != nil {
		return "", err
	}
	err := bcrypt.CompareHashAndPassword([]byte(storedUser.Password), []byte(user.Password))
	if err != nil {
		return "", err
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"user_id": storedUser.ID,
		"exp":     time.Now().Add(time.Hour * 12).Unix(),
	})
	tokenString, err := token.SignedString([]byte(os.Getenv("SECRET")))
	if err != nil {
		return "", err
	}
	return tokenString, nil
}
```

<h2>main.goの実装</h2>
バリデータをインスタンス化してからユースケースにDIしていきます。

```main.go
package main

import (
	"go-rest-api/controller"
	"go-rest-api/db"
	"go-rest-api/repository"
	"go-rest-api/router"
	"go-rest-api/usecase"
	"go-rest-api/validator"
)

func main() {
	db := db.NewDB()
    //ユーザーバリデータの新しいインスタンスを作成します。
	userValidator := validator.NewUserValidator()
    //タスクバリデータの新しいインスタンスを作成します。
	taskValidator := validator.NewTaskValidator()
	userRepository := repository.NewUserRepository(db)
	taskRepository := repository.NewTaskRepository(db)
    //作成したインスタンスをユースケースのコンストラクターに渡すため、引数に`userValidator`を追加
	userUsecase := usecase.NewUserUsecase(userRepository, userValidator)
    //作成したインスタンスをユースケースのコンストラクターに渡すため、引数に`taskValidator`を追加
	taskUsecase := usecase.NewTaskUsecase(taskRepository, taskValidator)
	userController := controller.NewUserController(userUsecase)
	taskController := controller.NewTaskController(taskUsecase)
	e := router.NewRouter(userController, taskController)
	e.Logger.Fatal(e.Start(":8080"))
}
```

<h2>動作確認</h2>
下記のコマンドでプログラムを起動していきます。
`GO_ENV=dev go run main.go`

起動できました！

![スクリーンショット 2024-11-12 19.48.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628257/a6c3a9f3-af9b-6cc3-00f5-32ab56b57d4a.png)

パスワードを5文字にして6文字より少ない場合、バリデーションが表示されるか確認します。
POSTメソッドで、エンドポイントに`http://localhost:8080/login`を入力してSendボタンを押下します。

![スクリーンショット 2024-11-12 19.53.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628257/409c1bf3-2b60-c083-1031-5d99b5ad153b.png)

そうすると`500 Internal Server Error`とValitadorで設定した`password: limited min 6 max 30char.`が返ってきます。

次はtasksの新規作成時のバリデーションを確認していきます。
ログイン後、titleが無い状態でPOSTメソッドで、エンドポイントに`http://localhost:8080/tasks`を入力してSendボタンを押下します。

![スクリーンショット 2024-11-12 20.04.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628257/aea1835f-2d5c-c34e-1c03-5dc22f446493.png)

そうすると`500 Internal Server Error`とValitadorで設定した`"title: title is required."`が返ってきます。


次はtasksのtitleが10文字以上でPOSTメソッドで、エンドポイントに`http://localhost:8080/tasks`を入力してSendボタンを押下します。

![スクリーンショット 2024-11-12 20.09.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628257/950279aa-579e-b1b6-641d-5218d25d75b4.png)


そうすると`500 Internal Server Error`とValitadorで設定した`"title: limited max 10 char."`が返ってきます。
※ユーザーのバリデーションも同じような内容なので割愛

<h2>まとめ</h2>
今回はバリデーションの機能を実装していきました。
次はCORSとCSRFのMiddlewareを追加していきます。