From a31d8713ac8dba74d47b6418a3c7d5bcd93c736a Mon Sep 17 00:00:00 2001 From: Paulo Lima Date: Thu, 14 Nov 2019 17:54:32 -0300 Subject: [PATCH] first commit --- .gitignore | 3 + README.md | 3 + buffer/buffer.go | 129 ++++++++++++++++++++ buffer/buffer_test.go | 180 +++++++++++++++++++++++++++ buffer/mock.go | 11 ++ entry.go | 66 ++++++++++ entry_test.go | 98 +++++++++++++++ go.mod | 14 +++ go.sum | 30 +++++ json/encoder/map.go | 34 ++++++ json/encoder/map_test.go | 68 +++++++++++ json/encoder/struct.go | 115 ++++++++++++++++++ json/encoder/struct_test.go | 180 +++++++++++++++++++++++++++ json/extension.go | 34 ++++++ json/extension_test.go | 80 ++++++++++++ json/json.go | 23 ++++ json/json_test.go | 33 +++++ level.go | 31 +++++ level_test.go | 29 +++++ splunk.go | 142 ++++++++++++++++++++++ splunk_test.go | 236 ++++++++++++++++++++++++++++++++++++ strings/strings.go | 55 +++++++++ strings/strings_test.go | 56 +++++++++ 23 files changed, 1650 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 buffer/buffer.go create mode 100644 buffer/buffer_test.go create mode 100644 buffer/mock.go create mode 100644 entry.go create mode 100644 entry_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 json/encoder/map.go create mode 100644 json/encoder/map_test.go create mode 100644 json/encoder/struct.go create mode 100644 json/encoder/struct_test.go create mode 100644 json/extension.go create mode 100644 json/extension_test.go create mode 100644 json/json.go create mode 100644 json/json_test.go create mode 100644 level.go create mode 100644 level_test.go create mode 100644 splunk.go create mode 100644 splunk_test.go create mode 100644 strings/strings.go create mode 100644 strings/strings_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b2e28a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Created by .ignore support plugin (hsz.mobi) + +.idea/** diff --git a/README.md b/README.md new file mode 100644 index 0000000..a18891f --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Splunk Writer + +TODO \ No newline at end of file diff --git a/buffer/buffer.go b/buffer/buffer.go new file mode 100644 index 0000000..f4198ba --- /dev/null +++ b/buffer/buffer.go @@ -0,0 +1,129 @@ +package buffer + +import ( + "fmt" + "sync" + "time" +) + +const ( + DefaultCapacity = 100 + DefaultOnWait = 100 + DefaultExpiration = 60000 + DefaultBackoff = 10000 +) + +type Buffer interface { + Write(item interface{}) +} + +type buffer struct { + sync.Locker + cap int + size int + expiration time.Duration + chunks chan entry + items []interface{} + backoff time.Duration +} + +func (b *buffer) Write(item interface{}) { + b.Lock() + defer b.Unlock() + b.items[b.size] = item + b.size++ + if b.size >= b.cap { + b.clear() + } +} + +func (b *buffer) clear() { + if b.size > 0 { + events := b.items[:b.size] + b.size = 0 + b.items = make([]interface{}, b.cap) + go func() { + b.chunks <- entry{ + items: events, + retries: cap(b.chunks), + } + }() + } +} + +func (b *buffer) watcher() { + defer func() { + err := recover() + if err != nil { + fmt.Printf("%v\n", err) + } + }() + for { + time.Sleep(b.expiration) + b.Lock() + b.clear() + b.Unlock() + } +} + +type Config struct { + Cap int + OnWait int + Expiration time.Duration + BackOff time.Duration + OnOverflow func([]interface{}) error +} + +type entry struct { + items []interface{} + retries int +} + +func New(c Config) Buffer { + if c.Cap == 0 { + c.Cap = DefaultCapacity + } + if c.Expiration == 0 { + c.Expiration = DefaultExpiration + } + if c.BackOff == 0 { + c.BackOff = DefaultBackoff + } + if c.OnWait == 0 { + c.OnWait = DefaultOnWait + } + + b := &buffer{ + Locker: &sync.Mutex{}, + size: 0, + cap: c.Cap, + expiration: c.Expiration, + chunks: make(chan entry, c.OnWait), + items: make([]interface{}, c.Cap), + backoff: c.BackOff, + } + go b.watcher() + go b.consumer(c) + return b +} + +func (b *buffer) consumer(c Config) { + defer func() { + err := recover() + if err != nil { + fmt.Printf("%v\n", err) + } + }() + for events := range b.chunks { + err := c.OnOverflow(events.items) + if err != nil { + go func(events entry) { + events.retries-- + if events.retries >= 0 { + time.Sleep(b.backoff) + b.chunks <- events + } + }(events) + } + } +} diff --git a/buffer/buffer_test.go b/buffer/buffer_test.go new file mode 100644 index 0000000..bf9d3bc --- /dev/null +++ b/buffer/buffer_test.go @@ -0,0 +1,180 @@ +// +build unit + +package buffer + +import ( + "errors" + "github.com/stretchr/testify/assert" + "sync" + "testing" + "time" +) + +func TestBuffer_Write(t *testing.T) { + t.Parallel() + t.Run("when the buffer is not full", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + subject := &buffer{ + Locker: &sync.Mutex{}, + size: 0, + cap: 10, + items: make([]interface{}, 10), + chunks: make(chan []interface{}, 10), + } + subject.Write("something") + is.Equal(1, subject.size, "it should increment the size of the buffer in one unit") + is.Equal([]interface{}{"something", nil, nil, nil, nil, nil, nil, nil, nil, nil}, subject.items, "it should change the buffer's inner slice") + }) + t.Run("when the buffer is full", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + subject := &buffer{ + Locker: &sync.Mutex{}, + size: 0, + cap: 1, + items: make([]interface{}, 1), + chunks: make(chan []interface{}, 10), + } + subject.Write("something") + is.Equal(0, subject.size, "it should remain zero") + is.Equal([]interface{}{nil}, subject.items, "it should clean the buffer's inner slice") + timeout := time.NewTimer(10 * time.Millisecond) + select { + case actual := <-subject.chunks: + is.Equal([]interface{}{"something"}, actual, "it should read the expected slice") + case <-timeout.C: + is.Fail("nothing was published") + } + }) +} + +func TestNew(t *testing.T) { + t.Parallel() + t.Run("when the buffer expires", func(t *testing.T) { + t.Parallel() + t.Run("but the consumer returns an error", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + called := make(chan []interface{}, 1) + err := errors.New("") + subject := New(Config{ + OnOverflow: func(items []interface{}) error { + called <- items + err1 := err + err = nil + return err1 + }, + BackOff: 10 * time.Millisecond, + Expiration: 10 * time.Millisecond, + Cap: 10, + OnWait: 10, + }) + subject.Write(1) + subject.Write(2) + subject.Write(3) + timeout := time.NewTimer(20 * time.Millisecond) + select { + case items := <-called: + is.Equal([]interface{}{1, 2, 3}, items, "it should return the expected array") + case <-timeout.C: + is.Fail("nothing was published") + } + timeout = time.NewTimer(20 * time.Millisecond) + select { + case items := <-called: + is.Equal([]interface{}{1, 2, 3}, items, "it should return the expected array") + case <-timeout.C: + is.Fail("nothing was published") + } + }) + t.Run("and the consumer returns no error", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + called := make(chan []interface{}, 1) + subject := New(Config{ + OnOverflow: func(items []interface{}) error { + called <- items + return nil + }, + BackOff: 10 * time.Millisecond, + Expiration: 10 * time.Millisecond, + Cap: 10, + OnWait: 10, + }) + subject.Write(1) + subject.Write(2) + subject.Write(3) + timeout := time.NewTimer(20 * time.Millisecond) + select { + case items := <-called: + is.Equal([]interface{}{1, 2, 3}, items, "it should return the expected array") + case <-timeout.C: + is.Fail("nothing was published") + } + }) + + }) + t.Run("when the buffer overflow", func(t *testing.T) { + t.Run("but the consumer returns an error", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + called := make(chan []interface{}, 1) + err := errors.New("") + subject := New(Config{ + OnOverflow: func(items []interface{}) error { + called <- items + err1 := err + err = nil + return err1 + }, + BackOff: 10 * time.Millisecond, + Expiration: 100 * time.Millisecond, + Cap: 3, + OnWait: 10, + }) + subject.Write(1) + subject.Write(2) + subject.Write(3) + timeout := time.NewTimer(20 * time.Millisecond) + select { + case items := <-called: + is.Equal([]interface{}{1, 2, 3}, items, "it should return the expected array") + case <-timeout.C: + is.Fail("nothing was published") + } + timeout = time.NewTimer(20 * time.Millisecond) + select { + case items := <-called: + is.Equal([]interface{}{1, 2, 3}, items, "it should return the expected array") + case <-timeout.C: + is.Fail("nothing was published") + } + }) + t.Run("and the consumer returns no error", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + called := make(chan []interface{}, 1) + subject := New(Config{ + OnOverflow: func(items []interface{}) error { + called <- items + return nil + }, + BackOff: 10 * time.Millisecond, + Expiration: 100 * time.Millisecond, + Cap: 3, + OnWait: 10, + }) + subject.Write(1) + subject.Write(2) + subject.Write(3) + timeout := time.NewTimer(20 * time.Millisecond) + select { + case items := <-called: + is.Equal([]interface{}{1, 2, 3}, items, "it should return the expected array") + case <-timeout.C: + is.Fail("nothing was published") + } + }) + }) +} diff --git a/buffer/mock.go b/buffer/mock.go new file mode 100644 index 0000000..965209c --- /dev/null +++ b/buffer/mock.go @@ -0,0 +1,11 @@ +package buffer + +import "github.com/stretchr/testify/mock" + +type Mock struct { + mock.Mock +} + +func (m *Mock) Write(item interface{}) { + m.Called(item) +} diff --git a/entry.go b/entry.go new file mode 100644 index 0000000..9a90c29 --- /dev/null +++ b/entry.go @@ -0,0 +1,66 @@ +package splunk + +import ( + "fmt" + "reflect" +) + +type Entry map[string]interface{} + +func (log Entry) Add(name string, value interface{}) Entry { + log[name] = value + return log +} + +func NewEntry(p ...interface{}) Entry { + if len(p) == 1 { + c, ok := p[0].([]interface{}) + if ok { + p = c + } + } + normalized := Entry{} + for _, item := range p { + if item == nil { + continue + } + itemType := reflect.TypeOf(item) + v := reflect.ValueOf(item) + inner := Entry{} + switch itemType.Kind() { + case reflect.Map: + for _, key := range v.MapKeys() { + inner[fmt.Sprint(key.Interface())] = v.MapIndex(key).Interface() + } + case reflect.Ptr, reflect.Interface: + if !v.IsNil() { + inner = NewEntry(v.Elem().Interface()) + } + default: + inner[itemType.Name()] = item + } + normalized = Merge(normalized, inner) + } + return normalized +} + +func Merge(np Entry, other Entry) Entry { + collisions := map[string]int{} + r := Entry{} + for key, value := range np { + r[key] = value + } + for key, value := range other { + if _, ok := r[key]; ok { + var index int + if index, ok = collisions[key]; !ok { + index = 0 + } + index += 1 + collisions[key] = index + key = fmt.Sprintf("%s%d", key, index) + } + r[key] = value + } + return r +} diff --git a/entry_test.go b/entry_test.go new file mode 100644 index 0000000..d1f18a8 --- /dev/null +++ b/entry_test.go @@ -0,0 +1,98 @@ +// +build unit + +package splunk + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestEntry_Add(t *testing.T) { + t.Parallel() + is := assert.New(t) + subject := Entry{ + "A": 15, + } + subject.Add("B", 16) + expected := Entry{ + "A": 15, + "B": 16, + } + is.Equal(expected, subject, "it should update the entry") +} + +func TestNew(t *testing.T) { + t.Parallel() + t.Run("when an interface slice is given", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + type S struct { + B int + } + items := []interface{}{ + "A", + Entry{ + "X": "Y", + }, + nil, + S{15}, + &S{16}, + } + actual := NewEntry(items) + expected := Entry{ + "string": "A", + "X": "Y", + "S": S{15}, + "S1": S{16}, + } + is.Equal(expected, actual, "it should return the expected entry") + }) + t.Run("when multiple items are given", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + type S struct { + B int + } + items := []interface{}{ + "A", + Entry{ + "X": "Y", + }, + S{15}, + &S{16}, + } + actual := NewEntry(items...) + expected := Entry{ + "string": "A", + "X": "Y", + "S": S{15}, + "S1": S{16}, + } + is.Equal(expected, actual, "it should return the expected entry") + }) +} + +func TestMerge(t *testing.T) { + t.Parallel() + is := assert.New(t) + inputA := Entry{ + "A": 15, + "B": 16, + "C": 17, + } + inputB := Entry{ + "A": 20, + "E": 21, + "F": 22, + } + actual := Merge(inputA, inputB) + expected := Entry{ + "A": 15, + "B": 16, + "C": 17, + "A1": 20, + "E": 21, + "F": 22, + } + is.Equal(expected, actual, "it should return the expected value") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..67134d2 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/mundipagg/tracer-splunk-writer + +go 1.12 + +require ( + github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7 + github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428 + github.com/jarcoal/httpmock v1.0.3 + github.com/json-iterator/go v1.1.6 + github.com/modern-go/reflect2 v1.0.1 + github.com/mralves/tracer v1.7.3 + github.com/mundipagg/tracer-splunk-writer v1.1.8 + github.com/stretchr/testify v1.3.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0508078 --- /dev/null +++ b/go.sum @@ -0,0 +1,30 @@ +github.com/corpix/uarand v0.0.0 h1:mNbzro1GwUcZ1hmO2rWXytkR3JBxNxxctzjyuhO+Aig= +github.com/corpix/uarand v0.0.0/go.mod h1:JSm890tOkDN+M1jqN8pUGDKnzJrsVbJwSMHBY4zwz7M= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7 h1:ux/56T2xqZO/3cP1I2F86qpeoYPCOzk+KF/UH/Ar+lk= +github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= +github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428 h1:Mo9W14pwbO9VfRe+ygqZ8dFbPpoIK1HFrG/zjTuQ+nc= +github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo= +github.com/jarcoal/httpmock v1.0.1 h1:OXIOrglWeSllwHQGJ5X4PX4hFZK1DPCXSJVhMSJacg8= +github.com/jarcoal/httpmock v1.0.1/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= +github.com/jarcoal/httpmock v1.0.3 h1:Qgv39cyHvgEguAofjb5GomnBCm10Dq71K+k1Aq0h7/o= +github.com/jarcoal/httpmock v1.0.3/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= +github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mralves/tracer v1.7.3 h1:XFdPXX85HMidTmyzBUFdGBlLmFpRaazoaAEqmeBHYTE= +github.com/mralves/tracer v1.7.3/go.mod h1:PV/IuPDoIH4gY/vKi9Iev72t0CoVlwjErQ5DyWhH2TI= +github.com/mundipagg/tracer-splunk-writer v1.1.8 h1:yA86ommkfzhLAGElRq7lkzORuv/yq0ChAHU3Q3uxeDY= +github.com/mundipagg/tracer-splunk-writer v1.1.8/go.mod h1:rtO9Po02B572vH8E6xXrMjUQI0w/kowdLng6kAGQZdE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/v2pro/plz v0.0.0-20180227161703-2d49b86ea382 h1:SDwpMM36MwogjPMfRBmFy5koN6QC5NmdrXPxNRQZ6Ek= +github.com/v2pro/plz v0.0.0-20180227161703-2d49b86ea382/go.mod h1:6xoYDIZTeCY25tlsJC/zNlCh84xCKwBSAXwKF32tdIg= diff --git a/json/encoder/map.go b/json/encoder/map.go new file mode 100644 index 0000000..70433d4 --- /dev/null +++ b/json/encoder/map.go @@ -0,0 +1,34 @@ +package encoder + +import ( + "fmt" + "github.com/json-iterator/go" + "os" + "unsafe" +) + +type Map struct { + Strategy func(string) string +} + +func (enc *Map) IsEmpty(ptr unsafe.Pointer) bool { + s := (*string)(ptr) + return s == nil || *s == "" +} + +func (enc *Map) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) { + beforeBuffer := stream.Buffer() + defer func() { + err := recover() + if err != nil { + fmt.Fprintf(os.Stderr, "a error occurred while serialization of 'map', error: '%v'", err) + stream.SetBuffer(beforeBuffer) + } + }() + if enc.IsEmpty(ptr) { + stream.WriteString("") + } else { + s := (*string)(ptr) + stream.WriteString(enc.Strategy(*s)) + } +} diff --git a/json/encoder/map_test.go b/json/encoder/map_test.go new file mode 100644 index 0000000..58ccc58 --- /dev/null +++ b/json/encoder/map_test.go @@ -0,0 +1,68 @@ +// +build unit + +package encoder + +import ( + "bytes" + "github.com/json-iterator/go" + "github.com/stretchr/testify/assert" + "reflect" + "strconv" + "testing" + "unsafe" +) + +func TestMap_Encode(t *testing.T) { + t.Parallel() + t.Run("when the input is 'empty'", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + called := 0 + expected := "" + subject := &Map{ + Strategy: func(s string) string { + called++ + return expected + }, + } + input := "" + pointer := reflect.ValueOf(&input).Pointer() + buf := &bytes.Buffer{} + stream := jsoniter.NewStream(jsoniter.ConfigFastest, buf, 100) + subject.Encode(unsafe.Pointer(pointer), stream) + _ = stream.Flush() + is.Equal(strconv.Quote(expected), buf.String()) + }) + t.Run("when the input is not 'empty'", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + called := 0 + expected := "output" + subject := &Map{ + Strategy: func(s string) string { + called++ + return expected + }, + } + input := "input" + pointer := reflect.ValueOf(&input).Pointer() + buf := &bytes.Buffer{} + stream := jsoniter.NewStream(jsoniter.ConfigFastest, buf, 100) + subject.Encode(unsafe.Pointer(pointer), stream) + _ = stream.Flush() + is.Equal(strconv.Quote(expected), buf.String()) + }) +} + +func TestMap_IsEmpty(t *testing.T) { + t.Parallel() + is := assert.New(t) + subject := &Map{} + input := "input" + pointer := reflect.ValueOf(&input).Pointer() + is.False(subject.IsEmpty(unsafe.Pointer(pointer))) + input = "" + pointer = reflect.ValueOf(&input).Pointer() + is.True(subject.IsEmpty(unsafe.Pointer(pointer))) + is.True(subject.IsEmpty(nil)) +} diff --git a/json/encoder/struct.go b/json/encoder/struct.go new file mode 100644 index 0000000..45e8fc5 --- /dev/null +++ b/json/encoder/struct.go @@ -0,0 +1,115 @@ +package encoder + +import ( + "encoding/json" + "fmt" + "os" + "reflect" + "strings" + "unsafe" + + jsoniter "github.com/json-iterator/go" +) + +type Struct struct { + Type reflect.Type + Strategy func(string) string +} + +func (changer *Struct) IsEmpty(ptr unsafe.Pointer) bool { + return false +} + +func (changer *Struct) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) { + beforeBuffer := stream.Buffer() + defer func() { + err := recover() + if err != nil { + fmt.Fprintf(os.Stderr, "a error occurred while serialization of '%v', error: '%v'", changer.Type.Name(), err) + stream.SetBuffer(beforeBuffer) + } + }() + v := reflect.NewAt(changer.Type, ptr).Elem() + switch value := v.Interface().(type) { + case json.Marshaler: + valueJ, _ := value.MarshalJSON() + var valueM interface{} + _ = json.Unmarshal(valueJ, &valueM) + stream.WriteVal(valueM) + case error: + stream.WriteString(value.Error()) + default: + stream.WriteObjectStart() + numFields := v.NumField() + if numFields > 0 { + var i int + for i = 0; i < numFields; i++ { + fv := v.Field(i) + ft := changer.Type.Field(i) + if changer.writeField(ft, fv, stream, false) { + break + } + } + i++ + for ; i < numFields; i++ { + fv := v.Field(i) + ft := changer.Type.Field(i) + changer.writeField(ft, fv, stream, true) + } + } + stream.WriteObjectEnd() + } +} + +func (changer *Struct) writeField(structField reflect.StructField, value reflect.Value, stream *jsoniter.Stream, needsComma bool) bool { + if !value.CanInterface() { + return false + } + + tag := strings.TrimSpace(structField.Tag.Get("json")) + + if len(tag) == 0 { + if needsComma { + stream.WriteMore() + } + stream.WriteObjectField(changer.Strategy(structField.Name)) + stream.WriteVal(value.Interface()) + + } else { + pieces := strings.Split(tag, ",") + if len(pieces) > 1 { + if pieces[1] == "omitempty" { + isZero := func() (isZero bool) { + defer func() { + if recover() != nil { + isZero = false + } + }() + return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface()) + }() + isNil := func() (isNil bool) { + defer func() { + if recover() != nil { + isNil = false + } + }() + return value.IsNil() + }() + if isNil || isZero { + return false + } + } + } + + if pieces[0] == "-" { + return false + } + + if needsComma { + stream.WriteMore() + } + stream.WriteObjectField(changer.Strategy(pieces[0])) + stream.WriteVal(value.Interface()) + } + return true +} diff --git a/json/encoder/struct_test.go b/json/encoder/struct_test.go new file mode 100644 index 0000000..0bd77a4 --- /dev/null +++ b/json/encoder/struct_test.go @@ -0,0 +1,180 @@ +// +build unit + +package encoder + +import ( + "bytes" + "errors" + "github.com/json-iterator/go" + "github.com/stretchr/testify/assert" + "reflect" + "strings" + "testing" + "unsafe" +) + +func TestStruct_IsEmpty(t *testing.T) { + t.Parallel() + is := assert.New(t) + subject := &Struct{} + input := struct{}{} + pointer := reflect.ValueOf(&input).Pointer() + is.False(subject.IsEmpty(unsafe.Pointer(pointer))) +} + +type V struct { + A int +} + +func (V) MarshalJSON() ([]byte, error) { + return jsoniter.Marshal("custom") +} + +func TestStruct_Encode(t *testing.T) { + t.Parallel() + t.Run("when the value implements the json.Marshaller interface", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + input := V{15} + called := 0 + subject := &Struct{ + Strategy: func(s string) string { + called++ + return strings.ToLower(s) + }, + Type: reflect.TypeOf(input), + } + buf := &bytes.Buffer{} + stream := jsoniter.NewStream(jsoniter.ConfigFastest, buf, 100) + subject.Encode(unsafe.Pointer(reflect.ValueOf(&input).Pointer()), stream) + stream.Flush() + is.Equal(`"custom"`, buf.String(), "it should change the name of the field") + is.Equal(0, called, "it should not call the strategy ") + }) + t.Run("when the value implements the error interface", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + input := errors.New("error") + called := 0 + subject := &Struct{ + Strategy: func(s string) string { + called++ + return strings.ToLower(s) + }, + Type: reflect.TypeOf(input), + } + buf := &bytes.Buffer{} + + stream := jsoniter.NewStream(jsoniter.ConfigFastest, buf, 100) + ptr := reflect.New(reflect.TypeOf(input)) + ptr.Elem().Set(reflect.ValueOf(input)) + subject.Encode(unsafe.Pointer(ptr.Pointer()), stream) + stream.Flush() + is.Equal(`"error"`, buf.String(), "it should change the name of the field") + is.Equal(0, called, "it should not call the strategy ") + }) + t.Run("when the field does not have a json tag", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + input := struct { + A int + b int + }{ + A: 15, + b: 15, + } + called := 0 + subject := &Struct{ + Strategy: func(s string) string { + called++ + return strings.ToLower(s) + }, + Type: reflect.TypeOf(input), + } + buf := &bytes.Buffer{} + stream := jsoniter.NewStream(jsoniter.ConfigFastest, buf, 100) + subject.Encode(unsafe.Pointer(reflect.ValueOf(&input).Pointer()), stream) + stream.Flush() + is.Equal(`{"a":15}`, buf.String(), "it should change the name of the field") + is.Equal(1, called, "it should call the strategy exactly one time") + }) + t.Run("when the field does not have omitempty on it's tag", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + input := struct { + A int `json:"SuperParameter"` + }{ + A: 15, + } + called := 0 + subject := &Struct{ + Strategy: func(s string) string { + called++ + return strings.ToUpper(s) + }, + Type: reflect.TypeOf(input), + } + buf := &bytes.Buffer{} + stream := jsoniter.NewStream(jsoniter.ConfigFastest, buf, 100) + subject.Encode(unsafe.Pointer(reflect.ValueOf(&input).Pointer()), stream) + stream.Flush() + is.Equal(`{"SUPERPARAMETER":15}`, buf.String(), "it should change the name of the field") + is.Equal(1, called, "it should call the strategy exactly one time") + }) + t.Run("when the field does have omitempty on it's tag", func(t *testing.T) { + t.Parallel() + t.Run("but the field is not empty", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + v := 16 + input := struct { + A int `json:"SuperParameterA"` + B *int `json:"SuperParameterB,omitempty"` + C int `json:"SuperParameterC,omitempty"` + D *int `json:"SuperParameterD,omitempty"` + E int `json:"SuperParameterE"` + }{ + A: 15, + D: &v, + E: 17, + } + called := 0 + subject := &Struct{ + Strategy: func(s string) string { + called++ + return strings.ToUpper(s) + }, + Type: reflect.TypeOf(input), + } + buf := &bytes.Buffer{} + stream := jsoniter.NewStream(jsoniter.ConfigFastest, buf, 100) + subject.Encode(unsafe.Pointer(reflect.ValueOf(&input).Pointer()), stream) + stream.Flush() + is.Equal(`{"SUPERPARAMETERA":15,"SUPERPARAMETERD":16,"SUPERPARAMETERE":17}`, buf.String(), "it should change the name of the field") + is.Equal(3, called, "it should call the strategy exactly three times") + }) + t.Run("and the field is empty", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + input := struct { + A *int `json:"SuperParameter,omitempty"` + }{ + A: nil, + } + called := 0 + subject := &Struct{ + Strategy: func(s string) string { + called++ + return strings.ToUpper(s) + }, + Type: reflect.TypeOf(input), + } + buf := &bytes.Buffer{} + stream := jsoniter.NewStream(jsoniter.ConfigFastest, buf, 100) + subject.Encode(unsafe.Pointer(reflect.ValueOf(&input).Pointer()), stream) + stream.Flush() + is.Equal(`{}`, buf.String(), "it should change the name of the field") + is.Equal(0, called, "it should not call the strategy") + }) + }) +} diff --git a/json/extension.go b/json/extension.go new file mode 100644 index 0000000..5625dea --- /dev/null +++ b/json/extension.go @@ -0,0 +1,34 @@ +package json + +import ( + "reflect" + + jsoniter "github.com/json-iterator/go" + "github.com/modern-go/reflect2" + "github.com/mundipagg/tracer-splunk-writer/json/encoder" +) + +type CaseStrategyExtension struct { + jsoniter.DummyExtension + Strategy func(string) string +} + +func (cs *CaseStrategyExtension) CreateMapKeyEncoder(typ reflect2.Type) jsoniter.ValEncoder { + if typ.Kind() == reflect.String { + return &encoder.Map{ + Strategy: cs.Strategy, + } + } + return nil +} + +func (cs *CaseStrategyExtension) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder { + ty := typ.Type1() + if ty.Kind() == reflect.Struct { + return &encoder.Struct{ + Type: ty, + Strategy: cs.Strategy, + } + } + return nil +} diff --git a/json/extension_test.go b/json/extension_test.go new file mode 100644 index 0000000..f10d84e --- /dev/null +++ b/json/extension_test.go @@ -0,0 +1,80 @@ +// +build unit + +package json + +import ( + "github.com/modern-go/reflect2" + "github.com/mundipagg/tracer-splunk-writer/json/encoder" + "github.com/stretchr/testify/assert" + "reflect" + "testing" +) + +func TestCaseStrategyExtension_CreateMapKeyEncoder(t *testing.T) { + t.Parallel() + t.Run("when the key is a string", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + called := 0 + subject := &CaseStrategyExtension{ + Strategy: func(s string) string { + called++ + return s + }, + } + typ := reflect2.Type2(reflect.TypeOf("")) + actual := subject.CreateMapKeyEncoder(typ) + is.IsType(&encoder.Map{}, actual, "it should return a Map encoder") + is.Equal(0, called, "it should not call the strategy") + }) + t.Run("when the key is not a string", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + called := 0 + subject := &CaseStrategyExtension{ + Strategy: func(s string) string { + called++ + return s + }, + } + typ := reflect2.Type2(reflect.TypeOf(1)) + actual := subject.CreateMapKeyEncoder(typ) + is.Nil(actual, "it should return nil") + is.Equal(0, called, "it should not call the strategy") + }) +} + +func TestCaseStrategyExtension_CreateEncoder(t *testing.T) { + t.Parallel() + t.Run("when the key is a struct", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + called := 0 + subject := &CaseStrategyExtension{ + Strategy: func(s string) string { + called++ + return s + }, + } + typ := reflect2.Type2(reflect.TypeOf(struct { + }{})) + actual := subject.CreateEncoder(typ) + is.IsType(&encoder.Struct{}, actual, "it should return a Struct encoder") + is.Equal(0, called, "it should not call the strategy") + }) + t.Run("when the key is not a struct", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + called := 0 + subject := &CaseStrategyExtension{ + Strategy: func(s string) string { + called++ + return s + }, + } + typ := reflect2.Type2(reflect.TypeOf(1)) + actual := subject.CreateEncoder(typ) + is.Nil(actual, "it should return nil") + is.Equal(0, called, "it should not call the strategy") + }) +} diff --git a/json/json.go b/json/json.go new file mode 100644 index 0000000..71e435b --- /dev/null +++ b/json/json.go @@ -0,0 +1,23 @@ +package json + +import ( + "github.com/json-iterator/go" +) + +func New() jsoniter.API { + return NewWithCaseStrategy(func(s string) string { + return s + }) +} + +func NewWithCaseStrategy(strategy func(string) string) jsoniter.API { + json := jsoniter.Config{ + EscapeHTML: false, + MarshalFloatWith6Digits: false, + ObjectFieldMustBeSimpleString: true, + }.Froze() + json.RegisterExtension(&CaseStrategyExtension{ + Strategy: strategy, + }) + return json +} diff --git a/json/json_test.go b/json/json_test.go new file mode 100644 index 0000000..97a565c --- /dev/null +++ b/json/json_test.go @@ -0,0 +1,33 @@ +// +build unit + +package json + +import ( + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +func TestNewWithCaseStrategy(t *testing.T) { + t.Parallel() + is := assert.New(t) + called := 0 + subject := NewWithCaseStrategy(func(s string) string { + called++ + return strings.ToUpper(s) + }) + + bytes, err := subject.Marshal(map[string]string{"a": "b"}) + is.Nil(err, "it should return no error") + is.Equal(`{"A":"b"}`, string(bytes), "it should return the expected json") + is.Equal(1, called, "it should call the strategy exactly one time") +} + +func TestNew(t *testing.T) { + t.Parallel() + is := assert.New(t) + subject := New() + bytes, err := subject.Marshal(map[string]string{"a": "b"}) + is.Nil(err, "it should return no error") + is.Equal(`{"a":"b"}`, string(bytes), "it should return the expected json") +} diff --git a/level.go b/level.go new file mode 100644 index 0000000..91f798d --- /dev/null +++ b/level.go @@ -0,0 +1,31 @@ +package splunk + +import ( + "github.com/mralves/tracer" +) + +const ( + Debug = "Debug" + Information = "Information" + Warning = "Warning" + Error = "Error" + Fatal = "Fatal" + Verbose = "Verbose" +) + +func Level(level uint8) string { + switch level { + case tracer.Debug: + return Debug + case tracer.Informational: + return Information + case tracer.Notice, tracer.Warning: + return Warning + case tracer.Critical, tracer.Error: + return Error + case tracer.Alert, tracer.Fatal: + return Fatal + default: + return Verbose + } +} diff --git a/level_test.go b/level_test.go new file mode 100644 index 0000000..3cfd20b --- /dev/null +++ b/level_test.go @@ -0,0 +1,29 @@ +// +build unit + +package splunk + +import ( + "github.com/mralves/tracer" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestLevel(t *testing.T) { + t.Parallel() + is := assert.New(t) + cases := map[uint8]string{ + tracer.Debug: Debug, + tracer.Informational: Information, + tracer.Notice: Warning, + tracer.Warning: Warning, + tracer.Error: Error, + tracer.Critical: Error, + tracer.Alert: Fatal, + tracer.Fatal: Fatal, + 9: Verbose, + } + for input, expected := range cases { + actual := Level(input) + is.Equal(expected, actual, "it should return the expected value") + } +} diff --git a/splunk.go b/splunk.go new file mode 100644 index 0000000..6e018b1 --- /dev/null +++ b/splunk.go @@ -0,0 +1,142 @@ +package splunk + +import ( + "bytes" + "errors" + "fmt" + "net/http" + "os" + "regexp" + "sync" + "time" + + "strings" + + jsoniter "github.com/json-iterator/go" + "github.com/mralves/tracer" + "github.com/mundipagg/tracer-splunk-writer/buffer" + "github.com/mundipagg/tracer-splunk-writer/json" + s "github.com/mundipagg/tracer-splunk-writer/strings" +) + +type Writer struct { + sync.Locker + address string + key string + configLineLog map[string]interface{} + defaultPropertiesSplunk map[string]interface{} + defaultPropertiesApp map[string]interface{} + client *http.Client + buffer buffer.Buffer + minimumLevel uint8 + marshaller jsoniter.API + messageEnvelop string +} + +var punctuation = regexp.MustCompile(`(.+?)[?;:\\.,!]?$`) + +//Used when message contains properties to replace. +var r = strings.NewReplacer("{", "{{.", "}", "}}") + +func (sw *Writer) Write(entry tracer.Entry) { + go func(sw *Writer, entry tracer.Entry) { + defer func() { + if err := recover(); err != nil { + stderr("COULD NOT SEND SPLUNK TO SEQ BECAUSE %v", err) + } + }() + if entry.Level > sw.minimumLevel { + return + } + + properties := NewEntry(append(entry.Args, sw.defaultPropertiesApp)) + message := punctuation.FindStringSubmatch(s.Capitalize(entry.Message))[1] + message = s.ProcessString(r.Replace(message), properties) + + l := NewEntry(sw.configLineLog) + e := NewEntry(Entry{ + "AdditionalData": properties, + "Message": message, + "Severity": Level(entry.Level), + }, sw.defaultPropertiesSplunk) + l.Add("event", e) + + sw.buffer.Write(l) + }(sw, entry) +} + +func (sw *Writer) send(events []interface{}) error { + defer func() { + err := recover() + if err != nil { + fmt.Printf("%v\n", err) + } + }() + + body, err := sw.marshaller.Marshal(events) + if err != nil { + stderr("COULD NOT SEND LOG TO SPLUNK BECAUSE %v, log: %v", err, string(body)) + return err + } + + request, _ := http.NewRequest(http.MethodPost, sw.address, bytes.NewBuffer(body)) + if len(sw.key) > 0 { + request.Header.Set("Authorization", "Splunk "+sw.key) + } + request.Header.Set("Content-Type", "application/json") + + var response *http.Response + response, err = sw.client.Do(request) + if err != nil { + stderr("COULD NOT SEND LOG TO SPLUNK BECAUSE %v, log: %v", err, string(body)) + return err + } + response.Body.Close() + if response.StatusCode != 200 { + stderr("COULD NOT SEND LOG TO SPLUNK BECAUSE %v, log: %v", response.Status, string(body)) + return errors.New(fmt.Sprintf("request returned %v", response.StatusCode)) + } + + return nil +} + +func stderr(message string, args ...interface{}) { + fmt.Fprintf(os.Stderr, message+"\n", args...) +} + +type Config struct { + Address string + Key string + Application string + Buffer buffer.Config + MinimumLevel uint8 + Timeout time.Duration + ConfigLineLog Entry + DefaultPropertiesSplunk Entry + DefaultPropertiesApp Entry + MessageEnvelop string +} + +func New(config Config) *Writer { + writer := Writer{ + Locker: &sync.RWMutex{}, + address: config.Address, + key: config.Key, + client: &http.Client{ + Timeout: config.Timeout, + Transport: &http.Transport{ + TLSHandshakeTimeout: config.Timeout, + IdleConnTimeout: config.Timeout, + }, + }, + messageEnvelop: config.MessageEnvelop, + minimumLevel: config.MinimumLevel, + configLineLog: config.ConfigLineLog, + defaultPropertiesSplunk: config.DefaultPropertiesSplunk, + defaultPropertiesApp: config.DefaultPropertiesApp, + marshaller: json.NewWithCaseStrategy(s.UseAnnotation), + } + config.Buffer.OnOverflow = writer.send + writer.buffer = buffer.New(config.Buffer) + return &writer +} diff --git a/splunk_test.go b/splunk_test.go new file mode 100644 index 0000000..e3c7f28 --- /dev/null +++ b/splunk_test.go @@ -0,0 +1,236 @@ +//+build unit + +package splunk + +import ( + "errors" + "net/http" + "os" + "sync" + "testing" + "time" + + "github.com/icrowley/fake" + "github.com/jarcoal/httpmock" + "github.com/mralves/tracer" + "github.com/mundipagg/tracer-splunk-writer/buffer" + "github.com/mundipagg/tracer-splunk-writer/json" + "github.com/stretchr/testify/assert" +) + +func TestWriter_Write(t *testing.T) { + os.Stderr, _ = os.Open(os.DevNull) + t.Parallel() + t.Run("when the minimum level is higher than the log level received", func(t *testing.T) { + t.Parallel() + buf := &buffer.Mock{} + ref := time.Now() + stackTrace := tracer.GetStackTrace(3) + subject := &Writer{ + buffer: buf, + minimumLevel: tracer.Error, + } + + entry := tracer.Entry{ + Level: tracer.Debug, + Message: "Message", + StackTrace: stackTrace, + Time: ref, + Owner: "owner", + TransactionId: "Transaction", + Args: []interface{}{ + "Arg", + Entry{ + "Nested": "value", + }, + }, + } + subject.Write(entry) + time.Sleep(30 * time.Millisecond) + buf.AssertExpectations(t) + }) + t.Run("when the minimum level is lower than the log level received", func(t *testing.T) { + t.Parallel() + buf := &buffer.Mock{} + ref := time.Now() + stackTrace := tracer.GetStackTrace(3) + event := event{ + + Level: Error, + MessageTemplate: "Before Message After", + Properties: Entry{ + "string": "Arg", + "Nested": "value", + "Caller": stackTrace[0].String(), + "RequestKey": "Transaction", + "Name": "Default", + }, + Timestamp: ref.UTC().Format(time.RFC3339Nano), + } + buf.On("Write", event).Return() + subject := &Writer{ + buffer: buf, + minimumLevel: tracer.Debug, + messageEnvelop: "Before %v After", + defaultProperties: Entry{ + "Name": "Default", + }, + } + + entry := tracer.Entry{ + Level: tracer.Critical, + Message: "Message", + StackTrace: stackTrace, + Time: ref, + Owner: "owner", + TransactionId: "Transaction", + Args: []interface{}{ + "Arg", + Entry{ + "Nested": "value", + }, + }, + } + subject.Write(entry) + time.Sleep(30 * time.Millisecond) + buf.AssertExpectations(t) + }) +} + +func TestWriter_Send(t *testing.T) { + t.Parallel() + t.Run("when there is an invalid field value in event", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + c := &http.Client{} + activateNonDefault(c) + subject := &Writer{ + address: "http://log.io/", + client: c, + marshaller: json.New(), + } + err := subject.send([]interface{}{ + event{ + Properties: Entry{ + "C": make(chan int), + }, + }, + }) + is.NotNil(err, "it should return an error") + }) + t.Run("when the request fails", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + c := &http.Client{} + activateNonDefault(c) + url := "http://log.io/" + fake.Password(8, 8, false, false, false) + httpmock.RegisterResponder("POST", url, func(request *http.Request) (response *http.Response, err error) { + return nil, errors.New("failed") + }) + subject := &Writer{ + address: url, + client: c, + marshaller: json.New(), + } + err := subject.send([]interface{}{ + event{ + Properties: Entry{ + "C": 15, + }, + }, + }) + is.NotNil(err, "it should return an error") + }) + t.Run("when the request fails", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + c := &http.Client{} + activateNonDefault(c) + url := "http://log.io/" + fake.Password(8, 8, false, false, false) + httpmock.RegisterResponder("POST", url, func(request *http.Request) (response *http.Response, err error) { + is.Equal(http.Header{ + "X-Seq-Apikey": []string{"key"}, + "Content-Type": []string{"application/json"}, + }, request.Header, "it should return the expected header") + return nil, errors.New("failed") + }) + subject := &Writer{ + address: url, + client: c, + marshaller: json.New(), + key: "key", + } + err := subject.send([]interface{}{ + event{ + Properties: Entry{ + "C": 15, + }, + }, + }) + is.NotNil(err, "it should return an error") + }) + t.Run("when the request return an status unexpected", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + c := &http.Client{} + activateNonDefault(c) + url := "http://log.io/" + fake.Password(8, 8, false, false, false) + httpmock.RegisterResponder("POST", url, func(request *http.Request) (response *http.Response, err error) { + is.Equal(http.Header{ + "X-Seq-Apikey": []string{"key"}, + "Content-Type": []string{"application/json"}, + }, request.Header, "it should return the expected header") + return httpmock.NewBytesResponse(502, nil), nil + }) + subject := &Writer{ + address: url, + client: c, + marshaller: json.New(), + key: "key", + } + err := subject.send([]interface{}{ + event{ + Properties: Entry{ + "C": 15, + }, + }, + }) + is.NotNil(err, "it should return an error") + }) + t.Run("when the request return 201", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + c := &http.Client{} + activateNonDefault(c) + url := "http://log.io/" + fake.Password(8, 8, false, false, false) + httpmock.RegisterResponder("POST", url, func(request *http.Request) (response *http.Response, err error) { + is.Equal(http.Header{ + "X-Seq-Apikey": []string{"key"}, + "Content-Type": []string{"application/json"}, + }, request.Header, "it should return the expected header") + return httpmock.NewBytesResponse(201, nil), nil + }) + subject := &Writer{ + address: url, + client: c, + marshaller: json.New(), + key: "key", + } + err := subject.send([]interface{}{ + event{ + Properties: Entry{ + "C": 15, + }, + }, + }) + is.Nil(err, "it should return no error") + }) +} + +var lock sync.Mutex + +func activateNonDefault(c *http.Client) { + lock.Lock() + defer lock.Unlock() + httpmock.ActivateNonDefault(c) +} diff --git a/strings/strings.go b/strings/strings.go new file mode 100644 index 0000000..90ca8d3 --- /dev/null +++ b/strings/strings.go @@ -0,0 +1,55 @@ +package strings + +import ( + "bytes" + "fmt" + "html/template" + "regexp" + "strings" + + "github.com/iancoleman/strcase" +) + +var emptyOrWhitespacePattern = regexp.MustCompile(`^\s*$`) + +func IsBlank(str string) bool { + return emptyOrWhitespacePattern.MatchString(str) +} + +func ToPascalCase(str string) string { + return Capitalize(strcase.ToCamel(str)) +} + +func ToLowerCamelCase(str string) string { + return strcase.ToLowerCamel(str) +} + +func UseAnnotation(str string) string { + return str +} + +func Capitalize(str string) string { + if len(str) <= 1 { + return strings.ToUpper(str) + } + return strings.ToUpper(string(str[0])) + str[1:] +} + +func ProcessString(str string, vars interface{}) string { + tmpl, err := template.New("tmpl").Parse(str) + + if err != nil { + fmt.Println(err.Error()) + } + return process(tmpl, vars) +} + +func process(t *template.Template, vars interface{}) string { + var tmplBytes bytes.Buffer + + err := t.Execute(&tmplBytes, vars) + if err != nil { + fmt.Println(err.Error()) + } + return tmplBytes.String() +} diff --git a/strings/strings_test.go b/strings/strings_test.go new file mode 100644 index 0000000..7a4c03d --- /dev/null +++ b/strings/strings_test.go @@ -0,0 +1,56 @@ +// +build unit + +package strings + +import ( + "github.com/icrowley/fake" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestIsBlank(t *testing.T) { + t.Parallel() + t.Run("when string is empty", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + result := IsBlank("") + is.True(result, "it should return true") + }) + t.Run("when string only contains empty characters", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + result := IsBlank(" ") + is.True(result, "it should return true") + }) + t.Run("when string contain non whitespace characters", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + result := IsBlank(fake.CharactersN(6)) + is.False(result, "it should return false") + }) +} + +func TestToPascalCase(t *testing.T) { + t.Parallel() + t.Run("when a snake case string is passed", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + in := "pascal_case_word" + expected := "PascalCaseWord" + is.Equal(expected, ToPascalCase(in), "should return the word as pascal") + }) +} + +func TestCapitalize(t *testing.T) { + t.Parallel() + t.Run("when the string has more than one character", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + is.Equal("DeCaPiTaLiZe", Capitalize("deCaPiTaLiZe")) + }) + t.Run("when the string has one character", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + is.Equal("D", Capitalize("d")) + }) +}