Repository: JanDeDobbeleer/oh-my-posh
Pull request #1299 (2021-11-24 14:08:43)

fix(nightscape): do not fail on empty array
### Prerequisites

- [x] I have read and understood the `CONTRIBUTING` guide
- [x] The commit message follows the [conventional commits][cc] guidelines
- [x] Tests for the changes have been added (for bug fixes/features)
- [x] Docs have been added / updated (for bug fixes/features)

### Description

When receiving an empty array, the segment failed.
Also refactored the way it stores the data to avoid boxing and unboxing the response.

<!---

Tips:

If you're not comfortable with working with Git, we're working a guide (https://ohmyposh.dev/docs/contributing_git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary


JanDeDobbeleer: @shanselman when you have 5 minutes, care to have a quick look?

Patch:
diff --git a/docs/docs/segment_nightscout.md b/docs/docs/segment_nightscout.md
index 54456408..f651c1ad 100644
--- a/docs/docs/segment_nightscout.md
+++ b/docs/docs/segment_nightscout.md
@@ -79,7 +79,16 @@ if that color is visible against any of your backgrounds.
 
 ## Template Properties
 
+- .ID: `string` - The internal ID of the object
 - .Sgv: `int` - Your Serum Glucose Value (your sugar)
+- .Date: `int` - The unix timestamp of the entry
+- .DateString: `time` - The timestamp of the entry
+- .Trend: `int` - The trend of the entry
+- .Device: `string` - The device linked to the entry
+- .Type: `string` - The type of the entry
+- .UtcOffset: `int` - The UTC offset
+- .SysTime: `time` - The time on the system
+- .Mills: `int` - The amount of mills
 - .TrendIcon: `string` - By default, this will be something like ↑↑ or ↘ etc but you can
   override them with any glpyh as seen above
 
diff --git a/src/segment_nightscout.go b/src/segment_nightscout.go
index 6e63fa9d..f26d355e 100644
--- a/src/segment_nightscout.go
+++ b/src/segment_nightscout.go
@@ -2,6 +2,8 @@ package main
 
 import (
 	"encoding/json"
+	"errors"
+	"time"
 )
 
 // segment struct, makes templating easier
@@ -30,8 +32,17 @@ const (
 
 // NightscoutData struct contains the API data
 type NightscoutData struct {
-	Sgv       int64  `json:"sgv"`
-	Direction string `json:"direction"`
+	ID         string    `json:"_id"`
+	Sgv        int       `json:"sgv"`
+	Date       int64     `json:"date"`
+	DateString time.Time `json:"dateString"`
+	Trend      int       `json:"trend"`
+	Direction  string    `json:"direction"`
+	Device     string    `json:"device"`
+	Type       string    `json:"type"`
+	UtcOffset  int       `json:"utcOffset"`
+	SysTime    time.Time `json:"sysTime"`
+	Mills      int64     `json:"mills"`
 }
 
 func (ns *nightscout) enabled() bool {
@@ -82,46 +93,59 @@ func (ns *nightscout) string() string {
 }
 
 func (ns *nightscout) getResult() (*NightscoutData, error) {
-	url := ns.props.getString(URL, "")
-	// natural and understood NS timeout is 5, anything else is unusual
-	cacheTimeout := ns.props.getInt(NSCacheTimeout, 5)
-	response := &NightscoutData{}
-	if cacheTimeout > 0 {
-		// check if data stored in cache
-		val, found := ns.env.cache().get(url)
+	parseSingleElement := func(data []byte) (*NightscoutData, error) {
+		var result []*NightscoutData
+		err := json.Unmarshal(data, &result)
+		if err != nil {
+			return nil, err
+		}
+		if len(result) == 0 {
+			return nil, errors.New("no elements in the array")
+		}
+		return result[0], nil
+	}
+	getCacheValue := func(key string) (*NightscoutData, error) {
+		val, found := ns.env.cache().get(key)
 		// we got something from the cache
 		if found {
-			err := json.Unmarshal([]byte(val), response)
-			if err != nil {
-				return nil, err
+			if data, err := parseSingleElement([]byte(val)); err == nil {
+				return data, nil
 			}
-			return response, nil
 		}
+		return nil, errors.New("no data in cache")
 	}
 
+	url := ns.props.getString(URL, "")
 	httpTimeout := ns.props.getInt(HTTPTimeout, DefaultHTTPTimeout)
+	// natural and understood NS timeout is 5, anything else is unusual
+	cacheTimeout := ns.props.getInt(NSCacheTimeout, 5)
+
+	if cacheTimeout > 0 {
+		if data, err := getCacheValue(url); err == nil {
+			return data, nil
+		}
+	}
 
 	body, err := ns.env.doGet(url, httpTimeout)
 	if err != nil {
-		return &NightscoutData{}, err
+		return nil, err
 	}
 	var arr []*NightscoutData
 	err = json.Unmarshal(body, &arr)
 	if err != nil {
-		return &NightscoutData{}, err
+		return nil, err
 	}
 
-	firstelement := arr[0]
-	firstData, err := json.Marshal(firstelement)
+	data, err := parseSingleElement(body)
 	if err != nil {
-		return &NightscoutData{}, err
+		return nil, err
 	}
 
 	if cacheTimeout > 0 {
 		// persist new sugars in cache
-		ns.env.cache().set(url, string(firstData), cacheTimeout)
+		ns.env.cache().set(url, string(body), cacheTimeout)
 	}
-	return firstelement, nil
+	return data, nil
 }
 
 func (ns *nightscout) init(props *properties, env environmentInfo) {


Test patch:
diff --git a/src/segment_nightscout_test.go b/src/segment_nightscout_test.go
index b3286904..3e5cb370 100644
--- a/src/segment_nightscout_test.go
+++ b/src/segment_nightscout_test.go
@@ -17,6 +17,8 @@ func TestNSSegment(t *testing.T) {
 		JSONResponse    string
 		ExpectedString  string
 		ExpectedEnabled bool
+		CacheTimeout    int
+		CacheFoundFail  bool
 		Template        string
 		Error           error
 	}{
@@ -82,18 +84,65 @@ func TestNSSegment(t *testing.T) {
 			Error:           errors.New("Something went wrong"),
 			ExpectedEnabled: false,
 		},
+		{
+			Case:            "Empty array",
+			JSONResponse:    "[]",
+			ExpectedEnabled: false,
+		},
+		{
+			Case: "DoubleDown 50 from cache",
+			JSONResponse: `
+			[{"_id":"619d6fa819696e8ded5b2206","sgv":50,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"DoubleDown","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, // nolint:lll
+			Template:        " {{.Sgv}}{{.TrendIcon}}",
+			ExpectedString:  " 50↓↓",
+			ExpectedEnabled: true,
+			CacheTimeout:    10,
+		},
+		{
+			Case: "DoubleDown 50 from cache not found",
+			JSONResponse: `
+			[{"_id":"619d6fa819696e8ded5b2206","sgv":50,"date":1637707537000,"dateString":"2021-11-23T22:45:37.000Z","trend":4,"direction":"DoubleDown","device":"share2","type":"sgv","utcOffset":0,"sysTime":"2021-11-23T22:45:37.000Z","mills":1637707537000}]`, // nolint:lll
+			Template:        " {{.Sgv}}{{.TrendIcon}}",
+			ExpectedString:  " 50↓↓",
+			ExpectedEnabled: true,
+			CacheTimeout:    10,
+			CacheFoundFail:  true,
+		},
+		{
+			Case: "Error parsing response",
+			JSONResponse: `
+			4tffgt4e4567`,
+			Template:        " {{.Sgv}}{{.TrendIcon}}",
+			ExpectedString:  " 50↓↓",
+			ExpectedEnabled: false,
+			CacheTimeout:    10,
+		},
+		{
+			Case: "Faulty template",
+			JSONResponse: `
+			[{"sgv":50,"direction":"DoubleDown"}]`,
+			Template:        " {{.Sgv}}{{.Burp}}",
+			ExpectedString:  incorrectTemplate,
+			ExpectedEnabled: true,
+			CacheTimeout:    10,
+		},
 	}
 
 	for _, tc := range cases {
 		env := &MockedEnvironment{}
 		props := &properties{
 			values: map[Property]interface{}{
-				CacheTimeout: 0,
+				CacheTimeout: tc.CacheTimeout,
 				URL:          "FAKE",
 			},
 		}
 
+		cache := &MockedCache{}
+		cache.On("get", NSAPIURL).Return(tc.JSONResponse, !tc.CacheFoundFail)
+		cache.On("set", NSAPIURL, tc.JSONResponse, tc.CacheTimeout).Return()
+
 		env.On("doGet", NSAPIURL).Return([]byte(tc.JSONResponse), tc.Error)
+		env.On("cache", nil).Return(cache)
 
 		if tc.Template != "" {
 			props.values[SegmentTemplate] = tc.Template