Skip to content

Commit 475c089

Browse files
Refactor parameter loading. Support loading of maps.
1 parent c6c1896 commit 475c089

14 files changed

+821
-203
lines changed

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,12 @@ err := globalAWS.LoadConfigFromParameterStore(
4545

4646
Supported value types: `string`, `int`, `bool` ("true"/"false").
4747

48-
Complex type should be either a `struct`, or a `slice`. You can arbitrarily nest structs and slices.
48+
Complex type should be either a `struct`, a `map` or a `slice`. You can arbitrarily nest them.
4949

5050
For structs, use `global` or `json` tag to set field name.
5151

52+
For maps, the key name is the map key (maps must use strings as keys.)
53+
5254
For slices, all subscripts in Parameter Store must be integers.
5355

5456
### Shorthand for running on AWS ECS or Lambda

aws/aws_param_store.go

+16-131
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@ package aws
22

33
import (
44
"context"
5-
"reflect"
6-
"strconv"
7-
"strings"
85

96
"github.com/aws/aws-sdk-go-v2/aws"
107
"github.com/aws/aws-sdk-go-v2/service/ssm"
118
"github.com/railsware/go-global/v2"
9+
"github.com/railsware/go-global/v2/utils"
1210
)
1311

1412
const paramStoreSeparator = "/"
@@ -31,6 +29,11 @@ func LoadConfigFromParameterStore(awsConfig aws.Config, options LoadConfigOption
3129
}
3230
}()
3331

32+
reflectedConfig, err := utils.ReflectConfig(globalConfig)
33+
if err != nil {
34+
return err
35+
}
36+
3437
paramPaginator := ssm.NewGetParametersByPathPaginator(
3538
ssm.NewFromConfig(awsConfig),
3639
&ssm.GetParametersByPathInput{
@@ -40,145 +43,27 @@ func LoadConfigFromParameterStore(awsConfig aws.Config, options LoadConfigOption
4043
},
4144
)
4245

43-
var paramWarnings []global.Error
46+
var params []param
4447

4548
for paramPaginator.HasMorePages() {
4649
page, err := paramPaginator.NextPage(context.Background())
4750
if err != nil {
4851
return global.NewError("global: failed to load from Parameter Store: %v", err)
4952
}
50-
51-
for _, param := range page.Parameters {
52-
paramNameWithoutPrefix := (*param.Name)[len(options.ParamPrefix):]
53-
destination, err := findParamDestination(globalConfig, paramNameWithoutPrefix)
54-
if err != nil {
55-
if !err.Warning() {
56-
return global.NewError("global: %s: %v", paramNameWithoutPrefix, err)
57-
} else if !options.IgnoreUnmappedParams {
58-
paramWarnings = append(paramWarnings, global.NewWarning("%s: %v", paramNameWithoutPrefix, err))
59-
}
60-
continue
61-
}
62-
63-
err = writeParamToConfig(destination, *param.Value)
64-
if err != nil {
65-
if err.Warning() {
66-
paramWarnings = append(paramWarnings, global.NewWarning("%s: %v", paramNameWithoutPrefix, err))
67-
} else {
68-
return global.NewError("global: %v", err)
69-
}
70-
}
53+
for _, ssmParam := range page.Parameters {
54+
paramNameWithoutPrefix := (*ssmParam.Name)[len(options.ParamPrefix):]
55+
params = append(params, param{paramNameWithoutPrefix, *ssmParam.Value})
7156
}
7257
}
7358

74-
if paramWarnings != nil {
75-
var warningMessages []string
76-
for _, warning := range paramWarnings {
77-
warningMessages = append(warningMessages, warning.Error())
78-
}
59+
paramTree := buildParamTree(params)
7960

80-
return global.NewWarning("global: failed to read some parameters: %s", strings.Join(warningMessages, "; "))
81-
}
61+
errors := paramTree.Write(reflectedConfig)
8262

83-
return nil
63+
return errors.Join()
8464
}
8565

86-
func findParamDestination(globalConfig interface{}, name string) (reflect.Value, global.Error) {
87-
destination := reflect.ValueOf(globalConfig)
88-
89-
if destination.Kind() != reflect.Ptr {
90-
return reflect.Value{}, global.NewError("config must be a pointer to a structure")
91-
}
92-
93-
destination = destination.Elem()
94-
95-
if destination.Kind() != reflect.Struct && destination.Kind() != reflect.Array {
96-
return reflect.Value{}, global.NewError("config must be a pointer to a structure or array")
97-
}
98-
99-
// find nested field in config struct
100-
pathParts := strings.Split(name, paramStoreSeparator)
101-
for _, part := range pathParts {
102-
if destination.Kind() == reflect.Struct {
103-
destination = lookupFieldByName(destination, part)
104-
} else if destination.Kind() == reflect.Slice {
105-
index, err := strconv.Atoi(part)
106-
if err != nil || index < 0 {
107-
return reflect.Value{}, global.NewWarning("could not map param to array index")
108-
}
109-
if destination.Cap() <= index {
110-
// grow destination array to match
111-
destination.SetLen(destination.Cap())
112-
additionalLength := index - destination.Cap() + 1
113-
additionalElements := reflect.MakeSlice(destination.Type(), additionalLength, additionalLength)
114-
destination.Set(reflect.AppendSlice(destination, additionalElements))
115-
} else if destination.Len() <= index {
116-
destination.SetLen(index + 1)
117-
}
118-
destination = destination.Index(index)
119-
} else {
120-
return reflect.Value{}, global.NewWarning("could not map param to config field")
121-
}
122-
// resolve pointer, if struct was nil
123-
if destination.Kind() == reflect.Ptr {
124-
if destination.IsNil() {
125-
destination.Set(reflect.New(destination.Type().Elem()))
126-
}
127-
destination = destination.Elem()
128-
}
129-
}
130-
131-
// assign value to field
132-
if !destination.IsValid() {
133-
return reflect.Value{}, global.NewWarning("could not map param to config field")
134-
}
135-
136-
return destination, nil
137-
}
138-
139-
func writeParamToConfig(destination reflect.Value, value string) global.Error {
140-
if !destination.CanSet() {
141-
return global.NewWarning("config key is not writable")
142-
} else if destination.Kind() == reflect.String {
143-
destination.SetString(value)
144-
} else if destination.Kind() == reflect.Int {
145-
intval, err := strconv.Atoi(value)
146-
if err != nil {
147-
return global.NewWarning("cannot read int param value")
148-
} else {
149-
destination.SetInt(int64(intval))
150-
}
151-
} else if destination.Kind() == reflect.Bool {
152-
if value == "true" {
153-
destination.SetBool(true)
154-
} else if value == "false" {
155-
destination.SetBool(false)
156-
} else {
157-
return global.NewWarning("cannot read bool param value (must be true or false)")
158-
}
159-
} else {
160-
return global.NewWarning("cannot write param: config key is of unsupported type %s", destination.Kind())
161-
}
162-
163-
return nil
164-
}
165-
166-
func lookupFieldByName(structure reflect.Value, name string) reflect.Value {
167-
fieldByName := structure.FieldByName(name)
168-
if fieldByName.IsValid() {
169-
return fieldByName
170-
}
171-
172-
// TODO might be inefficient, but fine for one-time loading of a not-crazy-big config
173-
for i := 0; i < structure.NumField(); i++ {
174-
fieldTag := structure.Type().Field(i).Tag
175-
if fieldTag.Get("global") == name {
176-
return structure.Field(i)
177-
}
178-
if fieldTag.Get("json") == name {
179-
return structure.Field(i)
180-
}
181-
}
182-
183-
return reflect.Value{}
66+
type param struct {
67+
path string
68+
value string
18469
}

aws/build_tree.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package aws
2+
3+
import (
4+
"strings"
5+
6+
"github.com/railsware/go-global/v2/tree"
7+
)
8+
9+
// Builds a tree of parameters out of the array
10+
func buildParamTree(params []param) *tree.Node {
11+
paramTree := new(tree.Node)
12+
13+
for _, param := range params {
14+
pathParts := strings.Split(param.path, "/")
15+
destination := paramTree
16+
for _, part := range pathParts {
17+
if destination.Children == nil {
18+
destination.Children = make(map[string]*tree.Node)
19+
}
20+
newDestination, ok := destination.Children[part]
21+
if !ok {
22+
newDestination = &tree.Node{}
23+
destination.Children[part] = newDestination
24+
}
25+
destination = newDestination
26+
}
27+
destination.Value = param.value
28+
}
29+
return paramTree
30+
}

aws/build_tree_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package aws
2+
3+
import (
4+
"testing"
5+
6+
"github.com/railsware/go-global/v2/tree"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestBuildTree(t *testing.T) {
11+
params := []param{
12+
{"String", "string"},
13+
{"NestedSimple/Nested", "nested_string"},
14+
{"NestedDeep/SecondLevel/Nested", "deep_string"},
15+
{"NestedDeep/SecondLevel/Nested2", "deep_string_2"},
16+
}
17+
18+
expectedTree := &tree.Node{
19+
Children: map[string]*tree.Node{
20+
"String": {Value: "string"},
21+
"NestedSimple": {
22+
Children: map[string]*tree.Node{
23+
"Nested": {Value: "nested_string"},
24+
},
25+
},
26+
"NestedDeep": {
27+
Children: map[string]*tree.Node{
28+
"SecondLevel": {
29+
Children: map[string]*tree.Node{
30+
"Nested": {Value: "deep_string"},
31+
"Nested2": {Value: "deep_string_2"},
32+
},
33+
},
34+
},
35+
},
36+
},
37+
}
38+
39+
paramTree := buildParamTree(params)
40+
41+
assert.Equal(t, expectedTree, paramTree)
42+
}

aws/write_param_to_config_test.go

-71
This file was deleted.

0 commit comments

Comments
 (0)