src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/eval/vals/struct_map.go (about)

     1  package vals
     2  
     3  import (
     4  	"reflect"
     5  	"sync"
     6  
     7  	"src.elv.sh/pkg/strutil"
     8  )
     9  
    10  // StructMap may be implemented by a struct to make it accessible to Elvish code
    11  // as a map. Each exported, named field and getter method (a method taking no
    12  // argument and returning one value) becomes a field of the map, with the name
    13  // mapped to dash-case.
    14  //
    15  // Struct maps are indistinguishable from normal maps for Elvish code. The
    16  // operations Kind, Repr, Hash, Equal, Len, Index, HasKey and IterateKeys handle
    17  // struct maps consistently with maps; the Assoc and Dissoc operations convert
    18  // struct maps to maps.
    19  //
    20  // Example:
    21  //
    22  //	type someStruct struct {
    23  //	    // Provides the "foo-bar" field
    24  //	    FooBar int
    25  //	    lorem  string
    26  //	}
    27  //
    28  //	// Marks someStruct as a struct map
    29  //	func (someStruct) IsStructMap() { }
    30  //
    31  //	// Provides the "ipsum" field
    32  //	func (s SomeStruct) Ipsum() string { return s.lorem }
    33  //
    34  //	// Not a getter method; doesn't provide any field
    35  //	func (s SomeStruct) OtherMethod(int) { }
    36  type StructMap interface{ IsStructMap() }
    37  
    38  func promoteToMap(v StructMap) Map {
    39  	m := EmptyMap
    40  	for it := iterateStructMap(v); it.HasElem(); it.Next() {
    41  		m = m.Assoc(it.Elem())
    42  	}
    43  	return m
    44  }
    45  
    46  // PseudoMap may be implemented by a type to support map-like introspection. The
    47  // Repr, Index, HasKey and IterateKeys operations handle pseudo maps.
    48  type PseudoMap interface{ Fields() StructMap }
    49  
    50  // Keeps cached information about a structMap.
    51  type structMapInfo struct {
    52  	filledFields int
    53  	plainFields  int
    54  	// Dash-case names for all fields. The first plainFields elements
    55  	// corresponds to all the plain fields, while the rest corresponds to getter
    56  	// fields. May contain empty strings if the corresponding field is not
    57  	// reflected onto the structMap (i.e. unexported fields, unexported methods
    58  	// and non-getter methods).
    59  	fieldNames []string
    60  }
    61  
    62  var structMapInfos sync.Map
    63  
    64  // Gets the structMapInfo associated with a type, caching the result.
    65  func getStructMapInfo(t reflect.Type) structMapInfo {
    66  	if info, ok := structMapInfos.Load(t); ok {
    67  		return info.(structMapInfo)
    68  	}
    69  	info := makeStructMapInfo(t)
    70  	structMapInfos.Store(t, info)
    71  	return info
    72  }
    73  
    74  func makeStructMapInfo(t reflect.Type) structMapInfo {
    75  	n := t.NumField()
    76  	m := t.NumMethod()
    77  	fieldNames := make([]string, n+m)
    78  	filledFields := 0
    79  
    80  	for i := 0; i < n; i++ {
    81  		field := t.Field(i)
    82  		if field.PkgPath == "" && !field.Anonymous {
    83  			fieldNames[i] = strutil.CamelToDashed(field.Name)
    84  			filledFields++
    85  		}
    86  	}
    87  
    88  	for i := 0; i < m; i++ {
    89  		method := t.Method(i)
    90  		if method.PkgPath == "" && method.Type.NumIn() == 1 && method.Type.NumOut() == 1 {
    91  			fieldNames[i+n] = strutil.CamelToDashed(method.Name)
    92  			filledFields++
    93  		}
    94  	}
    95  
    96  	return structMapInfo{filledFields, n, fieldNames}
    97  }
    98  
    99  type structMapIterator struct {
   100  	m     reflect.Value
   101  	info  structMapInfo
   102  	index int
   103  }
   104  
   105  func iterateStructMap(m StructMap) *structMapIterator {
   106  	it := &structMapIterator{reflect.ValueOf(m), getStructMapInfo(reflect.TypeOf(m)), 0}
   107  	it.fixIndex()
   108  	return it
   109  }
   110  
   111  func (it *structMapIterator) fixIndex() {
   112  	fieldNames := it.info.fieldNames
   113  	for it.index < len(fieldNames) && fieldNames[it.index] == "" {
   114  		it.index++
   115  	}
   116  }
   117  
   118  func (it *structMapIterator) Elem() (any, any) {
   119  	return it.elem()
   120  }
   121  
   122  func (it *structMapIterator) elem() (string, any) {
   123  	name := it.info.fieldNames[it.index]
   124  	if it.index < it.info.plainFields {
   125  		return name, it.m.Field(it.index).Interface()
   126  	}
   127  	method := it.m.Method(it.index - it.info.plainFields)
   128  	return name, method.Call(nil)[0].Interface()
   129  }
   130  
   131  func (it *structMapIterator) HasElem() bool {
   132  	return it.index < len(it.info.fieldNames)
   133  }
   134  
   135  func (it *structMapIterator) Next() {
   136  	it.index++
   137  	it.fixIndex()
   138  }