Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions json/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package json

import (
"bytes"
"encoding/json"
"fmt"
"os"
)

type Parser struct {
path *string

conv func(any) string
awaited map[string]bool
}

func New(path *string) *Parser {
return &Parser{path: path}
}

func (p *Parser) Type() string {
if p.path == nil || *p.path == "" {
return "json"
}
return fmt.Sprintf("json[%s]", *p.path)
}

func (p *Parser) Parse(keys map[string]bool, conv func(any) string) (found, unknown map[string]string, err error) {
found = make(map[string]string)
unknown = make(map[string]string)

if p.path == nil || *p.path == "" {
return found, unknown, nil
}

data, err := os.ReadFile(*p.path)
if err != nil {
if os.IsNotExist(err) {
return found, unknown, nil
}
return nil, nil, fmt.Errorf("read json file %q: %w", *p.path, err)
}

if len(data) == 0 {
return found, unknown, nil
}

p.conv = conv
p.awaited = keys

return p.parse(data)
}

func (p *Parser) parse(data []byte) (found, unknown map[string]string, err error) {
var settings any

decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
if err = decoder.Decode(&settings); err != nil {
if err.Error() == "EOF" && len(data) == 0 {
return make(map[string]string), make(map[string]string), nil
}
return nil, nil, fmt.Errorf("unmarshal json: %w", err)
}

settingsMap, ok := settings.(map[string]any)
if !ok {
return make(map[string]string), make(map[string]string), nil
}

found, unknown = p.flatten(settingsMap)

return found, unknown, nil
}

func (p *Parser) flatten(settings map[string]any) (found, unknown map[string]string) {
found, unknown = make(map[string]string), make(map[string]string)
p.flattenDFS(settings, "", found, unknown)
return found, unknown
}

func (p *Parser) flattenDFS(m map[string]any, prefix string, found, unknown map[string]string) {
for k, v := range m {

if v == nil {
continue
}

newKey := k
if prefix != "" {
newKey = prefix + "." + k
}

if p.awaited[newKey] {
found[newKey] = p.conv(v)
continue
}

if subMap, ok := v.(map[string]any); ok {
p.flattenDFS(subMap, newKey, found, unknown)
continue
}

unknown[newKey] = p.conv(v)
}
}
163 changes: 163 additions & 0 deletions json/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package json_test

import (
"os"
"testing"

zfg "github.com/chaindead/zerocfg"
"github.com/chaindead/zerocfg/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParse(t *testing.T) {
tests := []struct {
name string
input string
awaited map[string]bool
found map[string]string
unknown map[string]string
wantErr bool
}{
{
name: "simple key-value",
input: `{"str": "name", "int": 1}`,
awaited: map[string]bool{
"str": true,
},
found: map[string]string{
"str": `name`,
},
unknown: map[string]string{
"int": `1`,
},
},
{
name: "nested",
input: `{
"host": "localhost",
"database": {
"port": 5432,
"credentials": {
"username": "admin"
}
}
}`,
awaited: map[string]bool{
"database.credentials.username": true,
},
found: map[string]string{
"database.credentials.username": `admin`,
},
unknown: map[string]string{
"host": `localhost`,
"database.port": `5432`,
},
},
{
name: "array",
input: `{"tags": ["a", "b"]}`,
awaited: map[string]bool{
"tags": true,
},
found: map[string]string{
"tags": `["a","b"]`,
},
unknown: map[string]string{},
},
{
name: "map",
input: `{"options": {"k1": 1, "k2": "v2"}}`,
awaited: map[string]bool{
"options": true,
},
found: map[string]string{
"options": `{"k1":1,"k2":"v2"}`,
},
unknown: map[string]string{},
},
{
name: "null value is skipped",
input: `{"key1": "value1", "key2": null, "key3": 123}`,
awaited: map[string]bool{
"key1": true,
"key2": true,
"key3": true,
},
found: map[string]string{
"key1": `value1`,
"key3": `123`,
},
unknown: map[string]string{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.found == nil {
tt.found = map[string]string{}
}
if tt.unknown == nil {
tt.unknown = map[string]string{}
}

path := tempFile(t, tt.input)
p := json.New(&path)

found, unknown, err := p.Parse(tt.awaited, zfg.ToString)

if tt.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.found, found, "Found map mismatch")
assert.Equal(t, tt.unknown, unknown, "Unknown map mismatch")
}
})
}
}

func TestParse_Error(t *testing.T) {
path := tempFile(t, `{"invalid": "json",}`)
p := json.New(&path)

_, _, err := p.Parse(map[string]bool{}, zfg.ToString)
assert.Error(t, err)
assert.Contains(t, err.Error(), "unmarshal json")
}

func TestParse_EmptyInputNoError(t *testing.T) {
path := tempFile(t, ``)
p := json.New(&path)

found, unknown, err := p.Parse(map[string]bool{}, zfg.ToString)
assert.NoError(t, err)
assert.Empty(t, found)
assert.Empty(t, unknown)
}

func TestParse_FileNotExist(t *testing.T) {
nonExistentPath := "no_such_file.json"
p := json.New(&nonExistentPath)

found, unknown, err := p.Parse(map[string]bool{"some.key": true}, zfg.ToString)
require.NoError(t, err)
assert.Empty(t, found)
assert.Empty(t, unknown)
}

func tempFile(t *testing.T, data string) string {
f, err := os.CreateTemp("", "tmpjson-")
require.NoError(t, err)
t.Cleanup(func() {
f.Close()
os.Remove(f.Name())
})

_, err = f.WriteString(data)
require.NoError(t, err)

require.NoError(t, f.Close())

return f.Name()
}