package mapiter import ( "context" "reflect" "sync" "github.com/pkg/errors" ) // Iterate creates an iterator from arbitrary map types. This is not // the most efficient tool, but it's the quickest way to create an // iterator for maps. // Also, note that you cannot make any assumptions on the order of // pairs being returned. func Iterate(ctx context.Context, m interface{}) (Iterator, error) { mrv := reflect.ValueOf(m) if mrv.Kind() != reflect.Map { return nil, errors.Errorf(`argument must be a map (%s)`, mrv.Type()) } ch := make(chan *Pair) go func(ctx context.Context, ch chan *Pair, mrv reflect.Value) { defer close(ch) for _, key := range mrv.MapKeys() { value := mrv.MapIndex(key) pair := &Pair{ Key: key.Interface(), Value: value.Interface(), } select { case <-ctx.Done(): return case ch <- pair: } } }(ctx, ch, mrv) return New(ch), nil } // Source represents a map that knows how to create an iterator type Source interface { Iterate(context.Context) Iterator } // Pair represents a single pair of key and value from a map type Pair struct { Key interface{} Value interface{} } // Iterator iterates through keys and values of a map type Iterator interface { Next(context.Context) bool Pair() *Pair } type iter struct { ch chan *Pair mu sync.RWMutex next *Pair } // Visitor represents an object that handles each pair in a map type Visitor interface { Visit(interface{}, interface{}) error } // VisitorFunc is a type of Visitor based on a function type VisitorFunc func(interface{}, interface{}) error func (fn VisitorFunc) Visit(s interface{}, v interface{}) error { return fn(s, v) } func New(ch chan *Pair) Iterator { return &iter{ ch: ch, } } // Next returns true if there are more items to read from the iterator func (i *iter) Next(ctx context.Context) bool { i.mu.RLock() if i.ch == nil { i.mu.RUnlock() return false } i.mu.RUnlock() i.mu.Lock() defer i.mu.Unlock() select { case <-ctx.Done(): i.ch = nil return false case v, ok := <-i.ch: if !ok { i.ch = nil return false } i.next = v return true } //nolint:govet return false // never reached } // Pair returns the currently buffered Pair. Calling Next() will reset its value func (i *iter) Pair() *Pair { i.mu.RLock() defer i.mu.RUnlock() return i.next } // Walk walks through each element in the map func Walk(ctx context.Context, s Source, v Visitor) error { for i := s.Iterate(ctx); i.Next(ctx); { pair := i.Pair() if err := v.Visit(pair.Key, pair.Value); err != nil { return errors.Wrapf(err, `failed to visit key %s`, pair.Key) } } return nil } // AsMap returns the values obtained from the source as a map func AsMap(ctx context.Context, s interface{}, v interface{}) error { var iter Iterator switch reflect.ValueOf(s).Kind() { case reflect.Map: x, err := Iterate(ctx, s) if err != nil { return errors.Wrap(err, `failed to iterate over map type`) } iter = x default: ssrc, ok := s.(Source) if !ok { return errors.Errorf(`cannot iterate over %T: not a mapiter.Source type`, s) } iter = ssrc.Iterate(ctx) } dst := reflect.ValueOf(v) // dst MUST be a pointer to a map type if kind := dst.Kind(); kind != reflect.Ptr { return errors.Errorf(`dst must be a pointer to a map (%s)`, dst.Type()) } dst = dst.Elem() if dst.Kind() != reflect.Map { return errors.Errorf(`dst must be a pointer to a map (%s)`, dst.Type()) } if dst.IsNil() { dst.Set(reflect.MakeMap(dst.Type())) } // dst must be assignable if !dst.CanSet() { return errors.New(`dst is not writeable`) } keytyp := dst.Type().Key() valtyp := dst.Type().Elem() for iter.Next(ctx) { pair := iter.Pair() rvkey := reflect.ValueOf(pair.Key) rvvalue := reflect.ValueOf(pair.Value) if !rvkey.Type().AssignableTo(keytyp) { return errors.Errorf(`cannot assign key of type %s to map key of type %s`, rvkey.Type(), keytyp) } switch rvvalue.Kind() { // we can only check if we can assign to rvvalue to valtyp if it's non-nil case reflect.Invalid: rvvalue = reflect.New(valtyp).Elem() default: if !rvvalue.Type().AssignableTo(valtyp) { return errors.Errorf(`cannot assign value of type %s to map value of type %s`, rvvalue.Type(), valtyp) } } dst.SetMapIndex(rvkey, rvvalue) } return nil }