Line data Source code
1 : // Package rules は、カスタムバリデーションルールとそのエラー変換ユーティリティを提供します。
2 : // ここでは validator.ValidationErrors を i18n 対応のエラーマップ形式に変換します。
3 : package rules
4 :
5 : import (
6 : "log/slog"
7 : "strings"
8 :
9 : "github.com/go-playground/validator/v10"
10 :
11 : "resume/internal/shared/util"
12 : )
13 :
14 : // ErrorItem は、1 件のバリデーションエラー項目を表す構造体です。
15 : // 各エラーの翻訳キー(Code)と、埋め込みパラメータ(Params)を保持します。
16 : type ErrorItem struct {
17 : Code string `json:"code"`
18 : Params map[string]any `json:"params"`
19 : }
20 :
21 : // normalizeRuleTagLowerCamel は、validator のルールタグを lowerCamel に正規化します。
22 : // - snake/kebab/Pascal/camel/UPPER いずれの入力でも受ける
23 : // - go-playground/validator の一部タグ表記("startswith"/"endswith")も camel に寄せる
24 0 : func normalizeRuleTagLowerCamel(tag string) string {
25 0 : t := strings.TrimSpace(tag)
26 0 : // まず kebab を snake に
27 0 : t = strings.ReplaceAll(t, "-", "_")
28 0 :
29 0 : // validator の素のタグが "startswith"/"endswith" のため補正
30 0 : switch t {
31 0 : case "startswith":
32 0 : t = "starts_with"
33 0 : case "endswith":
34 0 : t = "ends_with"
35 : }
36 :
37 : // 何が来ても lowerCamel に統一
38 0 : return util.ToCamel(t)
39 : }
40 :
41 : // region KeyStore
42 :
43 : // MessageKeyStore は「キーが辞書に存在するかだけ」を見るためのインターフェース。
44 : type MessageKeyStore interface {
45 : HasKey(key string) bool
46 : }
47 :
48 : var msgKeyStore MessageKeyStore
49 :
50 : // SetMessageKeyStore は、バリデーションメッセージ用のキー存在チェックに使用する
51 : // MessageKeyStore 実装をグローバルに設定します。
52 0 : func SetMessageKeyStore(s MessageKeyStore) {
53 0 : msgKeyStore = s
54 0 : }
55 :
56 0 : func hasValidationKey(key string) bool {
57 0 : if msgKeyStore == nil {
58 0 : slog.Debug("hasValidationKey: msgKeyStore is nil", "key", key)
59 0 : return false
60 0 : }
61 0 : ok := msgKeyStore.HasKey(key)
62 0 : slog.Debug("hasValidationKey", "key", key, "ok", ok)
63 0 : return ok
64 : }
65 :
66 : // endregion
67 :
68 : // region Key Builders
69 :
70 : // rulesKey は、通常のバリデーションルール用のメッセージキーを生成します。
71 0 : func rulesKey(tagLowerCamel string) string { return "validation.rules." + tagLowerCamel }
72 :
73 : // customKey は、カスタムバリデーションルール用のメッセージキーを生成します。
74 0 : func customKey(tagLowerCamel string) string { return "validation.custom." + tagLowerCamel }
75 :
76 : // fieldSpecificKey は、フィールド固有のバリデーションメッセージキーを生成します。
77 0 : func fieldSpecificKey(scope, fieldKeyLowerCamel, tagLowerCamel string) string {
78 0 : return "validation.fieldspecific." + scope + "." + fieldKeyLowerCamel + "." + tagLowerCamel
79 0 : }
80 :
81 : // endregion
82 :
83 : // MapValidationErrors は validator.ValidationErrors を i18n 対応の
84 : // map[string][]ErrorItem 形式に変換します。
85 : // scope にはドメインスコープ(例: "domain.address")を指定します。
86 : func MapValidationErrors(
87 : verrs validator.ValidationErrors,
88 : scope string, // 例: "domain.address"
89 0 : ) map[string][]ErrorItem {
90 0 :
91 0 : out := map[string][]ErrorItem{}
92 0 :
93 0 : for _, fe := range verrs {
94 0 : fieldLowerCamel := util.ToCamel(fe.Field()) // ex) address_line1 -> addressLine1
95 0 : base := scope + "." + fieldLowerCamel // ex) domain.address.addressLine1
96 0 : //legacyKey := "validation.fields." + base // 旧互換
97 0 : labelKey := base + ".label" // 推奨: domain.*.label(存在判定しないでキーをそのまま渡す)
98 0 : //fieldParam := labelKey // デフォは labelKey
99 0 : tagLowerCamel := normalizeRuleTagLowerCamel(fe.Tag()) // ex) startswith -> startsWith
100 0 :
101 0 : // ★ sort_key は oneof として扱う(コードとテンプレ両方を oneof に統一)
102 0 : if tagLowerCamel == "sortKey" {
103 0 : tagLowerCamel = "oneof"
104 0 : }
105 :
106 0 : tagSnake := util.ToSnake(tagLowerCamel)
107 0 :
108 0 : // 直接キーを渡す(フロント側で解決)。未定義時はそのままキー表示になるが、Humanizeより整合的。
109 0 : //fieldParam := labelKey
110 0 :
111 0 : // パラメータ(辞書側で {field}, {param}, {min}, {max}, {len}, {value} などを利用)
112 0 : p := map[string]any{"field": labelKey}
113 0 :
114 0 : if param := strings.TrimSpace(fe.Param()); param != "" {
115 0 : p["param"] = param
116 0 :
117 0 : // ★ sort_key = identities の時、allowedSortKeys["identities"] を values に展開する
118 0 : if tagLowerCamel == "oneof" { // sort_key → oneof に統一されている
119 0 : if keys, ok := SortKeyAllowed[param]; ok && len(keys) > 0 {
120 0 : // "created_at provider uid email_at_signup"
121 0 : joined := strings.Join(keys, " ")
122 0 : p["values"] = joined // ★ フロントが欲しかった最終形
123 0 : p["oneof"] = joined // 従来 param の代わりに oneof をより正確に
124 0 : } else {
125 0 : // フォールバック(パラメータ名だけ)
126 0 : p["values"] = param
127 0 : p["oneof"] = param
128 0 : }
129 : }
130 :
131 0 : switch tagLowerCamel {
132 0 : case "min":
133 0 : p["min"] = param
134 0 : case "max":
135 0 : p["max"] = param
136 0 : case "len":
137 0 : p["len"] = param
138 0 : case "oneof":
139 0 : p["oneof"] = param
140 0 : case "gte", "lte", "gt", "lt", "gtfield", "gtefield", "ltfield", "ltefield":
141 0 : p["value"] = param
142 : // ToDo: gt,gte,lt,lteの場合は、paramにドメインキーも付与して辞書に差し込みたいかも。(フロントと接続後検証してチケット化)
143 : }
144 : }
145 :
146 : // 3種類の候補キーを生成
147 0 : fsKey := fieldSpecificKey(scope, fieldLowerCamel, tagLowerCamel)
148 0 : cKey := customKey(tagLowerCamel)
149 0 : rKey := rulesKey(tagLowerCamel)
150 0 : //key := rulesKey(tagLowerCamel)
151 0 :
152 0 : // --- 検索用(辞書内に存在するかを見る)キー: snake ベース ---
153 0 : // ※ builder は文字列を連結しているだけなので、最後の引数に snake を渡しても問題なし。
154 0 : fsKeyLookup := fieldSpecificKey(scope, fieldLowerCamel, tagSnake)
155 0 : cKeyLookup := customKey(tagSnake)
156 0 : // rKeyLookup := rulesKey(tagSnake) // 今回は rules は存在チェックしなくてもOKなら未使用でよい
157 0 :
158 0 : // 優先度:フィールド固有 > custom > rules
159 0 : key := rKey
160 0 : if hasValidationKey(cKeyLookup) {
161 0 : key = cKey
162 0 : }
163 0 : if hasValidationKey(fsKeyLookup) {
164 0 : key = fsKey
165 0 : }
166 :
167 0 : out[base] = append(
168 0 : out[base],
169 0 : ErrorItem{
170 0 : Code: key,
171 0 : Params: p,
172 0 : },
173 0 : )
174 : }
175 0 : return out
176 : }
|