package dto import "strings" // Приведение входа к каноничному виду ДО проверки правил. // // Зачем это нужно. В go-playground/validator тег `omitempty` НЕ пропускает // правило, если поле объявлено указателем, а указатель не nil. Помощник // `hasValue` (baked_in.go) устроен так: // // if fl.(*validate).fldIsPointer && getValue(field) != nil { // return true // } // // Для `*string`, указывающего на пустую строку, это возвращает true, то есть // «значение есть». В результате `omitempty,min=6` на поле `Secret` срабатывало // именно тогда, когда оператор НИЧЕГО не ввёл: панель отправляла `secret: ""`, // правило `min=6` применялось к пустой строке и отказывало. Панель при этом // писала под полем «оставьте пустым — сгенерируем автоматически», а сервер // умел это сделать: генерация в CreatePeer существовала и была недостижима. // // Чинить это тегом на одном поле бессмысленно: ловушка одинаково стоит на // фильтре списка пиров (очищенный `el-input` шлёт `?name=`, правило `min=1` // отказывает поиску), на необязательных отметках времени и на всяком будущем // необязательном поле-указателе. Поэтому нормализация — общий шаг конвейера, а // не особый случай «если пусто, подставь строку». // // Правило формулируется ПОФАКТИЧЕСКИ, для каждого поля отдельно, и это // сознательно. Пустая строка не везде означает «не задано»: у `remark` она // означает «очистить пометку», и общее «пусто → nil» молча лишило бы оператора // возможности её убрать. Ноль у `disabled` и `quotaBytes` — законное значение, // а не пропуск. // Normalizable — DTO, приводящее свой вход к каноничному виду. // // Вызывается слоем контроллеров между разбором тела и проверкой правил, то // есть ровно один раз и для всех дверей одинаково. type Normalizable interface { Normalize() } // blankToNil: «пусто или одни пробелы» становится «не задано». // // Применяется к полям, у которых отсутствие значения — законный вход. func blankToNil(field **string) { if *field == nil { return } trimmed := strings.TrimSpace(**field) if trimmed == "" { *field = nil return } *field = &trimmed } // trimValue убирает окружающие пробелы, сохраняя само поле заданным. // // Применяется к обязательным полям и к тем, у которых пустая строка — это // значение, а не пропуск. Пустой ввод после тримминга остаётся пустым и // получит внятный отказ от `required`, а не молча превратится в «не задано». func trimValue(field *string) { if field == nil { return } *field = strings.TrimSpace(*field) } // zeroToNil: ноль у необязательного числового поля означает «не задано». // // Применяется ТОЛЬКО там, где ноль не является осмысленным значением: // «показать 0 строк журнала» и «время начала — 1 января 1970 года» — это // пропуск фильтра, а не запрос. func zeroToNil[T int | int64](field **T) { if *field == nil { return } if **field == 0 { *field = nil } }