Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for encoding.TextUnmarshaler #6

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
36 changes: 21 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ AWS Lambda functions. It should be suitable for additional applications.

Set some parameters in [AWS Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html):

| Name | Value | Type | Key ID |
| ---------------------------- | -------------------- | ------------ | ------------- |
| /exmaple_service/prod/debug | false | String | - |
| /exmaple_service/prod/port | 8080 | String | - |
| /exmaple_service/prod/user | Ian | String | - |
| /exmaple_service/prod/rate | 0.5 | String | - |
| /exmaple_service/prod/secret | zOcZkAGB6aEjN7SAoVBT | SecureString | alias/aws/ssm |
| Name | Value | Type | Key ID |
| ---------------------------- | -------------------- | ------------ | ------------- |
| /example_service/prod/debug | false | String | - |
| /example_service/prod/port | 8080 | String | - |
| /example_service/prod/user | Ian | String | - |
| /example_service/prod/rate | 0.5 | String | - |
| /example_service/prod/secret | zOcZkAGB6aEjN7SAoVBT | SecureString | alias/aws/ssm |
| /example_service/prod/ts | 2020-04-14T21:26:00+02:00 | String | - |

Write some code:

Expand All @@ -36,11 +37,13 @@ import (
)

type Config struct {
Debug bool `smm:"debug" default:"true"`
Port int `smm:"port"`
User string `smm:"user"`
Rate float32 `smm:"rate"`
Secret string `smm:"secret" required:"true"`
Debug bool `smm:"debug" default:"true"`
Port int `smm:"port"`
User string `smm:"user"`
Rate float32 `smm:"rate"`
Secret string `smm:"secret" required:"true"`
Timestamp time.Time `smm:"ts"`
AltIp *net.IP `smm:"alt_ip"`
}

func main() {
Expand All @@ -50,8 +53,8 @@ func main() {
log.Fatal(err.Error())
}

format := "Debug: %v\nPort: %d\nUser: %s\nRate: %f\nSecret: %s\n"
_, err = fmt.Printf(format, c.Debug, c.Port, c.User, c.Rate, c.Secret)
format := "Debug: %v\nPort: %d\nUser: %s\nRate: %f\nSecret: %s\nTimestamp: %s\nAltIp: %v\n"
_, err = fmt.Printf(format, c.Debug, c.Port, c.User, c.Rate, c.Secret, c.Timestamp, c.AltIp)
if err != nil {
log.Fatal(err.Error())
}
Expand All @@ -66,6 +69,8 @@ Port: 8080
User: Ian
Rate: 0.500000
Secret: zOcZkAGB6aEjN7SAoVBT
Timestamp: 2020-04-14 21:26:00 +0200 +0200
AltIp: <nil>
```

[Additional examples](https://godoc.org/github.com/ianlopshire/go-ssm-config#pkg-examples) can be found in godoc.
Expand Down Expand Up @@ -101,8 +106,9 @@ ssmconfig supports these struct field types:
* int, int8, int16, int32, int64
* bool
* float32, float64
* encoding.TextUnmarshaler

More supported types may be added in the future.
More supported types may be added in the future. Field types that implement the `encoding.TextUnmarshaler` interface can be used directly or as a pointer.

## Licence

Expand Down
23 changes: 23 additions & 0 deletions ssmconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package ssmconfig

import (
"encoding"
"path"
"reflect"
"strconv"
Expand Down Expand Up @@ -33,6 +34,8 @@ type Provider struct {
SSM ssmiface.SSMAPI
}

var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()

// Process loads config values from smm (parameter store) into c. Encrypted parameters
// will automatically be decrypted. c must be a pointer to a struct.
//
Expand Down Expand Up @@ -129,7 +132,27 @@ func (p *Provider) getParameters(spec structSpec) (params map[string]string, inv
return params, invalidParams, nil
}

// Create a new instance of the field's type and call its UnmarshalText([]byte) method.
// Set the value after execution and fail if the method returns an error.
func unmarshalText(t reflect.Type, v reflect.Value, s string) error {
if v.IsNil() {
v.Set(reflect.New(t.Elem()))
}
err := v.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(s))
if err != nil {
return errors.Errorf("could not decode %q into type %v: %v", s, t.String(), err)
}

return nil
}

func setValue(v reflect.Value, s string) error {
t := v.Type()
if t.Implements(textUnmarshalerType) {
return unmarshalText(t, v, s)
} else if reflect.PtrTo(t).Implements(textUnmarshalerType) {
return unmarshalText(t, v.Addr(), s)
}
switch v.Kind() {
case reflect.String:
v.SetString(s)
Expand Down
80 changes: 70 additions & 10 deletions ssmconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package ssmconfig_test

import (
"errors"
"net"
"reflect"
"testing"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ssm"
Expand All @@ -29,16 +31,22 @@ func (c *mockSSMClient) GetParameters(input *ssm.GetParametersInput) (*ssm.GetPa
func TestProvider_Process(t *testing.T) {
t.Run("base case", func(t *testing.T) {
var s struct {
S1 string `ssm:"/strings/s1"`
S2 string `ssm:"/strings/s2" default:"string2"`
I1 int `ssm:"/int/i1"`
I2 int `ssm:"/int/i2" default:"42"`
B1 bool `ssm:"/bool/b1"`
B2 bool `ssm:"/bool/b2" default:"false"`
F321 float32 `ssm:"/float32/f321"`
F322 float32 `ssm:"/float32/f322" default:"42.42"`
F641 float64 `ssm:"/float64/f641"`
F642 float64 `ssm:"/float64/f642" default:"42.42"`
S1 string `ssm:"/strings/s1"`
S2 string `ssm:"/strings/s2" default:"string2"`
I1 int `ssm:"/int/i1"`
I2 int `ssm:"/int/i2" default:"42"`
B1 bool `ssm:"/bool/b1"`
B2 bool `ssm:"/bool/b2" default:"false"`
F321 float32 `ssm:"/float32/f321"`
F322 float32 `ssm:"/float32/f322" default:"42.42"`
F641 float64 `ssm:"/float64/f641"`
F642 float64 `ssm:"/float64/f642" default:"42.42"`
TU1 time.Time `ssm:"/text_unmarshaler/time1"`
TU2 time.Time `ssm:"/text_unmarshaler/time2" default:"2020-04-14T21:26:00+02:00"`
TU3 time.Time `ssm:"/text_unmarshaler/time3"`
TUP1 *net.IP `ssm:"/text_unmarshaler/ipv41"`
TUP2 *net.IP `ssm:"/text_unmarshaler/ipv42" default:"127.0.0.1"`
TUP3 *net.IP `ssm:"/text_unmarshaler/ipv43"`
Invalid string
}

Expand All @@ -65,6 +73,14 @@ func TestProvider_Process(t *testing.T) {
Name: aws.String("/base/float64/f641"),
Value: aws.String("42.42"),
},
{
Name: aws.String("/base/text_unmarshaler/time1"),
Value: aws.String("2020-04-14T21:26:00+02:00"),
},
{
Name: aws.String("/base/text_unmarshaler/ipv41"),
Value: aws.String("127.0.0.1"),
},
},
},
}
Expand Down Expand Up @@ -94,6 +110,12 @@ func TestProvider_Process(t *testing.T) {
"/base/float32/f322",
"/base/float64/f641",
"/base/float64/f642",
"/base/text_unmarshaler/time1",
"/base/text_unmarshaler/time2",
"/base/text_unmarshaler/time3",
"/base/text_unmarshaler/ipv41",
"/base/text_unmarshaler/ipv42",
"/base/text_unmarshaler/ipv43",
}

if !reflect.DeepEqual(names, expectedNames) {
Expand Down Expand Up @@ -130,6 +152,26 @@ func TestProvider_Process(t *testing.T) {
if s.F642 != 42.42 {
t.Errorf("Process() F642 unexpected value: want %f, have %f", 42.42, s.F642)
}
tm, _ := time.Parse(time.RFC3339, "2020-04-14T21:26:00+02:00")
if !s.TU1.Equal(tm) {
t.Errorf("Process() TU1 unexpected value: want %v, have %v", tm, s.TU1)
}
if !s.TU2.Equal(tm) {
t.Errorf("Process() TU2 unexpected value: want %v, have %v", tm, s.TU2)
}
if !s.TU3.Equal(time.Time{}) {
t.Errorf("Process() TU3 unexpected value: want %v, have %v", time.Time{}, s.TU3)
}
ip := net.ParseIP("127.0.0.1")
if !s.TUP1.Equal(ip) {
t.Errorf("Process() TUP1 unexpected value: want %v, have %v", ip, s.TUP1)
}
if !s.TUP2.Equal(ip) {
t.Errorf("Process() TUP2 unexpected value: want %v, have %v", ip, s.TUP2)
}
if s.TUP3 != nil {
t.Errorf("Process() TUP3 unexpected value: want %v, have %v", nil, s.TUP3)
}
if s.Invalid != "" {
t.Errorf("Process() Missing unexpected value: want %q, have %q", "", s.Invalid)
}
Expand Down Expand Up @@ -179,6 +221,24 @@ func TestProvider_Process(t *testing.T) {
client: &mockSSMClient{},
shouldErr: true,
},
{
name: "invalid unmarshal text",
configPath: "/base/",
c: &struct {
TU1 time.Time `ssm:"/text_unmarshaler/time1" default:"notATime"`
}{},
client: &mockSSMClient{},
shouldErr: true,
},
{
name: "invalid unmarshal text",
configPath: "/base/",
c: &struct {
TUP1 net.IP `ssm:"/text_unmarshaler/ipv41" default:"notAnIP"`
}{},
client: &mockSSMClient{},
shouldErr: true,
},
{
name: "missing required parameter",
configPath: "/base/",
Expand Down