Skip to content

Commit c440378

Browse files
authored
Merge pull request #25 from TykTechnologies/TT-11478
[TT-11478] Fixing EnvsHandler
2 parents 0706cf3 + ae27572 commit c440378

File tree

5 files changed

+158
-87
lines changed

5 files changed

+158
-87
lines changed

exporter.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func (v *Viewer) DetailedConfigHandler(rw http.ResponseWriter, r *http.Request)
5858

5959
// EnvsHandler expose the environment variables of the configuration struct
6060
func (v *Viewer) EnvsHandler(rw http.ResponseWriter, r *http.Request) {
61-
if v.config == nil {
61+
if v.envs == nil {
6262
rw.WriteHeader(http.StatusInternalServerError)
6363
return
6464
}

exporter_test.go

+28-26
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,7 @@ func TestDetailedConfigHandler(t *testing.T) {
249249

250250
func TestEnvsHandler(t *testing.T) {
251251
tcs := []struct {
252-
testName string
253-
252+
testName string
254253
givenConfig interface{}
255254
givenPrefix string
256255
queryParamVal string
@@ -265,7 +264,7 @@ func TestEnvsHandler(t *testing.T) {
265264
"field_value",
266265
},
267266
expectedStatusCode: http.StatusOK,
268-
expectedJSONOutput: fmt.Sprintln(`["FIELDNAME:field_value"]`),
267+
expectedJSONOutput: fmt.Sprintln(`["FIELDNAME=field_value"]`),
269268
},
270269
{
271270
testName: "simple struct with prefix",
@@ -276,22 +275,21 @@ func TestEnvsHandler(t *testing.T) {
276275
},
277276
givenPrefix: "TEST_",
278277
expectedStatusCode: http.StatusOK,
279-
expectedJSONOutput: fmt.Sprintln(`["TEST_FIELDNAME:field_value"]`),
278+
expectedJSONOutput: fmt.Sprintln(`["TEST_FIELDNAME=field_value"]`),
279+
},
280+
{
281+
testName: "complex struct",
282+
givenConfig: complexStruct,
283+
expectedStatusCode: http.StatusOK,
284+
expectedJSONOutput: fmt.Sprintln(
285+
`["NAME=name_value",` +
286+
`"DATA_OBJECT1=1",` +
287+
`"DATA_OBJECT2=true",` +
288+
`"METADATA_ID=99",` +
289+
`"METADATA_VALUE=key99",` +
290+
`"OMITTEDVALUE=''"]`,
291+
),
280292
},
281-
// TODO: Uncomment this test once this issue is addressed:
282-
// https://github.com/TykTechnologies/structviewer/issues/7
283-
// {
284-
// testName: "complex struct struct",
285-
// givenConfig: complexStruct,
286-
// expectedStatusCode: http.StatusOK,
287-
// expectedJSONOutput: fmt.Sprintln(
288-
// `["NAME:name_value",` +
289-
// `"DATA_OBJECT1:1",` +
290-
// `"DATA_OBJECT2:true",` +
291-
// `"METADATA:map[key_99:{99 key99}]",` +
292-
// `"OMITTEDVALUE:"]`,
293-
// ),
294-
// },
295293
{
296294
testName: "valid field of complexStruct via query param",
297295
givenConfig: complexStruct,
@@ -330,8 +328,6 @@ func TestEnvsHandler(t *testing.T) {
330328

331329
for _, tc := range tcs {
332330
t.Run(tc.testName, func(t *testing.T) {
333-
// Create a request to pass to our handler. We don't have any query parameters for now, so we'll
334-
// pass 'nil' as the third parameter.
335331
req, err := http.NewRequest("GET", "/", nil)
336332
assert.NoError(t, err)
337333

@@ -341,19 +337,25 @@ func TestEnvsHandler(t *testing.T) {
341337
helper, err := New(&structViewerConfig, tc.givenPrefix)
342338
assert.NoError(t, err, "failed to instantiate viewer")
343339

344-
// We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response.
345340
rr := httptest.NewRecorder()
346341
handler := http.HandlerFunc(helper.EnvsHandler)
347342

348-
// Our handlers satisfy http.Handler, so we can call their ServeHTTP method
349-
// directly and pass in our Request and ResponseRecorder.
350343
handler.ServeHTTP(rr, req)
351344

352-
// Check the status code is what we expect.
353345
assert.Equal(t, tc.expectedStatusCode, rr.Code)
354346

355-
// Check the response body is what we expect.
356-
assert.JSONEq(t, tc.expectedJSONOutput, rr.Body.String())
347+
// Determine whether the expected output is an array by trying to unmarshal it into a slice
348+
var expectedArray, actualArray []string
349+
expectedArrayErr := json.Unmarshal([]byte(tc.expectedJSONOutput), &expectedArray)
350+
actualArrayErr := json.Unmarshal(rr.Body.Bytes(), &actualArray)
351+
352+
if expectedArrayErr == nil && actualArrayErr == nil {
353+
// Both JSON strings are arrays; compare using unordered comparison
354+
assert.ElementsMatch(t, expectedArray, actualArray)
355+
} else {
356+
// Not arrays, compare as ordered JSON strings
357+
assert.JSONEq(t, tc.expectedJSONOutput, rr.Body.String())
358+
}
357359
})
358360
}
359361
}

internal/test/main.go

+29-43
Original file line numberDiff line numberDiff line change
@@ -7,55 +7,41 @@ import (
77
"github.com/TykTechnologies/structviewer"
88
)
99

10-
// InnerStructType represents an inner struct.
11-
type InnerStructType struct {
12-
// DummyAddr represents an address.
13-
DummyAddr string `json:"dummy_addr"`
10+
type complexType struct {
11+
Data struct {
12+
Object1 int `json:"object_1,omitempty"`
13+
Object2 bool `json:"object_2,omitempty"`
14+
} `json:"data"`
15+
Metadata map[string]struct {
16+
ID int `json:"id,omitempty"`
17+
Value string `json:"value,omitempty"`
18+
} `json:"metadata,omitempty"`
19+
Random map[int]string `json:"random,omitempty"`
1420
}
1521

16-
// StructType represents a struct type.
17-
type StructType struct {
18-
// Enable represents status.
19-
Enable bool `json:"enable"`
20-
// Inner is an inner struct.
21-
Inner InnerStructType `json:"inner"`
22-
}
23-
24-
type testStruct struct {
25-
// Exported represents a sample exported field.
26-
Exported string `json:"exported"`
27-
notExported bool //lint:ignore U1000 Unused field is used for testing purposes.
28-
29-
// StrField is a struct field.
30-
StrField struct {
31-
// Test is a field of struct type.
32-
Test string `json:"test"`
33-
Other struct {
34-
// OtherTest represents a field of sub-struct.
35-
OtherTest bool `json:"other_test"`
36-
nonEmbedded string
37-
} `json:"other"`
38-
} `json:"str_field"`
39-
40-
// ST is another struct type.
41-
ST StructType `json:"st"`
42-
43-
// JSONExported includes a JSON tag.
44-
JSONExported int `json:"json_exported" structviewer:"obfuscate"`
22+
var complexStruct = complexType{
23+
Data: struct {
24+
Object1 int `json:"object_1,omitempty"`
25+
Object2 bool `json:"object_2,omitempty"`
26+
}{
27+
Object1: 1,
28+
Object2: true,
29+
},
30+
Metadata: map[string]struct {
31+
ID int `json:"id,omitempty"`
32+
Value string `json:"value,omitempty"`
33+
}{
34+
"key_99": {ID: 99, Value: "key99"},
35+
},
36+
Random: map[int]string{
37+
1: "one",
38+
2: "two",
39+
},
4540
}
4641

4742
func main() {
4843
config := &structviewer.Config{
49-
Object: &testStruct{
50-
Exported: "exported_value",
51-
ST: StructType{
52-
Enable: true,
53-
Inner: InnerStructType{
54-
DummyAddr: "dummy_addr_value",
55-
},
56-
},
57-
JSONExported: 10,
58-
},
44+
Object: complexStruct,
5945
Path: "./main.go",
6046
ParseComments: true,
6147
}

parser.go

+71-12
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,38 @@ import (
1414
// ParseEnvs parse Viewer config field, generating a string slice of prefix+key:value of each config field
1515
func (v *Viewer) ParseEnvs() []string {
1616
var envs []string
17-
envVars := v.envs
17+
envVars := v.configMap
1818

19-
if len(envVars) == 0 {
20-
envVars = parseEnvs(v.config, v.prefix, "")
19+
for _, envVar := range envVars {
20+
envs = append(envs, generateEnvStrings(envVar)...)
2121
}
2222

23-
for i := range envVars {
24-
env := envVars[i]
25-
envs = append(envs, v.prefix+env.String())
23+
return envs
24+
}
25+
26+
func generateEnvStrings(e *EnvVar) []string {
27+
var strEnvs []string
28+
29+
if e.isStruct {
30+
typedEnv, ok := e.Value.(map[string]*EnvVar)
31+
if !ok {
32+
return []string{""}
33+
}
34+
35+
for _, v := range typedEnv {
36+
strEnvs = append(strEnvs, generateEnvStrings(v)...)
37+
}
38+
39+
return strEnvs
2640
}
2741

28-
return envs
42+
if e.Value == "" || e.Value == nil {
43+
e.Value = `''`
44+
}
45+
46+
strEnvs = append(strEnvs, fmt.Sprintf("%v=%v", e.Env, e.Value))
47+
48+
return strEnvs
2949
}
3050

3151
// EnvNotation takes JSON notation of a configuration field (e.g, 'listen_port') and returns EnvVar object of the given
@@ -190,7 +210,6 @@ func (v *Viewer) get(field string, envs []*EnvVar) *EnvVar {
190210

191211
return nil
192212
}
193-
194213
func parseEnvs(config interface{}, prefix, configField string) []*EnvVar {
195214
var envs []*EnvVar
196215

@@ -201,7 +220,7 @@ func parseEnvs(config interface{}, prefix, configField string) []*EnvVar {
201220
newEnv := &EnvVar{}
202221
newEnv.setKey(field)
203222

204-
// Ensuring that the configField ends with a single dot (only if it is not empty)
223+
// Ensure that the configField ends with a single dot (only if it is not empty)
205224
if configField != "" && configField[len(configField)-1] != '.' {
206225
configField += "."
207226
}
@@ -217,8 +236,48 @@ func parseEnvs(config interface{}, prefix, configField string) []*EnvVar {
217236
newEnv.ConfigField = ""
218237
newEnv.isStruct = true
219238

239+
envs = append(envs, newEnv)
240+
} else if reflect.ValueOf(field.Value()).Kind() == reflect.Map {
241+
v := reflect.ValueOf(field.Value())
242+
keys := v.MapKeys()
243+
244+
kvEnvVar := map[string]*EnvVar{}
245+
246+
for _, key := range keys {
247+
value := v.MapIndex(key).Interface()
248+
249+
// Handle different key types by converting to a string representation
250+
keyStr := fmt.Sprintf("%v", key)
251+
mapEnv := &EnvVar{
252+
key: keyStr,
253+
field: keyStr,
254+
}
255+
256+
if reflect.TypeOf(value).Kind() == reflect.Struct {
257+
// Recursively process structs
258+
envsInner := parseEnvs(value, prefix+newEnv.key+"_", configField+newEnv.ConfigField)
259+
for i := range envsInner {
260+
kvEnvVar[envsInner[i].field] = envsInner[i]
261+
}
262+
} else {
263+
// Directly assign other map values to `mapEnv`
264+
mapEnv.Value = value
265+
envSuffix := strings.ToUpper(strings.ReplaceAll(keyStr, "_", ""))
266+
mapEnv.Env = prefix + newEnv.key + "_" + envSuffix
267+
mapEnv.ConfigField = configField + newEnv.ConfigField + "." + keyStr
268+
mapEnv.Obfuscated = getPointerBool(false)
269+
270+
kvEnvVar[keyStr] = mapEnv
271+
}
272+
}
273+
274+
newEnv.Value = kvEnvVar
275+
newEnv.ConfigField = ""
276+
newEnv.isStruct = true
277+
220278
envs = append(envs, newEnv)
221279
} else {
280+
// Use the existing `setValue` function to assign the value
222281
newEnv.setValue(field)
223282
newEnv.Env = prefix + newEnv.key
224283
newEnv.ConfigField = configField + newEnv.ConfigField
@@ -227,9 +286,9 @@ func parseEnvs(config interface{}, prefix, configField string) []*EnvVar {
227286
if field.IsZero() && field.Tag(StructViewerTag) == "obfuscate" {
228287
newEnv.Obfuscated = getPointerBool(true)
229288
}
230-
}
231289

232-
envs = append(envs, newEnv)
290+
envs = append(envs, newEnv)
291+
}
233292
}
234293
}
235294

@@ -310,7 +369,7 @@ type EnvVar struct {
310369

311370
// String returns a key:value string from EnvVar
312371
func (ev EnvVar) String() string {
313-
return fmt.Sprintf("%s:%s", ev.key, ev.Value)
372+
return fmt.Sprintf("%s:%s", ev.Env, ev.Value)
314373
}
315374

316375
func (ev *EnvVar) setKey(field *structs.Field) {

parser_test.go

+29-5
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func TestParseEnvsValues(t *testing.T) {
8989
Key: "Value",
9090
},
9191
expectedLen: 1,
92-
expectedEnvs: []string{"KEY:Value"},
92+
expectedEnvs: []string{"KEY=Value"},
9393
},
9494
{
9595
testName: "KEY:VALUE with json tag",
@@ -99,7 +99,7 @@ func TestParseEnvsValues(t *testing.T) {
9999
Key: "Value",
100100
},
101101
expectedLen: 1,
102-
expectedEnvs: []string{"JSONNAME:Value"},
102+
expectedEnvs: []string{"JSONNAME=Value"},
103103
},
104104
{
105105
testName: "KEY:VALUE with json tag and omitempty",
@@ -109,7 +109,7 @@ func TestParseEnvsValues(t *testing.T) {
109109
Key: "Value",
110110
},
111111
expectedLen: 1,
112-
expectedEnvs: []string{"JSONNAME:Value"},
112+
expectedEnvs: []string{"JSONNAME=Value"},
113113
},
114114
{
115115
testName: "KEY:VALUE with json '-' tag",
@@ -119,7 +119,30 @@ func TestParseEnvsValues(t *testing.T) {
119119
Key: "Value",
120120
},
121121
expectedLen: 1,
122-
expectedEnvs: []string{"KEY:Value"},
122+
expectedEnvs: []string{"KEY=Value"},
123+
},
124+
{
125+
testName: "struct with nested map and struct",
126+
testStruct: struct {
127+
Key string
128+
Map map[string]string
129+
Str struct {
130+
Inner string
131+
}
132+
}{
133+
Key: "Value",
134+
Map: map[string]string{
135+
"key1": "value1",
136+
"key2": "value2",
137+
},
138+
Str: struct {
139+
Inner string
140+
}{
141+
Inner: "inner",
142+
},
143+
},
144+
expectedLen: 4,
145+
expectedEnvs: []string{"KEY=Value", "MAP_KEY1=value1", "MAP_KEY2=value2", "STR_INNER=inner"},
123146
},
124147
}
125148

@@ -132,7 +155,8 @@ func TestParseEnvsValues(t *testing.T) {
132155
envs := helper.ParseEnvs()
133156

134157
assert.Len(t, envs, tc.expectedLen)
135-
assert.EqualValues(t, tc.expectedEnvs, envs)
158+
// Compare arrays disregarding the order
159+
assert.ElementsMatch(t, tc.expectedEnvs, envs)
136160
})
137161
}
138162
}

0 commit comments

Comments
 (0)