From 158f14bbc5eb6f938faa1c6100a3b84f003fce2b Mon Sep 17 00:00:00 2001
From: Volker Schukai <volker.schukai@schukai.com>
Date: Mon, 24 Oct 2022 20:28:20 +0200
Subject: [PATCH] feat add path value #1

---
 file_test.go       |  1 +
 go.mod             |  3 +-
 go.sum             |  4 +++
 import.go          | 54 ++++++++++++++++++++++++++--
 issue-1_test.go    | 89 ++++++++++++++++++++++++++++++++++++++++++++++
 path-value.go      | 28 +++++++++++++++
 path-value_test.go | 58 ++++++++++++++++++++++++++++++
 7 files changed, 233 insertions(+), 4 deletions(-)
 create mode 100644 issue-1_test.go
 create mode 100644 path-value.go
 create mode 100644 path-value_test.go

diff --git a/file_test.go b/file_test.go
index ef8db61..51b3280 100644
--- a/file_test.go
+++ b/file_test.go
@@ -360,6 +360,7 @@ C:
 	})
 
 }
+
 func addSample2() {
 
 	e := &mockFile{}
diff --git a/go.mod b/go.mod
index f02d0d8..dbbb1af 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
 	github.com/magiconair/properties v1.8.6
 	github.com/pelletier/go-toml/v2 v2.0.5
 	github.com/r3labs/diff/v3 v3.0.0
-	github.com/stretchr/testify v1.8.0
+	github.com/stretchr/testify v1.8.1
 	gitlab.schukai.com/oss/libraries/go/application/xflags v1.9.0
 	gitlab.schukai.com/oss/libraries/go/network/http-negotiation v1.3.0
 	gitlab.schukai.com/oss/libraries/go/utilities/pathfinder v0.3.1
@@ -21,6 +21,7 @@ require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/stretchr/objx v0.5.0 // indirect
 	github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
 	golang.org/x/net v0.1.0 // indirect
 	golang.org/x/sys v0.1.0 // indirect
diff --git a/go.sum b/go.sum
index c2d8f12..7b4782a 100644
--- a/go.sum
+++ b/go.sum
@@ -23,10 +23,14 @@ github.com/r3labs/diff/v3 v3.0.0 h1:ZhPwNxn9gW5WLPBV9GCYaVbMdLOSmJ0DeKdCiSbOLUI=
 github.com/r3labs/diff/v3 v3.0.0/go.mod h1:wCkTySAiDnZao1sZrVTDIzuzgLZ+cNPGn3LC8DlIg5g=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
 github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
 gitlab.schukai.com/oss/libraries/go/application/xflags v1.9.0 h1:bSnwEV56JZQWBQCasMtCLXTkaSgdFhnoefJB4/2KN8c=
diff --git a/import.go b/import.go
index f53d918..486f02b 100644
--- a/import.go
+++ b/import.go
@@ -6,6 +6,7 @@ package configuration
 import (
 	"bytes"
 	"encoding/json"
+	"reflect"
 
 	"github.com/imdario/mergo"
 	"github.com/magiconair/properties"
@@ -46,7 +47,7 @@ func importProperties[C any](config *C, reader io.Reader) error {
 
 }
 
-func (s *Settings[C]) importStream(r reader) {
+func (s *Settings[C]) importStream(r reader, f ...func(n *C)) {
 	var c C
 	var err error
 
@@ -79,6 +80,12 @@ func (s *Settings[C]) importStream(r reader) {
 		s.errors = append(s.errors, err)
 	}
 
+	if f != nil {
+		for _, fn := range f {
+			fn(&c)
+		}
+	}
+
 	if err := mergo.Merge(&s.config, c); err != nil {
 		s.errors = append(s.errors, err)
 	}
@@ -90,6 +97,42 @@ func (s *Settings[C]) importStreams() {
 	}
 }
 
+func replacePath(p string, c any) {
+
+	if reflect.TypeOf(c).Kind() != reflect.Ptr {
+		panic("c must be a pointer")
+	}
+
+	if reflect.TypeOf(c).Elem().Kind() != reflect.Struct {
+		panic("c must be a pointer to a struct")
+	}
+
+	fields := reflect.VisibleFields(reflect.TypeOf(c).Elem())
+	for _, field := range fields {
+
+		r := reflect.ValueOf(c).Elem().FieldByName(field.Name)
+		if field.Type.Kind() == reflect.Struct {
+			if r.CanAddr() {
+				replacePath(p, r.Addr().Interface())
+			}
+			continue
+		}
+
+		_, ok := r.Interface().(pathInterface)
+		if ok {
+
+			if r.CanSet() {
+				if !path.IsAbs(r.String()) {
+					r.SetString(path.Join(p, r.String()))
+				}
+			}
+
+		}
+
+	}
+
+}
+
 func (s *Settings[C]) importFiles() {
 
 	s.fileWatch.Lock()
@@ -116,7 +159,9 @@ func (s *Settings[C]) importFiles() {
 		}
 
 		r := (io.Reader)(f)
-		s.importStream(reader{s.files.format, r})
+		s.importStream(reader{s.files.format, r}, func(c *C) {
+			replacePath(d, c)
+		})
 		f.Close()
 	}
 
@@ -129,7 +174,10 @@ func (s *Settings[C]) importFiles() {
 			continue
 		}
 
-		s.importStream(reader{f.format, r})
+		s.importStream(reader{f.format, r}, func(c *C) {
+			d := path.Dir(f.path)
+			replacePath(d, c)
+		})
 
 		r.Close()
 	}
diff --git a/issue-1_test.go b/issue-1_test.go
new file mode 100644
index 0000000..311fb3c
--- /dev/null
+++ b/issue-1_test.go
@@ -0,0 +1,89 @@
+package configuration
+
+import (
+	"bytes"
+	"github.com/stretchr/testify/assert"
+	"path"
+	"testing"
+	"time"
+)
+
+type ConfigIssue1SubStruct struct {
+	DA PathValue `yaml:"DA"`
+}
+
+type ConfigIssue1Struct struct {
+	A string                `yaml:"A"`
+	B PathValue             `yaml:"B"`
+	C PathValue             `yaml:"C"`
+	D ConfigIssue1SubStruct `yaml:"D"`
+}
+
+func addSampleIssue1() {
+
+	e := &mockFile{}
+
+	content := []byte(
+		`---
+A: "Hello!"
+B: "/tmp"
+C: "xyz.html"
+D:
+    DA: "tmp.xyz"
+...
+
+`)
+
+	e.buffer = bytes.NewBuffer(content)
+
+	e.FileInfo = mockFileInfo{
+		name: "test",
+		size: int64(len(content)),
+		mode: 0,
+		mod:  time.Now(),
+		dir:  false,
+		sys:  nil,
+	}
+
+	fileSamples = append(fileSamples, fileSample{
+		file: e,
+	})
+
+}
+
+func TestIssue1(t *testing.T) {
+
+	fileSamples = []fileSample{}
+	defer func() { fileSamples = nil }()
+
+	addSampleIssue1()
+
+	defaults := ConfigIssue1Struct{}
+
+	mockFs := mockFS{}
+
+	c := New(defaults)
+	c.SetMnemonic("test")
+	c.SetFileFormat(Yaml)
+	c.SetFilesystem(mockFs)
+
+	// setDefaultdirectories can be returned errors
+	c.SetDefaultDirectories().ResetErrors()
+	c.Import()
+
+	if c.HasErrors() {
+		t.Error("Expected not error", c.Errors())
+	}
+
+	assert.Equal(t, c.Config().A, "Hello!")
+	assert.Equal(t, c.Config().B, PathValue("/tmp"))
+
+	if !path.IsAbs(string(c.Config().C)) {
+		t.Error("Expected absolute path got ", c.Config().C)
+	}
+
+	if !path.IsAbs(string(c.Config().D.DA)) {
+		t.Error("Expected absolute path got ", c.Config().D.DA)
+	}
+
+}
diff --git a/path-value.go b/path-value.go
new file mode 100644
index 0000000..56856fa
--- /dev/null
+++ b/path-value.go
@@ -0,0 +1,28 @@
+package configuration
+
+import "path"
+
+type pathInterface interface {
+	String() string
+	Dir() string
+	Ext() string
+	Base() string
+}
+
+type PathValue string
+
+func (p PathValue) Dir() string {
+	return path.Dir(string(p))
+}
+
+func (p PathValue) Ext() string {
+	return path.Ext(string(p))
+}
+
+func (p PathValue) String() string {
+	return string(p)
+}
+
+func (p PathValue) Base() string {
+	return path.Base(string(p))
+}
diff --git a/path-value_test.go b/path-value_test.go
new file mode 100644
index 0000000..87a67c7
--- /dev/null
+++ b/path-value_test.go
@@ -0,0 +1,58 @@
+package configuration
+
+import (
+	"path"
+	"testing"
+)
+
+func TestPathValue(t *testing.T) {
+
+	data := []struct {
+		path     string
+		expected string
+		base     string
+	}{
+		{"/a/b/c", "/a/b", "c"},
+		{"/a/b/c/", "/a/b/c", "c"}, // trailing slash are ignored
+		{"/a/b/c/d.txt", "/a/b/c", "d.txt"},
+		{"/a/b/c/d/", "/a/b/c/d", "d"},
+		{"/a/b/c/d/e", "/a/b/c/d", "e"},
+		{"/a/b/../c/d/e", "/a/c/d", "e"},
+	}
+
+	for _, d := range data {
+		t.Run("Dir-"+d.path, func(t *testing.T) {
+			p := PathValue(d.path)
+			if p.Dir() != d.expected {
+				t.Errorf("Expected %s, got %s", d.expected, p.Dir())
+			}
+		})
+	}
+
+	for _, d := range data {
+		t.Run("Base-"+d.path, func(t *testing.T) {
+			p := PathValue(d.path)
+			if p.Base() != d.base {
+				t.Errorf("Expected %s, got %s", d.base, p.Base())
+			}
+		})
+	}
+
+	for _, d := range data {
+		t.Run("String-"+d.path, func(t *testing.T) {
+			p := PathValue(d.path)
+			if p.String() != string(p) {
+				t.Errorf("Expected %s, got %s", string(p), p.String())
+			}
+		})
+	}
+
+	for _, d := range data {
+		t.Run("Ext-"+d.path, func(t *testing.T) {
+			p := PathValue(d.path)
+			if p.Ext() != path.Ext(d.path) {
+				t.Errorf("Expected %s, got %s", path.Ext(d.path), p.Ext())
+			}
+		})
+	}
+}
-- 
GitLab