Skip to content

Commit aaaa016

Browse files
authored
Merge pull request #1 from containeroo/toml
add support for toml files
2 parents 4a0f3e5 + 9b47a5b commit aaaa016

15 files changed

+344
-142
lines changed

Makefile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.PHONY: patch minor major tag
2+
3+
# Find the latest tag (default to 0.0.0 if none found)
4+
LATEST_TAG := $(shell git tag --list 'v*' --sort=-v:refname | head -n 1)
5+
VERSION := $(shell echo $(LATEST_TAG) | sed 's/^v//' || echo "0.0.0")
6+
7+
patch: ## Create a new patch release (x.y.Z+1)
8+
@NEW_VERSION=$$(echo "$(VERSION)" | awk -F. '{printf "%d.%d.%d", $$1, $$2, $$3+1}') && \
9+
git tag "v$${NEW_VERSION}" && \
10+
echo "Tagged v$${NEW_VERSION}"
11+
12+
minor: ## Create a new minor release (x.Y+1.0)
13+
@NEW_VERSION=$$(echo "$(VERSION)" | awk -F. '{printf "%d.%d.0", $$1, $$2+1}') && \
14+
git tag "v$${NEW_VERSION}" && \
15+
echo "Tagged v$${NEW_VERSION}"
16+
17+
major: ## Create a new major release (X+1.0.0)
18+
@NEW_VERSION=$$(echo "$(VERSION)" | awk -F. '{printf "%d.0.0", $$1+1}') && \
19+
git tag "v$${NEW_VERSION}" && \
20+
echo "Tagged v$${NEW_VERSION}"
21+
22+
tag: ## Show latest tag
23+
@echo "Latest version: $(LATEST_TAG)"
24+

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# Resolver Package
22

3-
The `resolver` package provides a flexible and extensible way to resolve configuration values from various sources including environment variables, files, JSON, YAML, INI, and key-value files. It uses a prefix-based system to identify which resolver to use and returns the resolved value or an error if something goes wrong.
3+
The `resolver` package provides a flexible and extensible way to resolve configuration values from various sources including environment variables, files, `JSON`, `YAML`, `INI`, `TOML` and key-value files. It uses a prefix-based system to identify which resolver to use and returns the resolved value or an error if something goes wrong.
44

55
## Installation
66

77
```bash
8-
go get github.com/yourusername/yourrepo/resolver
8+
go get github.com/containeroo/resolver/resolver
99
```
1010

1111
## Usage
@@ -22,6 +22,8 @@ The primary entry point is the `ResolveVariable` function. It takes a single str
2222
Example: `yaml:/config/app.yaml//server.port` returns `port` under `server` in `app.yaml`.It is also possible to indexing into arrays (e.g., `yaml:/config/app.yaml//servers.0.host`).
2323
- `ini`: – Resolves values from an INI file. Can specify a section and key, or just a key in the default section.
2424
Example: `ini:/config/app.ini//Section.Key` returns the value of `Key` under `Section`.
25+
- `toml`: – Resolves values from a TOML file.
26+
Example: `toml:/config/app.toml//server.host` returns `host` under `server` in `app.toml`.
2527
- No prefix – Returns the value as-is, unchanged.
2628

2729
## Example
@@ -34,7 +36,7 @@ import (
3436
"log"
3537
"os"
3638

37-
"github.com/containeroo/portpatrol/resolver"
39+
"github.com/containeroo/resolver"
3840
)
3941

4042
func main() {

coverage.out

Lines changed: 0 additions & 92 deletions
This file was deleted.

env.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ import (
55
"os"
66
)
77

8-
// EnvResolver resolves values using environment variables.
9-
// Usage: "env:MY_VAR" -> returns value of MY_VAR
8+
// Resolves a value from environment variables.
9+
// The value after the prefix should be the name of the environment variable.
10+
// Example:
11+
// "env:MY_ENV_VAR"
12+
// would return the value of the MY_ENV_VAR environment variable.
13+
//
14+
// If the variable is not set, an error is returned.
1015
type EnvResolver struct{}
1116

1217
func (r *EnvResolver) Resolve(value string) (string, error) {

file.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@ import (
88
"strings"
99
)
1010

11-
// Resolves values from key = values files. The format can be:
12-
// "file:/config/app.txt//Key"
13-
// If no key is provided, returns the whole file.
11+
// Resolves a value by reading a key from a plain key=value text file.
12+
// The value after the prefix should be in the format "path/to/file.txt//Key"
13+
// If no key is provided, returns the entire file as a string.
14+
// Example:
15+
// "file:/config/app.txt//USERNAME"
16+
// would search for a line like "USERNAME = alice" and return "alice".
17+
//
18+
// Lines are matched by exact key name before the equals sign.
19+
// If no key is provided (no "//" present), returns the entire file as string.
1420
type KeyValueFileResolver struct{}
1521

1622
func (f *KeyValueFileResolver) Resolve(value string) (string, error) {

go.mod

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module github.com/containeroo/resolver
2+
3+
go 1.24.2
4+
5+
require (
6+
github.com/pelletier/go-toml/v2 v2.2.4
7+
github.com/stretchr/testify v1.10.0
8+
gopkg.in/ini.v1 v1.67.0
9+
gopkg.in/yaml.v3 v3.0.1
10+
)
11+
12+
require (
13+
github.com/davecgh/go-spew v1.1.1 // indirect
14+
github.com/pmezard/go-difflib v1.0.0 // indirect
15+
)

go.sum

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
4+
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
5+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
8+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
9+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
10+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
11+
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
12+
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
13+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
14+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

ini.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ import (
88
"gopkg.in/ini.v1"
99
)
1010

11+
// Resolves a value by loading an INI file and extracting a section.key pair.
12+
// The value after the prefix should be in the format "path/to/file.ini//Section.Key"
13+
// If no section is provided, the default section is used.
14+
// If no key is provided, returns the entire INI file as a string.
15+
// Example:
16+
// "ini:/config/app.ini//Database.User"
17+
// would load app.ini, locate the [Database] section, and return the value of User.
18+
//
19+
// Keys are navigated via "Section.Key" notation.
20+
// If no key is provided (no "//" present), returns the entire INI file as string.
1121
type INIResolver struct{}
1222

1323
func (r *INIResolver) Resolve(value string) (string, error) {

json.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func (r *JSONResolver) Resolve(value string) (string, error) {
3232
return strings.TrimSpace(string(data)), nil
3333
}
3434

35-
var content map[string]interface{}
35+
var content map[string]any
3636
if err := json.Unmarshal(data, &content); err != nil {
3737
return "", fmt.Errorf("failed to parse JSON in '%s': %w", filePath, err)
3838
}

resolver.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ type Resolver interface {
1212

1313
// Prefixes for different resolvers
1414
const (
15-
envPrefix = "env:"
16-
jsonPrefix = "json:"
17-
yamlPrefix = "yaml:"
18-
iniPrefix = "ini:"
19-
filePrefix = "file:"
15+
envPrefix string = "env:"
16+
filePrefix string = "file:"
17+
iniPrefix string = "ini:"
18+
jsonPrefix string = "json:"
19+
tomlPrefix string = "toml:"
20+
yamlPrefix string = "yaml:"
2021
)
2122

2223
// Global registry of resolvers
@@ -26,6 +27,7 @@ var resolvers = map[string]Resolver{
2627
yamlPrefix: &YAMLResolver{},
2728
iniPrefix: &INIResolver{},
2829
filePrefix: &INIResolver{},
30+
tomlPrefix: &TOMLResolver{},
2931
}
3032

3133
// ResolveVariable attempts to resolve the given value by checking for known prefixes.

toml.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package resolver
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
"github.com/pelletier/go-toml/v2"
9+
)
10+
11+
// Resolves a value by loading a TOML file and extracting a nested key.
12+
// The value after the prefix should be in the format "path/to/file.toml//key1.key2.keyN"
13+
// If no key is provided, returns the entire TOML file as a string.
14+
// Example:
15+
// "toml:/config/app.toml//server.host"
16+
// would load app.toml, parse it as TOML, and then return the value at server.host.
17+
//
18+
// Keys are navigated via dot notation.
19+
// If no key is provided (no "//" present), returns the entire TOML file as string.
20+
type TOMLResolver struct{}
21+
22+
func (r *TOMLResolver) Resolve(value string) (string, error) {
23+
filePath, keyPath := splitFileAndKey(value)
24+
filePath = os.ExpandEnv(filePath)
25+
26+
data, err := os.ReadFile(filePath)
27+
if err != nil {
28+
return "", fmt.Errorf("failed to read TOML file '%s': %w", filePath, err)
29+
}
30+
31+
// Validate TOML syntax by decoding into a dummy struct
32+
var validationTarget struct{}
33+
if err := toml.Unmarshal(data, &validationTarget); err != nil {
34+
return "", fmt.Errorf("failed to parse TOML in '%s': %w", filePath, err)
35+
}
36+
37+
// Decode into navigable structure
38+
var content map[string]any
39+
if err := toml.Unmarshal(data, &content); err != nil {
40+
return "", fmt.Errorf("failed to parse TOML in '%s': %w", filePath, err)
41+
}
42+
43+
if keyPath == "" {
44+
return strings.TrimSpace(string(data)), nil
45+
}
46+
47+
val, err := navigateData(content, strings.Split(keyPath, "."))
48+
if err != nil {
49+
return "", fmt.Errorf("key path '%s' not found in TOML '%s': %w", keyPath, filePath, err)
50+
}
51+
52+
if strVal, ok := val.(string); ok {
53+
return strVal, nil
54+
}
55+
56+
tomlVal, err := toml.Marshal(val)
57+
if err != nil {
58+
return "", fmt.Errorf("failed to encode TOML value: %w", err)
59+
}
60+
61+
return strings.TrimSpace(string(tomlVal)), nil
62+
}

0 commit comments

Comments
 (0)