Skip to content

Commit 4a0f3e5

Browse files
committed
init
0 parents  commit 4a0f3e5

17 files changed

+1329
-0
lines changed

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Resolver Package
2+
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.
4+
5+
## Installation
6+
7+
```bash
8+
go get github.com/yourusername/yourrepo/resolver
9+
```
10+
11+
## Usage
12+
13+
The primary entry point is the `ResolveVariable` function. It takes a single string and attempts to resolve it based on its prefix:
14+
15+
- `env`: – Resolves environment variables.
16+
Example: `env:PATH` returns the value of the `PATH` environment variable.
17+
- `file`: – Resolves values from a simple key-value file.
18+
Example: `file:/config/app.txt//KeyName` returns the value associated with `KeyName` in `app.txt`.
19+
- `json`: – Resolves values from a JSON file. Supports dot notation for nested keys.
20+
Example: `json:/config/app.json//database.host` returns `host` field under `database` in `app.json`. It is also possible to indexing into arrays (e.g., `json:/config/app.json//servers.0.host`).
21+
- `yaml`: – Resolves values from a YAML file. Supports dot notation for nested keys.
22+
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`).
23+
- `ini`: – Resolves values from an INI file. Can specify a section and key, or just a key in the default section.
24+
Example: `ini:/config/app.ini//Section.Key` returns the value of `Key` under `Section`.
25+
- No prefix – Returns the value as-is, unchanged.
26+
27+
## Example
28+
29+
```go
30+
package main
31+
32+
import (
33+
"fmt"
34+
"log"
35+
"os"
36+
37+
"github.com/containeroo/portpatrol/resolver"
38+
)
39+
40+
func main() {
41+
os.Setenv("MY_VAR", "HelloWorld")
42+
43+
val, err := resolver.ResolveVariable("env:MY_VAR")
44+
if err != nil {
45+
log.Fatal(err)
46+
}
47+
fmt.Println(val) // Output: HelloWorld
48+
49+
// Resolve a JSON key:
50+
// Given a JSON file: /config/app.json
51+
// {
52+
// "server": {
53+
// "host": "localhost",
54+
// "port": 8080
55+
// }
56+
// }
57+
jsonVal, err := resolver.ResolveVariable("json:/config/app.json//server.host")
58+
if err != nil {
59+
log.Fatal(err)
60+
}
61+
fmt.Println(jsonVal) // Output: localhost
62+
}
63+
```

coverage.out

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
mode: set
2+
github.com/containeroo/portpatrol/pkg/resolver/env.go:12.61,14.12 2 1
3+
github.com/containeroo/portpatrol/pkg/resolver/env.go:14.12,16.3 1 1
4+
github.com/containeroo/portpatrol/pkg/resolver/env.go:17.2,17.17 1 1
5+
github.com/containeroo/portpatrol/pkg/resolver/file.go:16.70,21.16 4 1
6+
github.com/containeroo/portpatrol/pkg/resolver/file.go:21.16,23.3 1 1
7+
github.com/containeroo/portpatrol/pkg/resolver/file.go:24.2,26.19 2 1
8+
github.com/containeroo/portpatrol/pkg/resolver/file.go:26.19,28.3 1 1
9+
github.com/containeroo/portpatrol/pkg/resolver/file.go:31.2,32.16 2 1
10+
github.com/containeroo/portpatrol/pkg/resolver/file.go:32.16,34.3 1 0
11+
github.com/containeroo/portpatrol/pkg/resolver/file.go:35.2,35.45 1 1
12+
github.com/containeroo/portpatrol/pkg/resolver/file.go:39.65,41.21 2 1
13+
github.com/containeroo/portpatrol/pkg/resolver/file.go:41.21,44.58 3 1
14+
github.com/containeroo/portpatrol/pkg/resolver/file.go:44.58,46.4 1 1
15+
github.com/containeroo/portpatrol/pkg/resolver/file.go:48.2,48.77 1 1
16+
github.com/containeroo/portpatrol/pkg/resolver/ini.go:13.61,18.16 4 1
17+
github.com/containeroo/portpatrol/pkg/resolver/ini.go:18.16,20.3 1 1
18+
github.com/containeroo/portpatrol/pkg/resolver/ini.go:22.2,22.19 1 1
19+
github.com/containeroo/portpatrol/pkg/resolver/ini.go:22.19,25.17 2 1
20+
github.com/containeroo/portpatrol/pkg/resolver/ini.go:25.17,27.4 1 0
21+
github.com/containeroo/portpatrol/pkg/resolver/ini.go:28.3,28.46 1 1
22+
github.com/containeroo/portpatrol/pkg/resolver/ini.go:32.2,34.21 3 1
23+
github.com/containeroo/portpatrol/pkg/resolver/ini.go:34.21,38.3 2 1
24+
github.com/containeroo/portpatrol/pkg/resolver/ini.go:38.8,41.3 2 1
25+
github.com/containeroo/portpatrol/pkg/resolver/ini.go:43.2,44.16 2 1
26+
github.com/containeroo/portpatrol/pkg/resolver/ini.go:44.16,46.3 1 1
27+
github.com/containeroo/portpatrol/pkg/resolver/ini.go:48.2,49.34 2 1
28+
github.com/containeroo/portpatrol/pkg/resolver/ini.go:49.34,51.3 1 1
29+
github.com/containeroo/portpatrol/pkg/resolver/ini.go:53.2,53.24 1 1
30+
github.com/containeroo/portpatrol/pkg/resolver/json.go:21.62,26.16 4 1
31+
github.com/containeroo/portpatrol/pkg/resolver/json.go:26.16,28.3 1 1
32+
github.com/containeroo/portpatrol/pkg/resolver/json.go:30.2,30.19 1 1
33+
github.com/containeroo/portpatrol/pkg/resolver/json.go:30.19,33.3 1 1
34+
github.com/containeroo/portpatrol/pkg/resolver/json.go:35.2,36.55 2 1
35+
github.com/containeroo/portpatrol/pkg/resolver/json.go:36.55,38.3 1 0
36+
github.com/containeroo/portpatrol/pkg/resolver/json.go:40.2,41.16 2 1
37+
github.com/containeroo/portpatrol/pkg/resolver/json.go:41.16,43.3 1 1
38+
github.com/containeroo/portpatrol/pkg/resolver/json.go:45.2,46.9 2 1
39+
github.com/containeroo/portpatrol/pkg/resolver/json.go:46.9,50.3 2 1
40+
github.com/containeroo/portpatrol/pkg/resolver/json.go:51.2,51.20 1 1
41+
github.com/containeroo/portpatrol/pkg/resolver/resolver.go:33.52,34.42 1 1
42+
github.com/containeroo/portpatrol/pkg/resolver/resolver.go:34.42,35.39 1 1
43+
github.com/containeroo/portpatrol/pkg/resolver/resolver.go:35.39,37.4 1 1
44+
github.com/containeroo/portpatrol/pkg/resolver/resolver.go:39.2,39.19 1 1
45+
github.com/containeroo/portpatrol/pkg/resolver/testutils.go:5.42,8.2 2 1
46+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:10.53,13.15 3 1
47+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:13.15,15.3 1 1
48+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:16.2,16.47 1 1
49+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:31.73,33.25 2 1
50+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:33.25,34.33 1 1
51+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:35.31,38.11 2 1
52+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:38.11,40.5 1 1
53+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:41.4,41.17 1 1
54+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:43.22,46.18 2 1
55+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:46.18,48.5 1 1
56+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:49.4,49.35 1 1
57+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:49.35,51.5 1 1
58+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:52.4,52.23 1 1
59+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:54.11,56.60 1 1
60+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:59.2,59.21 1 1
61+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:64.83,65.25 1 1
62+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:66.30,68.28 1 1
63+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:68.28,70.18 2 1
64+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:70.18,72.5 1 0
65+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:73.4,73.22 1 1
66+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:75.3,75.16 1 1
67+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:76.10,78.39 1 1
68+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:82.57,84.26 1 1
69+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:85.30,86.24 1 1
70+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:86.24,88.18 2 1
71+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:88.18,90.5 1 0
72+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:91.4,91.21 1 1
73+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:93.3,93.17 1 1
74+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:94.21,95.27 1 1
75+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:95.27,97.18 2 1
76+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:97.18,99.5 1 0
77+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:100.4,100.21 1 1
78+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:102.3,102.17 1 1
79+
github.com/containeroo/portpatrol/pkg/resolver/utils.go:103.10,104.17 1 1
80+
github.com/containeroo/portpatrol/pkg/resolver/yaml.go:16.62,21.16 4 1
81+
github.com/containeroo/portpatrol/pkg/resolver/yaml.go:21.16,23.3 1 1
82+
github.com/containeroo/portpatrol/pkg/resolver/yaml.go:25.2,26.55 2 1
83+
github.com/containeroo/portpatrol/pkg/resolver/yaml.go:26.55,28.3 1 1
84+
github.com/containeroo/portpatrol/pkg/resolver/yaml.go:31.2,32.16 2 1
85+
github.com/containeroo/portpatrol/pkg/resolver/yaml.go:32.16,34.3 1 0
86+
github.com/containeroo/portpatrol/pkg/resolver/yaml.go:36.2,36.19 1 1
87+
github.com/containeroo/portpatrol/pkg/resolver/yaml.go:36.19,39.3 1 1
88+
github.com/containeroo/portpatrol/pkg/resolver/yaml.go:41.2,42.16 2 1
89+
github.com/containeroo/portpatrol/pkg/resolver/yaml.go:42.16,44.3 1 1
90+
github.com/containeroo/portpatrol/pkg/resolver/yaml.go:47.2,47.32 1 1
91+
github.com/containeroo/portpatrol/pkg/resolver/yaml.go:48.14,49.23 1 1
92+
github.com/containeroo/portpatrol/pkg/resolver/yaml.go:50.10,52.47 2 1

env.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package resolver
2+
3+
import (
4+
"fmt"
5+
"os"
6+
)
7+
8+
// EnvResolver resolves values using environment variables.
9+
// Usage: "env:MY_VAR" -> returns value of MY_VAR
10+
type EnvResolver struct{}
11+
12+
func (r *EnvResolver) Resolve(value string) (string, error) {
13+
res, found := os.LookupEnv(value)
14+
if !found {
15+
return "", fmt.Errorf("environment variable '%s' not found", value)
16+
}
17+
return res, nil
18+
}

env_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package resolver
2+
3+
import (
4+
"os"
5+
"testing"
6+
)
7+
8+
func TestEnvResolver_Resolve(t *testing.T) {
9+
resolver := &EnvResolver{}
10+
11+
os.Setenv("TEST_ENV_VAR", "test_value")
12+
os.Setenv("EMPTY_ENV_VAR", "")
13+
14+
t.Run("Resolve existing environment variable", func(t *testing.T) {
15+
val, err := resolver.Resolve("TEST_ENV_VAR")
16+
if err != nil {
17+
t.Errorf("unexpected error resolving 'TEST_ENV_VAR': %v", err)
18+
}
19+
if val != "test_value" {
20+
t.Errorf("expected 'test_value' but got '%s'", val)
21+
}
22+
})
23+
24+
t.Run("Resolve empty environment variable", func(t *testing.T) {
25+
val, err := resolver.Resolve("EMPTY_ENV_VAR")
26+
if err != nil {
27+
t.Errorf("unexpected error resolving 'EMPTY_ENV_VAR': %v", err)
28+
}
29+
if val != "" {
30+
t.Errorf("expected '' (empty string) but got '%s'", val)
31+
}
32+
})
33+
34+
t.Run("Resolve missing environment variable", func(t *testing.T) {
35+
_, err := resolver.Resolve("MISSING_ENV_VAR")
36+
if err == nil {
37+
t.Error("expected an error resolving 'MISSING_ENV_VAR', but got none")
38+
}
39+
})
40+
}

file.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package resolver
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"io"
7+
"os"
8+
"strings"
9+
)
10+
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.
14+
type KeyValueFileResolver struct{}
15+
16+
func (f *KeyValueFileResolver) Resolve(value string) (string, error) {
17+
filePath, keyPath := splitFileAndKey(value)
18+
filePath = os.ExpandEnv(filePath)
19+
20+
file, err := os.Open(filePath)
21+
if err != nil {
22+
return "", fmt.Errorf("Failed to open file '%s'. %v", filePath, err)
23+
}
24+
defer file.Close()
25+
26+
if keyPath != "" {
27+
return searchKeyInFile(file, keyPath)
28+
}
29+
30+
// No key specified, read the whole file
31+
data, err := io.ReadAll(file)
32+
if err != nil {
33+
return "", fmt.Errorf("Failed to read file '%s'. %v", filePath, err)
34+
}
35+
return strings.TrimSpace(string(data)), nil
36+
}
37+
38+
// searchKeyInFile searches for a specified key in a file and returns its associated value.
39+
func searchKeyInFile(file *os.File, key string) (string, error) {
40+
scanner := bufio.NewScanner(file)
41+
for scanner.Scan() {
42+
line := scanner.Text()
43+
pair := strings.SplitN(line, "=", 2)
44+
if len(pair) == 2 && strings.TrimSpace(pair[0]) == key {
45+
return strings.TrimSpace(pair[1]), nil
46+
}
47+
}
48+
return "", fmt.Errorf("Key '%s' not found in file '%s'.", key, file.Name())
49+
}

file_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package resolver
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func createKeyValueTestFile(t *testing.T) string {
12+
tempDir := t.TempDir()
13+
14+
testFilePath := filepath.Join(tempDir, "app.txt")
15+
fileContent := `Key1 = Value1
16+
Key2=Value2
17+
Key with spaces = TrimmedValue
18+
`
19+
err := os.WriteFile(testFilePath, []byte(fileContent), 0666)
20+
assert.NoError(t, err, "failed to create test file")
21+
22+
return testFilePath
23+
}
24+
25+
func TestKeyValueFileResolver_Resolve(t *testing.T) {
26+
t.Parallel()
27+
28+
resolver := &KeyValueFileResolver{}
29+
30+
t.Run("Resolve entire file", func(t *testing.T) {
31+
t.Parallel()
32+
testFilePath := createKeyValueTestFile(t)
33+
34+
val, err := resolver.Resolve(testFilePath)
35+
assert.NoError(t, err, "unexpected error resolving entire file")
36+
37+
expected := "Key1 = Value1\nKey2=Value2\nKey with spaces = TrimmedValue"
38+
assert.Equal(t, expected, val)
39+
})
40+
41+
t.Run("Resolve specific key", func(t *testing.T) {
42+
t.Parallel()
43+
testFilePath := createKeyValueTestFile(t)
44+
45+
val, err := resolver.Resolve(testFilePath + "//Key2")
46+
assert.NoError(t, err, "unexpected error resolving specific key")
47+
assert.Equal(t, "Value2", val)
48+
})
49+
50+
t.Run("Resolve key with spaces", func(t *testing.T) {
51+
t.Parallel()
52+
testFilePath := createKeyValueTestFile(t)
53+
54+
val, err := resolver.Resolve(testFilePath + "//Key with spaces")
55+
assert.NoError(t, err, "unexpected error resolving key with spaces")
56+
assert.Equal(t, "TrimmedValue", val)
57+
})
58+
59+
t.Run("Resolve missing key", func(t *testing.T) {
60+
t.Parallel()
61+
testFilePath := createKeyValueTestFile(t)
62+
63+
_, err := resolver.Resolve(testFilePath + "//NonExistentKey")
64+
assert.Error(t, err, "expected an error resolving a missing key, but got none")
65+
})
66+
67+
t.Run("Resolve non-existing file", func(t *testing.T) {
68+
t.Parallel()
69+
tempDir := t.TempDir()
70+
nonExistentFile := filepath.Join(tempDir, "nonexistent.txt")
71+
72+
_, err := resolver.Resolve(nonExistentFile)
73+
assert.Error(t, err, "expected an error resolving a non-existing file, but got none")
74+
})
75+
}

ini.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package resolver
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
"gopkg.in/ini.v1"
9+
)
10+
11+
type INIResolver struct{}
12+
13+
func (r *INIResolver) Resolve(value string) (string, error) {
14+
filePath, keyPath := splitFileAndKey(value)
15+
filePath = os.ExpandEnv(filePath)
16+
17+
cfg, err := ini.Load(filePath)
18+
if err != nil {
19+
return "", fmt.Errorf("failed to read INI file '%s': %w", filePath, err)
20+
}
21+
22+
if keyPath == "" {
23+
// No key path means return the entire INI file
24+
data, err := os.ReadFile(filePath)
25+
if err != nil {
26+
return "", fmt.Errorf("failed to read INI file '%s': %w", filePath, err)
27+
}
28+
return strings.TrimSpace(string(data)), nil
29+
}
30+
31+
// KeyPath can be "Section.Key" or just "Key" (default section)
32+
parts := strings.Split(keyPath, ".")
33+
var sectionName, keyName string
34+
if len(parts) == 1 {
35+
// No explicit section, default section assumed
36+
sectionName = "DEFAULT"
37+
keyName = parts[0]
38+
} else {
39+
sectionName = parts[0]
40+
keyName = strings.Join(parts[1:], ".")
41+
}
42+
43+
section, err := cfg.GetSection(sectionName)
44+
if err != nil {
45+
return "", fmt.Errorf("section '%s' not found in INI '%s': %w", sectionName, filePath, err)
46+
}
47+
48+
k := section.Key(keyName)
49+
if k == nil || k.String() == "" {
50+
return "", fmt.Errorf("key '%s' not found in section '%s' of INI '%s'", keyName, sectionName, filePath)
51+
}
52+
53+
return k.String(), nil
54+
}

0 commit comments

Comments
 (0)