github.com/vmware/go-vcloud-director/v2@v2.24.0/govcd/generic_functions.go (about)

     1  package govcd
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  )
     7  
     8  // oneOrError is used to cover up a common pattern in this codebase which is usually used in
     9  // GetXByName functions.
    10  // API endpoint returns N elements for an object we are looking (most commonly because API does not
    11  // support filtering) and final filtering by Name must be done in code.
    12  // After filtering returned entities one must be sure that exactly one was found and handle 3 cases:
    13  // * If 0 entities are found - an error containing ErrorEntityNotFound must be returned
    14  // * If >1 entities are found - an error containing the number of entities must be returned
    15  // * If 1 entity was found - return it
    16  //
    17  // An example of code that was previously handled in non generic way - we had a lot of these
    18  // occurrences throughout the code:
    19  //
    20  // if len(nsxtEdgeClusters) == 0 {
    21  //     // ErrorEntityNotFound is injected here for the ability to validate problem using ContainsNotFound()
    22  //     return nil, fmt.Errorf("%s: no NSX-T Tier-0 Edge Cluster with name '%s' for Org VDC with id '%s' found",
    23  //             ErrorEntityNotFound, name, vdc.Vdc.ID)
    24  // }
    25  
    26  //	if len(nsxtEdgeClusters) > 1 {
    27  //	        return nil, fmt.Errorf("more than one (%d) NSX-T Edge Cluster with name '%s' for Org VDC with id '%s' found",
    28  //	                len(nsxtEdgeClusters), name, vdc.Vdc.ID)
    29  //	}
    30  func oneOrError[E any](key, value string, entitySlice []*E) (*E, error) {
    31  	if len(entitySlice) > 1 {
    32  		return nil, fmt.Errorf("got more than one entity by %s '%s' %d", key, value, len(entitySlice))
    33  	}
    34  
    35  	if len(entitySlice) == 0 {
    36  		// No entity found - returning ErrorEntityNotFound as it must be wrapped in the returned error
    37  		return nil, fmt.Errorf("%s: got zero entities by %s '%s'", ErrorEntityNotFound, key, value)
    38  	}
    39  
    40  	return entitySlice[0], nil
    41  }
    42  
    43  // localFilter performs filtering of a type E based on a field name `fieldName` and its
    44  // expected string value `expectedFieldValue`. Common use case for GetAllX methods where API does
    45  // not support filtering and it must be done on the client side.
    46  //
    47  // Note. The field name `fieldName` must be present in a given type E (letter casing is important)
    48  func localFilter[E any](entityLabel string, entities []*E, fieldName, expectedFieldValue string) ([]*E, error) {
    49  	if len(entities) == 0 {
    50  		return nil, fmt.Errorf("zero entities provided for filtering")
    51  	}
    52  
    53  	filteredValues := make([]*E, 0)
    54  	for _, entity := range entities {
    55  
    56  		// Need to deference pointer because `reflect` package requires to work with types and not
    57  		// pointers to types
    58  		var entityValue E
    59  		if entity != nil {
    60  			entityValue = *entity
    61  		} else {
    62  			return nil, fmt.Errorf("given entity for %s is a nil pointer", entityLabel)
    63  		}
    64  
    65  		value := reflect.ValueOf(entityValue)
    66  		field := value.FieldByName(fieldName)
    67  
    68  		if !field.IsValid() {
    69  			return nil, fmt.Errorf("the struct for %s does not have the field '%s'", entityLabel, fieldName)
    70  		}
    71  
    72  		if field.Type().Name() != "string" {
    73  			return nil, fmt.Errorf("field '%s' is not string type, it has type '%s'", fieldName, field.Type().Name())
    74  		}
    75  
    76  		if field.String() == expectedFieldValue {
    77  			filteredValues = append(filteredValues, entity)
    78  		}
    79  	}
    80  
    81  	return filteredValues, nil
    82  }
    83  
    84  // localFilterOneOrError performs local filtering using `genericLocalFilter()` and
    85  // additionally verifies that only a single result is present using `oneOrError()`. Common use case
    86  // for GetXByName methods where API does not support filtering and it must be done on client side.
    87  func localFilterOneOrError[E any](entityLabel string, entities []*E, fieldName, expectedFieldValue string) (*E, error) {
    88  	if fieldName == "" || expectedFieldValue == "" {
    89  		return nil, fmt.Errorf("expected field name and value must be specified to filter %s", entityLabel)
    90  	}
    91  
    92  	filteredValues, err := localFilter(entityLabel, entities, fieldName, expectedFieldValue)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  
    97  	return oneOrError(fieldName, expectedFieldValue, filteredValues)
    98  }