From 74209da9580c7ae63898664437dc7d021010d29a Mon Sep 17 00:00:00 2001 From: Bobby <30593201+luciferreeves@users.noreply.github.com> Date: Thu, 18 Dec 2025 16:29:07 +0530 Subject: add config and makefile --- .gitignore | 12 +++++- Makefile | 39 ++++++++++++++++++ config/config.go | 47 +++++++++++++++++++++ config/types.go | 47 +++++++++++++++++++++ go.mod | 5 +++ go.sum | 2 + utils/env/functions.go | 97 +++++++++++++++++++++++++++++++++++++++++++ utils/env/parsers.go | 28 +++++++++++++ utils/env/setters.go | 107 ++++++++++++++++++++++++++++++++++++++++++++++++ utils/env/validators.go | 15 +++++++ 10 files changed, 397 insertions(+), 2 deletions(-) create mode 100644 Makefile create mode 100644 config/config.go create mode 100644 config/types.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 utils/env/functions.go create mode 100644 utils/env/parsers.go create mode 100644 utils/env/setters.go create mode 100644 utils/env/validators.go diff --git a/.gitignore b/.gitignore index aaadf73..a9c5aff 100644 --- a/.gitignore +++ b/.gitignore @@ -28,5 +28,13 @@ go.work.sum .env # Editor/IDE -# .idea/ -# .vscode/ +.idea/ +.vscode/ + +# OS files +.DS_Store +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e12df4f --- /dev/null +++ b/Makefile @@ -0,0 +1,39 @@ +BINARY_NAME=lain +BUILD_PATH=bin/$(BINARY_NAME) +MAIN_PATH=$(BINARY_NAME)/main.go + +.PHONY: setup clean tidy build run dev all + +setup: + @echo "Setting up environment..." + @go mod download + @go mod tidy + @echo "Environment setup complete." + +clean: + @echo "Cleaning up..." + @rm -rf bin + @echo "Cleanup complete." + +tidy: + @echo "Tidying modules..." + @go mod tidy + @echo "Modules tidied." + +build: + @echo "Building..." + @go build -o $(BUILD_PATH) $(MAIN_PATH) || true + @echo "Build complete." + +run: + @if [ ! -f $(BUILD_PATH) ]; then echo "Binary not found. Building binary..."; $(MAKE) -s build; fi + @echo "Running..." + @$(BUILD_PATH) || true + +dev: + @echo "Running in development mode..." + @go run $(MAIN_PATH) || true + +all: setup clean build run + +.SILENT: \ No newline at end of file diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..a3835c5 --- /dev/null +++ b/config/config.go @@ -0,0 +1,47 @@ +package config + +import ( + "lain/utils/env" + "log" + + "github.com/joho/godotenv" +) + +var ( + Server ServerConfig + MailServer MailServerConfig + Database DatabaseConfig + MinIO MinIOConfig + AIServer AIServerConfig + Session SessionConfig +) + +func init() { + if err := godotenv.Load(); err != nil { + log.Println("No .env file found, using environment variables") + } + + if err := env.Parse(&Server); err != nil { + log.Fatalf("Failed to parse ServerConfig: %v", err) + } + + if err := env.Parse(&MailServer); err != nil { + log.Fatalf("Failed to parse MailServerConfig: %v", err) + } + + if err := env.Parse(&Database); err != nil { + log.Fatalf("Failed to parse DatabaseConfig: %v", err) + } + + if err := env.Parse(&MinIO); err != nil { + log.Fatalf("Failed to parse MinIOConfig: %v", err) + } + + if err := env.Parse(&AIServer); err != nil { + log.Fatalf("Failed to parse AIServerConfig: %v", err) + } + + if err := env.Parse(&Session); err != nil { + log.Fatalf("Failed to parse SessionConfig: %v", err) + } +} diff --git a/config/types.go b/config/types.go new file mode 100644 index 0000000..a8bcf01 --- /dev/null +++ b/config/types.go @@ -0,0 +1,47 @@ +package config + +import "time" + +type ServerConfig struct { + Host string `env:"SERVER_HOST" default:"localhost"` + Port int `env:"SERVER_PORT" default:"8080"` + AppSecret string `env:"APP_SECRET" default:"mysecret"` + AllowedDomains []string `env:"ALLOWED_DOMAINS" default:"localhost"` +} + +type MailServerConfig struct { + IMAPHost string `env:"IMAP_HOST" default:""` + IMAPPort int `env:"IMAP_PORT" default:"993"` + IMAPTLS bool `env:"IMAP_TLS" default:"true"` + SMTPHost string `env:"SMTP_HOST" default:""` + SMTPPort int `env:"SMTP_PORT" default:"587"` + SMTPTLS bool `env:"SMTP_TLS" default:"true"` +} + +type DatabaseConfig struct { + Host string `env:"DB_HOST" default:"localhost"` + Port int `env:"DB_PORT" default:"5432"` + User string `env:"DB_USER" default:"postgres"` + Pass string `env:"DB_PASS" default:""` + Name string `env:"DB_NAME" default:"lain"` + SSLMode string `env:"DB_SSLMODE" default:"disable"` +} + +type MinIOConfig struct { + Endpoint string `env:"MINIO_ENDPOINT" default:"localhost:9000"` + AccessKey string `env:"MINIO_ACCESS_KEY" default:""` + SecretKey string `env:"MINIO_SECRET_KEY" default:""` + BucketName string `env:"MINIO_BUCKET_NAME" default:"lain"` + UseSSL bool `env:"MINIO_USE_SSL" default:"false"` +} + +type AIServerConfig struct { + URL string `env:"AI_SERVER_URL" default:""` + AuthKey string `env:"AI_SERVER_AUTH_KEY" default:""` +} + +type SessionConfig struct { + CookieName string `env:"SESSION_COOKIE_NAME" default:"lain_session"` + Timeout time.Duration `env:"SESSION_TIMEOUT" default:"24h"` + SecureCookie bool `env:"SESSION_SECURE_COOKIE" default:"false"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..09a12f7 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module lain + +go 1.25.5 + +require github.com/joho/godotenv v1.5.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d61b19e --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/utils/env/functions.go b/utils/env/functions.go new file mode 100644 index 0000000..de03793 --- /dev/null +++ b/utils/env/functions.go @@ -0,0 +1,97 @@ +package env + +import ( + "os" + "reflect" + "strconv" + "strings" + "time" +) + +func getEnv(key, defaultVal string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultVal +} + +func getEnvBool(key string, defaultVal bool) bool { + if value := os.Getenv(key); value != "" { + if parsed, err := strconv.ParseBool(value); err == nil { + return parsed + } + } + return defaultVal +} + +func getEnvDuration(key string, defaultVal time.Duration) time.Duration { + if value := os.Getenv(key); value != "" { + if parsed, err := time.ParseDuration(value); err == nil { + return parsed + } + } + return defaultVal +} + +func getEnvInt(key string, defaultVal int64) int64 { + if value := os.Getenv(key); value != "" { + if parsed, err := strconv.ParseInt(value, 10, 64); err == nil { + return parsed + } + } + return defaultVal +} + +func getEnvFloat(key string, defaultVal float64) float64 { + if value := os.Getenv(key); value != "" { + if parsed, err := strconv.ParseFloat(value, 64); err == nil { + return parsed + } + } + return defaultVal +} + +func getEnvStringSlice(key string, defaultVal []string) []string { + if value := os.Getenv(key); value != "" { + parts := strings.Split(value, ",") + result := make([]string, 0, len(parts)) + for _, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed != "" { + result = append(result, trimmed) + } + } + return result + } + return defaultVal +} + +func Defaults[T any](config *T) *T { + v := reflect.ValueOf(config) + if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { + return config + } + + elem := v.Elem() + t := elem.Type() + newStruct := reflect.New(t) + newElem := newStruct.Elem() + + for i := range elem.NumField() { + field := newElem.Field(i) + fieldType := t.Field(i) + + if !field.CanSet() { + continue + } + + defaultVal := fieldType.Tag.Get("default") + if defaultVal == "" { + continue + } + + setFieldDefault(field, defaultVal) + } + + return newStruct.Interface().(*T) +} diff --git a/utils/env/parsers.go b/utils/env/parsers.go new file mode 100644 index 0000000..c1fdb53 --- /dev/null +++ b/utils/env/parsers.go @@ -0,0 +1,28 @@ +package env + +func Parse(config any) error { + elem, t, err := validateConfigInput(config) + if err != nil { + return err + } + + for i := range elem.NumField() { + field := elem.Field(i) + fieldType := t.Field(i) + + if !field.CanSet() { + continue + } + + envKey := fieldType.Tag.Get("env") + defaultVal := fieldType.Tag.Get("default") + + if envKey == "" { + continue + } + + setFieldFromEnv(field, envKey, defaultVal) + } + + return nil +} diff --git a/utils/env/setters.go b/utils/env/setters.go new file mode 100644 index 0000000..8b53a1d --- /dev/null +++ b/utils/env/setters.go @@ -0,0 +1,107 @@ +package env + +import ( + "os" + "reflect" + "strconv" + "strings" + "time" +) + +func setFieldFromEnv(field reflect.Value, envKey, defaultVal string) { + switch field.Kind() { + case reflect.String: + field.SetString(getEnv(envKey, defaultVal)) + case reflect.Bool: + defaultBool, _ := strconv.ParseBool(defaultVal) + field.SetBool(getEnvBool(envKey, defaultBool)) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + defaultInt, _ := strconv.ParseInt(defaultVal, 10, 64) + field.SetInt(getEnvInt(envKey, defaultInt)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + defaultUint, _ := strconv.ParseUint(defaultVal, 10, 64) + setUintField(field, envKey, defaultUint) + case reflect.Float32, reflect.Float64: + defaultFloat, _ := strconv.ParseFloat(defaultVal, 64) + field.SetFloat(getEnvFloat(envKey, defaultFloat)) + case reflect.Slice: + setSliceField(field, envKey, defaultVal) + default: + setDurationField(field, envKey, defaultVal) + } +} + +func setUintField(field reflect.Value, envKey string, defaultVal uint64) { + if value := os.Getenv(envKey); value != "" { + if parsed, err := strconv.ParseUint(value, 10, 64); err == nil { + field.SetUint(parsed) + return + } + } + field.SetUint(defaultVal) +} + +func setDurationField(field reflect.Value, envKey, defaultVal string) { + if field.Type() == reflect.TypeFor[time.Duration]() { + defaultDuration, _ := time.ParseDuration(defaultVal) + field.Set(reflect.ValueOf(getEnvDuration(envKey, defaultDuration))) + } +} + +func setSliceField(field reflect.Value, envKey, defaultVal string) { + if field.Type().Elem().Kind() == reflect.String { + var defaultSlice []string + if defaultVal != "" { + parts := strings.SplitSeq(defaultVal, ",") + for part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed != "" { + defaultSlice = append(defaultSlice, trimmed) + } + } + } + result := getEnvStringSlice(envKey, defaultSlice) + field.Set(reflect.ValueOf(result)) + } +} + +func setFieldDefault(field reflect.Value, defaultVal string) { + switch field.Kind() { + case reflect.String: + field.SetString(defaultVal) + case reflect.Bool: + if defaultBool, err := strconv.ParseBool(defaultVal); err == nil { + field.SetBool(defaultBool) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if defaultInt, err := strconv.ParseInt(defaultVal, 10, 64); err == nil { + field.SetInt(defaultInt) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if defaultUint, err := strconv.ParseUint(defaultVal, 10, 64); err == nil { + field.SetUint(defaultUint) + } + case reflect.Float32, reflect.Float64: + if defaultFloat, err := strconv.ParseFloat(defaultVal, 64); err == nil { + field.SetFloat(defaultFloat) + } + case reflect.Slice: + if field.Type().Elem().Kind() == reflect.String && defaultVal != "" { + parts := strings.Split(defaultVal, ",") + result := make([]string, 0, len(parts)) + for _, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed != "" { + result = append(result, trimmed) + } + } + field.Set(reflect.ValueOf(result)) + } + default: + if field.Type() == reflect.TypeFor[time.Duration]() { + if defaultDuration, err := time.ParseDuration(defaultVal); err == nil { + field.Set(reflect.ValueOf(defaultDuration)) + } + } + } +} diff --git a/utils/env/validators.go b/utils/env/validators.go new file mode 100644 index 0000000..fa9b17f --- /dev/null +++ b/utils/env/validators.go @@ -0,0 +1,15 @@ +package env + +import ( + "fmt" + "reflect" +) + +func validateConfigInput(config any) (reflect.Value, reflect.Type, error) { + v := reflect.ValueOf(config) + if v.Kind() != reflect.Pointer || v.Elem().Kind() != reflect.Struct { + return reflect.Value{}, nil, fmt.Errorf("config must be a pointer to struct") + } + elem := v.Elem() + return elem, elem.Type(), nil +} -- cgit v1.2.3