Skip to content

Commit

Permalink
fix(Praqma#137): use nullable boolean type for the Release boolean fi…
Browse files Browse the repository at this point in the history
…elds
  • Loading branch information
prizov committed Aug 31, 2022
1 parent ac34dfe commit 9fdc8a2
Show file tree
Hide file tree
Showing 13 changed files with 400 additions and 99 deletions.
7 changes: 7 additions & 0 deletions examples/minimal-example-overwrite.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## This is a minimal example.
## It will use your current kube context and will deploy Tiller without RBAC service account.
## For the full config spec and options, check https://github.com/Praqma/helmsman/blob/master/docs/desired_state_specification.md

apps:
jenkins:
enabled: false
83 changes: 83 additions & 0 deletions internal/app/custom_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package app

import (
"encoding/json"
"github.com/invopop/jsonschema"
"reflect"
"strconv"
)

// truthy and falsy NullBool values
var (
True = NullBool{HasValue: true, Value: true}
False = NullBool{HasValue: true, Value: false}
)

// NullBool represents a bool that may be null.
type NullBool struct {
Value bool
HasValue bool // true if bool is not null
}

func (b NullBool) MarshalJSON() ([]byte, error) {
value := b.HasValue && b.Value
return json.Marshal(value)
}

func (b *NullBool) UnmarshalJSON(data []byte) error {
var unmarshalledJson bool

err := json.Unmarshal(data, &unmarshalledJson)
if err != nil {
return err
}

b.Value = unmarshalledJson
b.HasValue = true

return nil
}

func (b *NullBool) UnmarshalText(text []byte) error {
str := string(text)
if len(str) < 1 {
return nil
}

value, err := strconv.ParseBool(str)
if err != nil {
return err
}

b.HasValue = true
b.Value = value

return nil
}

// JSONSchema instructs the jsonschema generator to represent NullBool type as boolean
func (NullBool) JSONSchema() *jsonschema.Schema {
return &jsonschema.Schema{
Type: "boolean",
}
}

type MergoTransformer func(typ reflect.Type) func(dst, src reflect.Value) error

func (m MergoTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
return m(typ)
}

// NullBoolTransformer is a custom imdario/mergo transformer for the NullBool type
func NullBoolTransformer(typ reflect.Type) func(dst, src reflect.Value) error {
if typ != reflect.TypeOf(NullBool{}) {
return nil
}

return func(dst, src reflect.Value) error {
if src.FieldByName("HasValue").Bool() {
dst.Set(src)
}
return nil
}
}
179 changes: 179 additions & 0 deletions internal/app/custom_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package app

import (
"bytes"
"encoding/json"
"reflect"
"testing"
)

func TestNullBool_MarshalJSON(t *testing.T) {
tests := []struct {
name string
value NullBool
want []byte
wantErr bool
}{
{
name: "should be false",
want: []byte(`false`),
wantErr: false,
},
{
name: "should be true",
want: []byte(`true`),
value: NullBool{HasValue: true, Value: true},
wantErr: false,
},
{
name: "should be false when HasValue is false",
want: []byte(`false`),
value: NullBool{HasValue: false, Value: true},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.value.MarshalJSON()
if (err != nil) != tt.wantErr {
t.Errorf("MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("MarshalJSON() got = %v, want %v", got, tt.want)
}
})
}
}

func TestNullBool_UnmarshalJSON(t *testing.T) {
type output struct {
Value NullBool `json:"value"`
}
tests := []struct {
name string
data []byte
want output
wantErr bool
}{
{
name: "should have value set to false",
data: []byte(`{"value": false}`),
want: output{NullBool{HasValue: true, Value: false}},
},
{
name: "should have value set to true",
data: []byte(`{"value": true}`),
want: output{NullBool{HasValue: true, Value: true}},
},
{
name: "should have value unset",
data: []byte("{}"),
want: output{NullBool{HasValue: false, Value: false}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got output
if err := json.NewDecoder(bytes.NewReader(tt.data)).Decode(&got); (err != nil) != tt.wantErr {
t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("UnmarshalJSON() got = %v, want %v", got, tt.want)
}
})
}
}

func TestNullBool_UnmarshalText(t *testing.T) {
tests := []struct {
name string
text []byte
want NullBool
wantErr bool
}{
{
name: "should have the value set to false",
text: []byte("false"),
want: NullBool{HasValue: true, Value: false},
},
{
name: "should have the value set to true",
text: []byte("false"),
want: NullBool{HasValue: true, Value: false},
},
{
name: "should have the value unset",
text: []byte(""),
want: NullBool{HasValue: false, Value: false},
},
{
name: "should return an error on wrong input",
text: []byte("wrong_input"),
wantErr: true,
want: NullBool{HasValue: false, Value: false},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got NullBool
if err := got.UnmarshalText(tt.text); (err != nil) != tt.wantErr {
t.Errorf("UnmarshalText() error = %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("UnmarshalText() got = %v, want %v", got, tt.want)
}
})
}
}

func TestNullBoolTransformer(t *testing.T) {
type args struct {
dst NullBool
src NullBool
}
tests := []struct {
name string
args args
want NullBool
}{
{
name: "should overwrite true to false when the dst has the value",
args: args{
dst: NullBool{HasValue: true, Value: true},
src: NullBool{HasValue: true, Value: false},
},
want: NullBool{HasValue: true, Value: false},
},
{
name: "shouldn't overwrite when the value is unset",
args: args{
dst: NullBool{HasValue: true, Value: true},
src: NullBool{HasValue: false, Value: false},
},
want: NullBool{HasValue: true, Value: true},
},
{
name: "shouldn overwrite when the value is set and equal true",
args: args{
dst: NullBool{HasValue: true, Value: false},
src: NullBool{HasValue: true, Value: true},
},
want: NullBool{HasValue: true, Value: true},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dst := tt.args.dst
src := tt.args.src

transformer := NullBoolTransformer(reflect.TypeOf(NullBool{}))

transformer(reflect.ValueOf(&dst).Elem(), reflect.ValueOf(src))

if !reflect.DeepEqual(dst, tt.want) {
t.Errorf("NullBoolTransformer() = %v, want %v", dst, tt.want)
}
})
}
}
2 changes: 1 addition & 1 deletion internal/app/decision_maker.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func (cs *currentState) decide(r *Release, n *Namespace, p *plan, c *ChartInfo,
return nil
}

if !r.Enabled {
if !r.Enabled.Value {
if ok := cs.releaseExists(r, ""); ok {
p.addDecision(prefix+" is desired to be DELETED.", r.Priority, remove)
r.uninstall(p)
Expand Down
Loading

0 comments on commit 9fdc8a2

Please sign in to comment.