mirror of
https://github.com/taigrr/jety.git
synced 2026-04-13 16:58:20 -07:00
Compare commits
1 Commits
master
...
cd/unmarsh
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c0fc0320c |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
cover.out
|
||||
44
README.md
44
README.md
@@ -15,7 +15,7 @@ Originally built to support [grlx](http://github.com/gogrlx/grlx).
|
||||
go get github.com/taigrr/jety
|
||||
```
|
||||
|
||||
Requires Go 1.26.1 or later.
|
||||
Requires Go 1.26.2 or later.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -133,7 +133,6 @@ export MYAPP_SERVICES_CLOUD_VAR=override_value
|
||||
| ------------------------ | ------------------------ |
|
||||
| `Set(key, value)` | Set a value |
|
||||
| `SetDefault(key, value)` | Set a default value |
|
||||
| `Delete(key)` | Remove a key |
|
||||
| `Sub(key)` | Get scoped sub-config |
|
||||
| `Get(key)` | Get raw value |
|
||||
| `GetString(key)` | Get as string |
|
||||
@@ -148,6 +147,8 @@ export MYAPP_SERVICES_CLOUD_VAR=override_value
|
||||
| `IsSet(key)` | Check if key has a value |
|
||||
| `AllKeys()` | List all known keys |
|
||||
| `AllSettings()` | Get all values as a map |
|
||||
| `Unmarshal(target)` | Unmarshal config to struct |
|
||||
| `UnmarshalKey(key, target)` | Unmarshal a key to struct |
|
||||
|
||||
### Environment
|
||||
|
||||
@@ -156,6 +157,45 @@ export MYAPP_SERVICES_CLOUD_VAR=override_value
|
||||
| `WithEnvPrefix(prefix)` | Filter env vars by prefix (strips prefix from keys) |
|
||||
| `SetEnvPrefix(prefix)` | Set prefix for env var lookups |
|
||||
|
||||
## Struct Unmarshaling
|
||||
|
||||
Unmarshal your configuration directly into Go structs:
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Debug bool `json:"debug"`
|
||||
}
|
||||
|
||||
jety.SetConfigFile("config.json")
|
||||
jety.SetConfigType("json")
|
||||
jety.ReadInConfig()
|
||||
|
||||
var cfg Config
|
||||
if err := jety.Unmarshal(&cfg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
For nested sections, use `UnmarshalKey`:
|
||||
|
||||
```go
|
||||
type DatabaseConfig struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
var dbCfg DatabaseConfig
|
||||
if err := jety.UnmarshalKey("database", &dbCfg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
Struct tags use `json:"..."` since the unmarshaling uses JSON round-trip internally.
|
||||
Dot notation works with `UnmarshalKey` for deeply nested values (e.g., `"services.api"`).
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](LICENSE) file.
|
||||
|
||||
@@ -111,3 +111,11 @@ func AllKeys() []string {
|
||||
func AllSettings() map[string]any {
|
||||
return defaultConfigManager.AllSettings()
|
||||
}
|
||||
|
||||
func Unmarshal(target any) error {
|
||||
return defaultConfigManager.Unmarshal(target)
|
||||
}
|
||||
|
||||
func UnmarshalKey(key string, target any) error {
|
||||
return defaultConfigManager.UnmarshalKey(key, target)
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/taigrr/jety
|
||||
|
||||
go 1.26.1
|
||||
go 1.26.2
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.6.0
|
||||
|
||||
49
unmarshal.go
Normal file
49
unmarshal.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package jety
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Unmarshal unmarshals the full combined configuration into the provided
|
||||
// struct pointer. It works by marshaling the settings map to JSON and then
|
||||
// unmarshaling into the target, so struct field tags should use `json:"key"`.
|
||||
// The target must be a non-nil pointer to a struct.
|
||||
func (c *ConfigManager) Unmarshal(target any) error {
|
||||
settings := c.AllSettings()
|
||||
return mapToStruct(settings, target)
|
||||
}
|
||||
|
||||
// UnmarshalKey unmarshals a specific key's value into the provided struct
|
||||
// pointer. The key must refer to a map value (e.g., a nested TOML/YAML/JSON
|
||||
// section). Supports dot notation for nested keys.
|
||||
func (c *ConfigManager) UnmarshalKey(key string, target any) error {
|
||||
c.mutex.RLock()
|
||||
v, ok := c.resolve(key)
|
||||
c.mutex.RUnlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("key %q not found", key)
|
||||
}
|
||||
m, ok := v.Value.(map[string]any)
|
||||
if !ok {
|
||||
// If the value is not a map, try direct JSON round-trip
|
||||
data, err := json.Marshal(v.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot marshal value for key %q: %w", key, err)
|
||||
}
|
||||
return json.Unmarshal(data, target)
|
||||
}
|
||||
return mapToStruct(m, target)
|
||||
}
|
||||
|
||||
// mapToStruct converts a map[string]any to a struct via JSON round-trip.
|
||||
func mapToStruct(m map[string]any, target any) error {
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot marshal config: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(data, target); err != nil {
|
||||
return fmt.Errorf("cannot unmarshal config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
333
unmarshal_test.go
Normal file
333
unmarshal_test.go
Normal file
@@ -0,0 +1,333 @@
|
||||
package jety
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnmarshal(t *testing.T) {
|
||||
type ServerConfig struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
type AppConfig struct {
|
||||
Name string `json:"name"`
|
||||
Debug bool `json:"debug"`
|
||||
Server ServerConfig `json:"server"`
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
configFile := filepath.Join(dir, "config.json")
|
||||
err := os.WriteFile(configFile, []byte(`{
|
||||
"name": "myapp",
|
||||
"debug": true,
|
||||
"server": {
|
||||
"host": "localhost",
|
||||
"port": 8080
|
||||
}
|
||||
}`), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cm := NewConfigManager()
|
||||
cm.SetConfigFile(configFile)
|
||||
if err := cm.SetConfigType("json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cm.ReadInConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var cfg AppConfig
|
||||
if err := cm.Unmarshal(&cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if cfg.Name != "myapp" {
|
||||
t.Errorf("expected name 'myapp', got %q", cfg.Name)
|
||||
}
|
||||
if !cfg.Debug {
|
||||
t.Error("expected debug to be true")
|
||||
}
|
||||
if cfg.Server.Host != "localhost" {
|
||||
t.Errorf("expected host 'localhost', got %q", cfg.Server.Host)
|
||||
}
|
||||
if cfg.Server.Port != 8080 {
|
||||
t.Errorf("expected port 8080, got %d", cfg.Server.Port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalWithDefaults(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
configFile := filepath.Join(dir, "config.json")
|
||||
err := os.WriteFile(configFile, []byte(`{
|
||||
"host": "example.com"
|
||||
}`), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cm := NewConfigManager()
|
||||
cm.SetDefault("port", 3000)
|
||||
cm.SetDefault("timeout", 30)
|
||||
cm.SetConfigFile(configFile)
|
||||
if err := cm.SetConfigType("json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cm.ReadInConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := cm.Unmarshal(&cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if cfg.Host != "example.com" {
|
||||
t.Errorf("expected host 'example.com', got %q", cfg.Host)
|
||||
}
|
||||
if cfg.Port != 3000 {
|
||||
t.Errorf("expected port 3000, got %d", cfg.Port)
|
||||
}
|
||||
if cfg.Timeout != 30 {
|
||||
t.Errorf("expected timeout 30, got %d", cfg.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalKey(t *testing.T) {
|
||||
type DatabaseConfig struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
configFile := filepath.Join(dir, "config.json")
|
||||
err := os.WriteFile(configFile, []byte(`{
|
||||
"database": {
|
||||
"host": "db.example.com",
|
||||
"port": 5432,
|
||||
"name": "mydb"
|
||||
},
|
||||
"cache": {
|
||||
"host": "cache.example.com",
|
||||
"port": 6379
|
||||
}
|
||||
}`), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cm := NewConfigManager()
|
||||
cm.SetConfigFile(configFile)
|
||||
if err := cm.SetConfigType("json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cm.ReadInConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var dbCfg DatabaseConfig
|
||||
if err := cm.UnmarshalKey("database", &dbCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if dbCfg.Host != "db.example.com" {
|
||||
t.Errorf("expected host 'db.example.com', got %q", dbCfg.Host)
|
||||
}
|
||||
if dbCfg.Port != 5432 {
|
||||
t.Errorf("expected port 5432, got %d", dbCfg.Port)
|
||||
}
|
||||
if dbCfg.Name != "mydb" {
|
||||
t.Errorf("expected name 'mydb', got %q", dbCfg.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalKeyNotFound(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
var target struct{}
|
||||
err := cm.UnmarshalKey("nonexistent", &target)
|
||||
if err == nil {
|
||||
t.Error("expected error for missing key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalKeyScalarValue(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
cm.Set("count", 42)
|
||||
|
||||
var count int
|
||||
if err := cm.UnmarshalKey("count", &count); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != 42 {
|
||||
t.Errorf("expected 42, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalKeyNestedDotNotation(t *testing.T) {
|
||||
type Inner struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
configFile := filepath.Join(dir, "config.json")
|
||||
err := os.WriteFile(configFile, []byte(`{
|
||||
"services": {
|
||||
"api": {
|
||||
"value": "hello"
|
||||
}
|
||||
}
|
||||
}`), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cm := NewConfigManager()
|
||||
cm.SetConfigFile(configFile)
|
||||
if err := cm.SetConfigType("json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cm.ReadInConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var inner Inner
|
||||
if err := cm.UnmarshalKey("services.api", &inner); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if inner.Value != "hello" {
|
||||
t.Errorf("expected 'hello', got %q", inner.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalTOML(t *testing.T) {
|
||||
type Config struct {
|
||||
Title string `json:"title"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
configFile := filepath.Join(dir, "config.toml")
|
||||
err := os.WriteFile(configFile, []byte(`
|
||||
title = "My App"
|
||||
port = 9090
|
||||
`), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cm := NewConfigManager()
|
||||
cm.SetConfigFile(configFile)
|
||||
if err := cm.SetConfigType("toml"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cm.ReadInConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := cm.Unmarshal(&cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if cfg.Title != "My App" {
|
||||
t.Errorf("expected title 'My App', got %q", cfg.Title)
|
||||
}
|
||||
if cfg.Port != 9090 {
|
||||
t.Errorf("expected port 9090, got %d", cfg.Port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalYAML(t *testing.T) {
|
||||
type Config struct {
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
configFile := filepath.Join(dir, "config.yaml")
|
||||
err := os.WriteFile(configFile, []byte(`
|
||||
name: testapp
|
||||
tags:
|
||||
- web
|
||||
- api
|
||||
enabled: true
|
||||
`), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cm := NewConfigManager()
|
||||
cm.SetConfigFile(configFile)
|
||||
if err := cm.SetConfigType("yaml"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cm.ReadInConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := cm.Unmarshal(&cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if cfg.Name != "testapp" {
|
||||
t.Errorf("expected name 'testapp', got %q", cfg.Name)
|
||||
}
|
||||
if len(cfg.Tags) != 2 || cfg.Tags[0] != "web" || cfg.Tags[1] != "api" {
|
||||
t.Errorf("expected tags [web api], got %v", cfg.Tags)
|
||||
}
|
||||
if !cfg.Enabled {
|
||||
t.Error("expected enabled to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultUnmarshal(t *testing.T) {
|
||||
// Test package-level Unmarshal function
|
||||
type Config struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// Reset default manager
|
||||
defaultConfigManager = NewConfigManager()
|
||||
Set("key", "value")
|
||||
|
||||
var cfg Config
|
||||
if err := Unmarshal(&cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Key != "value" {
|
||||
t.Errorf("expected 'value', got %q", cfg.Key)
|
||||
}
|
||||
|
||||
// Restore
|
||||
defaultConfigManager = NewConfigManager()
|
||||
}
|
||||
|
||||
func TestDefaultUnmarshalKey(t *testing.T) {
|
||||
defaultConfigManager = NewConfigManager()
|
||||
Set("section", map[string]any{"field": "data"})
|
||||
|
||||
type Section struct {
|
||||
Field string `json:"field"`
|
||||
}
|
||||
var sec Section
|
||||
if err := UnmarshalKey("section", &sec); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sec.Field != "data" {
|
||||
t.Errorf("expected 'data', got %q", sec.Field)
|
||||
}
|
||||
|
||||
defaultConfigManager = NewConfigManager()
|
||||
}
|
||||
Reference in New Issue
Block a user