chore: move to go folder

This commit is contained in:
Nicolas Meienberger
2025-08-20 22:15:43 +02:00
parent 83b4296cfc
commit a0be690eb9
21 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
// Package core provides the configuration loading functionality for the application.
package core
import (
"github.com/go-playground/validator/v10"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
type Config struct {
VolumeRootHost string `mapstructure:"volume_root" validate:"required"`
}
func LoadConfig() Config {
var config Config
viper.AutomaticEnv()
viper.BindEnv("volume_root", "VOLUME_ROOT")
if err := viper.Unmarshal(&config); err != nil {
log.Error().Err(err).Msg("Failed to load configuration")
panic("Failed to load configuration: " + err.Error())
}
validator := validator.New()
if err := validator.Struct(config); err != nil {
log.Error().Err(err).Msg("Configuration validation failed")
panic("Configuration validation failed: " + err.Error())
}
log.Info().Msgf("Loaded configuration: %+v", config)
return config
}

68
go/internal/core/log.go Normal file
View File

@@ -0,0 +1,68 @@
package core
import (
stdlog "log"
"os"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func init() {
console := zerolog.ConsoleWriter{
Out: os.Stderr,
TimeFormat: time.ANSIC,
}
logger := zerolog.New(console).With().Timestamp().Caller().Logger()
zerolog.SetGlobalLevel(zerolog.InfoLevel)
log.Logger = logger
stdlog.SetFlags(0)
stdlog.SetOutput(console)
}
func colorStatus(code int) string {
switch {
case code >= 200 && code < 300:
// Green background
return "\033[42m" + strconv.Itoa(code) + "\033[0m"
case code >= 300 && code < 400:
// Cyan background
return "\033[46m" + strconv.Itoa(code) + "\033[0m"
case code >= 400 && code < 500:
// Yellow background
return "\033[43m" + strconv.Itoa(code) + "\033[0m"
default:
// Red background
return "\033[41m" + strconv.Itoa(code) + "\033[0m"
}
}
// GinLogger is a middleware for Gin that logs HTTP requests
// using zerolog.
func GinLogger() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
code := c.Writer.Status()
method := c.Request.Method
path := c.Request.URL.Path
// logPath check if the path should be logged normally or with debug
switch {
case code >= 200 && code < 300:
log.Info().Str("method", method).Str("path", path).Msgf("Request status=%s", colorStatus(code))
case code >= 300 && code < 400:
log.Warn().Str("method", method).Str("path", path).Msgf("Request status=%s", colorStatus(code))
case code >= 400:
log.Error().Str("method", method).Str("path", path).Msgf("Request status=%s", colorStatus(code))
}
}
}

View File

@@ -0,0 +1,18 @@
package core
import (
"regexp"
"strings"
)
var nonAlnum = regexp.MustCompile(`[^a-z0-9_-]+`)
var hyphenRuns = regexp.MustCompile(`[-_]{2,}`)
func Slugify(input string) string {
s := strings.ToLower(strings.TrimSpace(input))
s = nonAlnum.ReplaceAllString(s, "-")
s = hyphenRuns.ReplaceAllString(s, "-")
s = strings.Trim(s, "-")
return s
}

129
go/internal/core/utils.go Normal file
View File

@@ -0,0 +1,129 @@
package core
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
"strings"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)
type FieldError struct {
Field string `json:"field"`
Tag string `json:"tag"`
Param string `json:"param,omitempty"`
Message string `json:"message"`
}
type ValidationError struct {
Errors []FieldError `json:"errors"`
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed (%d errors)", len(e.Errors))
}
// DecodeStrict decodes a JSON object from raw, rejects unknown fields, and
// validates struct tags (binding:"..."). Returns a ValidationError for
// semantic issues so callers can map it to HTTP 422.
func DecodeStrict[T any](raw json.RawMessage) (T, error) {
var out T
trimmed := bytes.TrimSpace(raw)
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
return out, &ValidationError{
Errors: []FieldError{{
Field: "",
Tag: "required",
Message: "config is required",
}},
}
}
if trimmed[0] != '{' {
return out, fmt.Errorf("config must be a JSON object")
}
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&out); err != nil {
return out, fmt.Errorf("invalid JSON: %w", err)
}
// Ensure no trailing junk after the object
if err := dec.Decode(&struct{}{}); err != io.EOF {
if err == nil {
return out, errors.New("unexpected trailing data after JSON object")
}
return out, fmt.Errorf("invalid JSON: %w", err)
}
if binding.Validator == nil {
return out, errors.New("validator not initialized")
}
if err := binding.Validator.ValidateStruct(out); err != nil {
if verrs, ok := err.(validator.ValidationErrors); ok {
return out, toValidationError[T](verrs)
}
return out, err
}
return out, nil
}
func toValidationError[T any](verrs validator.ValidationErrors) *ValidationError {
errs := make([]FieldError, 0, len(verrs))
t := reflect.TypeOf((*T)(nil)).Elem()
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
for _, fe := range verrs {
name := fe.Field()
if t.Kind() == reflect.Struct {
if sf, ok := t.FieldByName(fe.StructField()); ok {
if tag := sf.Tag.Get("json"); tag != "" && tag != "-" {
name = strings.Split(tag, ",")[0]
} else {
// fallback to lower-camel for nicer output
name = lowerCamel(name)
}
}
}
errs = append(errs, FieldError{
Field: name,
Tag: fe.Tag(),
Param: fe.Param(),
Message: defaultMsg(fe),
})
}
return &ValidationError{Errors: errs}
}
func defaultMsg(fe validator.FieldError) string {
switch fe.Tag() {
case "required":
return ""
case "min":
return fmt.Sprintf("must be at least %s", fe.Param())
case "max":
return fmt.Sprintf("must be at most %s", fe.Param())
case "oneof":
return "must be one of: " + fe.Param()
case "hostname", "ip":
return "must be a valid " + fe.Tag()
}
return fe.Error()
}
func lowerCamel(s string) string {
if s == "" {
return s
}
r := []rune(s)
r[0] = []rune(strings.ToLower(string(r[0])))[0]
return string(r)
}