simplified gopher-toolbox

This commit is contained in:
Pedro Pérez 2024-11-22 10:44:21 +01:00
parent 99e7fbf47a
commit 4b8db71df7
19 changed files with 83 additions and 1278 deletions

3
.env.example Normal file
View File

@ -0,0 +1,3 @@
DATASOURCE=
ASYMMETRICKEY=
DURATION=

View File

@ -1,83 +0,0 @@
package binding
import (
"errors"
"net/http"
"reflect"
"strconv"
)
type FormBinding struct{}
func (FormBinding) Bind(r *http.Request, obj any) error {
if r == nil {
return errors.New("request is nil")
}
if err := r.ParseForm(); err != nil {
return err
}
if r.Form == nil {
return errors.New("form is nil")
}
return mapForm(obj, r.Form)
}
func mapForm(obj any, form map[string][]string) error {
val := reflect.ValueOf(obj)
if val.Kind() != reflect.Ptr || val.IsNil() {
return errors.New("obj must be a non-nil pointer")
}
val = val.Elem()
if val.Kind() != reflect.Struct {
return errors.New("obj must be a pointer to a struct")
}
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)
formTag := fieldType.Tag.Get("form")
if formTag == "" {
formTag = fieldType.Name
}
if values, ok := form[formTag]; ok && len(values) > 0 {
value := values[0]
switch field.Kind() {
case reflect.String:
field.SetString(value)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
intValue, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
field.SetInt(intValue)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
uintValue, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return err
}
field.SetUint(uintValue)
case reflect.Float32, reflect.Float64:
floatValue, err := strconv.ParseFloat(value, 64)
if err != nil {
return err
}
field.SetFloat(floatValue)
case reflect.Bool:
boolValue, err := strconv.ParseBool(value)
if err != nil {
return err
}
field.SetBool(boolValue)
default:
return errors.New("unsupported field type")
}
}
}
return nil
}

View File

@ -1,64 +0,0 @@
package binding
import (
"github.com/stretchr/testify/require"
"testing"
)
func Test_mapForm(t *testing.T) {
var someStruct struct {
StringType string `form:"stringtype"`
IntType int `form:"inttype"`
Int8Type int8 `form:"int8type"`
Int16Type int16 `form:"int16type"`
Int32Type int32 `form:"int32type"`
Int64Type int64 `form:"int64type"`
UintType uint `form:"uinttype"`
Uint8Type uint8 `form:"uint8type"`
Uint16Type uint16 `form:"uint16type"`
Uint32Type uint32 `form:"uint32type"`
Uint64Type uint64 `form:"uint64type"`
Float32Type float32 `form:"float32type"`
Float64Type float64 `form:"float64type"`
BoolType bool `form:"booltype"`
}
formData := map[string][]string{
"stringtype": {"stringType"},
"inttype": {"-2147483647"},
"int8type": {"-127"},
"int16type": {"-32767"},
"int32type": {"-2147483647"},
"int64type": {"-9223372036854775807"},
"uinttype": {"4294967295"},
"uint8type": {"255"},
"uint16type": {"65535"},
"uint32type": {"4294967295"},
"uint64type": {"18446744073709551615"},
"float32type": {
"3.1415927",
},
"float64type": {
"3.141592653589793",
},
"booltype": {"true"},
}
err := mapForm(&someStruct, formData)
require.NoError(t, err)
require.Equal(t, "stringType", someStruct.StringType)
require.Equal(t, int(-2147483647), someStruct.IntType)
require.Equal(t, int8(-127), someStruct.Int8Type)
require.Equal(t, int16(-32767), someStruct.Int16Type)
require.Equal(t, int32(-2147483647), someStruct.Int32Type)
require.Equal(t, int64(-9223372036854775807), someStruct.Int64Type)
require.Equal(t, uint(4294967295), someStruct.UintType)
require.Equal(t, uint8(255), someStruct.Uint8Type)
require.Equal(t, uint16(65535), someStruct.Uint16Type)
require.Equal(t, uint32(4294967295), someStruct.Uint32Type)
require.Equal(t, uint64(18446744073709551615), someStruct.Uint64Type)
require.Equal(t, float32(3.1415927), someStruct.Float32Type)
require.Equal(t, float64(3.141592653589793), someStruct.Float64Type)
require.Equal(t, true, someStruct.BoolType)
t.Log(someStruct)
}

View File

@ -1,60 +1,90 @@
package config package config
import ( import (
"fmt" "bufio"
"gopher-toolbox/token"
"io"
"log/slog" "log/slog"
"os" "os"
"strings"
"time" "time"
"aidanwoods.dev/go-paseto"
) )
type App struct { type App struct {
DataSource string DataSource string
UseCache bool
// TODO: Extract Security field and use as ExtendedApp (or think a strategy for better library management)
Security Security Security Security
AppInfo AppInfo AppInfo AppInfo
} }
type AppInfo struct { type AppInfo struct {
GinMode string
Version string Version string
} }
type Security struct { type Security struct {
Token *token.Paseto AsymmetricKey paseto.V4AsymmetricSecretKey
StripeKey string PublicKey paseto.V4AsymmetricPublicKey
Duration time.Duration Duration time.Duration
} }
func NewLogger(level slog.Level) { func New(version string) *App {
now := time.Now().Format("2006-01-02") var err error
if _, err := os.Stat("logs"); os.IsNotExist(err) {
os.Mkdir("logs", 0755) err = loadEnvFile()
if err != nil {
slog.Error("error loading env file", "error", err)
panic(err)
} }
f, _ := os.OpenFile(fmt.Sprintf("logs/log%s.log", now), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
mw := io.MultiWriter(os.Stdout, f)
logger := slog.New(slog.NewTextHandler(mw, &slog.HandlerOptions{ var durationTime time.Duration
AddSource: true, var ak paseto.V4AsymmetricSecretKey
Level: level,
}))
slog.SetDefault(logger) ak, err = paseto.NewV4AsymmetricSecretKeyFromHex(os.Getenv("ASYMMETRICKEY"))
} if err != nil {
ak = paseto.NewV4AsymmetricSecretKey()
}
pk := ak.Public()
func LogLevel(level string) slog.Level { duration := os.Getenv("DURATION")
switch level { if duration != "" {
case "debug": durationTime, err = time.ParseDuration(duration)
return slog.LevelDebug if err != nil {
case "info": durationTime = time.Hour * 24 * 7
return slog.LevelInfo }
case "warn": }
return slog.LevelWarn
case "error": return &App{
return slog.LevelError DataSource: os.Getenv("DATASOURCE"),
default: Security: Security{
return slog.LevelDebug AsymmetricKey: ak,
PublicKey: pk,
Duration: durationTime,
},
AppInfo: AppInfo{
Version: version,
},
} }
} }
func loadEnvFile() error {
file, err := os.Open(".env")
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if len(line) == 0 || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
os.Setenv(key, value)
}
return scanner.Err()
}

View File

@ -12,7 +12,7 @@ import (
_ "github.com/jackc/pgx/v5/stdlib" _ "github.com/jackc/pgx/v5/stdlib"
) )
func NewPostgresPool(dataSource string) *pgxpool.Pool { func NewPGXPool(dataSource string) *pgxpool.Pool {
dbPool, err := pgxpool.New(context.Background(), dataSource) dbPool, err := pgxpool.New(context.Background(), dataSource)
if err != nil { if err != nil {
slog.Error("error connecting to database", "error", err) slog.Error("error connecting to database", "error", err)
@ -42,19 +42,10 @@ func NewMySQL(dataSource string) (*sql.DB, error) {
d.SetMaxIdleConns(maxIdleDbConn) d.SetMaxIdleConns(maxIdleDbConn)
d.SetConnMaxLifetime(maxDbLifetime) d.SetConnMaxLifetime(maxDbLifetime)
err = testDB(d) if err := d.Ping(); err != nil {
if err != nil {
slog.Error("error pinging database", "error", err) slog.Error("error pinging database", "error", err)
return nil, err return nil, err
} }
return d, nil return d, nil
} }
func testDB(d *sql.DB) error {
err := d.Ping()
if err != nil {
return err
}
return nil
}

41
go.mod
View File

@ -3,65 +3,32 @@ module gopher-toolbox
go 1.23.2 go 1.23.2
require ( require (
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.22.1
github.com/google/uuid v1.6.0
github.com/jackc/pgconn v1.14.3 github.com/jackc/pgconn v1.14.3
github.com/jackc/pgx/v5 v5.7.1 github.com/jackc/pgx/v5 v5.7.1
github.com/justinas/nosurf v1.1.1
github.com/o1egl/paseto v1.0.0
github.com/stretchr/testify v1.9.0
github.com/xuri/excelize/v2 v2.9.0 github.com/xuri/excelize/v2 v2.9.0
github.com/zepyrshut/esfaker v0.0.0-20241017072233-b4a5efb1f24d
) )
require ( require (
github.com/bytedance/sonic v1.11.6 // indirect aidanwoods.dev/go-result v0.1.0 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/stretchr/testify v1.9.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
) )
require ( require (
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect aidanwoods.dev/go-paseto v1.5.2
github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb // indirect
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-gonic/gin v1.10.0
github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/pkg/errors v0.8.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
golang.org/x/crypto v0.28.0 // indirect golang.org/x/crypto v0.28.0 // indirect
golang.org/x/net v0.30.0 // indirect golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.8.0 // indirect golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect golang.org/x/text v0.19.0
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

95
go.sum
View File

@ -1,40 +1,10 @@
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= aidanwoods.dev/go-paseto v1.5.2 h1:9aKbCQQUeHCqis9Y6WPpJpM9MhEOEI5XBmfTkFMSF/o=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= aidanwoods.dev/go-paseto v1.5.2/go.mod h1:7eEJZ98h2wFi5mavCcbKfv9h86oQwut4fLVeL/UBFnw=
github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb h1:6Z/wqhPFZ7y5ksCEV/V5MXOazLaeu/EW97CU5rz8NWk= aidanwoods.dev/go-result v0.1.0 h1:y/BMIRX6q3HwaorX1Wzrjo3WUdiYeyWbvGe18hKS3K8=
github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= aidanwoods.dev/go-result v0.1.0/go.mod h1:yridkWghM7AXSFA6wzx0IbsurIm1Lhuro3rYef8FBHM=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
@ -54,35 +24,8 @@ github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=
github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/o1egl/paseto v1.0.0 h1:bwpvPu2au176w4IBlhbyUv/S5VPptERIA99Oap5qUd0=
github.com/o1egl/paseto v1.0.0/go.mod h1:5HxsZPmw/3RI2pAwGo1HhOOwSdvBpcuVzO7uDkm+CLU=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
@ -90,39 +33,18 @@ github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/zepyrshut/esfaker v0.0.0-20241017072233-b4a5efb1f24d h1:o52tUkQBIDD6s2v2OHmXIsZQIKTEiVMPeov2SmvXJWk=
github.com/zepyrshut/esfaker v0.0.0-20241017072233-b4a5efb1f24d/go.mod h1:HgsPkO8n/XumWNHKfMZNV9UgC9/sUghpxVFuQcPJd2o=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
@ -131,21 +53,12 @@ golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -1,107 +0,0 @@
# gorender
Simple y minimalista librería para procesar plantillas utilizando la librería
estándar de Go `html/template`.
## Características
- Procesamiento de plantillas utilizando `html/template`.
- Soporte para caché de plantillas.
- Soporte para paginación de elementos como tablas o múltiples blogs.
- Posibilidad de añadir funciones personalizadas a las plantillas.
- Configuración sencilla con opciones por defecto que se pueden sobreescribir.
Inspirado en `Gin`.
## Instalación
```bash
go get github.com/zepyrshut/gorender
```
## Uso mínimo
Las plantillas deben tener la siguiente estructura, observa que las páginas a
procesar están dentro de `pages`. Los demás componentes como bases y fragmentos
pueden estar en el directorio raíz o dentro de un directorio.
Puedes cambiar el nombre del directorio `template` y `pages`. Ejemplo en la
siguiente sección.
```
template/
├── pages/
│ └── page.html
├── base.html
└── fragment.html
```
```go
import (
"github.com/zepyrshut/gorender"
)
func main() {
ren := gorender.New()
// ...
td := &gorender.TemplateData{}
ren.Template(w, r, "index.html", td)
// ...
}
```
## Personalización
> Recuerda que si habilitas el caché, no podrás ver los cambios que realices
> durante el desarrollo.
```go
func dummyFunc() string {
return "dummy"
}
func main() {
customFuncs := template.FuncMap{
"dummyFunc": dummyFunc,
}
renderOpts := &gorender.Render{
EnableCache: true,
TemplatesPath: "template/path",
PageTemplatesPath: "template/path/pages",
Functions: customFuncs,
}
ren := gorender.New(gorender.WithRenderOptions(renderOpts))
// ...
td := &gorender.TemplateData{}
ren.Template(w, r, "index.html", td)
// ...
}
```
## Agradecimientos
- [Protección CSRF justinas/nosurf](https://github.com/justinas/nosurf)
- [Valicación go-playground/validator](https://github.com/go-playground/validator)
## Descargo de responsabilidad
Esta librería fue creada para usar las plantillas en mis proyectos privados, es
posible que también solucione tu problema. Sin embargo, no ofrezco ninguna
garantía de que funcione para todos los casos de uso, tenga el máximo
rendimiento o esté libre de errores.
Si decides integrarla en tu proyecto, te recomiendo que la pruebes para
asegurarte de que cumple con tus expectativas y requisitos.
Si encuentras problemas o tienes sugerencias de mejora, puedes colocar tus
aportaciones a través de _issues_ o _pull requests_ en el repositorio. Estaré
encantado de ayudarte.

View File

@ -1,95 +0,0 @@
package gorender
import (
"strings"
spanish "github.com/go-playground/locales/es"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
esTranslations "github.com/go-playground/validator/v10/translations/es"
)
type FormData struct {
HasErrors bool
Errors map[string]string
Values map[string]string
}
func NewForm() FormData {
return FormData{
HasErrors: false,
Errors: map[string]string{},
Values: map[string]string{},
}
}
// AddError añade errores a la estructura FormData, es un mapa cuya clave es una
// cadena de carecteres. Hay que tener en cuenta que cuando se hace una
// validación, se llama a esta función cuya clave es el nombre del campo con lo
// cual si hay más de un error de validación se sobreescriben el anterior y sólo
// se muestra el último error.
func (fd *FormData) AddError(field, message string) {
fd.HasErrors = true
fd.Errors[field] = message
}
func (fd *FormData) AddValue(field, value string) {
fd.Values[field] = value
}
type ValidationError struct {
Field string
Reason string
}
func (fd *FormData) ValidateStruct(s interface{}) (map[string]string, error) {
spanishTranslator := spanish.New()
uni := ut.New(spanishTranslator, spanishTranslator)
trans, _ := uni.GetTranslator("es")
validate := validator.New()
_ = esTranslations.RegisterDefaultTranslations(validate, trans)
errors := make(map[string]string)
var validationErrors []ValidationError
err := validate.Struct(s)
if err != nil {
if _, ok := err.(*validator.InvalidValidationError); ok {
fd.AddError("form-error", "Error de validación de datos.")
return errors, err
}
for _, err := range err.(validator.ValidationErrors) {
fieldName, _ := trans.T(err.Field())
message := strings.Replace(err.Translate(trans), err.Field(), fieldName, -1)
validationErrors = append(validationErrors, ValidationError{
Field: strings.ToLower(err.Field()),
Reason: correctMessage(message),
})
}
for _, err := range validationErrors {
errors[err.Field] = err.Reason
}
if len(errors) > 0 {
fd.Errors = errors
fd.HasErrors = true
}
return errors, err
}
return errors, nil
}
func correctMessage(s string) string {
s = strings.TrimSpace(s)
runes := []rune(s)
runes[0] = []rune(strings.ToUpper(string(runes[0])))[0]
if runes[len(runes)-1] != '.' {
runes = append(runes, '.')
}
return string(runes)
}

View File

@ -1,71 +0,0 @@
package gorender
import (
"bufio"
"fmt"
"os"
"strings"
)
func or(a, b string) bool {
if a == "" && b == "" {
return false
}
return true
}
// containsErrors hace una función similar a "{{ with index ... }}" con el
// añadido de que puede pasarle más de un argumento y comprobar si alguno de
// ellos está en el mapa de errores.
//
// Ejemplo:
//
// {{ if containsErrors .FormData.Errors "name" "email" }}
// {{index .FormData.Errors "name" }}
// {{index .FormData.Errors "email" }}
// {{ end }}
func containsErrors(errors map[string]string, names ...string) bool {
for _, name := range names {
if _, ok := errors[name]; ok {
return true
}
}
return false
}
func loadTranslations(language string) map[string]string {
translations := make(map[string]string)
filePath := fmt.Sprintf("%s.translate", language)
file, err := os.Open(filePath)
if err != nil {
fmt.Println("Error opening translation file:", err)
return translations
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
parts := strings.Split(line, "=")
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
translations[key] = value
}
}
if err := scanner.Err(); err != nil {
fmt.Println("Error reading translation file:", err)
}
return translations
}
func translateKey(key string) string {
translations := loadTranslations("es_ES")
translated := translations[key]
if translated != "" {
return translated
}
return key
}

View File

@ -1,153 +0,0 @@
package gorender
import (
"net/http"
"strconv"
)
// Pages contiene la información de paginación.
type Pages struct {
// totalElements son la cantidad de elementos totales a paginar. Pueden ser
// total de filas o total de páginas de blog.
totalElements int
// showElements muestra la cantidad máxima de elementos a mostrar en una
// página.
showElements int
// currentPage es la página actual, utilizado como ayuda para mostrar la
// página activa.
currentPage int
}
// Page contiene la información de una página.
type Page struct {
// number es el número de página.
number int
// active es un dato lógico que indica si la página es la actual.
active bool
}
// NewPages crea un nuevo objeto para paginación.
func NewPages(totalElements, showElements, currentPage int) Pages {
if showElements <= 0 {
showElements = 1
}
if currentPage <= 0 {
currentPage = 1
}
p := Pages{totalElements, showElements, currentPage}
if p.currentPage > p.TotalPages() {
p.currentPage = p.TotalPages()
}
return p
}
// Limit devuelve la cantidad de elementos máximos a mostrar por página.
func (p *Pages) Limit() int {
return p.showElements
}
// TotalPages devuelve la cantidad total de páginas.
func (p *Pages) TotalPages() int {
return (p.totalElements + p.showElements - 1) / p.showElements
}
// IsFirst indica si la página actual es la primera.
func (p *Pages) IsFirst() bool {
return p.currentPage == 1
}
// IsLast indica si la página actual es la última.
func (p *Pages) IsLast() bool {
return p.currentPage == p.TotalPages()
}
// HasPrevious indica si hay una página anterior.
func (p *Pages) HasPrevious() bool {
return p.currentPage > 1
}
// HasNext indica si hay una página siguiente.
func (p *Pages) HasNext() bool {
return p.currentPage < p.TotalPages()
}
// Previous devuelve el número de la página anterior.
func (p *Pages) Previous() int {
return p.currentPage - 1
}
// Next devuelve el número de la página siguiente.
func (p *Pages) Next() int {
return p.currentPage + 1
}
func (p *Page) NumberOfPage() int {
return p.number
}
// IsActive indica si la página es la actual.
func (p *Page) IsActive() bool {
return p.active
}
// Pages devuelve un arreglo de páginas para mostrar en la paginación. El
// parametro pagesShow indica la cantidad de páginas a mostrar, asignable desde
// la plantilla.
func (p *Pages) Pages(pagesShow int) []*Page {
var pages []*Page
startPage := p.currentPage - (pagesShow / 2)
endPage := p.currentPage + (pagesShow/2 - 1)
if startPage < 1 {
startPage = 1
endPage = pagesShow
}
if endPage > p.TotalPages() {
endPage = p.TotalPages()
startPage = p.TotalPages() - pagesShow + 1
if startPage < 1 {
startPage = 1
}
}
for i := startPage; i <= endPage; i++ {
pages = append(pages, &Page{i, i == p.currentPage})
}
return pages
}
func PaginateArray[T any](items []T, currentPage, itemsPerPage int) []T {
totalItems := len(items)
startIndex := (currentPage - 1) * itemsPerPage
endIndex := startIndex + itemsPerPage
if startIndex > totalItems {
startIndex = totalItems
}
if endIndex > totalItems {
endIndex = totalItems
}
return items[startIndex:endIndex]
}
func PaginationParams(r *http.Request) (int, int, int) {
limit := r.FormValue("limit")
if limit == "" {
limit = "50"
}
page := r.FormValue("page")
if page == "" || page == "0" {
page = "1"
}
limitInt, _ := strconv.Atoi(limit)
pageInt, _ := strconv.Atoi(page)
offset := (pageInt - 1) * limitInt
actualPage := offset/limitInt + 1
return limitInt, offset, actualPage
}

View File

@ -1,185 +0,0 @@
package gorender
import (
"bytes"
"errors"
"html/template"
"io/fs"
"log/slog"
"net/http"
"path/filepath"
"github.com/justinas/nosurf"
)
type templateCache map[string]*template.Template
type Render struct {
EnableCache bool
// TemplatesPath es la ruta donde se encuentran las plantillas de la
// aplicación, pueden ser bases, fragmentos o ambos. Lo que quieras.
TemplatesPath string
// PageTemplatesPath es la ruta donde se encuentran las plantillas de las
// páginas de la aplicación. Estas son las que van a ser llamadas para
// mostrar en pantalla.
PageTemplatesPath string
Functions template.FuncMap
templateCache templateCache
}
type OptionFunc func(*Render)
type Data map[string]interface{}
type FeedbackData map[string]string
type TemplateData struct {
Data Data
// SessionData contiene los datos de la sesión del usuario.
SessionData interface{}
// FeedbackData tiene como función mostrar los mensajes habituales de
// información, advertencia, éxito y error. No va implícitamente relacionado
// con los errores de validación de formularios pero pueden ser usados para
// ello.
FeedbackData FeedbackData
// FormData es una estructura que contiene los errores de validación de los
// formularios además de los valores que se han introducido en los campos.
FormData FormData
CSRFToken string
Page Pages
}
func WithRenderOptions(opts *Render) OptionFunc {
return func(re *Render) {
re.TemplatesPath = opts.TemplatesPath
re.PageTemplatesPath = opts.PageTemplatesPath
if opts.Functions != nil {
for k, v := range opts.Functions {
re.Functions[k] = v
}
}
if opts.EnableCache {
re.EnableCache = opts.EnableCache
re.templateCache, _ = re.createTemplateCache()
}
}
}
func New(opts ...OptionFunc) *Render {
functions := template.FuncMap{
"translateKey": translateKey,
"or": or,
"containsErrors": containsErrors,
}
config := &Render{
EnableCache: false,
TemplatesPath: "templates",
PageTemplatesPath: "templates/pages",
Functions: functions,
templateCache: templateCache{},
}
return config.apply(opts...)
}
func (re *Render) apply(opts ...OptionFunc) *Render {
for _, opt := range opts {
opt(re)
}
return re
}
func addDefaultData(td *TemplateData, r *http.Request) *TemplateData {
td.CSRFToken = nosurf.Token(r)
return td
}
func (re *Render) Template(w http.ResponseWriter, r *http.Request, tmpl string, td *TemplateData) error {
var tc templateCache
var err error
if td == nil {
td = &TemplateData{}
}
if re.EnableCache {
tc = re.templateCache
} else {
slog.Info("creating template cache")
tc, err = re.createTemplateCache()
if err != nil {
slog.Error("error creating template cache:", "error", err)
return err
}
}
t, ok := tc[tmpl]
if !ok {
return errors.New("can't get template from cache")
}
buf := new(bytes.Buffer)
td = addDefaultData(td, r)
err = t.Execute(buf, td)
if err != nil {
slog.Error("error executing template:", "error", err)
return err
}
_, err = buf.WriteTo(w)
if err != nil {
slog.Error("error writing template to browser:", "error", err)
}
return nil
}
func findHTMLFiles(root string) ([]string, error) {
var files []string
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && filepath.Ext(path) == ".gohtml" {
files = append(files, path)
}
return nil
})
if err != nil {
return nil, err
}
return files, nil
}
func (re *Render) createTemplateCache() (templateCache, error) {
myCache := templateCache{}
pagesTemplates, err := findHTMLFiles(re.PageTemplatesPath)
if err != nil {
return myCache, err
}
files, err := findHTMLFiles(re.TemplatesPath)
if err != nil {
return myCache, err
}
for _, file := range pagesTemplates {
name := filepath.Base(file)
ts, err := template.New(name).Funcs(re.Functions).ParseFiles(append(files, file)...)
if err != nil {
return myCache, err
}
myCache[name] = ts
}
return myCache, nil
}

View File

@ -19,18 +19,4 @@ const (
ErrorGettingAll string = "error_getting_all" ErrorGettingAll string = "error_getting_all"
InvalidEntityID string = "invalid_entity_id" InvalidEntityID string = "invalid_entity_id"
NotImplemented string = "not_implemented" NotImplemented string = "not_implemented"
UserUsernameKey string = "user_username_key"
UserEmailKey string = "user_email_key"
UsernameAlReadyExists string = "username_already_exists"
EmailAlreadyExists string = "email_already_exists"
IncorrectPassword string = "incorrect_password"
ErrorGeneratingToken string = "error_generating_token"
LoggedIn string = "logged_in"
CategoryNameKey string = "category_name_key"
CategoryAlreadyExists string = "category_already_exists"
ItemsNameKey string = "items_name_key"
NameAlreadyExists string = "name_already_exists"
) )

View File

@ -1,42 +0,0 @@
package middleware
import (
"gopher-toolbox/token"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
func Test_authMiddleware(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
paseto := token.New()
user := token.UserPayload{
Username: "test",
// Permissions: map[string]bool{
// "view_customer": true,
// },
}
publicToken, error := paseto.Create(user)
require.NoError(t, error)
payload, error := paseto.Verify(publicToken)
require.NoError(t, error)
c.Request = httptest.NewRequest("GET", "/", nil)
c.Request.Header.Set("Authorization", "Bearer "+publicToken)
authorizationHeader := c.GetHeader("Authorization")
require.Equal(t, "Bearer "+publicToken, authorizationHeader)
authMW := AuthMiddleware(paseto)
authMW(c)
payloadFromMW, exists := c.Get("payload")
require.True(t, exists)
require.Equal(t, payload, payloadFromMW)
}

View File

@ -1,77 +0,0 @@
package middleware
import (
"context"
"gopher-toolbox/token"
"log/slog"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func AuthMiddleware(token *token.Paseto) gin.HandlerFunc {
return func(c *gin.Context) {
authorizationHeader := c.GetHeader("Authorization")
if len(authorizationHeader) == 0 {
slog.Error("authorization header is required")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "authorization_header_required"})
return
}
fields := strings.Fields(authorizationHeader)
if len(fields) != 2 || fields[0] != "Bearer" {
slog.Error("invalid authorization header")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid_authorization_header"})
return
}
accessToken := fields[1]
payload, err := token.Verify(accessToken)
if err != nil {
slog.Error("error verifying token", "error", err.Error())
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid_signature"})
return
}
c.Set("payload", payload)
c.Next()
}
}
func PermissionMiddleware(requiredPermissions ...string) gin.HandlerFunc {
return func(c *gin.Context) {
// payloadInterface, exists := c.Get("payload")
// if !exists {
// c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
// return
// }
// payload, ok := payloadInterface.(*token.Payload)
// if !ok {
// c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "invalid payload type"})
// return
// }
// for _, requiredPermission := range requiredPermissions {
// hasPermission, exists := payload.User.Permissions[requiredPermission]
// if !exists || !hasPermission {
// c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": fmt.Sprintf("Permission '%s' required", requiredPermission)})
// return
// }
// }
c.Next()
}
}
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := uuid.New().String()
ctx := context.WithValue(c.Request.Context(), "request_id", requestID)
c.Request = c.Request.WithContext(ctx)
c.Writer.Header().Set("X-Request-ID", requestID)
c.Next()
}
}

View File

@ -1,55 +0,0 @@
package testutils
import (
"bytes"
"encoding/json"
"net/http"
)
func NewHTTPRequestJSON(method, url string, body interface{}, queryParams map[string]string) (*http.Request, error) {
bodyBytes, err := json.Marshal(body)
if err != nil {
return nil, err
}
request, err := http.NewRequest(method, url, bytes.NewReader(bodyBytes))
if err != nil {
return nil, err
}
request.Header.Set("Content-Type", "application/json")
if len(queryParams) > 0 {
query := request.URL.Query()
for k, v := range queryParams {
query.Add(k, v)
}
request.URL.RawQuery = query.Encode()
}
return request, nil
}
func NewHTTPRequest(method, url string, params map[string]string) (*http.Request, error) {
request, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, err
}
if len(params) > 0 {
query := request.URL.Query()
for k, v := range params {
query.Add(k, v)
}
request.URL.RawQuery = query.Encode()
}
return request, nil
}
func Decode(input interface{}, output interface{}) error {
bytes, err := json.Marshal(input)
if err != nil {
return err
}
return json.Unmarshal(bytes, output)
}

View File

@ -1,67 +0,0 @@
package token
import (
"crypto/ed25519"
"time"
"github.com/google/uuid"
"github.com/o1egl/paseto"
)
type UserPayload struct {
Username string `json:"username"`
// TODO: Add permissions
}
type Payload struct {
UUID uuid.UUID `json:"token_uuid"`
User UserPayload `json:"user"`
IssuedAt time.Time `json:"issued_at"`
ExpiredAt time.Time `json:"expired_at"`
}
type Paseto struct {
paseto *paseto.V2
publicKey ed25519.PublicKey
privateKey ed25519.PrivateKey
}
func New() *Paseto {
publicKey, privateKey, _ := ed25519.GenerateKey(nil)
return &Paseto{
paseto: paseto.NewV2(),
publicKey: publicKey,
privateKey: privateKey,
}
}
func NewPayload(user UserPayload) *Payload {
// TODO: add documentation and advert to developers: tokenID != user.UUID
tokenID, err := uuid.NewRandom()
if err != nil {
return NewPayload(user)
}
payload := &Payload{
UUID: tokenID,
User: user,
IssuedAt: time.Now(),
ExpiredAt: time.Now().Add(time.Hour * 24 * 7),
}
return payload
}
func (m *Paseto) Create(user UserPayload) (string, error) {
return m.paseto.Sign(m.privateKey, NewPayload(user), nil)
}
func (m *Paseto) Verify(token string) (*Payload, error) {
var payload Payload
err := m.paseto.Verify(token, m.publicKey, &payload, nil)
if err != nil {
return nil, err
}
return &payload, nil
}

View File

@ -1,62 +0,0 @@
package token
import (
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/zepyrshut/esfaker"
)
func Test_New(t *testing.T) {
paseto := New()
require.NotNil(t, paseto.paseto)
require.NotNil(t, paseto.privateKey)
require.NotNil(t, paseto.publicKey)
}
func Test_NewPayload(t *testing.T) {
user := createRandomUser()
payload := NewPayload(user)
require.True(t, isValidUUID(payload.UUID))
require.Equal(t, user, payload.User)
}
func Test_CreateToken(t *testing.T) {
token := New()
user := createRandomUser()
signature, err := token.Create(user)
require.Nil(t, err)
require.NotEmpty(t, signature)
}
func Test_VerifyToken(t *testing.T) {
token := New()
user := createRandomUser()
signature, _ := token.Create(user)
payload, err := token.Verify(signature)
require.Nil(t, err)
require.Equal(t, user, payload.User)
}
func Test_VerifyToken_InvalidToken(t *testing.T) {
token := New()
_, err := token.Verify("invalid-token")
require.NotNil(t, err)
}
func isValidUUID(u uuid.UUID) bool {
_, err := uuid.Parse(u.String())
return err == nil
}
func createRandomUser() UserPayload {
return UserPayload{
Username: esfaker.Chars(5, 10),
}
}

View File

@ -1,16 +1,16 @@
package utils package utils
import ( import (
"bufio"
"fmt" "fmt"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
"log/slog" "log/slog"
"os"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"unicode" "unicode"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
) )
func CorrectTimezone(timeStamp time.Time) time.Time { func CorrectTimezone(timeStamp time.Time) time.Time {
@ -34,34 +34,10 @@ func GetBoolFromString(s string) bool {
return false return false
} }
func LoadEnvFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if len(line) == 0 || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
os.Setenv(key, value)
}
return scanner.Err()
}
func Slugify(s string) string { func Slugify(s string) string {
s = strings.ToLower(s) s = strings.ToLower(s)
t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC) t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
s, _, _ = transform.String(t, s) s, _, _ = transform.String(t, s)
s = strings.ReplaceAll(s, " ", "-") s = strings.ReplaceAll(s, " ", "-")