0.0.0.0 Initial commit, basic core functionality
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.idea
|
||||||
|
data
|
||||||
|
dev
|
||||||
|
logs
|
||||||
28
LICENSE
Normal file
28
LICENSE
Normal 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
70
README.md
Normal 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
149
config.go
Normal 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
77
examples/main.go
Normal 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
7
go.mod
Normal 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
4
go.sum
Normal 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
13
test.toml
Normal 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"
|
||||||
Reference in New Issue
Block a user