updated README

This commit is contained in:
anthdm 2024-06-07 11:26:58 +02:00
parent f253292898
commit 0680543167
4 changed files with 361 additions and 0 deletions

View file

@ -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

View file

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

View file

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