Compare commits

44 Commits

Author SHA1 Message Date
pedro 3175c5c23a add sse handle with std lib 2025-07-22 00:11:49 +02:00
pedro 62d3553e7a change ticker interval 2025-07-22 00:08:40 +02:00
pedro 8670442dbc update broker 2025-07-22 00:04:52 +02:00
pedro 941deaf9df add router switch and pgx changes 2025-05-29 13:06:48 +02:00
pedro 48fe13eecc router improvements 2025-05-29 13:06:38 +02:00
pedro d54fbcacf0 adjustments in render 2025-05-24 20:23:10 +02:00
pedro 2fea2e6ac5 fixes and improvements in broker and render 2025-05-23 14:31:23 +02:00
pedro ba604bb8b4 creating sse broker 2025-05-15 20:45:08 +02:00
pedro 155e7a4116 update library 2025-05-15 14:33:40 +02:00
pedro 2aa592252c add templating engine 2025-05-11 12:21:16 +02:00
pedro 827cb66b4e changes 2025-05-11 11:54:21 +02:00
pedro 4144e694c0 rename module to go-blocks 2025-05-11 02:59:35 +02:00
pedro b4e0cd69cc update dependencies 2025-05-11 02:41:24 +02:00
pedro 95f6ae80c7 remove something function 2025-05-11 02:35:33 +02:00
pedro 37efa69b96 bump v2 2025-05-11 02:33:58 +02:00
pedro 15ad9dd15f add something func 2025-05-11 02:23:39 +02:00
pedro f1fb183935 fix log level 2025-04-17 12:23:55 +02:00
pedro 049f366e2e fix mail for using env vars 2025-04-17 12:23:43 +02:00
pedro b9356ae0e1 changes in app and pgx 2025-03-27 06:01:02 +01:00
pedro 08a5e7fec6 update app.go 2025-03-26 00:16:45 +01:00
pedro e0d263e7ec minor corrections 2025-03-17 15:27:13 +01:00
pedro bbbb4a28d3 add consts for environment and logs 2025-03-17 15:24:10 +01:00
pedro 3cadf06599 updated app.go 2025-02-26 05:09:17 +01:00
pedro eeab4c25cb remove asymmetric key from logs 2025-02-19 00:59:53 +01:00
pedro e58bc93505 update asymmetric key 2025-02-19 00:42:54 +01:00
pedro 5686b58666 add FormatDateSpanish 2025-01-28 15:57:29 +01:00
pedro 4782a04fa7 new consts 2025-01-02 18:46:39 +01:00
pedro 47bc5d2f66 new consts 2024-12-30 02:20:28 +01:00
pedro bc5aece048 load .env file with param 2024-12-28 19:31:31 +01:00
pedro c5e848c941 experiments with pgtype numeric 2024-12-16 23:37:34 +01:00
pedro 8b7b7883f1 update app/app.go 2024-12-16 20:24:58 +01:00
pedro 28696c6e39 add keys 2024-12-13 01:08:01 +01:00
pedro 5412d9079f update .env-example 2024-12-10 22:03:05 +01:00
pedro 563e9ba5c0 add mailer mechanism 2024-12-10 21:51:20 +01:00
pedro 343d4893f0 update readme and add license 2024-11-25 12:29:32 +01:00
pedro 198bceb9ba switch to mysql driver, separate pgx logic, handle no-change migration 2024-11-23 17:08:03 +01:00
pedro 21a9683bf1 remove handlers package and duplicate keys 2024-11-23 12:43:10 +01:00
pedro dfe6b90f5e add sentence generation function and create README.md 2024-11-23 11:10:48 +01:00
pedro 74b1b5f9e4 rework new app 2024-11-22 12:52:53 +01:00
pedro 4b8db71df7 simplified gopher-toolbox 2024-11-22 10:44:21 +01:00
pedro 99e7fbf47a create constants for error messages and keys in handlers package 2024-11-14 23:08:59 +01:00
pedro 3e5f504d8d add form binding and mapping functionality with tests 2024-11-11 21:40:32 +01:00
pedro b2be864c5d add LoadEnvFile and Slugify functions to utils package 2024-11-10 00:40:45 +01:00
pedro 73557af314 refactor template cache: rename types and add logging 2024-11-09 15:52:34 +01:00
34 changed files with 1866 additions and 1459 deletions
+17
View File
@@ -0,0 +1,17 @@
ENV_MODE=testing / development / production
LOG_LEVEL=debug / info / warn / error
TIMEZONE=Europe/Madrid
PASETO_ASYMMETRIC_KEY=some_key
PASETO_DURATION=168h
SMTP_HOST=
SMTP_PORT=
SMTP_USER=
SMTP_PASS=
DATABASE_ONE_DRIVER_NAME=pgx / mysql / pg
DATABASE_ONE_DATA_SOURCE=datasource
DATABASE_ONE_MIGRATE=boolean
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Pedro Pérez Banda
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+46
View File
@@ -0,0 +1,46 @@
# Gopher Toolbox
It is a library that gathers the boilerplate code used in different projects.
It includes the following:
- Database controller implementations:
- [PGX Pool](github.com/jackc/pgx/v5)
- MySQL
- Utilities for converting [pgtype](github.com/jackc/pgx/v5/pgtype) to Go types.
- Random data generation for unit tests, similar to the [Faker](https://faker.readthedocs.io/en/master/)
in Python.
```go
MaleName() string
FemaleName() string
Name() string
LastName() string
Email(beforeAt string) string
Int(min, max int64) int64
Float(min, max float64) float64
Bool() bool
Chars(min, max int) string
AllChars(min, max int) string
AllCharsOrEmpty(min, max int) string
AllCharsOrNil(min, max int) *string
NumericString(length int) string
Sentence(min, max int) string
```
- Conversion of Excel files to structured types. You pass the struct type to the
function `Convert[T any](bookPath, sheetName string)`, and it will return the
data as `dataExcel []T`.
- Constants for HTTP handlers.
- Miscelaneous utilities
```go
CorrectTimezone(timeStamp time.Time) time.Time
GetBool(value string) bool
LogAndReturnError(err error, message string) error
GetBoolFromString(s string) bool
Slugify(s string) string
```
+406
View File
@@ -0,0 +1,406 @@
package goblocks
import (
"bufio"
"database/sql"
"embed"
"errors"
"fmt"
"io"
"log/slog"
"os"
"strings"
"time"
"aidanwoods.dev/go-paseto"
"github.com/alexedwards/scs/v2"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/iofs"
_ "github.com/jackc/pgx/v5/stdlib"
)
var (
logFile *os.File
logLevel string
)
type Environment string
const (
EnvironmentTesting Environment = "testing"
EnvironmentDevelopment Environment = "development"
EnvironmentProduction Environment = "production"
)
type LogLevel slog.Level
type DatabaseConfig struct {
DriverName string
DataSource string
Migrate bool
}
type Config struct {
// default "no-name-defined"
Name string
// default "v0.0.0"
Version string
// default "development"
EnvMode Environment
// default "debug"
LogLevel slog.Level
// default "UTC"
Timezone string
// default nil
Paseto *Paseto
// default map[string]DatabaseConfig{}
Databases map[string]DatabaseConfig
// default false
CreateRouter bool
// default false
CreateSSEBroker bool
// default false
CreateTemplates bool
// default false
CreateSession bool
// default false
CreateMailer bool
}
type App struct {
config Config
Router *Router
SSEBroker *SSEBroker
Templates *Render
Session *scs.SessionManager
Mailer Mailer
}
type Paseto struct {
SecretKey paseto.V4AsymmetricSecretKey
PublicKey paseto.V4AsymmetricPublicKey
Duration time.Duration
}
func NewApp(config ...Config) *App {
cfg := Config{
Name: "",
Version: "",
EnvMode: EnvironmentDevelopment,
LogLevel: slog.LevelDebug,
Timezone: "UTC",
Paseto: nil,
Databases: make(map[string]DatabaseConfig),
CreateRouter: false,
CreateSSEBroker: false,
CreateSession: false,
CreateMailer: false,
CreateTemplates: false,
}
slog.Debug("NewApp", "config", cfg)
if len(config) > 0 {
cfg = config[0]
if cfg.LogLevel == slog.LevelDebug {
cfg.LogLevel = slog.LevelDebug
}
if cfg.Timezone == "" {
cfg.Timezone = "UTC"
}
}
if cfg.Name == "" {
cfg.Name = "no-name-defined"
}
if cfg.Version == "" {
cfg.Version = "v0.0.0"
}
if cfg.EnvMode == "" && os.Getenv("ENV_MODE") != "" {
cfg.EnvMode = Environment(os.Getenv("ENV_MODE"))
}
if os.Getenv("LOG_LEVEL") != "" {
logLevel = os.Getenv("LOG_LEVEL")
switch logLevel {
case "debug":
cfg.LogLevel = slog.LevelDebug
case "info":
cfg.LogLevel = slog.LevelInfo
case "warn":
cfg.LogLevel = slog.LevelWarn
case "error":
cfg.LogLevel = slog.LevelError
default:
cfg.LogLevel = slog.LevelInfo
}
}
if cfg.Timezone == "" && os.Getenv("TIMEZONE") != "" {
cfg.Timezone = os.Getenv("TIMEZONE")
}
loc, err := time.LoadLocation(cfg.Timezone)
if err != nil {
slog.Error("error loading timezone", "error", err, "timezone", cfg.Timezone)
loc = time.UTC
}
time.Local = loc
startRotativeLogger(cfg.LogLevel)
if cfg.Paseto == nil {
var ak paseto.V4AsymmetricSecretKey
var err error
if os.Getenv("PASETO_SECRET_KEY") != "" {
slog.Debug("using paseto secret key from env")
ak, err = paseto.NewV4AsymmetricSecretKeyFromHex(os.Getenv("PASETO_SECRET_KEY"))
if err != nil {
slog.Error("error creating secret key", "error", err)
ak = paseto.NewV4AsymmetricSecretKey()
}
} else {
ak = paseto.NewV4AsymmetricSecretKey()
}
pk := ak.Public()
duration := time.Hour * 24 * 7 // 7 days by default
if os.Getenv("PASETO_DURATION") != "" {
durationStr := os.Getenv("PASETO_DURATION")
durationInt, err := time.ParseDuration(durationStr)
if err != nil {
slog.Error("error parsing PASETO_DURATION", "error", err, "duration", durationStr)
} else {
duration = durationInt
}
}
cfg.Paseto = &Paseto{
SecretKey: ak,
PublicKey: pk,
Duration: duration,
}
}
app := &App{config: cfg}
if cfg.CreateRouter {
app.Router = newRouter()
}
// Create PGX pools automatically if there are entries in Databases with driver 'pgx'
for dbName, dbConfig := range cfg.Databases {
if dbConfig.DriverName == "pgx" {
slog.Debug("creating pgx pool", "database", dbName)
app.newPGXPool(dbName)
}
}
slog.Info(
"app config",
"name", cfg.Name,
"version", cfg.Version,
"env_mode", cfg.EnvMode,
"log_level", cfg.LogLevel,
"timezone", cfg.Timezone,
"paseto_public_key", cfg.Paseto.PublicKey.ExportHex(),
"paseto_duration", cfg.Paseto.Duration.String(),
"databases", cfg.Databases,
"create_sse_broker", cfg.CreateSSEBroker,
"create_templates", cfg.CreateTemplates,
"create_session", cfg.CreateSession,
"create_mailer", cfg.CreateMailer,
)
if cfg.EnvMode != EnvironmentProduction {
slog.Debug("paseto_secret_key", "key", cfg.Paseto.SecretKey.ExportHex())
}
if cfg.CreateSSEBroker {
slog.Debug("creating sse broker")
app.SSEBroker = newSSEBroker()
}
if cfg.CreateTemplates {
slog.Debug("creating templates")
app.Templates = NewHTMLRender()
if cfg.EnvMode == EnvironmentProduction {
app.Templates.EnableCache = true
}
}
if cfg.CreateSession {
slog.Debug("creating session")
app.Session = scs.New()
}
if cfg.CreateMailer {
slog.Debug("creating mailer")
app.Mailer = newMailer()
}
return app
}
func (a *App) Name() string {
return a.config.Name
}
func (a *App) Version() string {
return a.config.Version
}
func (a *App) EnvMode() Environment {
return a.config.EnvMode
}
func (a *App) LogLevel() slog.Level {
return a.config.LogLevel
}
func (a *App) Timezone() string {
return a.config.Timezone
}
func (a *App) Paseto() *Paseto {
return a.config.Paseto
}
func (a *App) Datasource(name string) string {
config, exists := a.config.Databases[name]
if !exists {
slog.Error("database configuration not found", "name", name)
return ""
}
return config.DataSource
}
// MigrateDB migrates the database. The migrations must stored in the
// "database/migrations" directory inside cmd directory along with the main.go.
//
// cmd/main.go
//
// cmd/database/migrations/*.sql
func (a *App) Migrate(dbName string, database embed.FS) {
dbConfig, exists := a.config.Databases[dbName]
if !exists {
slog.Error("database configuration not found", "name", dbName)
return
}
if !dbConfig.Migrate {
slog.Info("migration disabled", "database", dbName)
return
}
dbConn, err := sql.Open(dbConfig.DriverName, dbConfig.DataSource)
if err != nil {
slog.Error("error opening database connection", "error", err, "database", dbName)
return
}
defer dbConn.Close()
d, err := iofs.New(database, "database/migrations")
if err != nil {
slog.Error("error creating migration source", "error", err, "database", dbName)
return
}
m, err := migrate.NewWithSourceInstance("iofs", d, dbConfig.DataSource)
if err != nil {
slog.Error("error creating migration instance", "error", err, "database", dbName)
return
}
err = m.Up()
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
slog.Error("cannot migrate", "error", err, "database", dbName)
panic(err)
}
if errors.Is(err, migrate.ErrNoChange) {
slog.Info("migration has no changes", "database", dbName)
return
}
slog.Info("migration done", "database", dbName)
}
func LoadEnvFile(envDirectory string) error {
file, err := os.Open(envDirectory)
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 newLogger(level slog.Level) {
if err := os.MkdirAll("logs", 0755); err != nil {
fmt.Println("error creating logs directory:", err)
return
}
now := time.Now().Format("2006-01-02")
f, err := os.OpenFile(fmt.Sprintf("logs/log%s.log", now), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
fmt.Println("error opening log file:", err)
return
}
mw := io.MultiWriter(os.Stdout, f)
logger := slog.New(slog.NewTextHandler(mw, &slog.HandlerOptions{
AddSource: os.Getenv("ENV_MODE") == "" || os.Getenv("ENV_MODE") == "development",
Level: level,
}))
if logFile != nil {
logFile.Close() // Cierra el archivo anterior antes de rotar
}
logFile = f
slog.SetDefault(logger)
}
func startRotativeLogger(level slog.Level) {
newLogger(level)
ticker := time.NewTicker(time.Hour * 24)
go func() {
for range ticker.C {
newLogger(level)
}
}()
}
+55
View File
@@ -0,0 +1,55 @@
package goblocks
import (
"fmt"
"log/slog"
"regexp"
"strings"
"time"
"unicode"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
)
func CorrectTimezone(timeStamp time.Time) time.Time {
loc, _ := time.LoadLocation("Europe/Madrid")
return timeStamp.In(loc)
}
func LogAndReturnError(err error, message string) error {
slog.Error(message, "error", err.Error())
return fmt.Errorf("%s: %w", message, err)
}
func GetBoolFromString(s string) bool {
if s == "S" || s == "s" {
return true
}
return false
}
func Slugify(s string) string {
s = strings.ToLower(s)
t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
s, _, _ = transform.String(t, s)
s = strings.ReplaceAll(s, " ", "-")
var b strings.Builder
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
b.WriteRune(r)
}
}
s = b.String()
re := regexp.MustCompile(`-+`)
s = re.ReplaceAllString(s, "-")
s = strings.Trim(s, "-")
return s
}
+253
View File
@@ -0,0 +1,253 @@
package goblocks
import (
"bufio"
"encoding/json"
"fmt"
"html/template"
"log/slog"
"net/http"
"strings"
"sync"
"time"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/valyala/fasthttp"
)
type Message struct {
Event string `json:"event"`
Data any `json:"data"`
}
type Client struct {
ID string
Send chan Message
Close chan struct{}
}
type SSEBroker struct {
clients map[string]*Client
mu sync.RWMutex
register chan *Client
unregister chan *Client
broadcast chan Message
done chan struct{}
}
func newSSEBroker() *SSEBroker {
b := &SSEBroker{
clients: make(map[string]*Client),
register: make(chan *Client),
unregister: make(chan *Client),
broadcast: make(chan Message),
done: make(chan struct{}),
}
go b.run()
return b
}
func (b *SSEBroker) run() {
for {
select {
case client := <-b.register:
b.mu.Lock()
b.clients[client.ID] = client
b.mu.Unlock()
slog.Info("client registered", "client_id", client.ID)
slog.Debug("clients", "clients", b.clients)
case client := <-b.unregister:
b.mu.Lock()
if _, ok := b.clients[client.ID]; ok {
delete(b.clients, client.ID)
close(client.Send)
close(client.Close)
slog.Info("client unregistered", "client_id", client.ID)
}
b.mu.Unlock()
case message := <-b.broadcast:
b.mu.RLock()
for _, client := range b.clients {
select {
case client.Send <- message:
default:
// Si el canal está lleno, se desconecta el cliente
close(client.Send)
close(client.Close)
b.mu.RUnlock()
b.mu.Lock()
delete(b.clients, client.ID)
b.mu.Unlock()
b.mu.RLock()
slog.Warn("client removed due to full channel", "client_id", client.ID)
}
}
b.mu.RUnlock()
case <-b.done:
return
}
}
}
func (b *SSEBroker) RegisterClient(clientID string) *Client {
client := &Client{
ID: clientID,
Send: make(chan Message, 10),
Close: make(chan struct{}),
}
b.register <- client
return client
}
func (b *SSEBroker) UnregisterClient(clientID string) {
b.mu.RLock()
if client, ok := b.clients[clientID]; ok {
b.mu.RUnlock()
b.unregister <- client
} else {
b.mu.RUnlock()
}
}
func (b *SSEBroker) Broadcast(event string, data ...any) {
var payload any
if len(data) > 0 {
payload = data[0]
}
b.broadcast <- Message{Event: event, Data: payload}
}
// Interface para abstraer las diferencias entre Fiber y HTTP estándar
type SSEWriter interface {
Write(data []byte) (int, error)
Flush() error
}
// Wrapper para http.ResponseWriter
type httpSSEWriter struct {
w http.ResponseWriter
}
func (h *httpSSEWriter) Write(data []byte) (int, error) {
return h.w.Write(data)
}
func (h *httpSSEWriter) Flush() error {
if flusher, ok := h.w.(http.Flusher); ok {
flusher.Flush()
}
return nil
}
// Wrapper para bufio.Writer
type fiberSSEWriter struct {
w *bufio.Writer
}
func (f *fiberSSEWriter) Write(data []byte) (int, error) {
return f.w.Write(data)
}
func (f *fiberSSEWriter) Flush() error {
return f.w.Flush()
}
func (b *SSEBroker) handleSSEConnection(clientID string, client *Client, writer SSEWriter) {
slog.Info("SSE connection established", "client_id", clientID)
defer func() {
b.UnregisterClient(clientID)
slog.Info("SSE connection closed", "client_id", clientID)
}()
fmt.Fprintf(writer, "event: client_id\n")
fmt.Fprintf(writer, "data: %s\n\n", clientID)
writer.Flush()
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for {
select {
case message := <-client.Send:
var data string
switch v := message.Data.(type) {
case string:
data = v
case template.HTML:
data = string(v)
default:
jsonData, err := json.Marshal(v)
if err != nil {
slog.Error("error marshaling message", "error", err)
continue
}
data = string(jsonData)
}
fmt.Fprintf(writer, "event: %s\n", message.Event)
scanner := bufio.NewScanner(strings.NewReader(data))
for scanner.Scan() {
fmt.Fprintf(writer, "data: %s\n", scanner.Text())
}
fmt.Fprint(writer, "\n")
if err := writer.Flush(); err != nil {
slog.Warn("flush error, closing client", "client_id", clientID)
return
}
case <-ticker.C:
fmt.Fprintf(writer, "event: ping\n")
fmt.Fprintf(writer, "data: %s\n\n", "ping")
writer.Flush()
case <-client.Close:
return
}
}
}
func (b *SSEBroker) HandleFiberCtxSSE(c *fiber.Ctx) error {
c.Set("Content-Type", "text/event-stream")
c.Set("Cache-Control", "no-cache")
c.Set("Connection", "keep-alive")
c.Set("Transfer-Encoding", "chunked")
clientID := uuid.New().String()
client := b.RegisterClient(clientID)
c.Status(fiber.StatusOK).Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) {
writer := &fiberSSEWriter{w: w}
b.handleSSEConnection(clientID, client, writer)
}))
return nil
}
func (b *SSEBroker) HandleHTTPSSE(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Transfer-Encoding", "chunked")
clientID := uuid.New().String()
client := b.RegisterClient(clientID)
writer := &httpSSEWriter{w: w}
go func() {
b.handleSSEConnection(clientID, client, writer)
}()
<-r.Context().Done()
slog.Info("client disconnected", "client_id", clientID)
}
func (b *SSEBroker) Shutdown() {
close(b.done)
}
-59
View File
@@ -1,59 +0,0 @@
package config
import (
"fmt"
"gopher-toolbox/token"
"io"
"log/slog"
"os"
"time"
)
type App struct {
DataSource string
UseCache bool
Security Security
AppInfo AppInfo
}
type AppInfo struct {
GinMode string
Version string
}
type Security struct {
Token *token.Paseto
StripeKey string
Duration time.Duration
}
func NewLogger(level slog.Level) {
now := time.Now().Format("2006-01-02")
if _, err := os.Stat("logs"); os.IsNotExist(err) {
os.Mkdir("logs", 0755)
}
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{
AddSource: true,
Level: level,
}))
slog.SetDefault(logger)
}
func LogLevel(level string) slog.Level {
switch level {
case "debug":
return slog.LevelDebug
case "info":
return slog.LevelInfo
case "warn":
return slog.LevelWarn
case "error":
return slog.LevelError
default:
return slog.LevelDebug
}
}
+48
View File
@@ -0,0 +1,48 @@
package goblocks
// TODO: review consts
const (
// Handlers keys
InvalidRequest = "invalid_request"
MalformedJSON = "malformed_json"
TokenBlacklisted = "token_blacklisted"
TokenInvalid = "token_invalid"
ValidationFailed = "validation_failed"
UntilBeforeTo = "until_before_to"
InternalError = "internal_error"
NotFound = "not_found"
Created = "created"
Updated = "updated"
Deleted = "deleted"
Enabled = "enabled"
Disabled = "disabled"
Retrieved = "retrieved"
ErrorCreating = "error_creating"
ErrorUpdating = "error_updating"
ErrorEnabling = "error_enabling"
ErrorDisabling = "error_disabling"
ErrorGetting = "error_getting"
ErrorGettingAll = "error_getting_all"
ErrorMailing = "error_mailing"
InvalidEntityID = "invalid_entity_id"
NotImplemented = "not_implemented"
NotPassValidation = "not_pass_validation"
NotEnoughBalance = "not_enough_balance"
InvalidIdentifier = "invalid_identifier"
// User keys (DB)
UserUsernameKey = "username_key"
UserEmailKey = "email_key"
UsernameAlreadyExists = "username_already_exists"
UserSessionKey = "user_session_key"
EmailAlreadyExists = "email_already_exists"
PhoneNumberKey = "phone_number_key"
PhoneAlreadyExists = "phone_already_exists"
NoRowsAffected = "no rows in result set"
// Auth
TokenPayload = "token_payload"
LoggedIn = "logged_in"
IncorrectPassword = "incorrect_password"
ErrorGeneratingToken = "error_generating_token"
)
-60
View File
@@ -1,60 +0,0 @@
package db
import (
"context"
"database/sql"
"log/slog"
"time"
_ "github.com/jackc/pgconn"
_ "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
_ "github.com/jackc/pgx/v5/stdlib"
)
func NewPostgresPool(dataSource string) *pgxpool.Pool {
dbPool, err := pgxpool.New(context.Background(), dataSource)
if err != nil {
slog.Error("error connecting to database", "error", err)
panic(err)
}
if err := dbPool.Ping(context.Background()); err != nil {
slog.Error("error pinging database, maybe incorrect datasource", "error", err)
panic(err)
}
slog.Info("connected to database")
return dbPool
}
const maxOpenDbConn = 10
const maxIdleDbConn = 5
const maxDbLifetime = time.Minute * 5
func NewMySQL(dataSource string) (*sql.DB, error) {
d, err := sql.Open("mysql", dataSource)
if err != nil {
slog.Error("error connecting to database", "error", err)
}
d.SetMaxOpenConns(maxOpenDbConn)
d.SetMaxIdleConns(maxIdleDbConn)
d.SetConnMaxLifetime(maxDbLifetime)
err = testDB(d)
if err != nil {
slog.Error("error pinging database", "error", err)
return nil, err
}
return d, nil
}
func testDB(d *sql.DB) error {
err := d.Ping()
if err != nil {
return err
}
return nil
}
-104
View File
@@ -1,104 +0,0 @@
package esfaker
import (
"math/rand"
"strings"
)
const uppercaseAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
const lowercaseAlphabet = "abcdefghijklmnopqrstuvwxyz"
const numbers = "0123456789"
const symbols = "!@#$%^&*()_+{}|:<>?~"
var maleNames = []string{
"Pedro", "Juan", "Pepe", "Francisco", "Luis", "Carlos", "Javier", "José", "Antonio", "Manuel",
}
var femaleNames = []string{
"María", "Ana", "Isabel", "Laura", "Carmen", "Rosa", "Julia", "Elena", "Sara", "Lucía",
}
var lastNames = []string{
"García", "Fernández", "González", "Rodríguez", "López", "Martínez", "Sánchez", "Pérez", "Gómez", "Martín",
}
func MaleName() string {
return maleNames[rand.Intn(len(maleNames))]
}
func FemaleName() string {
return femaleNames[rand.Intn(len(femaleNames))]
}
func Name() string {
allNames := append(maleNames, femaleNames...)
return allNames[rand.Intn(len(allNames))]
}
func LastName() string {
return lastNames[rand.Intn(len(lastNames))]
}
func Email(beforeAt string) string {
return beforeAt + "@" + Chars(5, 10) + ".local"
}
func Int(min, max int64) int64 {
return min + rand.Int63n(max-min+1)
}
func Float(min, max float64) float64 {
return min + rand.Float64()*(max-min)
}
func Bool() bool {
return rand.Intn(2) == 0
}
func Chars(min, max int) string {
var sb strings.Builder
k := len(lowercaseAlphabet)
for i := 0; i < rand.Intn(max-min+1)+min; i++ {
c := lowercaseAlphabet[rand.Intn(k)]
sb.WriteByte(c)
}
return sb.String()
}
func AllChars(min, max int) string {
allChars := uppercaseAlphabet + lowercaseAlphabet + numbers + symbols
var sb strings.Builder
k := len(allChars)
for i := 0; i < rand.Intn(max-min+1)+min; i++ {
c := allChars[rand.Intn(k)]
sb.WriteByte(c)
}
return sb.String()
}
func AllCharsOrEmpty(min, max int) string {
if Bool() {
return ""
}
return AllChars(min, max)
}
func AllCharsOrNil(min, max int) *string {
if Bool() {
return nil
}
s := AllChars(min, max)
return &s
}
func NumericString(length int) string {
var sb strings.Builder
for i := 0; i < length; i++ {
sb.WriteByte(numbers[rand.Intn(len(numbers))])
}
return sb.String()
}
Binary file not shown.
-36
View File
@@ -1,36 +0,0 @@
# excel2struct
Convierte una hoja de excel compatible con la librería [Excelize](https://github.com/qax-os/excelize) a un tipo estructurado de Go. La primera fila debe coincidir con la etiqueta XLSX, sensible a las mayúsculas.
| Id | Nombre | Apellidos | Email | Género | Balance |
|----|--------|-----------|--------------------------|--------|---------|
| 1 | Caryl | Kimbrough | ckimbrough0@fotki.com | true | 571.08 |
| 2 | Robin | Bozward | rbozward1@thetimes.co.uk | true | 2162.89 |
| 3 | Tabbie | Kaygill | tkaygill2@is.gd | false | 703.94 |
```go
type User struct {
Id int `xlsx:"Id"`
Name string `xlsx:"Nombre"`
LastName string `xlsx:"Apellidos"`
Email string `xlsx:"Email"`
Gender bool `xlsx:"Género"`
Balance float32 `xlsx:"Balance"`
}
```
```go
func main() {
data := exceltostruct.Convert[User]("Book1.xlsx", "Sheet1")
fmt.Println(data)
}
```
```bash
[{1 Caryl Kimbrough ckimbrough0@fotki.com true 571.08} {2 Robin Bozward rbozward1@thetimes.co.uk true 2162.89} {3 Tabbie Kaygill tkaygill2@is.gd false 703.94}]
```
Donde el primer parámetro es la ruta donde está ubicada la hoja de cálculo y la segunda el nombre de la hoja.
Tipos compatibles: **int**, **float32**, **bool** y **string**.
-63
View File
@@ -1,63 +0,0 @@
package exceltostruct
import (
"github.com/xuri/excelize/v2"
"reflect"
"strconv"
)
func Convert[T any](bookPath, sheetName string) (dataExcel []T) {
f, _ := excelize.OpenFile(bookPath)
rows, _ := f.GetRows(sheetName)
firstRow := map[string]int{}
for i, row := range rows[0] {
firstRow[row] = i
}
t := new(T)
dataExcel = make([]T, 0, len(rows)-1)
for _, row := range rows[1:] {
v := reflect.ValueOf(t)
if v.Kind() == reflect.Pointer {
v = v.Elem()
}
for i := 0; i < v.NumField(); i++ {
tag := v.Type().Field(i).Tag.Get("xlsx")
objType := v.Field(i).Type().String()
if j, ok := firstRow[tag]; ok {
field := v.Field(i)
if len(row) > j {
d := row[j]
elementConverted := convertType(objType, d)
field.Set(reflect.ValueOf(elementConverted))
}
}
}
dataExcel = append(dataExcel, *t)
}
return dataExcel
}
func convertType(objType string, value string) any {
switch objType {
case "int":
valueInt, _ := strconv.Atoi(value)
return valueInt
case "bool":
valueBool, _ := strconv.ParseBool(value)
return valueBool
case "float32":
valueFloat, _ := strconv.ParseFloat(value, 32)
return float32(valueFloat)
case "string":
return value
}
return value
}
+33 -51
View File
@@ -1,67 +1,49 @@
module gopher-toolbox module github.com/zepyrshut/go-blocks/v2
go 1.23.2 go 1.24
require ( require (
github.com/go-playground/locales v0.14.1 github.com/alexedwards/scs/v2 v2.8.0
github.com/go-playground/universal-translator v0.18.1 github.com/go-sql-driver/mysql v1.9.2
github.com/go-playground/validator/v10 v10.22.1 github.com/gofiber/fiber/v2 v2.52.9
github.com/golang-migrate/migrate/v4 v4.18.3
github.com/google/uuid v1.6.0 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.4
github.com/justinas/nosurf v1.1.1 github.com/stretchr/testify v1.10.0
github.com/o1egl/paseto v1.0.0 github.com/valyala/fasthttp v1.51.0
github.com/stretchr/testify v1.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.3.1 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/andybalholm/brotli v1.1.0 // 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 (
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
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/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/gin-gonic/gin v1.10.0 github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
aidanwoods.dev/go-paseto v1.5.4
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 golang.org/x/crypto v0.38.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect golang.org/x/sync v0.14.0 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect golang.org/x/sys v0.33.0 // indirect
github.com/pkg/errors v0.8.0 // indirect golang.org/x/text v0.25.0
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/richardlehane/mscfb 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/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+97 -109
View File
@@ -1,40 +1,51 @@
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= aidanwoods.dev/go-paseto v1.5.4 h1:MH+SBroZEk5Q5pjhVh4l48HIbrdWhWI3SZmA/DXhnuw=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= aidanwoods.dev/go-paseto v1.5.4/go.mod h1:Rn37AIcqrvSMu0YPw65CrlEUuoyKL6Yw6B0htrGr3EU=
github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb h1:6Z/wqhPFZ7y5ksCEV/V5MXOazLaeu/EW97CU5rz8NWk= aidanwoods.dev/go-result v0.3.1 h1:ee98hpohYUVYbI+pa6gUHTyoRerIudgjky/IPSowDXQ=
github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= aidanwoods.dev/go-result v0.3.1/go.mod h1:GKnFg8p/BKulVD3wsfULiPhpPmrTWyiTIbz8EWuUqSk=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
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/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
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=
@@ -50,96 +61,75 @@ github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUO
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
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/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
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 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 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/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/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/o1egl/paseto v1.0.0 h1:bwpvPu2au176w4IBlhbyUv/S5VPptERIA99Oap5qUd0= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/o1egl/paseto v1.0.0/go.mod h1:5HxsZPmw/3RI2pAwGo1HhOOwSdvBpcuVzO7uDkm+CLU= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
github.com/zepyrshut/esfaker v0.0.0-20241017072233-b4a5efb1f24d h1:o52tUkQBIDD6s2v2OHmXIsZQIKTEiVMPeov2SmvXJWk= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
github.com/zepyrshut/esfaker v0.0.0-20241017072233-b4a5efb1f24d/go.mod h1:HgsPkO8n/XumWNHKfMZNV9UgC9/sUghpxVFuQcPJd2o= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
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/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
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/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -147,5 +137,3 @@ 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=
-107
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.
-95
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)
}
-71
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
}
-153
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
}
-182
View File
@@ -1,182 +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
TemplateCache TemplateCache
Functions template.FuncMap
}
type OptionFunc func(*Render)
type TemplateData struct {
Data map[string]interface{}
// 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 map[string]string
// 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",
TemplateCache: TemplateCache{},
Functions: functions,
}
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 re.EnableCache {
tc = re.TemplateCache
} else {
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) == ".html" {
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 function := range re.Functions {
slog.Info("function found", "function", function)
}
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
}
+55
View File
@@ -0,0 +1,55 @@
package goblocks
import (
"bytes"
"fmt"
"net/smtp"
"os"
"text/template"
)
type Mailer struct {
smtpHost string
smtpPort string
smtpUser string
smtpPass string
}
func newMailer() Mailer {
return Mailer{
smtpHost: os.Getenv("SMTP_HOST"),
smtpPort: os.Getenv("SMTP_PORT"),
smtpUser: os.Getenv("SMTP_USER"),
smtpPass: os.Getenv("SMTP_PASS"),
}
}
func (m Mailer) SendMail(to []string, templateName string, data interface{}) error {
templateContent := getTemplate(templateName)
if templateContent == "" {
return fmt.Errorf("template %s not found", templateName)
}
tmpl, err := template.New("email").Parse(templateContent)
if err != nil {
return err
}
buf := new(bytes.Buffer)
if err := tmpl.Execute(buf, data); err != nil {
return err
}
auth := smtp.PlainAuth(m.smtpUser, m.smtpUser, m.smtpPass, m.smtpHost)
return smtp.SendMail(m.smtpHost+":"+m.smtpPort, auth, m.smtpUser, to, buf.Bytes())
}
func getTemplate(templateName string) string {
templatePath := "templates/mail/" + templateName + ".gotmpl"
content, err := os.ReadFile(templatePath)
if err != nil {
fmt.Printf("Error leyendo plantilla: %v\n", err)
return ""
}
return string(content)
}
-42
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)
}
-77
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()
}
}
+65
View File
@@ -0,0 +1,65 @@
package goblocks
import (
"database/sql"
"log/slog"
"sync"
"time"
_ "github.com/go-sql-driver/mysql"
)
const maxOpenDbConn = 10
const maxIdleDbConn = 5
const maxDbLifetime = time.Minute * 5
var (
mysqlDBs = make(map[string]*sql.DB)
mysqlMutex sync.RWMutex
)
func (a *App) NewMySQL(name string) (*sql.DB, error) {
mysqlMutex.Lock()
defer mysqlMutex.Unlock()
if db, exists := mysqlDBs[name]; exists {
return db, nil
}
d, err := sql.Open("mysql", a.Datasource(name))
if err != nil {
slog.Error("error connecting to database", "error", err, "name", name)
return nil, err
}
d.SetMaxOpenConns(maxOpenDbConn)
d.SetMaxIdleConns(maxIdleDbConn)
d.SetConnMaxLifetime(maxDbLifetime)
if err := d.Ping(); err != nil {
slog.Error("error pinging database", "error", err, "name", name)
return nil, err
}
mysqlDBs[name] = d
slog.Info("connected to database", "name", name)
return d, nil
}
func (a *App) GetMySQL(name string) (*sql.DB, bool) {
mysqlMutex.RLock()
defer mysqlMutex.RUnlock()
db, exists := mysqlDBs[name]
return db, exists
}
func (a *App) CloseMySQLDBs() {
mysqlMutex.Lock()
defer mysqlMutex.Unlock()
for name, db := range mysqlDBs {
db.Close()
delete(mysqlDBs, name)
slog.Info("closed database connection", "name", name)
}
}
-38
View File
@@ -1,38 +0,0 @@
package pgutils
import (
"log/slog"
"strconv"
"github.com/jackc/pgx/v5/pgtype"
)
func NumericToFloat64(n pgtype.Numeric) float64 {
value, err := n.Value()
if err != nil {
slog.Error("error getting numeric value", "error", err)
return 0
}
strValue, ok := value.(string)
if !ok {
slog.Error("error converting numeric value to string")
return 0
}
floatValue, err := strconv.ParseFloat(strValue, 64)
if err != nil {
slog.Error("error converting string to float", "error", err)
return 0
}
return floatValue
}
func FloatToNumeric(number float64) (value pgtype.Numeric) {
parse := strconv.FormatFloat(number, 'f', -1, 64)
if err := value.Scan(parse); err != nil {
slog.Error("error scanning float to numeric", "error", err)
}
return value
}
+163
View File
@@ -0,0 +1,163 @@
package goblocks
import (
"context"
"log/slog"
"math"
"math/big"
"strconv"
"sync"
_ "github.com/jackc/pgconn"
_ "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
_ "github.com/jackc/pgx/v5/stdlib"
)
var (
pgxPools = make(map[string]*pgxpool.Pool)
pgxMutex sync.RWMutex
)
func (a *App) newPGXPool(name string) *pgxpool.Pool {
slog.Debug("newPGXPool", "name", name, "datasource", a.Datasource(name))
pgxMutex.Lock()
defer pgxMutex.Unlock()
if pool, exists := pgxPools[name]; exists {
return pool
}
dbPool, err := pgxpool.New(context.Background(), a.Datasource(name))
if err != nil {
slog.Error("error connecting to database", "error", err, "name", name)
panic(err)
}
if err := dbPool.Ping(context.Background()); err != nil {
slog.Error("error pinging database, maybe incorrect datasource", "error", err, "name", name)
panic(err)
}
pgxPools[name] = dbPool
slog.Info("connected to database", "name", name)
return dbPool
}
func (a *App) GetPGXPool(name string) *pgxpool.Pool {
pgxMutex.RLock()
defer pgxMutex.RUnlock()
pool, exists := pgxPools[name]
if !exists {
slog.Error("database connection not found", "name", name)
return nil
}
return pool
}
func (a *App) ClosePGXPool(name string) {
pgxMutex.Lock()
defer pgxMutex.Unlock()
pool, exists := pgxPools[name]
if !exists {
slog.Error("database connection not found", "name", name)
return
}
pool.Close()
delete(pgxPools, name)
slog.Info("closed database connection", "name", name)
}
func NumericToFloat64(n pgtype.Numeric) float64 {
val, err := n.Value()
if err != nil {
slog.Error("error getting numeric value", "error", err)
return 0
}
strValue, ok := val.(string)
if !ok {
slog.Error("error converting numeric value to string")
return 0
}
floatValue, err := strconv.ParseFloat(strValue, 64)
if err != nil {
slog.Error("error converting string to float", "error", err)
return 0
}
return floatValue
}
func NumericToInt64(n pgtype.Numeric) int64 {
return n.Int.Int64() * int64(math.Pow(10, float64(n.Exp)))
}
func FloatToNumeric(number float64, precision int) (value pgtype.Numeric) {
parse := strconv.FormatFloat(number, 'f', precision, 64)
slog.Debug("parse", "parse", parse)
if err := value.Scan(parse); err != nil {
slog.Error("error scanning numeric", "error", err)
}
return value
}
func AddNumeric(a, b pgtype.Numeric) pgtype.Numeric {
minExp := min(a.Exp, b.Exp)
aInt := new(big.Int).Set(a.Int)
bInt := new(big.Int).Set(b.Int)
for a.Exp > minExp {
aInt.Mul(aInt, big.NewInt(10))
a.Exp--
}
for b.Exp > minExp {
bInt.Mul(bInt, big.NewInt(10))
b.Exp--
}
resultado := new(big.Int).Add(aInt, bInt)
return pgtype.Numeric{
Int: resultado,
Exp: minExp,
Valid: true,
}
}
func SubtractNumeric(a, b pgtype.Numeric) pgtype.Numeric {
minExp := a.Exp
if b.Exp < minExp {
minExp = b.Exp
}
aInt := new(big.Int).Set(a.Int)
bInt := new(big.Int).Set(b.Int)
for a.Exp > minExp {
aInt.Mul(aInt, big.NewInt(10))
a.Exp--
}
for b.Exp > minExp {
bInt.Mul(bInt, big.NewInt(10))
b.Exp--
}
resultado := new(big.Int).Sub(aInt, bInt)
return pgtype.Numeric{
Int: resultado,
Exp: minExp,
Valid: true,
}
}
+78
View File
@@ -0,0 +1,78 @@
package goblocks
import (
"fmt"
"log/slog"
"math/big"
"testing"
"github.com/jackc/pgx/v5/pgtype"
"github.com/stretchr/testify/assert"
)
func Test_NumericToFloat(t *testing.T) {
tests := []struct {
given pgtype.Numeric
expected float64
}{
{given: pgtype.Numeric{Int: big.NewInt(0), Exp: 0, Valid: true}, expected: 0},
{given: pgtype.Numeric{Int: big.NewInt(5), Exp: 0, Valid: true}, expected: 5},
{given: pgtype.Numeric{Int: big.NewInt(10), Exp: 0, Valid: true}, expected: 10},
{given: pgtype.Numeric{Int: big.NewInt(1000), Exp: -2, Valid: true}, expected: 10.00},
{given: pgtype.Numeric{Int: big.NewInt(1000), Exp: -3, Valid: true}, expected: 1.000},
{given: pgtype.Numeric{Int: big.NewInt(1000), Exp: -4, Valid: true}, expected: 0.1000},
{given: pgtype.Numeric{Int: big.NewInt(2555), Exp: -2, Valid: true}, expected: 25.55},
{given: pgtype.Numeric{Int: big.NewInt(-1), Exp: -2, Valid: true}, expected: -0.01},
}
for _, test := range tests {
t.Run(fmt.Sprintf("given %v, expected %v", test.given, test.expected), func(t *testing.T) {
actual := NumericToFloat64(test.given)
assert.Equal(t, test.expected, actual)
})
}
}
func Test_FloatToNumeric(t *testing.T) {
tests := []struct {
given float64
expected pgtype.Numeric
precision int
}{
{given: 0.0, expected: pgtype.Numeric{Int: big.NewInt(0), Exp: 0, Valid: true}, precision: 0},
{given: 25.50, expected: pgtype.Numeric{Int: big.NewInt(2550), Exp: -2, Valid: true}, precision: 2},
{given: 100.0, expected: pgtype.Numeric{Int: big.NewInt(1), Exp: 2, Valid: true}, precision: 0},
{given: 0.0001, expected: pgtype.Numeric{Int: big.NewInt(0), Exp: -2, Valid: true}, precision: 2},
{given: 0.0001, expected: pgtype.Numeric{Int: big.NewInt(1), Exp: -4, Valid: true}, precision: 4},
}
for _, test := range tests {
t.Run(fmt.Sprintf("given %v, expected %v", test.given, test.expected), func(t *testing.T) {
actual := FloatToNumeric(test.given, test.precision)
assert.Equal(t, test.expected, actual)
})
}
}
func Test_AddNumeric(t *testing.T) {
valueA := pgtype.Numeric{Int: big.NewInt(1), Exp: -3, Valid: true}
valueB := pgtype.Numeric{Int: big.NewInt(2), Exp: -2, Valid: true}
slog.Info("valueA", "valueA", valueA)
slog.Info("valueB", "valueB", valueB)
actual := AddNumeric(valueA, valueB)
slog.Info("actual", "actual", actual)
assert.Equal(t, pgtype.Numeric{Int: big.NewInt(21), Exp: -3, Valid: true}, actual)
}
func Test_SubtractNumeric(t *testing.T) {
valueA := pgtype.Numeric{Int: big.NewInt(1), Exp: -3, Valid: true}
valueB := pgtype.Numeric{Int: big.NewInt(2), Exp: -2, Valid: true}
actual := SubtractNumeric(valueA, valueB)
slog.Info("actual", "actual", actual)
assert.Equal(t, pgtype.Numeric{Int: big.NewInt(-19), Exp: -3, Valid: true}, actual)
}
+50
View File
@@ -0,0 +1,50 @@
package goblocks
import (
"encoding/json"
"log/slog"
"net/http"
"strings"
)
type stringWriter struct {
builder strings.Builder
header http.Header
}
func newStringWriter() *stringWriter {
return &stringWriter{
header: make(http.Header),
}
}
func (w *stringWriter) Header() http.Header {
return w.header
}
func (w *stringWriter) Write(b []byte) (int, error) {
return w.builder.Write(b)
}
func (w *stringWriter) WriteHeader(statusCode int) {}
func (a *App) JSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(v)
}
func (a *App) HTML(w http.ResponseWriter, r *http.Request, code int, layout string, page string, td *TemplateData) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(code)
err := a.Templates.Render(w, layout, page, td, r.Header.Get("HX-Request") != "true")
if err != nil {
slog.Error("error rendering", "layout", layout, "page", page, "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (a *App) RenderComponent(name string, td *TemplateData) (string, error) {
return a.Templates.RenderComponent(name, td)
}
+100
View File
@@ -0,0 +1,100 @@
package goblocks
import (
"net/http"
"os"
"path/filepath"
"slices"
"strings"
)
type Middleware func(http.Handler) http.Handler
type Router struct {
globalChain []Middleware
routeChain []Middleware
isSub bool
*http.ServeMux
}
func newRouter() *Router {
return &Router{ServeMux: http.NewServeMux()}
}
func (r *Router) Use(mw ...Middleware) {
if r.isSub {
r.routeChain = append(r.routeChain, mw...)
} else {
r.globalChain = append(r.globalChain, mw...)
}
}
func (r *Router) Group(basePath string, fn func(r *Router)) {
sub := &Router{
ServeMux: r.ServeMux,
routeChain: slices.Clone(r.routeChain),
isSub: true,
}
// Añadir middleware para manejar el basePath
sub.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if !strings.HasPrefix(req.URL.Path, basePath) {
http.NotFound(w, req)
return
}
req.URL.Path = strings.TrimPrefix(req.URL.Path, basePath)
next.ServeHTTP(w, req)
})
})
fn(sub)
}
func (r *Router) Static(urlPrefix, dir string) {
urlPrefix = strings.TrimSuffix(urlPrefix, "/")
fileServer := http.FileServer(http.Dir(dir))
fs := http.StripPrefix(urlPrefix, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
fullPath := filepath.Join(dir, req.URL.Path)
info, err := os.Stat(fullPath)
if err != nil {
http.NotFound(w, req)
return
}
if info.IsDir() {
http.NotFound(w, req)
return
}
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
fileServer.ServeHTTP(w, req)
}))
r.Handle(urlPrefix+"/", fs)
}
func (r *Router) HandleFunc(pattern string, h http.HandlerFunc) {
r.Handle(pattern, h)
}
func (r *Router) Handle(pattern string, h http.Handler) {
chain := slices.Clone(r.routeChain)
slices.Reverse(chain)
for _, mw := range chain {
h = mw(h)
}
r.ServeMux.Handle(pattern, h)
}
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
h := http.Handler(r.ServeMux)
chain := slices.Clone(r.globalChain)
slices.Reverse(chain)
for _, mw := range chain {
h = mw(h)
}
h.ServeHTTP(w, req)
}
+379
View File
@@ -0,0 +1,379 @@
package goblocks
import (
"bytes"
"errors"
"fmt"
"html/template"
"log/slog"
"maps"
"net/http"
"path/filepath"
"reflect"
"strconv"
"strings"
"time"
)
type (
templateCache map[string]*template.Template
TemplateData struct {
Data map[string]any
Pages Pages
}
RenderOptions func(*Render)
Render struct {
EnableCache bool
TemplatesPath string
Functions template.FuncMap
TemplateData TemplateData
templateCache templateCache
}
)
func defaultHTMLRender() *Render {
return &Render{
EnableCache: false,
TemplatesPath: "templates",
TemplateData: TemplateData{},
Functions: template.FuncMap{
"default": defaultIfEmpty,
"dict": Dict,
"duration": Duration,
},
templateCache: templateCache{},
}
}
func NewHTMLRender(opts ...RenderOptions) *Render {
config := defaultHTMLRender()
return config.apply(opts...)
}
func (re *Render) apply(opts ...RenderOptions) *Render {
for _, opt := range opts {
if opt != nil {
opt(re)
}
}
return re
}
func defaultIfEmpty(fallback string, value any) string {
str, ok := value.(string)
if !ok || strings.TrimSpace(str) == "" {
return fallback
}
return str
}
func cloneFuncMap(src template.FuncMap) template.FuncMap {
c := make(template.FuncMap)
maps.Copy(c, src)
return c
}
func (re *Render) Render(w http.ResponseWriter, layoutName, pageName string, td *TemplateData, useLayout bool) error {
if td == nil {
td = &TemplateData{}
}
if !useLayout {
path := filepath.Join(re.TemplatesPath, "pages", pageName)
funcs := cloneFuncMap(re.Functions)
tmpl, err := template.New(strings.TrimSuffix(pageName, ".gohtml")).Funcs(funcs).ParseFiles(path)
if err != nil {
return err
}
var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(&buf, strings.TrimSuffix(pageName, ".gohtml"), td); err != nil {
return err
}
_, err = buf.WriteTo(w)
return err
}
tmpl, err := re.loadTemplateWithLayout(layoutName, pageName)
if err != nil {
return err
}
var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(&buf, strings.TrimSuffix(layoutName, ".gohtml"), td); err != nil {
return err
}
_, err = buf.WriteTo(w)
return err
}
func (re *Render) RenderComponent(name string, td *TemplateData) (string, error) {
if td == nil {
td = &TemplateData{}
}
path := filepath.Join(re.TemplatesPath, "components", name)
files := []string{path}
matches, err := filepath.Glob(filepath.Join(re.TemplatesPath, "components", "*.gohtml"))
if err != nil {
slog.Error("error loading component files", "error", err)
return "", err
}
files = append(files, matches...)
funcs := cloneFuncMap(re.Functions)
tmpl, err := template.New(name).Funcs(funcs).ParseFiles(files...)
if err != nil {
slog.Error("error loading component files", "error", err)
return "", err
}
var buf bytes.Buffer
err = tmpl.ExecuteTemplate(&buf, strings.TrimSuffix(name, ".gohtml"), td)
if err != nil {
slog.Error("error executing component template", "error", err)
}
return buf.String(), err
}
func (re *Render) loadTemplateWithLayout(layoutName, pageName string) (*template.Template, error) {
cacheKey := layoutName + "::" + pageName
if re.EnableCache {
if tmpl, ok := re.templateCache[cacheKey]; ok {
return tmpl, nil
}
}
layoutPath := filepath.Join(re.TemplatesPath, "layouts", layoutName)
pagePath := filepath.Join(re.TemplatesPath, "pages", pageName)
files := []string{layoutPath, pagePath}
componentFiles, err := filepath.Glob(filepath.Join(re.TemplatesPath, "components", "*.gohtml"))
if err != nil {
return nil, err
}
files = append(files, componentFiles...)
funcs := cloneFuncMap(re.Functions)
tmpl, err := template.New(layoutName).Funcs(funcs).ParseFiles(files...)
if err != nil {
return nil, err
}
if re.EnableCache {
re.templateCache[cacheKey] = tmpl
slog.Debug("template cached", "key", cacheKey)
}
return tmpl, nil
}
// Pages contains pagination info.
type Pages struct {
// TotalElements indicates the total number of elements available for
// pagination.
TotalElements int
// ElementsPerPage defines the number of elements to display per page in
// pagination.
ElementsPerPage int
// ActualPage represents the current page number in pagination.
ActualPage int
}
func (p *Pages) PaginationParams(r *http.Request) {
limit := r.FormValue("limit")
page := r.FormValue("page")
if limit == "" {
if p.ElementsPerPage != 0 {
limit = strconv.Itoa(p.ElementsPerPage)
} else {
limit = "20"
}
}
if page == "" || page == "0" {
if p.ActualPage != 0 {
page = strconv.Itoa(p.ActualPage)
} else {
page = "1"
}
}
limitInt, _ := strconv.Atoi(limit)
pageInt, _ := strconv.Atoi(page)
offset := (pageInt - 1) * limitInt
currentPage := offset/limitInt + 1
p.ElementsPerPage = limitInt
p.ActualPage = currentPage
}
func (p Pages) PaginateArray(elements any) any {
itemsValue := reflect.ValueOf(elements)
if p.ActualPage < 1 {
p.ActualPage = 1
}
if p.ActualPage > p.TotalPages() {
p.ActualPage = p.TotalPages()
}
startIndex := (p.ActualPage - 1) * p.ElementsPerPage
endIndex := startIndex + p.ElementsPerPage
return itemsValue.Slice(startIndex, endIndex).Interface()
}
func (p Pages) CurrentPage() int {
return p.ActualPage
}
func (p Pages) TotalPages() int {
return (p.TotalElements + p.ElementsPerPage - 1) / p.ElementsPerPage
}
func (p Pages) IsFirst() bool {
return p.ActualPage == 1
}
func (p Pages) IsLast() bool {
return p.ActualPage == p.TotalPages()
}
func (p Pages) HasPrevious() bool {
return p.ActualPage > 1
}
func (p Pages) HasNext() bool {
return p.ActualPage < p.TotalPages()
}
func (p Pages) Previous() int {
if p.ActualPage > p.TotalPages() {
return p.TotalPages()
}
return p.ActualPage - 1
}
func (p Pages) Next() int {
if p.ActualPage < 1 {
return 1
}
return p.ActualPage + 1
}
func (p Pages) GoToPage(page int) int {
if page < 1 {
page = 1
} else if page > p.TotalPages() {
page = p.TotalPages()
}
return page
}
func (p Pages) First() int {
return p.GoToPage(1)
}
func (p Pages) Last() int {
return p.GoToPage(p.TotalPages())
}
// Page contiene la información de una página. Utilizado para la barra de
// paginación que suelen mostrarse en la parte inferior de una lista o tabla.
// Page represents a single page in pagination, including its number and active
// state. Useful for pagination bar.
type Page struct {
// Number is the numeric identifier of the page in pagination.
Number int
// Active indicates if the page is the currently selected page.
Active bool
}
func (p Page) NumberOfPage() int {
return p.Number
}
func (p Page) IsActive() bool {
return p.Active
}
// PageRange generates a slice of Page instances representing a range of pages
// to be displayed in a pagination bar.
func (p Pages) PageRange(maxPagesToShow int) []Page {
var pages []Page
totalPages := p.TotalPages()
startPage := p.ActualPage - (maxPagesToShow / 2)
endPage := p.ActualPage + (maxPagesToShow / 2)
if startPage < 1 {
startPage = 1
endPage = maxPagesToShow
}
if endPage > totalPages {
endPage = totalPages
startPage = totalPages - maxPagesToShow + 1
if startPage < 1 {
startPage = 1
}
}
for i := startPage; i <= endPage; i++ {
pages = append(pages, Page{
Number: i,
Active: i == p.ActualPage,
})
}
return pages
}
func Dict(values ...any) (map[string]any, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]any, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, errors.New("dict keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
}
func FormatDateSpanish(date time.Time) string {
months := []string{"enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"}
days := []string{"domingo", "lunes", "martes", "miércoles", "jueves", "viernes", "sábado"}
dayName := days[date.Weekday()]
day := date.Day()
month := months[date.Month()-1]
year := date.Year()
return dayName + ", " + strconv.Itoa(day) + " de " + month + " de " + strconv.Itoa(year)
}
func Duration(start, end time.Time) string {
if end.IsZero() {
end = time.Now()
}
d := end.Sub(start)
h := int(d.Hours())
m := int(d.Minutes()) % 60
return fmt.Sprintf("%d:%02d", h, m)
}
-55
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)
}
-67
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
}
-62
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),
}
}
-28
View File
@@ -1,28 +0,0 @@
package utils
import (
"fmt"
"log/slog"
"time"
)
func CorrectTimezone(timeStamp time.Time) time.Time {
loc, _ := time.LoadLocation("Europe/Madrid")
return timeStamp.In(loc)
}
func GetBool(value string) bool {
return value == "true"
}
func LogAndReturnError(err error, message string) error {
slog.Error(message, "error", err.Error())
return fmt.Errorf("%s: %w", message, err)
}
func GetBoolFromString(s string) bool {
if s == "S" || s == "s" {
return true
}
return false
}