From b5af8729a17a4ee5270ea640097d5a4620cf4e06 Mon Sep 17 00:00:00 2001 From: Volker Schukai <volker.schukai@schukai.com> Date: Thu, 12 Sep 2024 15:09:52 +0200 Subject: [PATCH] feat: New BollValue Type #16, enhance pathvalue and update go lang to 1.22 --- .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/vcs.xml | 1 + configuration.iml | 1 + devenv.nix | 6 +- go.mod | 10 +- go.sum | 6 + import_test.go | 2 + path-value.go | 31 --- properties_test.go | 12 +- value-bool.go | 147 ++++++++++++ value-bool_test.go | 273 +++++++++++++++++++++++ value-path.go | 55 +++++ path-value_test.go => value-path_test.go | 0 13 files changed, 507 insertions(+), 42 deletions(-) create mode 100644 .idea/codeStyles/codeStyleConfig.xml delete mode 100644 path-value.go create mode 100644 value-bool.go create mode 100644 value-bool_test.go create mode 100644 value-path.go rename path-value_test.go => value-path_test.go (100%) diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ +<component name="ProjectCodeStyleConfiguration"> + <state> + <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" /> + </state> +</component> \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..f680695 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,6 @@ <project version="4"> <component name="VcsDirectoryMappings"> <mapping directory="" vcs="Git" /> + <mapping directory="$PROJECT_DIR$/../http-negotiation" vcs="Git" /> </component> </project> \ No newline at end of file diff --git a/configuration.iml b/configuration.iml index 789c0e8..5f86599 100644 --- a/configuration.iml +++ b/configuration.iml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <module type="JAVA_MODULE" version="4"> + <component name="Go" enabled="true" /> <component name="NewModuleRootManager" inherit-compiler-output="true"> <exclude-output /> <content url="file://$MODULE_DIR$"> diff --git a/devenv.nix b/devenv.nix index a32c8d7..4320c83 100644 --- a/devenv.nix +++ b/devenv.nix @@ -166,7 +166,11 @@ ''; enterShell = '' - + + export CGO_CFLAGS=-O2 -U_FORTIFY_SOURCE;CGO_ENABLED=0 + export CGO_LDFLAGS=-U_FORTIFY_SOURCE + + cat <<'EOF' > CONTRIBUTING.md # Contributing to schukai GmbH Projects diff --git a/go.mod b/go.mod index 09471b9..21ab482 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,20 @@ module gitlab.schukai.com/oss/libraries/go/application/configuration -go 1.21 +go 1.22.0 + +toolchain go1.22.6 require ( github.com/imdario/mergo v1.0.0 github.com/kinbiko/jsonassert v1.1.1 github.com/magiconair/properties v1.8.7 - github.com/pelletier/go-toml/v2 v2.2.1 + github.com/pelletier/go-toml/v2 v2.2.3 github.com/r3labs/diff/v3 v3.0.1 github.com/stretchr/testify v1.9.0 gitlab.schukai.com/oss/libraries/go/network/http-negotiation v1.3.2 gitlab.schukai.com/oss/libraries/go/utilities/pathfinder v0.9.2 gitlab.schukai.com/oss/libraries/go/utilities/watch v0.4.0 - golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 gopkg.in/yaml.v3 v3.0.1 ) @@ -22,7 +24,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/sys v0.19.0 // indirect + golang.org/x/sys v0.25.0 // indirect ) replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16 diff --git a/go.sum b/go.sum index 24787c6..21693bc 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,8 @@ github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOS github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/r3labs/diff/v3 v3.0.1 h1:CBKqf3XmNRHXKmdU7mZP1w7TV0pDyVCis1AUHtA4Xtg= @@ -47,12 +49,16 @@ golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcH golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/import_test.go b/import_test.go index 98fbd03..969063f 100644 --- a/import_test.go +++ b/import_test.go @@ -15,8 +15,10 @@ func TestReadExample3(t *testing.T) { config := struct { Host string + Flag bool }{ Host: "localhost", + Flag: true, } c := New(config) diff --git a/path-value.go b/path-value.go deleted file mode 100644 index b80e8c5..0000000 --- a/path-value.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2022 schukai GmbH -// SPDX-License-Identifier: AGPL-3.0 - -package configuration - -import "path" - -type pathInterface interface { - String() string - Dir() string - Ext() string - Base() string -} - -type PathValue string - -func (p PathValue) Dir() string { - return path.Dir(string(p)) -} - -func (p PathValue) Ext() string { - return path.Ext(string(p)) -} - -func (p PathValue) String() string { - return string(p) -} - -func (p PathValue) Base() string { - return path.Base(string(p)) -} diff --git a/properties_test.go b/properties_test.go index 9940ba4..b3aed6a 100644 --- a/properties_test.go +++ b/properties_test.go @@ -14,7 +14,7 @@ func TestGetMapForProperties(t *testing.T) { config.A = "A" config.B = true config.C.CA = "CA" - config.C.CB = true + config.C.CB = false config.C.CD.CDA = "CDA" s := New(config) @@ -23,10 +23,10 @@ func TestGetMapForProperties(t *testing.T) { assert.Equal(t, len(e), 0) - assert.Equal(t, m["A"], "A") - assert.Equal(t, m["B"], "true") - assert.Equal(t, m["C.CA"], "CA") - assert.Equal(t, m["C.CB"], "true") - assert.Equal(t, m["C.CB.CDA"], "CDA") + assert.Equal(t, "A", m["A"]) + assert.Equal(t, "true", m["B"]) + assert.Equal(t, "CA", m["C.CA"]) + assert.Equal(t, "false", m["C.CB"]) + assert.Equal(t, "CDA", m["C.CB.CDA"]) } diff --git a/value-bool.go b/value-bool.go new file mode 100644 index 0000000..cbe42ed --- /dev/null +++ b/value-bool.go @@ -0,0 +1,147 @@ +// Copyright 2022 schukai GmbH +// SPDX-License-Identifier: AGPL-3.0 + +// Copyright 2022 schukai GmbH +// SPDX-License-Identifier: AGPL-3.0 + +package configuration + +import ( + "encoding/json" + "errors" + "gopkg.in/yaml.v3" + "strings" +) + +type BoolValuer interface { + IsTrue() bool + IsFalse() bool + ToBool() bool + IsValid() bool + String() string + Raw() string +} + +// BoolValue represents a boolean value stored as a string +type BoolValue struct { + value bool // Holds the value bool value + valid bool // Tracks if the value is valid ("true" or "false") + raw string +} + +// NewBoolValue creates a new BoolValue and validates it +func NewBoolValue(raw string) (BoolValue, error) { + bv := BoolValue{raw: raw} + err := bv.parseAndValidate() + return bv, err +} + +// parseAndValidate processes and validates the raw string +func (p *BoolValue) parseAndValidate() error { + switch strings.ToLower(p.raw) { + case "true", "yes", "1", "on": + p.value = true + p.valid = true + case "false", "no", "0", "off": + p.value = false + p.valid = true + default: + p.valid = false + return errors.New("invalid BoolValue: must be 'true' or 'false'") + } + return nil +} + +// IsTrue returns true if the value represents a logical true +func (p BoolValue) IsTrue() bool { + return p.value && p.valid +} + +// IsFalse returns true if the value represents a logical false +func (p BoolValue) IsFalse() bool { + return !p.IsTrue() +} + +// ToBool converts the BoolValue to a native bool type +func (p BoolValue) ToBool() bool { + return p.IsTrue() +} + +// IsValid checks whether the value is valid +func (p BoolValue) IsValid() bool { + return p.valid +} + +// FromBool converts a native bool to a BoolValue +func FromBool(b bool) BoolValue { + if b { + return BoolValue{value: true, valid: true, raw: "true"} + } + return BoolValue{value: false, valid: true, raw: "false"} +} + +// String returns the raw string value of BoolValue +func (p BoolValue) Raw() string { + return p.raw +} + +// String returns either "true" or "false" or if the value is invalid, the raw value +func (p BoolValue) String() string { + if p.valid { + if p.value { + return "true" + } + return "false" + } + + return p.raw +} + +// MarshalJSON serializes the BoolValue to JSON +func (p BoolValue) MarshalJSON() ([]byte, error) { + return json.Marshal(p.IsTrue()) +} + +// UnmarshalJSON deserializes the BoolValue from JSON +func (p *BoolValue) UnmarshalJSON(data []byte) error { + var b string + var err error + if err = json.Unmarshal(data, &b); err != nil { + return err + } + *p, err = NewBoolValue(b) + if err != nil { + return err + } + return nil +} + +// MarshalYAML serializes the BoolValue to YAML +func (p BoolValue) MarshalYAML() (interface{}, error) { + return p.IsTrue(), nil +} + +// UnmarshalYAML deserializes the BoolValue from YAML +func (p *BoolValue) UnmarshalYAML(value *yaml.Node) error { + var b string + var err error + if err = value.Decode(&b); err != nil { + return err + } + *p, err = NewBoolValue(b) + if err != nil { + return err + } + return nil +} + +// MarshalTOML serializes the BoolValue to TOML +func (p BoolValue) MarshalTOML() ([]byte, error) { + return []byte(p.raw), nil +} + +// UnmarshalTOML deserializes the BoolValue from TOML +func (p *BoolValue) UnmarshalTOML(data []byte) error { + p.raw = strings.Trim(string(data), `"`) + return p.parseAndValidate() +} diff --git a/value-bool_test.go b/value-bool_test.go new file mode 100644 index 0000000..20dbc04 --- /dev/null +++ b/value-bool_test.go @@ -0,0 +1,273 @@ +package configuration + +import ( + "encoding/json" + yaml "gopkg.in/yaml.v3" + "strings" + "testing" +) + +func TestBoolValueString(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"true", "true"}, + {"false", "false"}, + {"one", "one"}, + {"", ""}, + {" ", " "}, + {"off", "false"}, + {"on", "true"}, + } + + for _, tt := range tests { + t.Run(string(tt.input), func(t *testing.T) { + bv, _ := NewBoolValue(tt.input) + + result := bv.String() + if result != tt.expected { + t.Errorf("result = %s; expected %s", result, tt.expected) + } + }) + } +} +func TestBoolValueRaw(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"true", "true"}, + {"false", "false"}, + {"one", "one"}, + {"", ""}, + {" ", " "}, + {"off", "off"}, + {"on", "on"}, + } + + for _, tt := range tests { + t.Run(string(tt.input), func(t *testing.T) { + bv, _ := NewBoolValue(tt.input) + + result := bv.Raw() + if result != tt.expected { + t.Errorf("result = %s; expected %s", result, tt.expected) + } + }) + } +} + +func TestBoolValueIsTrue(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"true", true}, + {"false", false}, + {"one", false}, + {"", false}, + {" ", false}, + {"gurke", false}, + {"off", false}, + {"on", true}, + {"no", false}, + {"yes", true}, + {"0", false}, + {"1", true}, + } + + for _, tt := range tests { + t.Run(string(tt.input), func(t *testing.T) { + bv, _ := NewBoolValue(tt.input) + + result := bv.IsTrue() + if result != tt.expected { + t.Errorf("result = %t; expected %t", result, tt.expected) + } + }) + } +} + +// TestBoolValueYAMLSerialization tests the YAML serialization and deserialization of BoolValue +func TestBoolValueYAMLMarshaling(t *testing.T) { + cases := []struct { + name string + boolValue BoolValue + expected string + shouldFail bool + }{ + { + name: "True value", + boolValue: BoolValue{value: true, valid: true, raw: "true"}, + expected: "true", + }, + { + name: "False value", + boolValue: BoolValue{value: false, valid: true, raw: "false"}, + expected: "false", + }, + { + name: "Invalid value", + boolValue: BoolValue{valid: false, raw: "maybe"}, + expected: "false", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + data, err := yaml.Marshal(&c.boolValue) + if err != nil { + if !c.shouldFail { + t.Errorf("Marshaling failed when it should not have: %v", err) + } + return + } + + if strings.TrimSpace(string(data)) != c.expected { + t.Errorf("Expected %s but got %s", c.expected, string(data)) + } + }) + } +} + +func TestBoolValueYAMLUnmarshaling(t *testing.T) { + cases := []struct { + name string + yamlContent string + expected string + shouldFail bool + }{ + { + name: "True value", + yamlContent: `true`, + expected: "true", + }, + { + name: "False value", + yamlContent: `false`, + expected: "false", + }, + { + name: "Invalid value", + yamlContent: `maybe`, + shouldFail: true, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + + var unmarshal BoolValue + data := []byte(c.yamlContent) + err := yaml.Unmarshal(data, &unmarshal) + if err != nil { + if !c.shouldFail { + t.Errorf("Unmarshaling failed when it should not have: %v", err) + } + return + } + if unmarshal.String() != c.expected { + t.Errorf("Unmarshaled value %v does not match original %v", unmarshal, c.expected) + } + }) + } +} + +type BoolValueTestStruct struct { + Value BoolValue +} + +func TestBoolValueJsonDecode(t *testing.T) { + + cases := []struct { + name string + jsonContent string + expected string + shouldFail bool + }{ + { + name: "True value", + jsonContent: "{\"value\":\"true\"}", + expected: "true", + }, + { + name: "False value", + jsonContent: "{\"value\":\"false\"}", + expected: "false", + }, + { + name: "Invalid value", + jsonContent: "{\"value\":\"gurke\"}", + shouldFail: true, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + + var unmarshal BoolValueTestStruct + data := []byte(c.jsonContent) + err := json.Unmarshal(data, &unmarshal) + if err != nil { + if !c.shouldFail { + t.Errorf("Unmarshaling failed when it should not have: %v", err) + } + return + } + if unmarshal.Value.String() != c.expected { + t.Errorf("Unmarshaled value %v does not match original %v", unmarshal, c.expected) + } + }) + } +} + +func TestBoolValueYamlDecode(t *testing.T) { + + cases := []struct { + name string + yamlContent string + expected string + shouldFail bool + }{ + { + name: "True value", + yamlContent: ` +value: true +`, + expected: "true", + }, + { + name: "False value", + yamlContent: ` +value: false +`, + expected: "false", + }, + { + name: "Invalid value", + yamlContent: ` +value: gurke +`, + shouldFail: true, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + + var unmarshal BoolValueTestStruct + data := []byte(c.yamlContent) + err := yaml.Unmarshal(data, &unmarshal) + if err != nil { + if !c.shouldFail { + t.Errorf("Unmarshaling failed when it should not have: %v", err) + } + return + } + if unmarshal.Value.String() != c.expected { + t.Errorf("Unmarshaled value %v does not match original %v", unmarshal, c.expected) + } + }) + } +} diff --git a/value-path.go b/value-path.go new file mode 100644 index 0000000..1e33da3 --- /dev/null +++ b/value-path.go @@ -0,0 +1,55 @@ +// Copyright 2022 schukai GmbH +// SPDX-License-Identifier: AGPL-3.0 + +package configuration + +import "path" + +// deprecated +type PathValueInterface interface { + String() string + Dir() string + Ext() string + Base() string +} + +// PathValuer is an interface for path values +type PathValuer interface { + String() string + Dir() string + Ext() string + Base() string + IsAbs() bool + Join(parts ...string) PathValue + Clean() PathValue +} + +type PathValue string + +func (p PathValue) Dir() string { + return path.Dir(string(p)) +} + +func (p PathValue) Ext() string { + return path.Ext(string(p)) +} + +func (p PathValue) String() string { + return string(p) +} + +func (p PathValue) Base() string { + return path.Base(string(p)) +} + +func (p PathValue) IsAbs() bool { + return path.IsAbs(string(p)) +} + +func (p PathValue) Clean() PathValue { + return PathValue(path.Clean(string(p))) +} + +func (p PathValue) Join(parts ...string) PathValue { + return PathValue(path.Join(append([]string{string(p)}, parts...)...)) +} diff --git a/path-value_test.go b/value-path_test.go similarity index 100% rename from path-value_test.go rename to value-path_test.go -- GitLab