feat: initial commit
This commit is contained in:
9
internal/http/form/error.go
Normal file
9
internal/http/form/error.go
Normal file
@ -0,0 +1,9 @@
|
||||
package form
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrFieldNotFound = errors.New("field not found")
|
||||
ErrUnexpectedValue = errors.New("unexpected value")
|
||||
ErrMissingValue = errors.New("missing value")
|
||||
)
|
85
internal/http/form/field.go
Normal file
85
internal/http/form/field.go
Normal file
@ -0,0 +1,85 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Attrs map[string]any
|
||||
|
||||
type FieldUnmarshaller interface {
|
||||
Unmarshal(v string) error
|
||||
}
|
||||
|
||||
type ValidationFunc func(f *Field) error
|
||||
|
||||
type Field struct {
|
||||
name string
|
||||
attrs map[string]any
|
||||
validators []ValidationFunc
|
||||
}
|
||||
|
||||
func (f *Field) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f *Field) Get(key string) (value any, exists bool) {
|
||||
value, exists = f.attrs[key]
|
||||
return
|
||||
}
|
||||
|
||||
func (f *Field) Set(key string, value any) {
|
||||
f.attrs[key] = value
|
||||
}
|
||||
|
||||
func (f *Field) Del(key string) {
|
||||
delete(f.attrs, key)
|
||||
}
|
||||
|
||||
func (f *Field) Attrs() map[string]any {
|
||||
return f.attrs
|
||||
}
|
||||
|
||||
func (f *Field) Validate() error {
|
||||
for _, fn := range f.validators {
|
||||
if err := fn(f); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewField(name string, attrs Attrs, validators ...ValidationFunc) *Field {
|
||||
return &Field{
|
||||
name: name,
|
||||
attrs: attrs,
|
||||
validators: validators,
|
||||
}
|
||||
}
|
||||
|
||||
func FormFieldAttr[T any](f *Form, name string, attr string) (T, error) {
|
||||
field := f.Field(name)
|
||||
if field == nil {
|
||||
return *new(T), errors.WithStack(ErrFieldNotFound)
|
||||
}
|
||||
|
||||
value, err := FieldAttr[T](field, attr)
|
||||
if err != nil {
|
||||
return *new(T), errors.WithStack(err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func FieldAttr[T any](f *Field, name string) (T, error) {
|
||||
raw, ok := f.Get(name)
|
||||
if !ok {
|
||||
return *new(T), errors.WithStack(ErrMissingValue)
|
||||
}
|
||||
|
||||
value, ok := raw.(T)
|
||||
if !ok {
|
||||
return *new(T), errors.WithStack(ErrUnexpectedValue)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
131
internal/http/form/form.go
Normal file
131
internal/http/form/form.go
Normal file
@ -0,0 +1,131 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ValidationErrors map[string]error
|
||||
|
||||
type Form struct {
|
||||
fields []*Field
|
||||
errors ValidationErrors
|
||||
}
|
||||
|
||||
func (f *Form) Handle(r *http.Request) error {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := f.Parse(r.Form); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Form) HandleMultipart(r *http.Request, maxMemory int64) error {
|
||||
if err := r.ParseMultipartForm(maxMemory); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := f.ParseMultipart(r.MultipartForm); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Form) ParseMultipart(multi *multipart.Form) error {
|
||||
for _, f := range f.fields {
|
||||
name := f.Name()
|
||||
|
||||
values, exists := multi.Value[name]
|
||||
if !exists {
|
||||
f.Del("value")
|
||||
} else {
|
||||
f.Set("value", values)
|
||||
}
|
||||
|
||||
files, exists := multi.File[name]
|
||||
if !exists {
|
||||
f.Del("file")
|
||||
} else {
|
||||
f.Set("file", files)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Form) Parse(values url.Values) error {
|
||||
for _, f := range f.fields {
|
||||
name := f.Name()
|
||||
if !values.Has(name) {
|
||||
f.Del("value")
|
||||
continue
|
||||
}
|
||||
|
||||
value := values.Get(name)
|
||||
f.Set("value", value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Form) Validate() ValidationErrors {
|
||||
var errs ValidationErrors
|
||||
|
||||
for _, f := range f.fields {
|
||||
if err := f.Validate(); err != nil {
|
||||
if errs == nil {
|
||||
errs = make(ValidationErrors)
|
||||
}
|
||||
|
||||
errs[f.Name()] = err
|
||||
}
|
||||
}
|
||||
|
||||
f.errors = errs
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (f *Form) Field(name string) *Field {
|
||||
for _, field := range f.fields {
|
||||
if field.Name() == name {
|
||||
return field
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Form) Error(name string) (ValidationError, bool) {
|
||||
err, exists := f.errors[name]
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var validationErr ValidationError
|
||||
if errors.As(err, &validationErr) {
|
||||
return validationErr, true
|
||||
}
|
||||
|
||||
return NewValidationError("Unexpected error"), true
|
||||
}
|
||||
|
||||
func (f *Form) Extend(fields ...*Field) *Form {
|
||||
fields = append(f.fields, fields...)
|
||||
return New(fields...)
|
||||
}
|
||||
|
||||
func New(fields ...*Field) *Form {
|
||||
return &Form{
|
||||
fields: fields,
|
||||
errors: make(ValidationErrors),
|
||||
}
|
||||
}
|
27
internal/http/form/match_regexp.go
Normal file
27
internal/http/form/match_regexp.go
Normal file
@ -0,0 +1,27 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func MatchRegExp(pattern string, message string) func(f *Field) error {
|
||||
return func(f *Field) error {
|
||||
value, err := FieldAttr[string](f, "value")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
matches, err := regexp.MatchString(pattern, value)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if !matches {
|
||||
return NewValidationError(message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
22
internal/http/form/non_empty.go
Normal file
22
internal/http/form/non_empty.go
Normal file
@ -0,0 +1,22 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func NonEmpty(message string) func(f *Field) error {
|
||||
return func(f *Field) error {
|
||||
value, err := FieldAttr[string](f, "value")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return NewValidationError(message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
26
internal/http/form/validation.go
Normal file
26
internal/http/form/validation.go
Normal file
@ -0,0 +1,26 @@
|
||||
package form
|
||||
|
||||
type ValidationError interface {
|
||||
error
|
||||
Message() string
|
||||
}
|
||||
|
||||
type validationError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
// Error implements ValidationError.
|
||||
func (v *validationError) Error() string {
|
||||
return "validation error"
|
||||
}
|
||||
|
||||
// Message implements ValidationError.
|
||||
func (v *validationError) Message() string {
|
||||
return v.message
|
||||
}
|
||||
|
||||
var _ ValidationError = &validationError{}
|
||||
|
||||
func NewValidationError(message string) ValidationError {
|
||||
return &validationError{message: message}
|
||||
}
|
Reference in New Issue
Block a user