diff --git a/pkg/vars/map.go b/pkg/vars/map.go index f035dec..597f037 100644 --- a/pkg/vars/map.go +++ b/pkg/vars/map.go @@ -11,7 +11,7 @@ import ( "sync/atomic" ) -// Collection is collection of Variables safe for concurrent use. +// Map is collection of Variables safe for concurrent use. type Map struct { mu sync.RWMutex len int64 @@ -288,3 +288,253 @@ func (m *Map) UnmarshalJSON(data []byte) error { return nil } + +// Collection is collection of Variables safe for concurrent use. +type ReadOnlyMap struct { + mu sync.RWMutex + len int64 + db map[string]Variable +} + +func ReadOnlyMapFrom(m *Map) *ReadOnlyMap { + r := new(ReadOnlyMap) + m.Range(func(v Variable) bool { + _ = r.storeReadOnly(v.Name(), v, true) + return true + }) + return r +} + +// Get retrieves the value of the variable named by the key. +// It returns the value, which will be empty string if the variable is not set +// or value was empty. +func (m *ReadOnlyMap) Get(key string) (v Variable) { + m.mu.RLock() + defer m.mu.RUnlock() + v, ok := m.db[key] + if !ok { + return EmptyVariable + } + return v +} + +// Has reprts whether given variable exists. +func (m *ReadOnlyMap) Has(key string) bool { + m.mu.RLock() + defer m.mu.RUnlock() + _, ok := m.db[key] + return ok +} + +func (m *ReadOnlyMap) All() (all []Variable) { + m.Range(func(v Variable) bool { + all = append(all, v) + return true + }) + return +} + +// Load returns the variable stored in the Collection for a key, +// or EmptyVar if no value is present. +// The ok result indicates whether variable was found in the Collection. +func (m *ReadOnlyMap) Load(key string) (v Variable, ok bool) { + if !m.Has(key) { + return EmptyVariable, false + } + return m.Get(key), true +} + +// LoadOrDefault returns the existing value for the key if present. +// Much like LoadOrStore, but second argument willl be returned as +// Value whithout being stored into Map. +func (m *ReadOnlyMap) LoadOrDefault(key string, value any) (v Variable, loaded bool) { + m.mu.RLock() + defer m.mu.RUnlock() + + if len(key) > 0 { + if def, ok := value.(Variable); ok { + return def, false + } + } + // existing + if val, ok := m.db[key]; ok { + return val, true + } + + v, err := New(key, value, false) + if err != nil { + return EmptyVariable, false + } + return v, false +} + +// Range calls f sequentially for each key and value present in the map. +// If f returns false, range stops the iteration. +// +// Range does not necessarily correspond to any consistent snapshot of the Map's +// contents: no key will be visited more than once, but if the value for any key +// is stored or deleted concurrently, Range may reflect any mapping for that key +// from any point during the Range call. +// +// Range may be O(N) with the number of elements in the map even if f returns +// false after a constant number of calls. +func (m *ReadOnlyMap) Range(f func(v Variable) bool) { + m.mu.RLock() + keys := make([]string, len(m.db)) + i := 0 + for key := range m.db { + keys[i] = key + i++ + } + m.mu.RUnlock() + + sort.Strings(keys) + + m.mu.RLock() + for _, key := range keys { + v := m.db[key] + m.mu.RUnlock() + if !f(v) { + break + } + m.mu.RLock() + } + m.mu.RUnlock() +} + +// ToBytes returns []byte containing +// key = "value"\n. +func (m *ReadOnlyMap) ToBytes() []byte { + s := m.ToKeyValSlice() + + p := getParser() + defer p.free() + + for _, line := range s { + p.fmt.string(line + "\n") + } + return p.buf +} + +// ToKeyValSlice produces []string slice of strings in format key = "value". +func (m *ReadOnlyMap) ToKeyValSlice() []string { + r := []string{} + m.Range(func(v Variable) bool { + // we can do it directly on interface value since they all are Values + // implementing Stringer + r = append(r, v.Name()+"="+v.String()) + return true + }) + return r +} + +// Len of collection. +func (m *ReadOnlyMap) Len() int { + m.mu.RLock() + defer m.mu.RUnlock() + return int(atomic.LoadInt64(&m.len)) +} + +// GetWithPrefix return all variables with prefix if any as new Map +// and strip prefix from keys. +func (m *ReadOnlyMap) ExtractWithPrefix(prfx string) *ReadOnlyMap { + vars := new(ReadOnlyMap) + m.Range(func(v Variable) bool { + key := v.Name() + if len(key) >= len(prfx) && key[0:len(prfx)] == prfx { + _ = vars.storeReadOnly(key[len(prfx):], v, true) + } + return true + }) + return vars +} + +// LoadWithPrefix return all variables with prefix if any as new Map. +func (m *ReadOnlyMap) LoadWithPrefix(prfx string) (set *ReadOnlyMap, loaded bool) { + set = new(ReadOnlyMap) + m.Range(func(v Variable) bool { + key := v.Name() + if len(key) >= len(prfx) && key[0:len(prfx)] == prfx { + _ = set.storeReadOnly(key, v, true) + loaded = true + } + return true + }) + return set, loaded +} + +func (m *ReadOnlyMap) MarshalJSON() ([]byte, error) { + // Create a map to hold the key-value pairs of the synm.Map + var objMap = make(map[string]any) + + // Iterate over the synm.Map and add the key-value pairs to the map + m.Range(func(v Variable) bool { + objMap[v.Name()] = v.Any() + return true + }) + + // Use json.Marshal to convert the map to JSON + return json.Marshal(objMap) +} + +func (m *ReadOnlyMap) UnmarshalJSON(data []byte) error { + // Create a map to hold the key-value pairs from the JSON data + var objMap map[string]any + + // Use json.Unmarshal to parse the JSON data into the map + if err := json.Unmarshal(data, &objMap); err != nil { + return err + } + + // Iterate over the map and add the key-value pairs to the synm.Map + for key, value := range objMap { + if err := m.storeReadOnly(key, value, true); err != nil { + return err + } + } + + return nil +} + +// Store sets the value for a key. +// Error is returned when key or value parsing fails +// or variable is already set and is readonly. +func (m *ReadOnlyMap) store(key string, value any) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.db == nil { + m.db = make(map[string]Variable) + } + + curr, has := m.db[key] + if has && curr.ReadOnly() { + return errorf("%w: can not set value for %s", ErrReadOnly, key) + } + + if v, ok := value.(Variable); ok && v.Name() == key { + m.db[key] = v + if !has { + atomic.AddInt64(&m.len, 1) + } + return nil + } + + v, err := New(key, value, false) + if err != nil { + return err + } + m.db[key] = v + if !has { + atomic.AddInt64(&m.len, 1) + } + return err +} + +func (m *ReadOnlyMap) storeReadOnly(key string, value any, ro bool) error { + v, err := New(key, value, ro) + if err != nil { + return err + } + return m.store(key, v) +} diff --git a/pkg/vars/parser.go b/pkg/vars/parser.go index 9851c8e..85392d2 100644 --- a/pkg/vars/parser.go +++ b/pkg/vars/parser.go @@ -67,7 +67,7 @@ func parseBool(str string) (r bool, s string, e error) { switch str { case "1", "t", "T", "true", "TRUE", "True": r, s = true, "true" - case "0", "f", "F", "false", "FALSE", "False": + case "", "0", "f", "F", "false", "FALSE", "False": r, s = false, "false" default: r, s, e = false, "", errorf("%w: can not %s as bool", ErrValueConv, str) diff --git a/pkg/vars/varflag/flagset.go b/pkg/vars/varflag/flagset.go index 002ce0f..cbce1d0 100644 --- a/pkg/vars/varflag/flagset.go +++ b/pkg/vars/varflag/flagset.go @@ -28,6 +28,8 @@ type FlagSet struct { parsed bool } +var ErrInvalidFlagSetName = errors.New("invalid flag set name") + // NewFlagSet is wrapper to parse flags together. // e.g. under specific command. Where "name" is command name // to search before parsing the flags under this set. @@ -35,10 +37,10 @@ type FlagSet struct { // If argsn is -gt 0 then parser will stop after finding argsn+1 argument // which is not a flag. func NewFlagSet(name string, argn int) (*FlagSet, error) { - if name == "/" || (len(os.Args) > 0 && name == filepath.Base(os.Args[0]) || name == os.Args[0]) { - name = "/" + if name == "/" { + name = filepath.Base(os.Args[0]) } else if !ValidFlagName(name) { - return nil, fmt.Errorf("%w: name %q is not valid for flag set", ErrFlag, name) + return nil, fmt.Errorf("%w: %q is not valid name for flag set", ErrInvalidFlagSetName, name) } return &FlagSet{name: name, argn: argn}, nil } @@ -247,7 +249,7 @@ func (s *FlagSet) Parse(args []string) error { } var currargs []string - if s.name != "/" && s.name != "*" && s.name != filepath.Base(os.Args[0]) { + if s.name != "*" && s.name != filepath.Base(os.Args[0]) { for i, arg := range args { if arg == s.name { s.pos = i @@ -285,7 +287,7 @@ func (s *FlagSet) Parse(args []string) error { return err } if set.Present() { - if s.name == "/" { + if s.name == filepath.Base(os.Args[0]) { // update global flag command names for _, flag := range s.flags { if !flag.Present() { diff --git a/pkg/vars/vars.go b/pkg/vars/vars.go index 080ddbf..ce30c23 100644 --- a/pkg/vars/vars.go +++ b/pkg/vars/vars.go @@ -15,6 +15,10 @@ import ( var ( EmptyVariable = Variable{} EmptyValue = Value{} + NilValue = Value{ + kind: KindInvalid, + str: "nil", + } ) var (