From 9d6194ad5874e6d04179865b8eade668d60c7dde Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Mon, 29 Apr 2024 14:03:18 +0300 Subject: [PATCH 1/6] [support] Fix windows stubs --- support/support_windows.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/support/support_windows.go b/support/support_windows.go index 7bf2a269..127b91f6 100644 --- a/support/support_windows.go +++ b/support/support_windows.go @@ -29,6 +29,11 @@ func (i *Info) WithPackages(pkgs []Pkg) *Info { panic("UNSUPPORTED") } +// WithServices adds information about services +func (i *Info) WithServices(services []Service) *Info { + panic("UNSUPPORTED") +} + // ❗ WithPackages adds information about system apps func (i *Info) WithApps(apps ...App) *Info { panic("UNSUPPORTED") From ee80744147aa86a661fb9e506fb22dbb06753ffa Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Mon, 29 Apr 2024 15:21:17 +0300 Subject: [PATCH 2/6] [knf] Add 'Alias' method + Add property name validation + Code refactoring --- CHANGELOG.md | 6 ++ ek.go | 2 +- knf/example_test.go | 42 +++++++++ knf/knf.go | 208 ++++++++++++++++++++++++++------------------ knf/knf_test.go | 86 ++++++++++++------ 5 files changed, 228 insertions(+), 116 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35a97d60..48f93eea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## Changelog +### 12.120.0 + +- `[knf]` Added `Alias` method +- `[knf]` Added property name validation for all getters +- `[knf]` Code refactoring + ### 12.119.0 - `[initsystem]` Added [launchd](https://www.launchd.info) support diff --git a/ek.go b/ek.go index 23903b05..b217a9ba 100644 --- a/ek.go +++ b/ek.go @@ -21,7 +21,7 @@ import ( // ////////////////////////////////////////////////////////////////////////////////// // // VERSION is current ek package version -const VERSION = "12.119.0" +const VERSION = "12.120.0" // ////////////////////////////////////////////////////////////////////////////////// // diff --git a/knf/example_test.go b/knf/example_test.go index 33ca319a..a05019f7 100644 --- a/knf/example_test.go +++ b/knf/example_test.go @@ -120,6 +120,20 @@ func ExampleReload() { } } +func ExampleAlias() { + err := Global("/path/to/your/config.knf") + + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + // Add alias for renamed property "user:username" + Alias("user:username", "user:name") + + fmt.Printf("Value from config: %s\n", GetS("user:name")) +} + func ExampleGetS() { err := Global("/path/to/your/config.knf") @@ -373,6 +387,34 @@ func ExampleConfig_Reload() { } } +func ExampleConfig_Alias() { + cfg, err := Parse([]byte(` +[user] + username: john + uid: 512 + is-admin: true + priority: 3.7 + default-mode: 0644 + timeout: 3m + created: 1654424130 + timezone: Europe/Madrid + labels: system admin +`)) + + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + // Add alias for renamed property "user:username" + cfg.Alias("user:username", "user:name") + + fmt.Printf("Value from config: %s\n", cfg.GetS("user:name")) + + // Output: + // Value from config: john +} + func ExampleConfig_GetS() { cfg, err := Parse([]byte(` [user] diff --git a/knf/knf.go b/knf/knf.go index ca61ac20..817f53d9 100644 --- a/knf/knf.go +++ b/knf/knf.go @@ -11,6 +11,7 @@ package knf import ( "bytes" "errors" + "fmt" "os" "path" "strings" @@ -66,17 +67,18 @@ type IConfig interface { // ////////////////////////////////////////////////////////////////////////////////// // -// Config is basic config struct +// Config is basic configuration instance type Config struct { sections []string props []string data map[string]string + aliases map[string]string file string mx *sync.RWMutex } -// Validator is config property validator struct +// Validator is configuration property validator struct type Validator struct { Property string // Property name Func PropertyValidator // Validation function @@ -92,7 +94,7 @@ type DurationMod int64 // ////////////////////////////////////////////////////////////////////////////////// // var ( - ErrNilConfig = errors.New("Config is nil") + ErrNilConfig = errors.New("Configuration is nil") ErrCantReload = errors.New("Can't reload configuration file: path to file is empty") ErrCantMerge = errors.New("Can't merge configurations: given configuration is nil") ) @@ -165,6 +167,18 @@ func Reload() (map[string]bool, error) { return global.Reload() } +// Alias creates alias for configuration property +// +// It's useful for refactoring the configuration or for providing support for +// renamed properties +func Alias(old, new string) error { + if global == nil { + return ErrNilConfig + } + + return global.Alias(old, new) +} + // GetS returns configuration value as string func GetS(name string, defvals ...string) string { if global == nil { @@ -464,9 +478,42 @@ func (c *Config) Reload() (map[string]bool, error) { return changes, nil } +// Alias creates alias for configuration property +// +// It's useful for refactoring the configuration or for providing support for +// renamed properties +func (c *Config) Alias(old, new string) error { + if c == nil || c.mx == nil { + return ErrNilConfig + } + + switch { + case old == "": + return fmt.Errorf("Old property name is empty") + case new == "": + return fmt.Errorf("New property name is empty") + case !isValidPropName(old): + return fmt.Errorf("Old property name (%q) is invalid", old) + case !isValidPropName(new): + return fmt.Errorf("New property name (%q) is invalid", new) + } + + c.mx.Lock() + + if c.aliases == nil { + c.aliases = make(map[string]string) + } + + c.aliases[strings.ToLower(new)] = strings.ToLower(old) + + c.mx.Unlock() + + return nil +} + // GetS returns configuration value as string func (c *Config) GetS(name string, defvals ...string) string { - if c == nil || c.mx == nil { + if c == nil || c.mx == nil || !isValidPropName(name) { if len(defvals) == 0 { return "" } @@ -474,9 +521,7 @@ func (c *Config) GetS(name string, defvals ...string) string { return defvals[0] } - c.mx.RLock() - val := c.data[strings.ToLower(name)] - c.mx.RUnlock() + val := c.getValue(name) if val == "" { if len(defvals) == 0 { @@ -491,7 +536,7 @@ func (c *Config) GetS(name string, defvals ...string) string { // GetI returns configuration value as int func (c *Config) GetI(name string, defvals ...int) int { - if c == nil || c.mx == nil { + if c == nil || c.mx == nil || !isValidPropName(name) { if len(defvals) == 0 { return 0 } @@ -499,16 +544,12 @@ func (c *Config) GetI(name string, defvals ...int) int { return defvals[0] } - c.mx.RLock() - val := c.data[strings.ToLower(name)] - c.mx.RUnlock() - - return value.ParseInt(val, defvals...) + return value.ParseInt(c.getValue(name), defvals...) } // GetI64 returns configuration value as int64 func (c *Config) GetI64(name string, defvals ...int64) int64 { - if c == nil || c.mx == nil { + if c == nil || c.mx == nil || !isValidPropName(name) { if len(defvals) == 0 { return 0 } @@ -516,16 +557,12 @@ func (c *Config) GetI64(name string, defvals ...int64) int64 { return defvals[0] } - c.mx.RLock() - val := c.data[strings.ToLower(name)] - c.mx.RUnlock() - - return value.ParseInt64(val, defvals...) + return value.ParseInt64(c.getValue(name), defvals...) } // GetU returns configuration value as uint func (c *Config) GetU(name string, defvals ...uint) uint { - if c == nil || c.mx == nil { + if c == nil || c.mx == nil || !isValidPropName(name) { if len(defvals) == 0 { return 0 } @@ -533,16 +570,12 @@ func (c *Config) GetU(name string, defvals ...uint) uint { return defvals[0] } - c.mx.RLock() - val := c.data[strings.ToLower(name)] - c.mx.RUnlock() - - return value.ParseUint(val, defvals...) + return value.ParseUint(c.getValue(name), defvals...) } // GetU64 returns configuration value as uint64 func (c *Config) GetU64(name string, defvals ...uint64) uint64 { - if c == nil || c.mx == nil { + if c == nil || c.mx == nil || !isValidPropName(name) { if len(defvals) == 0 { return 0 } @@ -550,16 +583,12 @@ func (c *Config) GetU64(name string, defvals ...uint64) uint64 { return defvals[0] } - c.mx.RLock() - val := c.data[strings.ToLower(name)] - c.mx.RUnlock() - - return value.ParseUint64(val, defvals...) + return value.ParseUint64(c.getValue(name), defvals...) } // GetF returns configuration value as floating number func (c *Config) GetF(name string, defvals ...float64) float64 { - if c == nil || c.mx == nil { + if c == nil || c.mx == nil || !isValidPropName(name) { if len(defvals) == 0 { return 0.0 } @@ -567,16 +596,12 @@ func (c *Config) GetF(name string, defvals ...float64) float64 { return defvals[0] } - c.mx.RLock() - val := c.data[strings.ToLower(name)] - c.mx.RUnlock() - - return value.ParseFloat(val, defvals...) + return value.ParseFloat(c.getValue(name), defvals...) } // GetB returns configuration value as boolean func (c *Config) GetB(name string, defvals ...bool) bool { - if c == nil || c.mx == nil { + if c == nil || c.mx == nil || !isValidPropName(name) { if len(defvals) == 0 { return false } @@ -584,16 +609,12 @@ func (c *Config) GetB(name string, defvals ...bool) bool { return defvals[0] } - c.mx.RLock() - val := c.data[strings.ToLower(name)] - c.mx.RUnlock() - - return value.ParseBool(val, defvals...) + return value.ParseBool(c.getValue(name), defvals...) } // GetM returns configuration value as file mode func (c *Config) GetM(name string, defvals ...os.FileMode) os.FileMode { - if c == nil || c.mx == nil { + if c == nil || c.mx == nil || !isValidPropName(name) { if len(defvals) == 0 { return os.FileMode(0) } @@ -601,16 +622,12 @@ func (c *Config) GetM(name string, defvals ...os.FileMode) os.FileMode { return defvals[0] } - c.mx.RLock() - val := c.data[strings.ToLower(name)] - c.mx.RUnlock() - - return value.ParseMode(val, defvals...) + return value.ParseMode(c.getValue(name), defvals...) } // GetD returns configuration value as duration func (c *Config) GetD(name string, mod DurationMod, defvals ...time.Duration) time.Duration { - if c == nil || c.mx == nil { + if c == nil || c.mx == nil || !isValidPropName(name) { if len(defvals) == 0 { return time.Duration(0) } @@ -618,16 +635,12 @@ func (c *Config) GetD(name string, mod DurationMod, defvals ...time.Duration) ti return defvals[0] } - c.mx.RLock() - val := c.data[strings.ToLower(name)] - c.mx.RUnlock() - - return value.ParseDuration(val, time.Duration(mod), defvals...) + return value.ParseDuration(c.getValue(name), time.Duration(mod), defvals...) } // GetTD returns configuration value as time duration func (c *Config) GetTD(name string, defvals ...time.Duration) time.Duration { - if c == nil || c.mx == nil { + if c == nil || c.mx == nil || !isValidPropName(name) { if len(defvals) == 0 { return time.Duration(0) } @@ -635,16 +648,12 @@ func (c *Config) GetTD(name string, defvals ...time.Duration) time.Duration { return defvals[0] } - c.mx.RLock() - val := c.data[strings.ToLower(name)] - c.mx.RUnlock() - - return value.ParseTimeDuration(val, defvals...) + return value.ParseTimeDuration(c.getValue(name), defvals...) } // GetTS returns configuration timestamp value as time func (c *Config) GetTS(name string, defvals ...time.Time) time.Time { - if c == nil || c.mx == nil { + if c == nil || c.mx == nil || !isValidPropName(name) { if len(defvals) == 0 { return time.Time{} } @@ -652,16 +661,12 @@ func (c *Config) GetTS(name string, defvals ...time.Time) time.Time { return defvals[0] } - c.mx.RLock() - val := c.data[strings.ToLower(name)] - c.mx.RUnlock() - - return value.ParseTimestamp(val, defvals...) + return value.ParseTimestamp(c.getValue(name), defvals...) } // GetTS returns configuration value as timezone func (c *Config) GetTZ(name string, defvals ...*time.Location) *time.Location { - if c == nil || c.mx == nil { + if c == nil || c.mx == nil || !isValidPropName(name) { if len(defvals) == 0 { return nil } @@ -669,16 +674,12 @@ func (c *Config) GetTZ(name string, defvals ...*time.Location) *time.Location { return defvals[0] } - c.mx.RLock() - val := c.data[strings.ToLower(name)] - c.mx.RUnlock() - - return value.ParseTimezone(val, defvals...) + return value.ParseTimezone(c.getValue(name), defvals...) } // GetL returns configuration value as list func (c *Config) GetL(name string, defvals ...[]string) []string { - if c == nil || c.mx == nil { + if c == nil || c.mx == nil || !isValidPropName(name) { if len(defvals) == 0 { return nil } @@ -686,16 +687,12 @@ func (c *Config) GetL(name string, defvals ...[]string) []string { return defvals[0] } - c.mx.RLock() - val := c.data[strings.ToLower(name)] - c.mx.RUnlock() - - return value.ParseList(val, defvals...) + return value.ParseList(c.getValue(name), defvals...) } // Is checks if given property contains given value func (c *Config) Is(name string, value any) bool { - if c == nil || c.mx == nil { + if c == nil || c.mx == nil || !isValidPropName(name) { return false } @@ -732,6 +729,9 @@ func (c *Config) HasSection(section string) bool { c.mx.RLock() defer c.mx.RUnlock() + // The "section" variable contains an invalid name for a property, so the user + // can't read the value as a property, but we can store information about + // sections. return c.data[strings.ToLower(section)] == "!" } @@ -741,10 +741,7 @@ func (c *Config) HasProp(name string) bool { return false } - c.mx.RLock() - defer c.mx.RUnlock() - - return c.data[strings.ToLower(name)] != "" + return c.getValue(name) != "" } // Sections returns slice with section names @@ -761,12 +758,12 @@ func (c *Config) Sections() []string { // Props returns slice with properties names in some section func (c *Config) Props(section string) []string { - var result []string - if c == nil || !c.HasSection(section) { - return result + return nil } + var result []string + // Section name + delimiter snLength := len(section) + 1 @@ -821,3 +818,40 @@ func (c *Config) Validate(validators []*Validator) []error { } // ////////////////////////////////////////////////////////////////////////////////// // + +// getValue returns property value from the storage +func (c *Config) getValue(propName string) string { + if c == nil || c.mx == nil { + return "" + } + + c.mx.RLock() + defer c.mx.RUnlock() + + propName = strings.ToLower(propName) + + if c.aliases != nil && c.aliases[propName] != "" { + if c.data[c.aliases[propName]] != "" { + return c.data[c.aliases[propName]] + } + } + + return c.data[propName] +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// isValidPropName returns true if property name is valid +func isValidPropName(propName string) bool { + section, prop, ok := strings.Cut(propName, _SYMBOL_DELIMITER) + + switch { + case !ok, + strings.Trim(section, " ") == "", + strings.Trim(prop, " ") == "", + strings.Count(propName, _SYMBOL_DELIMITER) > 1: + return false + } + + return true +} diff --git a/knf/knf_test.go b/knf/knf_test.go index 0d98ab61..e67891cd 100644 --- a/knf/knf_test.go +++ b/knf/knf_test.go @@ -206,48 +206,54 @@ func (s *KNFSuite) TestErrors(c *check.C) { c.Assert(err, check.DeepEquals, ErrNilConfig) c.Assert(updated, check.IsNil) - c.Assert(GetS("test"), check.Equals, "") - c.Assert(GetI("test"), check.Equals, 0) - c.Assert(GetI("test"), check.Equals, 0) - c.Assert(GetU("test"), check.Equals, uint(0)) - c.Assert(GetI64("test"), check.Equals, int64(0)) - c.Assert(GetU64("test"), check.Equals, uint64(0)) - c.Assert(GetF("test"), check.Equals, 0.0) - c.Assert(GetB("test"), check.Equals, false) - c.Assert(GetM("test"), check.Equals, os.FileMode(0)) - c.Assert(GetD("test", Second), check.Equals, time.Duration(0)) - c.Assert(GetTD("test"), check.Equals, time.Duration(0)) - c.Assert(GetTS("test").IsZero(), check.Equals, true) - c.Assert(GetTZ("test"), check.IsNil) - c.Assert(GetL("test"), check.IsNil) - c.Assert(Is("test", ""), check.Equals, false) + c.Assert(GetS("test:test"), check.Equals, "") + c.Assert(GetI("test:test"), check.Equals, 0) + c.Assert(GetI("test:test"), check.Equals, 0) + c.Assert(GetU("test:test"), check.Equals, uint(0)) + c.Assert(GetI64("test:test"), check.Equals, int64(0)) + c.Assert(GetU64("test:test"), check.Equals, uint64(0)) + c.Assert(GetF("test:test"), check.Equals, 0.0) + c.Assert(GetB("test:test"), check.Equals, false) + c.Assert(GetM("test:test"), check.Equals, os.FileMode(0)) + c.Assert(GetD("test:test", Second), check.Equals, time.Duration(0)) + c.Assert(GetTD("test:test"), check.Equals, time.Duration(0)) + c.Assert(GetTS("test:test").IsZero(), check.Equals, true) + c.Assert(GetTZ("test:test"), check.IsNil) + c.Assert(GetL("test:test"), check.IsNil) + c.Assert(Is("test:test", ""), check.Equals, false) c.Assert(HasSection("test"), check.Equals, false) - c.Assert(HasProp("test"), check.Equals, false) + c.Assert(HasProp("test:test"), check.Equals, false) c.Assert(Sections(), check.HasLen, 0) c.Assert(Props("test"), check.HasLen, 0) c.Assert(Validate([]*Validator{}), check.DeepEquals, []error{ErrNilConfig}) + c.Assert(Alias("test:test", "test:test"), check.NotNil) c.Assert(global.Merge(nil), check.NotNil) config := &Config{mx: &sync.RWMutex{}} - c.Assert(config.GetS("test"), check.Equals, "") - c.Assert(config.GetI("test"), check.Equals, 0) - c.Assert(config.GetF("test"), check.Equals, 0.0) - c.Assert(config.GetB("test"), check.Equals, false) - c.Assert(config.GetM("test"), check.Equals, os.FileMode(0)) - c.Assert(config.GetD("test", Second), check.Equals, time.Duration(0)) - c.Assert(config.GetTD("test"), check.Equals, time.Duration(0)) - c.Assert(config.GetTS("test").IsZero(), check.Equals, true) - c.Assert(config.GetTZ("test"), check.IsNil) - c.Assert(config.GetL("test"), check.IsNil) - c.Assert(config.Is("test", ""), check.Equals, true) + c.Assert(config.GetS("test:test"), check.Equals, "") + c.Assert(config.GetI("test:test"), check.Equals, 0) + c.Assert(config.GetF("test:test"), check.Equals, 0.0) + c.Assert(config.GetB("test:test"), check.Equals, false) + c.Assert(config.GetM("test:test"), check.Equals, os.FileMode(0)) + c.Assert(config.GetD("test:test", Second), check.Equals, time.Duration(0)) + c.Assert(config.GetTD("test:test"), check.Equals, time.Duration(0)) + c.Assert(config.GetTS("test:test").IsZero(), check.Equals, true) + c.Assert(config.GetTZ("test:test"), check.IsNil) + c.Assert(config.GetL("test:test"), check.IsNil) + c.Assert(config.Is("test:test", ""), check.Equals, true) c.Assert(config.HasSection("test"), check.Equals, false) - c.Assert(config.HasProp("test"), check.Equals, false) + c.Assert(config.HasProp("test:test"), check.Equals, false) c.Assert(config.Sections(), check.HasLen, 0) c.Assert(config.Props("test"), check.HasLen, 0) c.Assert(config.Validate([]*Validator{}), check.HasLen, 0) c.Assert(config.Merge(nil), check.NotNil) + c.Assert(config.Alias("", ""), check.NotNil) + c.Assert(config.Alias("test", ""), check.NotNil) + c.Assert(config.Alias("test", "test"), check.NotNil) + c.Assert(config.Alias("test:test", "test"), check.NotNil) + updated, err = config.Reload() c.Assert(updated, check.IsNil) @@ -309,6 +315,17 @@ func (s *KNFSuite) TestMerging(c *check.C) { c.Assert(c1.GetS("extra:test1"), check.Equals, "extra-data") } +func (s *KNFSuite) TestAlias(c *check.C) { + err := Global(s.ConfigPath) + + c.Assert(global, check.NotNil) + c.Assert(err, check.IsNil) + + c.Assert(Alias("string:test1", "string:testX"), check.IsNil) + + c.Assert(GetS("string:testX"), check.Equals, "test") +} + func (s *KNFSuite) TestSections(c *check.C) { err := Global(s.ConfigPath) @@ -517,6 +534,8 @@ func (s *KNFSuite) TestMacro(c *check.C) { func (s *KNFSuite) TestNil(c *check.C) { var nilConf *Config + c.Assert(nilConf.getValue("formatting:test1"), check.Equals, "") + c.Assert(nilConf.GetS("formatting:test1"), check.Equals, "") c.Assert(nilConf.GetI("formatting:test1"), check.Equals, 0) c.Assert(nilConf.GetI64("formatting:test1"), check.Equals, int64(0)) @@ -536,6 +555,7 @@ func (s *KNFSuite) TestNil(c *check.C) { c.Assert(nilConf.Sections(), check.HasLen, 0) c.Assert(nilConf.Props("formatting"), check.HasLen, 0) c.Assert(nilConf.File(), check.Equals, "") + c.Assert(nilConf.Alias("test:test", "test:test"), check.NotNil) _, err := nilConf.Reload() @@ -653,3 +673,13 @@ func (s *KNFSuite) TestKNFParserExceptions(c *check.C) { _, err = readData(r) c.Assert(err.Error(), check.Equals, "Error at line 3: Unknown property {abcd:test}") } + +// ////////////////////////////////////////////////////////////////////////////////// // + +func (s *KNFSuite) BenchmarkBasic(c *check.C) { + Global(s.ConfigPath) + + for i := 0; i < c.N; i++ { + GetS("string:test1") + } +} From 2b1d76a5198508bedd3bd2ab4feec31973656fb2 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Mon, 29 Apr 2024 16:05:46 +0300 Subject: [PATCH 3/6] [knf] Improvements --- CHANGELOG.md | 1 + knf/knf.go | 4 ++++ knf/knf_test.go | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48f93eea..fe2554d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - `[knf]` Added `Alias` method - `[knf]` Added property name validation for all getters - `[knf]` Code refactoring +- `[knf]` Added more tests ### 12.119.0 diff --git a/knf/knf.go b/knf/knf.go index 817f53d9..beb80c86 100644 --- a/knf/knf.go +++ b/knf/knf.go @@ -715,6 +715,10 @@ func (c *Config) Is(name string, value any) bool { return c.GetM(name) == t case time.Duration: return c.GetD(name, Second) == t + case time.Time: + return c.GetTS(name).Unix() == t.Unix() + case *time.Location: + return fmt.Sprint(c.GetTZ(name)) == fmt.Sprint(t) } return false diff --git a/knf/knf_test.go b/knf/knf_test.go index e67891cd..b9637083 100644 --- a/knf/knf_test.go +++ b/knf/knf_test.go @@ -485,6 +485,39 @@ func (s *KNFSuite) TestTimeDuration(c *check.C) { c.Assert(GetTD("time-duration:test5"), check.Equals, time.Duration(0)) } +func (s *KNFSuite) TestTimestamp(c *check.C) { + err := Global(s.ConfigPath) + + c.Assert(global, check.NotNil) + c.Assert(err, check.IsNil) + + c.Assert(GetTS("timestamp:test1"), check.DeepEquals, time.Unix(0, 0)) + c.Assert(GetTS("timestamp:test2"), check.DeepEquals, time.Unix(1709629048, 0)) + c.Assert(GetTS("timestamp:test3"), check.DeepEquals, time.Time{}) +} + +func (s *KNFSuite) TestTimezone(c *check.C) { + err := Global(s.ConfigPath) + + c.Assert(global, check.NotNil) + c.Assert(err, check.IsNil) + + l, _ := time.LoadLocation("Europe/Zurich") + + c.Assert(GetTZ("timezone:test1"), check.DeepEquals, l) + c.Assert(GetTZ("timezone:test2"), check.IsNil) +} + +func (s *KNFSuite) TestList(c *check.C) { + err := Global(s.ConfigPath) + + c.Assert(global, check.NotNil) + c.Assert(err, check.IsNil) + + c.Assert(GetL("list:test1"), check.HasLen, 0) + c.Assert(GetL("list:test2"), check.HasLen, 2) +} + func (s *KNFSuite) TestIs(c *check.C) { err := Global(s.ConfigPath) @@ -501,6 +534,10 @@ func (s *KNFSuite) TestIs(c *check.C) { c.Assert(Is("integer:test1", int64(1)), check.Equals, true) c.Assert(Is("file-mode:test1", os.FileMode(0644)), check.Equals, true) c.Assert(Is("duration:test2", time.Minute), check.Equals, true) + c.Assert(Is("timestamp:test2", time.Unix(1709629048, 0)), check.Equals, true) + + l, _ := time.LoadLocation("Europe/Zurich") + c.Assert(Is("timezone:test1", l), check.Equals, true) c.Assert(Is("integer:test1", nil), check.Equals, false) } From b6a74be03c34620f842c9cb63d458699384790c4 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Mon, 29 Apr 2024 16:16:59 +0300 Subject: [PATCH 4/6] [sliceutil] Added method 'IsEqual' --- CHANGELOG.md | 3 ++- sliceutil/example_test.go | 20 ++++++++++++++++---- sliceutil/sliceutil.go | 18 ++++++++++++++++++ sliceutil/sliceutil_test.go | 16 ++++++++++++++++ 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe2554d4..4b928b9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ ### 12.120.0 -- `[knf]` Added `Alias` method +- `[knf]` Added methods `Alias` and `Config.Alias` - `[knf]` Added property name validation for all getters +- `[sliceutil]` Added method `IsEqual` - `[knf]` Code refactoring - `[knf]` Added more tests diff --git a/sliceutil/example_test.go b/sliceutil/example_test.go index 41d1cb22..864f392a 100644 --- a/sliceutil/example_test.go +++ b/sliceutil/example_test.go @@ -17,28 +17,40 @@ func ExampleCopy() { s1 := []string{"A", "B", "C"} s2 := Copy(s1) - fmt.Printf("%v", s2) + fmt.Printf("%v\n", s2) // Output: [A B C] } +func ExampleIsEqual() { + s1 := []int{1, 2, 3, 4} + s2 := []int{1, 2, 3, 4} + s3 := []int{1, 3, 2, 4} + + fmt.Printf("%v == %v → %t\n", s1, s2, IsEqual(s1, s2)) + fmt.Printf("%v == %v → %t\n", s2, s3, IsEqual(s2, s3)) + // Output: + // [1 2 3 4] == [1 2 3 4] → true + // [1 2 3 4] == [1 3 2 4] → false +} + func ExampleStringToInterface() { s := []string{"A", "B"} - fmt.Printf("%v", StringToInterface(s)) + fmt.Printf("%v\n", StringToInterface(s)) // Output: [A B] } func ExampleIntToInterface() { s := []int{1, 2} - fmt.Printf("%v", IntToInterface(s)) + fmt.Printf("%v\n", IntToInterface(s)) // Output: [1 2] } func ExampleErrorToString() { s := []error{fmt.Errorf("error1")} - fmt.Printf("%v", ErrorToString(s)) + fmt.Printf("%v\n", ErrorToString(s)) // Output: [error1] } diff --git a/sliceutil/sliceutil.go b/sliceutil/sliceutil.go index 9ed47ff0..0bdc468d 100644 --- a/sliceutil/sliceutil.go +++ b/sliceutil/sliceutil.go @@ -26,6 +26,24 @@ func Copy[K comparable](slice []K) []K { return s } +// IsEqual compares two slices and returns true if the slices are equal +func IsEqual[K comparable](s1, s2 []K) bool { + switch { + case s1 == nil && s2 == nil: + return true + case len(s1) != len(s2): + return false + } + + for i := range s1 { + if s1[i] != s2[i] { + return false + } + } + + return true +} + // StringToInterface converts slice with strings to slice with any func StringToInterface(data []string) []any { if len(data) == 0 { diff --git a/sliceutil/sliceutil_test.go b/sliceutil/sliceutil_test.go index 28ad796d..0dbf8a37 100644 --- a/sliceutil/sliceutil_test.go +++ b/sliceutil/sliceutil_test.go @@ -35,6 +35,22 @@ func (s *SliceSuite) TestCopy(c *C) { c.Assert(Copy([]string{"A"}), DeepEquals, []string{"A"}) } +func (s *SliceSuite) TestIsEqual(c *C) { + s1 := []int{1, 2, 3, 4} + s2 := []int{1, 2, 3, 4} + s3 := []int{1, 3, 2, 4} + s4 := []int{1, 2, 3} + + var s5, s6 []int + + c.Assert(IsEqual(s1, nil), Equals, false) + c.Assert(IsEqual(nil, s2), Equals, false) + c.Assert(IsEqual(s1, s2), Equals, true) + c.Assert(IsEqual(s1, s3), Equals, false) + c.Assert(IsEqual(s1, s4), Equals, false) + c.Assert(IsEqual(s5, s6), Equals, true) +} + func (s *SliceSuite) TestStringToInterface(c *C) { source := []string{"1", "2", "3"} From e432d67d6b458f145373a845619129085f8d9f1a Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Mon, 29 Apr 2024 16:19:15 +0300 Subject: [PATCH 5/6] [knf] Improvements --- knf/knf.go | 3 +++ knf/knf_test.go | 1 + 2 files changed, 4 insertions(+) diff --git a/knf/knf.go b/knf/knf.go index beb80c86..ad2102d1 100644 --- a/knf/knf.go +++ b/knf/knf.go @@ -19,6 +19,7 @@ import ( "time" "github.com/essentialkaos/ek/v12/knf/value" + "github.com/essentialkaos/ek/v12/sliceutil" ) // ////////////////////////////////////////////////////////////////////////////////// // @@ -719,6 +720,8 @@ func (c *Config) Is(name string, value any) bool { return c.GetTS(name).Unix() == t.Unix() case *time.Location: return fmt.Sprint(c.GetTZ(name)) == fmt.Sprint(t) + case []string: + return sliceutil.IsEqual(c.GetL(name), t) } return false diff --git a/knf/knf_test.go b/knf/knf_test.go index b9637083..fd69f334 100644 --- a/knf/knf_test.go +++ b/knf/knf_test.go @@ -535,6 +535,7 @@ func (s *KNFSuite) TestIs(c *check.C) { c.Assert(Is("file-mode:test1", os.FileMode(0644)), check.Equals, true) c.Assert(Is("duration:test2", time.Minute), check.Equals, true) c.Assert(Is("timestamp:test2", time.Unix(1709629048, 0)), check.Equals, true) + c.Assert(Is("list:test2", []string{"Test1", "Test2"}), check.Equals, true) l, _ := time.LoadLocation("Europe/Zurich") c.Assert(Is("timezone:test1", l), check.Equals, true) From f7cf0683d5e5ce6fbc32d526ba200d28f3507d6e Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Mon, 29 Apr 2024 16:26:08 +0300 Subject: [PATCH 6/6] Update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e7f2f911..ba807315 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ go get -u github.com/essentialkaos/ek/v12 * [aligo](https://kaos.sh/aligo) — Utility for checking and viewing Golang struct alignment info * [artefactor](https://kaos.sh/artefactor) — Utility for downloading artefacts from GitHub +* [atlassian-cloud-backuper](https://kaos.sh/atlassian-cloud-backuper) — Tool for backuping Atlassian cloud services (_Jira and Confluence_) * [Bastion](https://kaos.sh/bastion) — Utility for temporary disabling access to server * [bibop](https://kaos.sh/bibop) — Utility for testing command-line tools * [bop](https://kaos.sh/bop) — Utility for generating bibop tests for RPM packages @@ -129,6 +130,7 @@ go get -u github.com/essentialkaos/ek/v12 * [path](https://kaos.sh/path) — Dead simple tool for working with paths * [perfecto](https://kaos.sh/perfecto) — Tool for checking perfectly written RPM specs * [RBInstall](https://kaos.sh/rbinstall) — Utility for installing prebuilt ruby to RBEnv +* [RDS](https://kaos.sh/rds) — Tool for Redis orchestration * [Redis CLI Monitor](https://kaos.sh/redis-cli-monitor) — Tiny redis client for renamed MONITOR commands * [Redis Latency Monitor](https://kaos.sh/redis-latency-monitor) — Tiny Redis client for latency measurement * [Redis Monitor Top](https://kaos.sh/redis-monitor-top) — Tiny Redis client for aggregating stats from MONITOR flow