feat: initial commit

This commit is contained in:
2025-02-21 18:42:56 +01:00
commit ee4a65b345
81 changed files with 3441 additions and 0 deletions

View 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")
)

View 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
View 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),
}
}

View 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
}
}

View 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
}
}

View 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}
}