package pkg import ( "context" "database/sql" "database/sql/driver" "errors" "fmt" "github.com/jackc/pgtype" "html/template" "io" "net/http" "net/mail" "net/url" "regexp" "strconv" "strings" "time" ) var tagsRegex = regexp.MustCompile("[^a-z0-9-]+") type Attribute struct { Key, Val string } type InputField struct { Name string Label string Type string Val string Is string Required bool Attributes []template.HTMLAttr Errors []error } func (field *InputField) Scan(value interface{}) error { if value == nil { field.Val = "" return nil } switch v := value.(type) { case time.Time: if field.Type == "date" { field.Val = v.Format("2006-01-02") } else if field.Type == "time" { field.Val = v.Format("15:04") } else { field.Val = v.Format(time.RFC3339) } default: field.Val = fmt.Sprintf("%v", v) } return nil } func (field *InputField) Value() (driver.Value, error) { return field.Val, nil } func (field *InputField) FillValue(r *http.Request) { field.Val = strings.TrimSpace(r.FormValue(field.Name)) } func (field *InputField) Integer() int { value, err := strconv.Atoi(field.Val) if err != nil { panic(err) } return value } func (field *InputField) IntegerOrNil() interface{} { if field.Val != "" { if i := field.Integer(); i > 0 { return i } } return nil } func (field *InputField) Float64() float64 { value, err := strconv.ParseFloat(field.Val, 64) if err != nil { panic(err) } return value } func (field *InputField) String() string { return field.Val } type SelectOption struct { Value string Label string Group string } type SelectField struct { Name string Label string Selected []string Options []*SelectOption Attributes []template.HTMLAttr Required bool Multiple bool EmptyLabel string Errors []error } func (field *SelectField) Scan(value interface{}) error { if value == nil { field.Selected = append(field.Selected, "") return nil } if str, ok := value.(string); ok { if array, err := pgtype.ParseUntypedTextArray(str); err == nil { for _, element := range array.Elements { field.Selected = append(field.Selected, element) } return nil } } field.Selected = append(field.Selected, fmt.Sprintf("%v", value)) return nil } func (field *SelectField) Value() (driver.Value, error) { return field.String(), nil } func (field *SelectField) String() string { if field.Selected == nil { return "" } return field.Selected[0] } func (field *SelectField) OrNull() interface{} { if field.String() == "" { return sql.NullString{} } return field } func (field *SelectField) FillValue(r *http.Request) { field.Selected = r.Form[field.Name] } func (field *SelectField) HasValidOptions() bool { for _, selected := range field.Selected { if !field.isValidOption(selected) { return false } } return true } func (field *SelectField) IsSelected(v string) bool { for _, selected := range field.Selected { if selected == v { return true } } return false } func (field *SelectField) FindOption(value string) *SelectOption { for _, option := range field.Options { if option.Value == value { return option } } return nil } func (field *SelectField) isValidOption(selected string) bool { return field.FindOption(selected) != nil } func (field *SelectField) Clear() { field.Selected = []string{} } func MustGetOptions(ctx context.Context, conn *Conn, sql string, args ...interface{}) []*SelectOption { rows, err := conn.Query(ctx, sql, args...) if err != nil { panic(err) } defer rows.Close() var options []*SelectOption for rows.Next() { option := &SelectOption{} err = rows.Scan(&option.Value, &option.Label) if err != nil { panic(err) } options = append(options, option) } if rows.Err() != nil { panic(rows.Err()) } return options } func MustGetGroupedOptions(ctx context.Context, conn *Conn, sql string, args ...interface{}) []*SelectOption { rows, err := conn.Query(ctx, sql, args...) if err != nil { panic(err) } defer rows.Close() var options []*SelectOption for rows.Next() { option := &SelectOption{} err = rows.Scan(&option.Value, &option.Label, &option.Group) if err != nil { panic(err) } options = append(options, option) } if rows.Err() != nil { panic(rows.Err()) } return options } func mustGetCountryOptions(ctx context.Context, conn *Conn, locale *Locale) []*SelectOption { return MustGetOptions(ctx, conn, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", locale.Language) } type RadioOption struct { Value string Label string } type RadioField struct { Name string Label string Selected string Options []*RadioOption Attributes []template.HTMLAttr Required bool Errors []error } func (field *RadioField) Scan(value interface{}) error { if value == nil { field.Selected = "" return nil } field.Selected = fmt.Sprintf("%v", value) return nil } func (field *RadioField) Value() (driver.Value, error) { return field.Selected, nil } func (field *RadioField) String() string { return field.Selected } func (field *RadioField) FillValue(r *http.Request) { field.Selected = strings.TrimSpace(r.FormValue(field.Name)) } func (field *RadioField) IsSelected(v string) bool { return field.Selected == v } func (field *RadioField) FindOption(value string) *RadioOption { for _, option := range field.Options { if option.Value == value { return option } } return nil } func (field *RadioField) isValidOption(selected string) bool { return field.FindOption(selected) != nil } type CheckField struct { Name string Label string Checked bool Attributes []template.HTMLAttr Required bool Errors []error } func (field *CheckField) FillValue(r *http.Request) { field.Checked = len(r.Form[field.Name]) > 0 } func (field *CheckField) Scan(value interface{}) error { if value == nil { field.Checked = false return nil } switch v := value.(type) { case bool: field.Checked = v default: field.Checked, _ = strconv.ParseBool(fmt.Sprintf("%v", v)) } return nil } func (field *CheckField) Value() (driver.Value, error) { return field.Checked, nil } type FileField struct { Name string Label string MaxSize int64 OriginalFileName string ContentType string Content []byte Required bool Errors []error } func (field *FileField) FillValue(r *http.Request) error { file, header, err := r.FormFile(field.Name) if err != nil { if err == http.ErrMissingFile { return nil } return err } defer file.Close() field.Content, err = io.ReadAll(file) if err != nil { return err } field.OriginalFileName = header.Filename field.ContentType = header.Header.Get("Content-Type") if len(field.Content) == 0 { field.ContentType = http.DetectContentType(field.Content) } return nil } type TagsField struct { Name string Label string Tags []string Attributes []template.HTMLAttr Required bool Errors []error } func (field *TagsField) Value() (driver.Value, error) { if field.Tags == nil { return []string{}, nil } return field.Tags, nil } func (field *TagsField) Scan(value interface{}) error { if value == nil { return nil } if str, ok := value.(string); ok { if array, err := pgtype.ParseUntypedTextArray(str); err == nil { for _, element := range array.Elements { field.Tags = append(field.Tags, element) } return nil } } field.Tags = append(field.Tags, fmt.Sprintf("%v", value)) return nil } func (field *TagsField) FillValue(r *http.Request) { field.Tags = strings.Split(tagsRegex.ReplaceAllString(r.FormValue(field.Name), ","), ",") if len(field.Tags) == 1 && len(field.Tags[0]) == 0 { field.Tags = []string{} } } func (field *TagsField) String() string { return strings.Join(field.Tags, ",") } type ToggleField struct { Name string Label string Selected string FirstOption *ToggleOption SecondOption *ToggleOption Attributes []template.HTMLAttr Errors []error } type ToggleOption struct { Value string Label string Description string } func (field *ToggleField) FillValue(r *http.Request) { field.Selected = strings.TrimSpace(r.FormValue(field.Name)) if field.Selected != field.FirstOption.Value && field.Selected != field.SecondOption.Value { field.Selected = field.FirstOption.Value } } type FormValidator struct { Valid bool } func newFormValidator() *FormValidator { return &FormValidator{true} } func (v *FormValidator) AllOK() bool { return v.Valid } func (v *FormValidator) CheckRequiredInput(field *InputField, message string) bool { return v.checkInput(field, field.Val != "", message) } func (v *FormValidator) CheckInputMinLength(field *InputField, min int, message string) bool { return v.checkInput(field, len(field.Val) >= min, message) } func (v *FormValidator) CheckValidEmailInput(field *InputField, message string) bool { _, err := mail.ParseAddress(field.Val) return v.checkInput(field, err == nil, message) } func (v *FormValidator) CheckValidVATINInput(ctx context.Context, conn *Conn, field *InputField, country string, message string) bool { return v.checkInput(field, conn.MustGetBool(ctx, "select input_is_valid($1 || $2, 'vatin')", country, field.Val), message) } func (v *FormValidator) CheckValidPhoneInput(ctx context.Context, conn *Conn, field *InputField, country string, message string) bool { return v.checkInput(field, conn.MustGetBool(ctx, "select input_is_valid_phone($1, $2)", field.Val, country), message) } func (v *FormValidator) CheckValidIBANInput(ctx context.Context, conn *Conn, field *InputField, message string) bool { return v.checkInput(field, conn.MustGetBool(ctx, "select input_is_valid($1, 'iban')", field.Val), message) } func (v *FormValidator) CheckValidBICInput(ctx context.Context, conn *Conn, field *InputField, message string) bool { return v.checkInput(field, conn.MustGetBool(ctx, "select input_is_valid($1, 'bic')", field.Val), message) } func (v *FormValidator) CheckPasswordConfirmation(password *InputField, confirm *InputField, message string) bool { return v.checkInput(confirm, password.Val == confirm.Val, message) } func (v *FormValidator) CheckValidSelectOption(field *SelectField, message string) bool { return v.checkSelect(field, field.HasValidOptions(), message) } func (v *FormValidator) CheckAtMostOneOfEachGroup(field *SelectField, message string) bool { repeated := false groups := map[string]bool{} for _, selected := range field.Selected { option := field.FindOption(selected) if exists := groups[option.Group]; exists { repeated = true break } groups[option.Group] = true } return v.checkSelect(field, !repeated, message) } func (v *FormValidator) CheckValidURL(field *InputField, message string) bool { _, err := url.ParseRequestURI(field.Val) return v.checkInput(field, err == nil, message) } func (v *FormValidator) CheckValidDate(field *InputField, message string) bool { _, err := time.Parse("2006-01-02", field.Val) return v.checkInput(field, err == nil, message) } func (v *FormValidator) CheckValidPostalCode(ctx context.Context, conn *Conn, field *InputField, country string, message string) bool { pattern := "^" + conn.MustGetText(ctx, ".{1,255}", "select postal_code_regex from country where country_code = $1", country) + "$" match, err := regexp.MatchString(pattern, field.Val) if err != nil { panic(err) } return v.checkInput(field, match, message) } func (v *FormValidator) CheckValidInteger(field *InputField, min int, max int, message string) bool { value, err := strconv.Atoi(field.Val) return v.checkInput(field, err == nil && value >= min && value <= max, message) } func (v *FormValidator) CheckValidDecimal(field *InputField, min float64, max float64, message string) bool { value, err := strconv.ParseFloat(field.Val, 64) return v.checkInput(field, err == nil && value >= min && value <= max, message) } func (v *FormValidator) checkInput(field *InputField, ok bool, message string) bool { if !ok { field.Errors = append(field.Errors, errors.New(message)) v.Valid = false } return ok } func (v *FormValidator) checkSelect(field *SelectField, ok bool, message string) bool { if !ok { field.Errors = append(field.Errors, errors.New(message)) v.Valid = false } return ok }