GoでリクエストのJSONの一部をソートしたりエンコードしたり

# 概要
Goがまだ扱い慣れてないので、色々触ってみようということで色々触ってみました🙄
リクエストボディの一部をソートしてからBase64でエンコードしてみただけですが、色々ということにしといてください。


# 準備
要件的な何かを準備して、どういう思考プロセスで進めたか残しておきます。

- ユーザーは名前と年齢とタスクを持つ
- ユーザーはタスクを複数持つ
- タスクはタイトルと締切を持つ
- タスクは締切の昇順でソートする
- リクエストはユーザー情報にユーザーが持つタスクを含んだJSON形式(`application/json`)で送る(例：`example.json`)
- ソートした内容のJSONをBase64でエンコードする

```json:example.json
{
  "name": "taro",
  "age": 20,
  "tasks": [
    {
      "task": "task2",
      "deadline": "2023/01/31"
    },
    {
      "task": "task1",
      "deadline": "2022/12/31"
    },
    {
      "task": "task4",
      "deadline": "2023/03/31"
    },
    {
      "task": "task3",
      "deadline": "2023/02/28"
    }
  ]
}
```


# やること整理
今回はそれほどややこしくないですが、実務では複雑な仕様が多々あります。
ややこしいことを実現するためには、まずややこしいことを構成している内容を1つずつ分解していく作業が大事なんじゃないでしょうか。
それら分解したパーツを組み合わせて複雑なものを作り上げるということです、知らんけど。

で、今回の要件的な何かを達成するために、こういうふうにタスクを分解すればいいんじゃね？と考えて分解してみました。
- ユーザーとタスクの構造体を作る
- リクエストを受け取れるようにする
- リクエストをユーザー構造体、タスク構造体にパースする
- パースしたタスク構造体スライスをソートする
- ソートした構造体からJSONを作りBase64エンコードする

# やってみた

- ユーザーとタスクの構造体を作る

まずは構造体を作ります。
jsonでパースするため、jsonのキー名のタグ(`` `json:"xxx"` ``)を各フィールドに記載しておきます。
```go:main.go
type user struct {
	Name  string `json:"name"`
	Age   int    `json:"age"`
	Tasks []task `json:"tasks"`
}

type task struct {
	Task     string `json:"task"`
	Deadline string `json:"deadline"`
}
```

- リクエストを受け取れるようにする

`8080`番ポートでサーバーを立てて、`/run`にリクエストすると`run`関数が走るようにしてみました。
```go:main.go
func main() {
	http.HandleFunc("/run", run)
	server := http.Server{
		Addr: ":8080",
	}
	if err := server.ListenAndServe(); err != nil {
		log.Fatalln(err)
	}
}
```

- リクエストをユーザー構造体、タスク構造体にパースする
- パースしたタスク構造体スライスをソートする
  - タスク構造体のDeadlineが`string`なので`time.Time`型にパースする
  - パースする際の日付のフォーマットを準備する
- ソートした構造体からJSONを作りBase64エンコードする

日付のソートをしようとする中で「deadlineフィールドが`string`型のままでは日付が正しくソートされないのでは？」と思ったので、さらにタスクを分解して考えてみました。

run関数の中でリクエストボディのパース、ソート、エンコードをします。
ソートとエンコードは別関数にしました。

```go:main.go
func run(w http.ResponseWriter, r *http.Request) {
	var u user
	if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
		log.Fatalln(err)
		return
	}
	sortDeadline(&u.Tasks)
	prettyJSON, err := json.MarshalIndent(u, "", "  ")
	if err != nil {
		log.Fatalln(err)
	}
	encodeJSON := encodeBase64(prettyJSON)
	fmt.Println(encodeJSON)
}

func encodeBase64(jsonData []byte) string {
	return base64.StdEncoding.EncodeToString(jsonData)
}

func sortDeadline(t *[]task) {
	dateFormat := "2006/01/02"
	sort.Slice(*t, func(i, j int) bool {
		dateI, err := time.Parse(dateFormat, (*t)[i].Deadline)
		if err != nil {
			return false
		}
		dateJ, err := time.Parse(dateFormat, (*t)[j].Deadline)
		if err != nil {
			return true
		}
		return dateI.Before(dateJ)
	})
}
```

これで完成です。
一応、デコードしてみて、タスクが締切の昇順でソートされているか確認します。
デコード用のコードを追加したコード全体は次のようになります。

```go:main.go
package main

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"sort"
	"time"
)

type user struct {
	Name  string `json:"name"`
	Age   int    `json:"age"`
	Tasks []task `json:"tasks"`
}

type task struct {
	Task     string `json:"task"`
	Deadline string `json:"deadline"`
}

func main() {
	http.HandleFunc("/run", run)
	server := http.Server{
		Addr: ":8080",
	}
	if err := server.ListenAndServe(); err != nil {
		log.Fatalln(err)
	}
}

func run(w http.ResponseWriter, r *http.Request) {
	var u user
	if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
		log.Fatalln(err)
		return
	}
	sortDeadline(&u.Tasks)
	prettyJSON, err := json.MarshalIndent(u, "", "  ")
	if err != nil {
		log.Fatalln(err)
	}
	encodeJSON := encodeBase64(prettyJSON)
	fmt.Println(encodeJSON)
	decodeJSON := decodeBase64(encodeJSON)
	fmt.Println(decodeJSON)
}

func encodeBase64(jsonData []byte) string {
	return base64.StdEncoding.EncodeToString(jsonData)
}

func decodeBase64(data string) string {
	dec, err := base64.StdEncoding.DecodeString(data)
	if err != nil {
		log.Fatalln(err)
	}
	return string(dec)
}

func sortDeadline(t *[]task) {
	dateFormat := "2006/01/02"
	sort.Slice(*t, func(i, j int) bool {
		dateI, err := time.Parse(dateFormat, (*t)[i].Deadline)
		if err != nil {
			return false
		}
		dateJ, err := time.Parse(dateFormat, (*t)[j].Deadline)
		if err != nil {
			return true
		}
		return dateI.Before(dateJ)
	})
}
```

ではcurlでリクエストしてみます。
```sh:コマンドライン
curl -XPOST -H "Content-Type: application/json" -d @./example.json localhost:8080/run
```
```:結果
---エンコード結果は長いので省略---
{
  "name": "taro",
  "age": 20,
  "tasks": [
    {
      "task": "task1",
      "deadline": "2022/12/31"
    },
    {
      "task": "task2",
      "deadline": "2023/01/31"
    },
    {
      "task": "task3",
      "deadline": "2023/02/28"
    },
    {
      "task": "task4",
      "deadline": "2023/03/31"
    }
  ]
}
```

`example.json`のtasks(タスク)がdeadline(締切)の昇順でソートされてます。
めでたし。

# 最後に
作成していく中でjsonのMarshalには、整形して読みやすくするための`json.MarshalIndent()`があるんだなぁとか、Base64エンコードもURL用のもの(`URLEncoding`)があるんだなぁとか、`time.Time`型には時間の前後判定を楽にしてくれる`Before`メソッドがあるんだなぁとか様々な学びがありました。
実はソート用の関数(`sortDeadline`)も最初はポインタで渡さずコピーを受け取る形にしてました。
これは直接書き換えればいっかと思ってポインタで渡すようにリファクタしたりもしてたので、その辺も学びポイントになりました。
あとは、`example.json`ファイルを読み込んだときのエンコード結果と、リクエストで`example.json`の内容を投げたときのエンコード結果が違ってて、調べてみるとファイルを読み込んだときは、VSCodeで行末改行設定を入れてたおかげで行末改行もエンコードされてるために結果が違ってたということもありました。
Goは標準パッケージだけでも色々とできるので、まだまだ触ってみないとと思いました(小並感)。

<details><summary>ポインタで渡さなかったときのsortDeadline関数</summary>

```go
func run(w http.ResponseWriter, r *http.Request) {
    // 前略
	u.Tasks = sortDeadline(u.Tasks)
    // 後略
}

func sortDeadline(t []task) []task {
	dateFormat := "2006/01/02"
	sort.Slice(t, func(i, j int) bool {
		dateI, err := time.Parse(dateFormat, t[i].Deadline)
		if err != nil {
			return false
		}
		dateJ, err := time.Parse(dateFormat, t[j].Deadline)
		if err != nil {
			return true
		}
		return dateI.Before(dateJ)
	})
	return t
}
```
</details>

## 参考
https://zenn.dev/senk/articles/674e720b6a8700