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