updated README
This commit is contained in:
parent
f253292898
commit
0680543167
4 changed files with 361 additions and 0 deletions
|
@ -12,6 +12,13 @@ After installation you can create a new project by running:
|
|||
gothkit [myprojectname]
|
||||
```
|
||||
|
||||
CD into your project and start the development server
|
||||
```
|
||||
cd [myprojectname]
|
||||
|
||||
make dev
|
||||
```
|
||||
|
||||
# Getting started
|
||||
|
||||
## Migrations
|
||||
|
|
|
@ -47,6 +47,9 @@ db-status:
|
|||
db-reset:
|
||||
@GOOSE_DRIVER=$(DB_DRIVER) GOOSE_DBSTRING=$(DB_NAME) goose -dir=$(MIGRATION_DIR) reset
|
||||
|
||||
db-down:
|
||||
@GOOSE_DRIVER=$(DB_DRIVER) GOOSE_DBSTRING=$(DB_NAME) goose -dir=$(MIGRATION_DIR) down
|
||||
|
||||
db-up:
|
||||
@GOOSE_DRIVER=$(DB_DRIVER) GOOSE_DBSTRING=$(DB_NAME) goose -dir=$(MIGRATION_DIR) up
|
||||
|
||||
|
|
163
validate/rules.go
Normal file
163
validate/rules.go
Normal file
|
@ -0,0 +1,163 @@
|
|||
package validate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type Numeric interface {
|
||||
int | float64
|
||||
}
|
||||
|
||||
func In[T any](values []T) RuleSet {
|
||||
return RuleSet{
|
||||
Name: "in",
|
||||
RuleValue: values,
|
||||
ValidateFunc: func(set RuleSet) bool {
|
||||
for _, value := range values {
|
||||
v := set.FieldValue.(T)
|
||||
if reflect.DeepEqual(v, value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
MessageFunc: func(set RuleSet) string {
|
||||
return fmt.Sprintf("should be in %v", values)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Required() RuleSet {
|
||||
return RuleSet{
|
||||
Name: "required",
|
||||
MessageFunc: func(set RuleSet) string {
|
||||
return "is a required field"
|
||||
},
|
||||
ValidateFunc: func(rule RuleSet) bool {
|
||||
str, ok := rule.FieldValue.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return len(str) > 0
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Url() RuleSet {
|
||||
return RuleSet{
|
||||
Name: "url",
|
||||
MessageFunc: func(set RuleSet) string {
|
||||
return "is not a valid url"
|
||||
},
|
||||
ValidateFunc: func(set RuleSet) bool {
|
||||
u, ok := set.FieldValue.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return urlRegex.MatchString(u)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Email() RuleSet {
|
||||
return RuleSet{
|
||||
Name: "email",
|
||||
MessageFunc: func(set RuleSet) string {
|
||||
return "is not a valid email address"
|
||||
},
|
||||
ValidateFunc: func(set RuleSet) bool {
|
||||
email, ok := set.FieldValue.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return emailRegex.MatchString(email)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func LTE[T Numeric](n T) RuleSet {
|
||||
return RuleSet{
|
||||
Name: "lte",
|
||||
RuleValue: n,
|
||||
ValidateFunc: func(set RuleSet) bool {
|
||||
return set.FieldValue.(T) <= n
|
||||
},
|
||||
MessageFunc: func(set RuleSet) string {
|
||||
return fmt.Sprintf("should be lesser or equal than %v", n)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func GTE[T Numeric](n T) RuleSet {
|
||||
return RuleSet{
|
||||
Name: "gte",
|
||||
RuleValue: n,
|
||||
ValidateFunc: func(set RuleSet) bool {
|
||||
return set.FieldValue.(T) >= n
|
||||
},
|
||||
MessageFunc: func(set RuleSet) string {
|
||||
return fmt.Sprintf("should be greater or equal than %v", n)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func LT[T Numeric](n T) RuleSet {
|
||||
return RuleSet{
|
||||
Name: "lt",
|
||||
RuleValue: n,
|
||||
ValidateFunc: func(set RuleSet) bool {
|
||||
return set.FieldValue.(T) < n
|
||||
},
|
||||
MessageFunc: func(set RuleSet) string {
|
||||
return fmt.Sprintf("should be lesser than %v", n)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func GT[T Numeric](n T) RuleSet {
|
||||
return RuleSet{
|
||||
Name: "gt",
|
||||
RuleValue: n,
|
||||
ValidateFunc: func(set RuleSet) bool {
|
||||
return set.FieldValue.(T) > n
|
||||
},
|
||||
MessageFunc: func(set RuleSet) string {
|
||||
return fmt.Sprintf("should be greater than %v", n)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Max(n int) RuleSet {
|
||||
return RuleSet{
|
||||
Name: "max",
|
||||
RuleValue: n,
|
||||
ValidateFunc: func(set RuleSet) bool {
|
||||
str, ok := set.FieldValue.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return len(str) <= n
|
||||
},
|
||||
MessageFunc: func(set RuleSet) string {
|
||||
return fmt.Sprintf("should be maximum %d characters long", n)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Min(n int) RuleSet {
|
||||
return RuleSet{
|
||||
Name: "min",
|
||||
RuleValue: n,
|
||||
ValidateFunc: func(set RuleSet) bool {
|
||||
str, ok := set.FieldValue.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return len(str) >= n
|
||||
},
|
||||
MessageFunc: func(set RuleSet) string {
|
||||
return fmt.Sprintf("should be at least %d characters long", n)
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1 +1,189 @@
|
|||
package validate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
emailRegex = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
|
||||
urlRegex = regexp.MustCompile(`^(http(s)?://)?([\da-z\.-]+)\.([a-z\.]{2,6})([/\w \.-]*)*/?$`)
|
||||
)
|
||||
|
||||
type RuleFunc func() RuleSet
|
||||
|
||||
type RuleSet struct {
|
||||
Name string
|
||||
RuleValue any
|
||||
FieldValue any
|
||||
FieldName any
|
||||
ErrorMessage string
|
||||
MessageFunc func(RuleSet) string
|
||||
ValidateFunc func(RuleSet) bool
|
||||
}
|
||||
|
||||
func (set RuleSet) Message(msg string) RuleSet {
|
||||
set.ErrorMessage = msg
|
||||
return set
|
||||
}
|
||||
|
||||
type Errors map[string][]string
|
||||
|
||||
func (e Errors) Any() bool {
|
||||
return len(e) > 0
|
||||
}
|
||||
|
||||
func (e Errors) Add(name string, msg string) {
|
||||
if _, ok := e[name]; !ok {
|
||||
e[name] = []string{}
|
||||
}
|
||||
e[name] = append(e[name], msg)
|
||||
}
|
||||
|
||||
func (e Errors) Get(name string) []string {
|
||||
return e[name]
|
||||
}
|
||||
|
||||
func (e Errors) Has(name string) bool {
|
||||
return len(e[name]) > 0
|
||||
}
|
||||
|
||||
type Schema map[string][]RuleSet
|
||||
|
||||
func (schema Schema) Merge(other Schema) Schema {
|
||||
newSchema := Schema{}
|
||||
maps.Copy(newSchema, schema)
|
||||
maps.Copy(newSchema, other)
|
||||
return newSchema
|
||||
}
|
||||
|
||||
func Rules(rules ...RuleSet) []RuleSet {
|
||||
ruleSets := make([]RuleSet, len(rules))
|
||||
for i := 0; i < len(ruleSets); i++ {
|
||||
ruleSets[i] = rules[i]
|
||||
}
|
||||
return ruleSets
|
||||
}
|
||||
|
||||
// Validate validates data based on the given Schema.
|
||||
func Validate(data any, fields Schema) (Errors, bool) {
|
||||
errors := Errors{}
|
||||
return validate(data, fields, errors)
|
||||
}
|
||||
|
||||
// Request parses an http.Request into data and validates it based
|
||||
// on the given schema.
|
||||
func Request(r *http.Request, data any, schema Schema) (Errors, bool) {
|
||||
errors := Errors{}
|
||||
if err := parseRequest(r, data); err != nil {
|
||||
errors["_error"] = []string{err.Error()}
|
||||
}
|
||||
return validate(data, schema, errors)
|
||||
}
|
||||
|
||||
func validate(data any, schema Schema, errors Errors) (Errors, bool) {
|
||||
ok := true
|
||||
for fieldName, ruleSets := range schema {
|
||||
// reflect panics on un-exported variables.
|
||||
if !unicode.IsUpper(rune(fieldName[0])) {
|
||||
errors[fieldName] = []string{
|
||||
"cant marshal unexported field",
|
||||
}
|
||||
return errors, false
|
||||
}
|
||||
fieldValue := getFieldValueByName(data, fieldName)
|
||||
for _, set := range ruleSets {
|
||||
set.FieldValue = fieldValue
|
||||
set.FieldName = fieldName
|
||||
fieldName = string(unicode.ToLower([]rune(fieldName)[0])) + fieldName[1:]
|
||||
if !set.ValidateFunc(set) {
|
||||
ok = false
|
||||
msg := set.MessageFunc(set)
|
||||
if len(set.ErrorMessage) > 0 {
|
||||
msg = set.ErrorMessage
|
||||
}
|
||||
if _, ok := errors[fieldName]; !ok {
|
||||
errors[fieldName] = []string{}
|
||||
}
|
||||
errors[fieldName] = append(errors[fieldName], msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors, ok
|
||||
}
|
||||
|
||||
func getFieldValueByName(v any, name string) any {
|
||||
val := reflect.ValueOf(v)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
if val.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
fieldVal := val.FieldByName(name)
|
||||
if !fieldVal.IsValid() {
|
||||
return nil
|
||||
}
|
||||
return fieldVal.Interface()
|
||||
}
|
||||
|
||||
func parseRequest(r *http.Request, v any) error {
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType == "application/x-www-form-urlencoded" {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return fmt.Errorf("failed to parse form: %v", err)
|
||||
}
|
||||
val := reflect.ValueOf(v).Elem()
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Type().Field(i)
|
||||
formTag := field.Tag.Get("form")
|
||||
formValue := r.FormValue(formTag)
|
||||
|
||||
if formValue == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldVal := val.Field(i)
|
||||
switch fieldVal.Kind() {
|
||||
case reflect.Bool:
|
||||
// There are cases where frontend libraries use "on" as the bool value
|
||||
// think about toggles. Hence, let's try this first.
|
||||
if formValue == "on" {
|
||||
fieldVal.SetBool(true)
|
||||
} else if formValue == "off" {
|
||||
fieldVal.SetBool(false)
|
||||
return nil
|
||||
} else {
|
||||
boolVal, err := strconv.ParseBool(formValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse bool: %v", err)
|
||||
}
|
||||
fieldVal.SetBool(boolVal)
|
||||
}
|
||||
case reflect.String:
|
||||
fieldVal.SetString(formValue)
|
||||
case reflect.Int:
|
||||
intVal, err := strconv.Atoi(formValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse int: %v", err)
|
||||
}
|
||||
fieldVal.SetInt(int64(intVal))
|
||||
case reflect.Float64:
|
||||
floatVal, err := strconv.ParseFloat(formValue, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse float: %v", err)
|
||||
}
|
||||
fieldVal.SetFloat(floatVal)
|
||||
default:
|
||||
return fmt.Errorf("unsupported kind %s", fieldVal.Kind())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue