Repository: openconfig/ygot (go)

Issue:
Feature Request: Add an ast.Walk style iterator of GoStructs
ygot already has a [`util.ForEachField`](https://pkg.go.dev/github.com/openconfig/ygot/util#ForEachField) iterator of the GoStruct.

Although this function uses in/out for state keeping, we may occasionally add more interfaces to accommodate new use cases, e.g. to allow shortcut visiting some child nodes.

We can add yet-another more generic API that mimics [`ast.Walk`](https://go.dev/src/go/ast/walk.go) and `ast.Visitor` pattern.

We can add a new `util.Walk` for GoStruct, and then `ForEachField` can probably be rewritten using `Walk`.

This allows users to keep using existing ForEachField for simple cases, and use `util.Walk` for more complicate traversal.

Pull request description:
Add an ast.Walk style iterator of GoStructs.
* ast.Walk: https://go.dev/src/go/ast/walk.go
* Rewrite ForEachField using the new `util.Walk` iterator, including supporting the PathQueryNodeMemo.
* Rewrite ForEachDataField and ForEachDataField2 using the new `util.Walk` iterator.

Resolves #889

Patch:
diff --git a/util/reflect.go b/util/reflect.go
index e2779df..bbb2430 100644
--- a/util/reflect.go
+++ b/util/reflect.go
@@ -21,7 +21,6 @@ import (
 
 	"github.com/kylelemons/godebug/pretty"
 	"github.com/openconfig/goyang/pkg/yang"
-	"github.com/openconfig/ygot/internal/yreflect"
 
 	log "github.com/golang/glog"
 
@@ -613,7 +612,7 @@ func (node *PathQueryNodeMemo) GetRoot() *PathQueryNodeMemo {
 // in, out are passed through from the caller to the iteration visitor function
 // and can be used to pass state in and out. They are not otherwise touched.
 // It returns a slice of errors encountered while processing the field.
-type FieldIteratorFunc func(ni *NodeInfo, in, out interface{}) Errors
+type FieldIteratorFunc func(ni *NodeInfo, in, out any) Errors
 
 // IterationAction is an enumeration representing different iteration actions.
 //
@@ -632,6 +631,7 @@ const (
 // in, out are passed through from the caller to the iteration visitor function
 // and can be used to pass state in and out. They are not otherwise touched.
 // It returns what next iteration action to take as well as an error.
+// TODO Deprecate the ForEachField and ForEachDataField functions in favor of Walk.
 type FieldIteratorFunc2 func(ni *NodeInfo, in, out any) (IterationAction, Errors)
 
 // ForEachField recursively iterates through the fields of value (which may be
@@ -648,175 +648,86 @@ type FieldIteratorFunc2 func(ni *NodeInfo, in, out any) (IterationAction, Errors
 // - iterFunction is executed on each scalar field.
 //
 // It returns a slice of errors encountered while processing the struct.
-func ForEachField(schema *yang.Entry, value interface{}, in, out interface{}, iterFunction FieldIteratorFunc) Errors {
-	if IsValueNil(value) {
-		return nil
-	}
-	return forEachFieldInternal(&NodeInfo{Schema: schema, FieldValue: reflect.ValueOf(value)}, in, out, iterFunction)
-}
-
-// forEachFieldInternal recursively iterates through the fields of value (which
-// may be any Go type) and executes iterFunction on each field that is present
-// within the supplied schema. Fields that are explicitly noted not to have
-// a schema (e.g., annotation fields) are skipped.
 //
-// - in, out are passed through from the caller to the iteration and can be used
-// arbitrarily in the iteration function to carry state and results.
-func forEachFieldInternal(ni *NodeInfo, in, out interface{}, iterFunction FieldIteratorFunc) Errors {
-	if IsValueNil(ni) {
-		return nil
-	}
-
-	// If the field is an annotation, then we do not process it any further, including
-	// skipping running the iterFunction.
-	if IsYgotAnnotation(ni.StructField) {
+// See util.Walk if more dynamic control of the traversal is needed.
+func ForEachField(schema *yang.Entry, value any, in, out any, iterFunction FieldIteratorFunc) Errors {
+	if IsValueNil(value) {
 		return nil
 	}
-
-	var errs Errors
-	errs = AppendErrs(errs, iterFunction(ni, in, out))
-
-	// Special processing where an "in" input value is provided.
-	var newPathQueryMemo func() *PathQueryNodeMemo
-	switch v := in.(type) {
-	case *PathQueryNodeMemo: // Memoization of path queries requested.
-		newPathQueryMemo = func() *PathQueryNodeMemo {
-			return &PathQueryNodeMemo{Parent: v, Memo: PathQueryMemo{}}
-		}
-	default:
-	}
-
-	v := ni.FieldValue
-	t := v.Type()
-
-	orderedMap, isOrderedMap := v.Interface().(goOrderedMap)
-
-	switch {
-	case isOrderedMap, IsTypeSlice(t), IsTypeMap(t):
-		schema := *(ni.Schema)
-		schema.ListAttr = nil
-
-		var relPath []string
-		if !schema.IsLeafList() {
-			// Leaf-list elements share the parent schema with listattr unset.
-			relPath = []string{schema.Name}
-		}
-
-		var elemType reflect.Type
-		switch {
-		case isOrderedMap:
-			var err error
-			elemType, err = yreflect.OrderedMapElementType(orderedMap)
-			if err != nil {
-				errs = AppendErr(errs, err)
-				return errs
-			}
-		default:
-			elemType = t.Elem()
+	var v Visitor
+	errCollector := new(DefaultWalkErrors)
+	if inMemo, ok := in.(*PathQueryNodeMemo); ok {
+		v = forEachFieldMemoVisitor{
+			// Any existing Memo map passed in will be ignored.
+			parent:         inMemo.Parent,
+			out:            out,
+			iterFunction:   iterFunction,
+			errorCollector: errCollector,
 		}
-
-		nn := *ni
-		// The schema for each element is the list schema minus the list
-		// attrs.
-		nn.Schema = &schema
-		nn.Parent = ni
-		nn.PathFromParent = relPath
-
-		visitListElement := func(k, v reflect.Value) {
-			nn := nn
-			nn.FieldValue = v
-			nn.FieldKey = k
-			switch in.(type) {
-			case *PathQueryNodeMemo: // Memoization of path queries requested.
-				errs = AppendErrs(errs, forEachFieldInternal(&nn, newPathQueryMemo(), out, iterFunction))
-			default:
-				errs = AppendErrs(errs, forEachFieldInternal(&nn, in, out, iterFunction))
-			}
+	} else {
+		v = &forEachFieldVisitor{
+			in:             in,
+			out:            out,
+			iterFunction:   iterFunction,
+			errorCollector: errCollector,
 		}
+	}
 
-		switch {
-		case IsNilOrInvalidValue(v):
-			// Traverse the type tree only from this point.
-			visitListElement(reflect.Value{}, reflect.Zero(elemType))
-		case IsTypeSlice(t):
-			for i := 0; i < ni.FieldValue.Len(); i++ {
-				visitListElement(reflect.Value{}, ni.FieldValue.Index(i))
-			}
-		case isOrderedMap:
-			var err error
-			nn.FieldKeys, err = yreflect.OrderedMapKeys(orderedMap)
-			if err != nil {
-				errs = AppendErr(errs, err)
-				return errs
-			}
-			if err := yreflect.RangeOrderedMap(orderedMap, func(k, v reflect.Value) bool {
-				visitListElement(k, v)
-				return true
-			}); err != nil {
-				errs = AppendErr(errs, err)
-			}
-		case IsTypeMap(t):
-			nn.FieldKeys = ni.FieldValue.MapKeys()
-			for _, key := range ni.FieldValue.MapKeys() {
-				visitListElement(key, ni.FieldValue.MapIndex(key))
-			}
-		}
+	// For back-compatibility with ForFieldField, we explicitly interleave the errors from
+	// both the traversal and the iterFunction into the shared Errors slice.
+	_ = Walk(v, WalkNodeFromGoStruct(value), DefaultWalkOptions().WithWalkErrors(errCollector).WithSchema(schema))
+	// We ignore the returned WalkErrors because we know it's a reference to v.errorCollector.
+	return errCollector.Errors
+}
 
-	case IsTypeStructPtr(t):
-		t = t.Elem()
-		if !IsNilOrInvalidValue(v) {
-			v = v.Elem()
-		}
-		fallthrough
-	case IsTypeStruct(t):
-		for i := 0; i < t.NumField(); i++ {
-			sf := t.Field(i)
-
-			// Do not handle annotation fields, since they have no schema.
-			if IsYgotAnnotation(sf) {
-				continue
-			}
+// iterFuncToIterFunc2 converts a FieldIteratorFunc to FieldIteratorFunc2.
+func iterFuncToIterFunc2(iterFunction FieldIteratorFunc) FieldIteratorFunc2 {
+	return func(ni *NodeInfo, in, out any) (IterationAction, Errors) {
+		return ContinueIteration, iterFunction(ni, in, out)
+	}
+}
 
-			nn := &NodeInfo{
-				Parent:      ni,
-				StructField: sf,
-				FieldValue:  reflect.Zero(sf.Type),
-			}
-			if !IsNilOrInvalidValue(v) {
-				nn.FieldValue = v.Field(i)
-			}
-			ps, err := SchemaPaths(nn.StructField)
-			if err != nil {
-				return NewErrs(err)
-			}
+type forEachFieldVisitor struct {
+	in, out        any
+	errorCollector *DefaultWalkErrors
+	iterFunction   FieldIteratorFunc
+}
 
-			for _, p := range ps {
-				nn.Schema = FirstChild(ni.Schema, p)
-				if nn.Schema == nil {
-					e := fmt.Errorf("forEachFieldInternal could not find child schema with path %v from schema name %s", p, ni.Schema.Name)
-					DbgPrint(e.Error())
-					log.Errorln(e)
-					continue
-				}
-				nn.PathFromParent = p
-				switch in.(type) {
-				case *PathQueryNodeMemo: // Memoization of path queries requested.
-					errs = AppendErrs(errs, forEachFieldInternal(nn, newPathQueryMemo(), out, iterFunction))
-				default:
-					errs = AppendErrs(errs, forEachFieldInternal(nn, in, out, iterFunction))
-				}
-			}
-		}
+func (vf *forEachFieldVisitor) Visit(node WalkNode) Visitor {
+	if node == nil {
+		return nil
+	}
+	ni := node.NodeInfo()
+	if err := vf.iterFunction(ni, vf.in, vf.out); err != nil {
+		vf.errorCollector.Collect(err)
 	}
+	return vf
+}
 
-	return errs
+type forEachFieldMemoVisitor struct {
+	parent         *PathQueryNodeMemo
+	out            any
+	errorCollector *DefaultWalkErrors
+	iterFunction   FieldIteratorFunc
 }
 
-// iterFuncToIterFunc2 converts a FieldIteratorFunc to FieldIteratorFunc2.
-func iterFuncToIterFunc2(iterFunction FieldIteratorFunc) FieldIteratorFunc2 {
-	return func(ni *NodeInfo, in, out interface{}) (IterationAction, Errors) {
-		return ContinueIteration, iterFunction(ni, in, out)
+func (vf forEachFieldMemoVisitor) Visit(node WalkNode) Visitor {
+	if node == nil {
+		return nil
 	}
+	// Each children sibling needs a dedicated (not shared) Memo data structure (map).
+	in := &PathQueryNodeMemo{
+		Parent: vf.parent,
+		Memo:   PathQueryMemo{},
+	}
+	ni := node.NodeInfo()
+	if err := vf.iterFunction(ni, in, vf.out); err != nil {
+		vf.errorCollector.Collect(err)
+	}
+	// Since we use a value receiver vf instead of a pointer receiver,
+	// vf is passed as a copy and it's safe to manipulate.
+	vf.parent = in
+	return vf
 }
 
 // ForEachDataField iterates the value supplied and calls the iterFunction for
@@ -825,13 +736,13 @@ func iterFuncToIterFunc2(iterFunction FieldIteratorFunc) FieldIteratorFunc2 {
 // without inspection by this function, and can be used by the caller to store
 // input and output during the iteration through the data tree.
 //
-// Deprecated: Use ForEachDataField2 instead.
-func ForEachDataField(value, in, out interface{}, iterFunction FieldIteratorFunc) Errors {
+// Deprecated: Use ForEachDataField2 or util.Walk instead.
+func ForEachDataField(value, in, out any, iterFunction FieldIteratorFunc) Errors {
 	if IsValueNil(value) {
 		return nil
 	}
 
-	return forEachDataFieldInternal(&NodeInfo{FieldValue: reflect.ValueOf(value)}, in, out, iterFuncToIterFunc2(iterFunction))
+	return ForEachDataField2(value, in, out, iterFuncToIterFunc2(iterFunction))
 }
 
 // ForEachDataField2 is an improved ForEachDataField that allows iteration over
@@ -843,141 +754,46 @@ func ForEachDataField(value, in, out interface{}, iterFunction FieldIteratorFunc
 // The in and out arguments are passed to the iterFunction without inspection
 // by this function, and can be used by the caller to store input and output
 // during the iteration through the data tree.
+//
+// See util.Walk if more dynamic control of the traversal is needed.
 func ForEachDataField2(value, in, out any, iterFunction FieldIteratorFunc2) Errors {
 	if IsValueNil(value) {
 		return nil
 	}
-
-	return forEachDataFieldInternal(&NodeInfo{FieldValue: reflect.ValueOf(value)}, in, out, iterFunction)
+	errCollector := new(DefaultWalkErrors)
+	v := &forEachDataField2Visitor{
+		in:             in,
+		out:            out,
+		iterFunction2:  iterFunction,
+		errorCollector: errCollector,
+	}
+	// For back-compatibility with ForFieldField, we explicitly interleave the errors from
+	// both the traversal and the iterFunction into the shared Errors slice.
+	_ = Walk(v, WalkNodeFromGoStruct(value), DefaultWalkOptions().WithWalkErrors(errCollector))
+	// We ignore the returned WalkErrors because we know it's a reference to v.errorCollector.
+	return errCollector.Errors
 }
 
-func forEachDataFieldInternal(ni *NodeInfo, in, out any, iterFunction FieldIteratorFunc2) Errors {
-	if IsValueNil(ni) {
-		return nil
-	}
+type forEachDataField2Visitor struct {
+	in, out        any
+	errorCollector *DefaultWalkErrors
+	iterFunction2  FieldIteratorFunc2
+}
 
-	if IsNilOrInvalidValue(ni.FieldValue) {
-		// Skip any fields that are nil within the data tree, since we
-		// do not need to iterate on them.
+func (vf *forEachDataField2Visitor) Visit(node WalkNode) Visitor {
+	if node == nil {
 		return nil
 	}
-
-	var errs Errors
-	// Run the iterator function for this field.
-	iterAction, err := iterFunction(ni, in, out)
-	errs = AppendErrs(errs, err)
-	switch iterAction {
-	case DoNotIterateDescendants:
-		return errs
-	case ContinueIteration:
-	default:
-		errs = AppendErr(errs, fmt.Errorf("unhandled IterationAction type: %v", iterAction))
-		return errs
+	ni := node.NodeInfo()
+	action, err := vf.iterFunction2(ni, vf.in, vf.out)
+	if err != nil {
+		vf.errorCollector.Collect(err)
 	}
-
-	v := ni.FieldValue
-	t := v.Type()
-
-	orderedMap, isOrderedMap := v.Interface().(goOrderedMap)
-
-	// Determine whether we need to recurse into the field, or whether it is
-	// a leaf or leaf-list, which are not recursed into when traversing the
-	// data tree.
-	switch {
-	case isOrderedMap:
-		// Handle the case of a keyed map, which is a YANG list.
-		if IsNilOrInvalidValue(v) {
-			return errs
-		}
-		nn := *ni
-		nn.Parent = ni
-		var err error
-		nn.FieldKeys, err = yreflect.OrderedMapKeys(orderedMap)
-		if err != nil {
-			errs = AppendErr(errs, err)
-			return errs
-		}
-		if err := yreflect.RangeOrderedMap(orderedMap, func(k, v reflect.Value) bool {
-			nn := nn
-			nn.FieldValue = v
-			nn.FieldKey = k
-			errs = AppendErrs(errs, forEachDataFieldInternal(&nn, in, out, iterFunction))
-			return true
-		}); err != nil {
-			errs = AppendErr(errs, err)
-		}
-	case IsTypeStructPtr(t):
-		// A struct pointer in a GoStruct is a pointer to another container within
-		// the YANG, therefore we dereference the pointer and then recurse. If the
-		// pointer is nil, then we do not need to do this since the data tree branch
-		// is unset in the schema.
-		t = t.Elem()
-		v = v.Elem()
-		fallthrough
-	case IsTypeStruct(t):
-		// Handle non-pointer structs by recursing into each field of the struct.
-		for i := 0; i < t.NumField(); i++ {
-			sf := t.Field(i)
-			nn := &NodeInfo{
-				Parent:      ni,
-				StructField: sf,
-				FieldValue:  reflect.Zero(sf.Type),
-			}
-
-			nn.FieldValue = v.Field(i)
-			ps, err := SchemaPaths(nn.StructField)
-			if err != nil {
-				return NewErrs(err)
-			}
-			// In the case that the field expands to >1 different data tree path,
-			// i.e., SchemaPaths above returns more than one path, then we recurse
-			// for each schema path. This ensures that the iterator
-			// function runs for all expansions of the data tree as well as the GoStruct
-			// fields.
-			for _, p := range ps {
-				nn.PathFromParent = p
-				if IsTypeSlice(sf.Type) || IsTypeMap(sf.Type) {
-					// Since lists can have path compression - where the path contains more
-					// than one element, ensure that the schema path we received is only two
-					// elements long. This protects against compression errors where there are
-					// trailing spaces (e.g., a path tag of config/bar/).
-					nn.PathFromParent = p[0:1]
-				}
-				errs = AppendErrs(errs, forEachDataFieldInternal(nn, in, out, iterFunction))
-			}
-		}
-	case IsTypeSlice(t):
-		// Only iterate in the data tree if the slice is of structs, otherwise
-		// for leaf-lists we only run once.
-		if !IsTypeStructPtr(t.Elem()) && !IsTypeStruct(t.Elem()) {
-			return errs
-		}
-
-		for i := 0; i < ni.FieldValue.Len(); i++ {
-			nn := *ni
-			nn.Parent = ni
-			// The name of the list is the same in each of the entries within the
-			// list therefore, we do not need to set the path to be different from
-			// the parent.
-			nn.PathFromParent = ni.PathFromParent
-			nn.FieldValue = ni.FieldValue.Index(i)
-			errs = AppendErrs(errs, forEachDataFieldInternal(&nn, in, out, iterFunction))
-		}
-	case IsTypeMap(t):
-		// Handle the case of a keyed map, which is a YANG list.
-		if IsNilOrInvalidValue(v) {
-			return errs
-		}
-		for _, key := range ni.FieldValue.MapKeys() {
-			nn := *ni
-			nn.Parent = ni
-			nn.FieldValue = ni.FieldValue.MapIndex(key)
-			nn.FieldKey = key
-			nn.FieldKeys = ni.FieldValue.MapKeys()
-			errs = AppendErrs(errs, forEachDataFieldInternal(&nn, in, out, iterFunction))
-		}
+	vf.errorCollector.Errors = AppendErrs(vf.errorCollector.Errors, err)
+	if action == DoNotIterateDescendants {
+		return nil
 	}
-	return errs
+	return vf
 }
 
 // GetNodes returns the nodes in the data tree at the indicated path, relative
diff --git a/util/walk.go b/util/walk.go
new file mode 100644
index 0000000..76ae370
--- /dev/null
+++ b/util/walk.go
@@ -0,0 +1,392 @@
+package util
+
+import (
+	"fmt"
+	"reflect"
+
+	log "github.com/golang/glog"
+	"github.com/openconfig/goyang/pkg/yang"
+	"github.com/openconfig/ygot/internal/yreflect"
+)
+
+// WalkNode is an abstraction of NodeInfo during the util.Walk.
+type WalkNode interface {
+	NodeInfo() *NodeInfo
+}
+
+// WalkErrors is an abstraction of collecting Errors during the util.Walk.
+type WalkErrors interface {
+	Collect(error)
+}
+
+// Visitor has a visit method that is invoked for each node encountered by Walk.
+type Visitor interface {
+	Visit(node WalkNode) (w Visitor)
+}
+
+// Walk traverses the nodes with a customized visitor.
+//
+// It traverses a GoStruct in depth-first order: It starts by calling
+// v.Visit(node); node must not be nil. If the visitor w returned by
+// v.Visit(node) is not nil, Walk is invoked recursively with visitor
+// w for each of the non-nil children of node, followed by a call of
+// w.Visit(nil).
+//
+// By default, the traversal only visit existing GoStruct fields.
+// If a customized schema is provided via WithSchema WalkOptions,
+// then the traversal will visit the schema entry even if the GoStruct does not populate it.
+//
+// The Visitor should handle its own error reporting and early termination.
+// Any error during the traversal that is not part of Visitor will be aggregated into the
+// returned WalkErrors.
+// If not overwritten, the returned WalkErrors is of DefaultWalkErrors type, which is an alias of Errors.
+func Walk(v Visitor, node WalkNode, o *WalkOptions) WalkErrors {
+	if o == nil {
+		o = &WalkOptions{}
+	}
+	if o.WalkErrors == nil {
+		o.WalkErrors = &DefaultWalkErrors{}
+	}
+	if o.schema == nil {
+		walkDataFieldInternal(v, node, o)
+		return o.WalkErrors
+	}
+	node = WalkNodeFromNodeInfo(&NodeInfo{
+		Schema:     o.schema,
+		FieldValue: node.NodeInfo().FieldValue,
+	})
+	walkFieldInternal(v, node, o)
+	return o.WalkErrors
+}
+
+var _ WalkNode = (*walkNodeInfo)(nil)
+
+type walkNodeInfo struct {
+	ni *NodeInfo
+}
+
+func (w *walkNodeInfo) NodeInfo() *NodeInfo {
+	return w.ni
+}
+
+// WalkNodeFromGoStruct converts a GoStruct to WalkNode with empty schema.
+func WalkNodeFromGoStruct(value any) WalkNode {
+	return WalkNodeFromNodeInfo(&NodeInfo{
+		FieldValue: reflect.ValueOf(value),
+	})
+}
+
+// WalkNodeFromNodeInfo converts a NodeInfo to WalkNode.
+func WalkNodeFromNodeInfo(ni *NodeInfo) WalkNode {
+	return &walkNodeInfo{ni: ni}
+}
+
+// WalkOptions are configurations of the Walk function.
+type WalkOptions struct {
+	WalkErrors
+	schema *yang.Entry
+}
+
+// DefaultWalkOptions initialize a WalkOptions.
+func DefaultWalkOptions() *WalkOptions {
+	return &WalkOptions{}
+}
+
+// WithSchema traverses schema entries even when a node is not populated with data.
+func (o *WalkOptions) WithSchema(schema *yang.Entry) *WalkOptions {
+	o.schema = schema
+	return o
+}
+
+// WithWalkErrors customizes the WalkErrors returned by the Walk function.
+// If unspecified, the DefaultWalkErrors is used.
+func (o *WalkOptions) WithWalkErrors(we WalkErrors) *WalkOptions {
+	o.WalkErrors = we
+	return o
+}
+
+// DefaultWalkErrors is the default WalkErrors used unless overwritten via WithWalkErrors.
+type DefaultWalkErrors struct {
+	Errors
+}
+
+var _ WalkErrors = (*DefaultWalkErrors)(nil)
+
+// Collect is used by Walk to aggregate errors during the traversal.
+func (e *DefaultWalkErrors) Collect(err error) {
+	e.Errors = AppendErr(e.Errors, err)
+}
+
+// walkFieldInternal traverses a GoStruct in depth-first order: It starts by calling
+// v.Visit(node); node must not be nil. If the visitor w returned by
+// v.Visit(node) is not nil, Walk is invoked recursively with visitor
+// w for each of the non-nil children of node, followed by a call of
+// w.Visit(nil).
+// It behaves similar to ForEachField to determine the set of children of a node.
+// Precondition: o.errorCollector must be initialized.
+func walkFieldInternal(visitor Visitor, node WalkNode, o *WalkOptions) {
+	ni := node.NodeInfo()
+	// Ignore nil field.
+	if IsValueNil(ni) {
+		return
+	}
+	// If the field is an annotation, then we do not process it any further, including
+	// skipping running the iterFunction.
+	if IsYgotAnnotation(ni.StructField) {
+		return
+	}
+	// walk the node itself
+	childVisitor := visitor.Visit(node)
+	if childVisitor == nil {
+		return
+	}
+	defer childVisitor.Visit(nil)
+
+	v := ni.FieldValue
+	t := v.Type()
+
+	// walk children
+	orderedMap, isOrderedMap := v.Interface().(goOrderedMap)
+
+	switch {
+	case isOrderedMap, IsTypeSlice(t), IsTypeMap(t):
+		schema := *(ni.Schema)
+		schema.ListAttr = nil
+
+		var relPath []string
+		if !schema.IsLeafList() {
+			// Leaf-list elements share the parent schema with listattr unset.
+			relPath = []string{schema.Name}
+		}
+
+		var elemType reflect.Type
+		switch {
+		case isOrderedMap:
+			var err error
+			elemType, err = yreflect.OrderedMapElementType(orderedMap)
+			if err != nil {
+				o.WalkErrors.Collect(err)
+				return
+			}
+		default:
+			elemType = t.Elem()
+		}
+
+		nn := *ni
+		// The schema for each element is the list schema minus the list
+		// attrs.
+		nn.Schema = &schema
+		nn.Parent = ni
+		nn.PathFromParent = relPath
+
+		visitListElement := func(k, v reflect.Value) {
+			nn := nn
+			nn.FieldValue = v
+			nn.FieldKey = k
+			walkFieldInternal(childVisitor, WalkNodeFromNodeInfo(&nn), o)
+		}
+
+		switch {
+		case IsNilOrInvalidValue(v):
+			// Traverse the type tree only from this point.
+			visitListElement(reflect.Value{}, reflect.Zero(elemType))
+		case IsTypeSlice(t):
+			for i := 0; i < ni.FieldValue.Len(); i++ {
+				visitListElement(reflect.Value{}, ni.FieldValue.Index(i))
+			}
+		case isOrderedMap:
+			var err error
+			nn.FieldKeys, err = yreflect.OrderedMapKeys(orderedMap)
+			if err != nil {
+				o.WalkErrors.Collect(err)
+				return
+			}
+			if err := yreflect.RangeOrderedMap(orderedMap, func(k, v reflect.Value) bool {
+				visitListElement(k, v)
+				return true
+			}); err != nil {
+				o.WalkErrors.Collect(err)
+			}
+		case IsTypeMap(t):
+			nn.FieldKeys = ni.FieldValue.MapKeys()
+			for _, key := range ni.FieldValue.MapKeys() {
+				visitListElement(key, ni.FieldValue.MapIndex(key))
+			}
+		}
+
+	case IsTypeStructPtr(t):
+		t = t.Elem()
+		if !IsNilOrInvalidValue(v) {
+			v = v.Elem()
+		}
+		fallthrough
+	case IsTypeStruct(t):
+		for i := 0; i < t.NumField(); i++ {
+			sf := t.Field(i)
+
+			// Do not handle annotation fields, since they have no schema.
+			if IsYgotAnnotation(sf) {
+				continue
+			}
+
+			nn := &NodeInfo{
+				Parent:      ni,
+				StructField: sf,
+			}
+			if !IsNilOrInvalidValue(v) {
+				nn.FieldValue = v.Field(i)
+			} else {
+				nn.FieldValue = reflect.Zero(sf.Type)
+			}
+			ps, err := SchemaPaths(nn.StructField)
+			if err != nil {
+				o.WalkErrors.Collect(err)
+				return
+			}
+
+			for _, p := range ps {
+				nn.Schema = FirstChild(ni.Schema, p)
+				if nn.Schema == nil {
+					e := fmt.Errorf("forEachFieldInternal could not find child schema with path %v from schema name %s", p, ni.Schema.Name)
+					DbgPrint(e.Error())
+					// TODO(wenovus) Consider making this into an error.
+					log.Errorln(e)
+					continue
+				}
+				nn.PathFromParent = p
+				walkFieldInternal(childVisitor, WalkNodeFromNodeInfo(nn), o)
+			}
+		}
+	}
+}
+
+// walkDataFieldInternal traverses a GoStruct in depth-first order: It starts by calling
+// v.Visit(node); node must not be nil. If the visitor w returned by
+// v.Visit(node) is not nil, Walk is invoked recursively with visitor
+// w for each of the non-nil children of node, followed by a call of
+// w.Visit(nil).
+// It behaves similar to ForEachDataField2 to determine the set of children of a node.
+// Precondition: o.errorCollector must be initialized.
+func walkDataFieldInternal(visitor Visitor, node WalkNode, o *WalkOptions) {
+	ni := node.NodeInfo()
+	if IsValueNil(ni) {
+		return
+	}
+
+	if IsNilOrInvalidValue(ni.FieldValue) {
+		// Skip any fields that are nil within the data tree, since we
+		// do not need to iterate on them.
+		return
+	}
+
+	// walk the node itself
+	childVisitor := visitor.Visit(node)
+	if childVisitor == nil {
+		return
+	}
+	defer childVisitor.Visit(nil)
+
+	v := ni.FieldValue
+	t := v.Type()
+
+	orderedMap, isOrderedMap := v.Interface().(goOrderedMap)
+
+	// Determine whether we need to recurse into the field, or whether it is
+	// a leaf or leaf-list, which are not recursed into when traversing the
+	// data tree.
+	switch {
+	case isOrderedMap:
+		// Handle the case of a keyed map, which is a YANG list.
+		if IsNilOrInvalidValue(v) {
+			return
+		}
+		nn := *ni
+		nn.Parent = ni
+		var err error
+		nn.FieldKeys, err = yreflect.OrderedMapKeys(orderedMap)
+		if err != nil {
+			o.WalkErrors.Collect(err)
+			return
+		}
+		if err := yreflect.RangeOrderedMap(orderedMap, func(k, v reflect.Value) bool {
+			nn := nn
+			nn.FieldValue = v
+			nn.FieldKey = k
+			walkDataFieldInternal(childVisitor, WalkNodeFromNodeInfo(&nn), o)
+			return true
+		}); err != nil {
+			o.WalkErrors.Collect(err)
+		}
+	case IsTypeStructPtr(t):
+		// A struct pointer in a GoStruct is a pointer to another container within
+		// the YANG, therefore we dereference the pointer and then recurse. If the
+		// pointer is nil, then we do not need to do this since the data tree branch
+		// is unset in the schema.
+		t = t.Elem()
+		v = v.Elem()
+		fallthrough
+	case IsTypeStruct(t):
+		// Handle non-pointer structs by recursing into each field of the struct.
+		for i := 0; i < t.NumField(); i++ {
+			sf := t.Field(i)
+			nn := &NodeInfo{
+				Parent:      ni,
+				StructField: sf,
+				FieldValue:  reflect.Zero(sf.Type),
+			}
+
+			nn.FieldValue = v.Field(i)
+			ps, err := SchemaPaths(nn.StructField)
+			if err != nil {
+				o.WalkErrors.Collect(err)
+				return
+			}
+			// In the case that the field expands to >1 different data tree path,
+			// i.e., SchemaPaths above returns more than one path, then we recurse
+			// for each schema path. This ensures that the iterator
+			// function runs for all expansions of the data tree as well as the GoStruct
+			// fields.
+			for _, p := range ps {
+				nn.PathFromParent = p
+				if IsTypeSlice(sf.Type) || IsTypeMap(sf.Type) {
+					// Since lists can have path compression - where the path contains more
+					// than one element, ensure that the schema path we received is only two
+					// elements long. This protects against compression errors where there are
+					// trailing spaces (e.g., a path tag of config/bar/).
+					nn.PathFromParent = p[0:1]
+				}
+				walkDataFieldInternal(childVisitor, WalkNodeFromNodeInfo(nn), o)
+			}
+		}
+	case IsTypeSlice(t):
+		// Only iterate in the data tree if the slice is of structs, otherwise
+		// for leaf-lists we only run once.
+		if !IsTypeStructPtr(t.Elem()) && !IsTypeStruct(t.Elem()) {
+			return
+		}
+
+		for i := 0; i < ni.FieldValue.Len(); i++ {
+			nn := *ni
+			nn.Parent = ni
+			// The name of the list is the same in each of the entries within the
+			// list therefore, we do not need to set the path to be different from
+			// the parent.
+			nn.PathFromParent = ni.PathFromParent
+			nn.FieldValue = ni.FieldValue.Index(i)
+			walkDataFieldInternal(childVisitor, WalkNodeFromNodeInfo(&nn), o)
+		}
+	case IsTypeMap(t):
+		// Handle the case of a keyed map, which is a YANG list.
+		if IsNilOrInvalidValue(v) {
+			return
+		}
+		for _, key := range ni.FieldValue.MapKeys() {
+			nn := *ni
+			nn.Parent = ni
+			nn.FieldValue = ni.FieldValue.MapIndex(key)
+			nn.FieldKey = key
+			nn.FieldKeys = ni.FieldValue.MapKeys()
+			walkDataFieldInternal(childVisitor, WalkNodeFromNodeInfo(&nn), o)
+		}
+	}
+}


Test patch:
diff --git a/util/reflect_test.go b/util/reflect_test.go
index 9137bb7..d3b4cb8 100644
--- a/util/reflect_test.go
+++ b/util/reflect_test.go
@@ -847,6 +847,13 @@ type PathErrorStruct struct {
 	Field *string
 }
 
+type BadBasicStructMissingPath struct {
+	Int32Field     int32
+	StringField    string
+	Int32PtrField  *int32  `path:"int32ptr"`
+	StringPtrField *string `path:"stringptr"`
+}
+
 type BasicStruct struct {
 	Int32Field     int32   `path:"int32"`
 	StringField    string  `path:"string"`
@@ -993,6 +1000,13 @@ var (
 		StringPtrField: toStringPtr("forty two ptr"),
 	}
 
+	badBasicStruct1 = BadBasicStructMissingPath{
+		Int32Field:     int32(42),
+		StringField:    "forty two",
+		Int32PtrField:  toInt32Ptr(4242),
+		StringPtrField: toStringPtr("forty two ptr"),
+	}
+
 	basicStruct2 = BasicStruct{
 		Int32Field:     int32(43),
 		StringField:    "forty three",
@@ -1214,6 +1228,76 @@ func TestForEachField(t *testing.T) {
 	}
 }
 
+type testWalkVisitor struct {
+	out string
+}
+
+var _ Visitor = &testWalkVisitor{}
+
+func (v *testWalkVisitor) Visit(node WalkNode) Visitor {
+	if node == nil {
+		return nil
+	}
+	v.out += node.NodeInfo().FieldValue.String() + "\n"
+	return v
+}
+
+func TestWalk(t *testing.T) {
+	tests := []struct {
+		desc         string
+		parentStruct any
+		options      *WalkOptions
+		wantOut      string
+		wantErr      Errors
+	}{
+		{
+			desc:         "nil",
+			parentStruct: nil,
+			wantOut:      ``,
+		},
+		{
+			desc:         "struct with schema",
+			options:      DefaultWalkOptions().WithSchema(forEachContainerSchema.Dir["basic-struct"]),
+			parentStruct: &basicStruct1,
+			wantOut:      "<*util.BasicStruct Value>\n<int32 Value>\nforty two\n<*int32 Value>\n<*string Value>\n",
+		},
+		{
+			desc:         "struct mismatched schema not an error",
+			options:      DefaultWalkOptions().WithSchema(forEachContainerSchema.Dir["basic-struct"].Dir["int32ptr"]),
+			parentStruct: &basicStruct1,
+			wantOut:      "<*util.BasicStruct Value>\n",
+		},
+		{
+			desc:         "struct no schema",
+			parentStruct: &basicStruct1,
+			wantOut:      "<*util.BasicStruct Value>\n<int32 Value>\nforty two\n<*int32 Value>\n<*string Value>\n",
+		},
+		{
+			desc:         "fail struct type missing path",
+			options:      DefaultWalkOptions().WithSchema(forEachContainerSchema.Dir["basic-struct"]),
+			parentStruct: &badBasicStruct1,
+			wantErr:      []error{fmt.Errorf("field Int32Field did not specify a path")},
+		},
+	}
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.desc, func(t *testing.T) {
+			node := WalkNodeFromGoStruct(tt.parentStruct)
+			v := &testWalkVisitor{}
+			errs := Walk(v, node, tt.options).(*DefaultWalkErrors).Errors
+			if diff := cmp.Diff(errs.String(), tt.wantErr.String()); diff != "" {
+				t.Errorf("error (-got, +want):\n%s", diff)
+			}
+			if errs != nil {
+				return
+			}
+			if diff := cmp.Diff(v.out, tt.wantOut); diff != "" {
+				t.Errorf("%s:\n%s", tt.desc, diff)
+			}
+		})
+	}
+}
+
 func TestForEachDataField(t *testing.T) {
 	tests := []struct {
 		desc         string