0.0.0.0 Initial commit, basic core functionality

This commit is contained in:
2025-01-08 15:41:41 -05:00
commit 049927d242
8 changed files with 352 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.idea
data
dev
logs

28
LICENSE Normal file
View File

@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2024, Lixen Wraith
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

70
README.md Normal file
View File

@ -0,0 +1,70 @@
# Config
A simple configuration management package for Go applications that supports TOML files and CLI arguments.
## Features
- TOML configuration with [tinytoml](https://github.com/LixenWraith/tinytoml)
- Command line argument overrides with dot notation
- Default config handling
- Atomic file operations
- No external dependencies beyond tinytoml
## Installation
```bash
go get github.com/example/config
```
## Usage
```go
type AppConfig struct {
Server struct {
Host string `toml:"host"`
Port int `toml:"port"`
} `toml:"server"`
}
func main() {
cfg := AppConfig{
Host: "localhost",
Port: 8080,
} // default config
exists, err := config.LoadConfig("config.toml", &cfg, os.Args[1:])
if err != nil {
log.Fatal(err)
}
if !exists {
if err := config.SaveConfig("config.toml", &cfg); err != nil {
log.Fatal(err)
}
}
}
```
### CLI Arguments
Override config values using dot notation:
```bash
./app --server.host localhost --server.port 8080
```
## API
### `LoadConfig(path string, config interface{}, args []string) (bool, error)`
Loads configuration from TOML file and CLI args. Returns true if config file exists.
### `SaveConfig(path string, config interface{}) error`
Saves configuration to TOML file atomically.
## Limitations
- Supports only basic Go types and structures supported by tinytoml
- CLI arguments must use `--key value` format
- Indirect dependency on [mapstructure](https://github.com/mitchellh/mapstructure) through tinytoml
## License
BSD-3

149
config.go Normal file
View File

@ -0,0 +1,149 @@
package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"github.com/LixenWraith/tinytoml"
)
type cliArg struct {
key string
value string
}
func Load(path string, config interface{}, args []string) (bool, error) {
if config == nil {
return false, fmt.Errorf("config cannot be nil")
}
configExists := false
if stat, err := os.Stat(path); err == nil && !stat.IsDir() {
configExists = true
data, err := os.ReadFile(path)
if err != nil {
return false, fmt.Errorf("failed to read config file: %w", err)
}
if err := tinytoml.Unmarshal(data, config); err != nil {
return false, fmt.Errorf("failed to parse config file: %w", err)
}
}
if len(args) > 0 {
overrides, err := parseArgs(args)
if err != nil {
return configExists, fmt.Errorf("failed to parse CLI args: %w", err)
}
if err := mergeConfig(config, overrides); err != nil {
return configExists, fmt.Errorf("failed to merge CLI args: %w", err)
}
}
return configExists, nil
}
func Save(path string, config interface{}) error {
v := reflect.ValueOf(config)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return fmt.Errorf("config must be a struct or pointer to struct")
}
data, err := tinytoml.Marshal(v.Interface())
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
tempFile := path + ".tmp"
if err := os.WriteFile(tempFile, data, 0644); err != nil {
return fmt.Errorf("failed to write temp config file: %w", err)
}
if err := os.Rename(tempFile, path); err != nil {
os.Remove(tempFile)
return fmt.Errorf("failed to save config file: %w", err)
}
return nil
}
func parseArgs(args []string) (map[string]interface{}, error) {
parsed := make([]cliArg, 0, len(args))
for i := 0; i < len(args); i++ {
arg := args[i]
if !strings.HasPrefix(arg, "--") {
continue
}
key := strings.TrimPrefix(arg, "--")
if i+1 >= len(args) || strings.HasPrefix(args[i+1], "--") {
parsed = append(parsed, cliArg{key: key, value: "true"})
continue
}
parsed = append(parsed, cliArg{key: key, value: args[i+1]})
i++
}
result := make(map[string]interface{})
for _, arg := range parsed {
keys := strings.Split(arg.key, ".")
current := result
for i, k := range keys[:len(keys)-1] {
if _, exists := current[k]; !exists {
current[k] = make(map[string]interface{})
}
if nested, ok := current[k].(map[string]interface{}); ok {
current = nested
} else {
return nil, fmt.Errorf("invalid nested key at %s", strings.Join(keys[:i+1], "."))
}
}
lastKey := keys[len(keys)-1]
if val, err := strconv.ParseBool(arg.value); err == nil {
current[lastKey] = val
} else if val, err := strconv.ParseInt(arg.value, 10, 64); err == nil {
current[lastKey] = val
} else if val, err := strconv.ParseFloat(arg.value, 64); err == nil {
current[lastKey] = val
} else {
current[lastKey] = arg.value
}
}
return result, nil
}
func mergeConfig(base interface{}, override map[string]interface{}) error {
baseValue := reflect.ValueOf(base)
if baseValue.Kind() != reflect.Ptr || baseValue.IsNil() {
return fmt.Errorf("base config must be a non-nil pointer")
}
data, err := json.Marshal(override)
if err != nil {
return fmt.Errorf("failed to marshal override values: %w", err)
}
if err := json.Unmarshal(data, base); err != nil {
return fmt.Errorf("failed to merge override values: %w", err)
}
return nil
}

77
examples/main.go Normal file
View File

@ -0,0 +1,77 @@
package main
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/LixenWraith/config"
)
type SMTPConfig struct {
Host string `toml:"host"`
Port string `toml:"port"`
FromAddr string `toml:"from_addr"`
AuthUser string `toml:"auth_user"`
AuthPass string `toml:"auth_pass"`
}
type ServerConfig struct {
Host string `toml:"host"`
Port int `toml:"port"`
ReadTimeout time.Duration `toml:"read_timeout"`
WriteTimeout time.Duration `toml:"write_timeout"`
MaxConns int `toml:"max_conns"`
}
type AppConfig struct {
SMTP SMTPConfig `toml:"smtp"`
Server ServerConfig `toml:"server"`
Debug bool `toml:"debug"`
}
func main() {
defaultConfig := AppConfig{
SMTP: SMTPConfig{
Host: "smtp.example.com",
Port: "587",
FromAddr: "noreply@example.com",
AuthUser: "admin",
AuthPass: "default123",
},
Server: ServerConfig{
Host: "localhost",
Port: 8080,
ReadTimeout: time.Second * 30,
WriteTimeout: time.Second * 30,
MaxConns: 1000,
},
Debug: true,
}
configPath := filepath.Join(".", "test.toml")
cfg := defaultConfig
// CLI argument usage example to override SMTP host and server port of existing and default config:
// ./main --smtp.host mail.example.com --server.port 9090
exists, err := config.Load(configPath, &cfg, os.Args[1:])
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err)
os.Exit(1)
}
if !exists {
if err := config.Save(configPath, &cfg); err != nil {
fmt.Fprintf(os.Stderr, "Failed to save default config: %v\n", err)
os.Exit(1)
}
fmt.Println("Created default config at:", configPath)
}
fmt.Printf("Running with config:\n")
fmt.Printf("Server: %s:%d\n", cfg.Server.Host, cfg.Server.Port)
fmt.Printf("SMTP: %s:%s\n", cfg.SMTP.Host, cfg.SMTP.Port)
fmt.Printf("Debug: %v\n", cfg.Debug)
}

7
go.mod Normal file
View File

@ -0,0 +1,7 @@
module config
go 1.23.4
require github.com/LixenWraith/tinytoml v0.0.0-20241125164826-37e61dcbf33b
require github.com/mitchellh/mapstructure v1.5.0 // indirect

4
go.sum Normal file
View File

@ -0,0 +1,4 @@
github.com/LixenWraith/tinytoml v0.0.0-20241125164826-37e61dcbf33b h1:zjNL89uvvL9xB65qKXQGrzVOAH0CWkxRmcbU2uyyUk4=
github.com/LixenWraith/tinytoml v0.0.0-20241125164826-37e61dcbf33b/go.mod h1:27w5bMp6NIrEuelM/8a+htswf0Dohs/AZ9tSsQ+lnN0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=

13
test.toml Normal file
View File

@ -0,0 +1,13 @@
debug = true
[server]
host = "localhost"
max_conns = 1000
port = 9090
read_timeout = 30000000000
write_timeout = 30000000000
[smtp]
auth_pass = "default123"
auth_user = "admin"
from_addr = "noreply@example.com"
host = "mail.example.com"
port = "587"