mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
chore: move to go folder
This commit is contained in:
34
go/internal/core/config.go
Normal file
34
go/internal/core/config.go
Normal 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
68
go/internal/core/log.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
18
go/internal/core/text-utils.go
Normal file
18
go/internal/core/text-utils.go
Normal 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
129
go/internal/core/utils.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user