今日もメンバーのコードレビューに半日以上の時間を割いたので自分の時間をあまり取れなくて開発が進捗しなかった。
隙間時間で openldap スキーマの情報 を使って validator を実装していた。バリデーションのようなものはまとめてエラーを返せるのが望ましい。go でマルチエラーを制御する仕組みが少し前に追加されていたことを思い出した。Go 1.20 Wrapping multiple errors のチュートリアル記事を読みながら openldap スキーマの validator を実装して複数のエラーを返すようにした。まだ抜け・漏れはあるかもしれないけど objectClass の validator は次のようになる。
type ObjectClassValidator struct {
objectClassMap map[string]ObjectClassDescription
}
func (v *ObjectClassValidator) Validate(attr map[string][]string) error {
var errs []error
var objClassDescs []ObjectClassDescription
hasStructuralKind := false
allAttrMap := map[string]struct{}{}
lowerAttrNameMap := make(map[string]struct{}, len(attr))
for k, values := range attr {
lowerName := strings.ToLower(ParseAttribute(k))
lowerAttrNameMap[lowerName] = struct{}{}
if lowerName == "objectclass" {
objClassDescs = make([]ObjectClassDescription, 0, len(values))
for _, objcName := range values {
lowerObjcName := strings.ToLower(objcName)
ocd, ok := v.objectClassMap[lowerObjcName]
if !ok {
msg := fmt.Sprintf("'%s' objectClass is not defined", objcName)
errs = append(errs, NewErrorValidation(msg))
continue
}
if ocd.Kind != nil && *ocd.Kind == Structural {
hasStructuralKind = true
}
objClassDescs = append(objClassDescs, ocd)
for mustAttr := range ocd.GetMust() {
allAttrMap[strings.ToLower(mustAttr)] = struct{}{}
}
for mayAttr := range ocd.GetMay() {
allAttrMap[strings.ToLower(mayAttr)] = struct{}{}
}
}
}
}
// require at least 1 structural objectClass
if !hasStructuralKind {
errs = append(errs, NewErrorValidation("no structural objectClass"))
}
// must in objectClass requires must attributes
for _, ocd := range objClassDescs {
for mustAttr := range ocd.GetMust() {
if _, ok := lowerAttrNameMap[strings.ToLower(mustAttr)]; !ok {
msg := fmt.Sprintf("'%s' objectClass requires '%s'", ocd.GetName(), mustAttr)
errs = append(errs, NewErrorValidation(msg))
continue
}
}
}
// all attribute keys are allowed may/must in objectClass
for attrName := range lowerAttrNameMap {
if _, ok := allAttrMap[attrName]; !ok {
msg := fmt.Sprintf("'%s' is not allowed by objectClass", attrName)
errs = append(errs, NewErrorValidation(msg))
continue
}
}
return errors.Join(errs...)
}
func NewObjectClassValidator(
objectClasses []ObjectClassDescription,
) *ObjectClassValidator {
ocm := make(map[string]ObjectClassDescription, len(objectClasses))
for _, v := range objectClasses {
for _, n := range v.Name {
ocm[strings.ToLower(n)] = v
}
}
return &ObjectClassValidator{
objectClassMap: ocm,
}
}
次のように複数のエラーを返すテストケースを定義する。
{
name: "multiple errors",
objClasses: objectClasses1,
attr: map[string][]string{
"objectClass": []string{"top", "posixAccount", "myObjeClass"},
"uid": nil,
"mail": nil,
"mail;lang-ja;phonetic": nil,
"employeeType;lang-ja": nil,
"homeDirectory": nil,
"GIDNumber": nil,
"street": nil,
},
expected: []error{
openldap.NewErrorValidation("'employeetype' is not allowed by objectClass"),
openldap.NewErrorValidation("'mail' is not allowed by objectClass"),
openldap.NewErrorValidation("'myObjeClass' objectClass is not defined"),
openldap.NewErrorValidation("'posixAccount' objectClass requires 'cn'"),
openldap.NewErrorValidation("'posixAccount' objectClass requires 'uidNumber'"),
openldap.NewErrorValidation("'street' is not allowed by objectClass"),
openldap.NewErrorValidation("no structural objectClass"),
},
},
テストコードで次のように返ってくる error を unwrap することで検証できる。
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
validator := openldap.NewObjectClassValidator(tt.objClasses)
err := validator.Validate(tt.attr)
if actual, ok := err.(interface{ Unwrap() []error }); ok {
t.Log("\n" + err.Error())
if diff := cmp.Diff(tt.expected, actual.Unwrap(), opts...); diff != "" {
t.Error(diff)
return
}
}
})
}