Skip to content

Commit

Permalink
feat: implement vars.ReadOnlyMap
Browse files Browse the repository at this point in the history
fix: ensure that bool is parsed aslo from empty string
fix: simplify varflag FlagSet name check
feat: add vars.NilValue next to vars.EmptyValue to define nil value

Signed-off-by: Marko Kungla <marko@mkungla.dev>
  • Loading branch information
mkungla committed Jun 2, 2024
1 parent 784a063 commit 333b014
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 7 deletions.
252 changes: 251 additions & 1 deletion pkg/vars/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
2 changes: 1 addition & 1 deletion pkg/vars/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 7 additions & 5 deletions pkg/vars/varflag/flagset.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,19 @@ 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.
// argsn is number of command line arguments allowed within this set.
// 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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down
4 changes: 4 additions & 0 deletions pkg/vars/vars.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import (
var (
EmptyVariable = Variable{}
EmptyValue = Value{}
NilValue = Value{
kind: KindInvalid,
str: "nil",
}
)

var (
Expand Down

0 comments on commit 333b014

Please sign in to comment.