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

     1  package govcd
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"net/url"
     7  	"strings"
     8  )
     9  
    10  // crudConfig contains configuration that must be supplied when invoking generic functions that are defined
    11  // in `openapi_generic_inner_entities.go` and `openapi_generic_outer_entities.go`
    12  type crudConfig struct {
    13  	// Mandatory parameters
    14  
    15  	// entityLabel contains friendly entity name that is used for logging meaningful errors
    16  	entityLabel string
    17  
    18  	// endpoint in the usual format (e.g. types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentIpDiscoveryProfiles)
    19  	endpoint string
    20  
    21  	// Optional parameters
    22  
    23  	// endpointParams contains a slice of strings that will be used to construct the request URL. It will
    24  	// initially replace '%s' placeholders in the `endpoint` (if any) and will add them as suffix
    25  	// afterwards
    26  	endpointParams []string
    27  
    28  	// queryParameters will be passed as GET queries to the URL. Usually they are used for API filtering parameters
    29  	queryParameters url.Values
    30  	// additionalHeader can be used to pass additional headers for API calls. One of the common purposes is to pass
    31  	// tenant context
    32  	additionalHeader map[string]string
    33  }
    34  
    35  // validate should catch errors in consuming generic CRUD functions and should never produce false
    36  // positives.
    37  func (c crudConfig) validate() error {
    38  	// crudConfig misconfiguration - we can panic so that developer catches the problem during
    39  	// development of this SDK
    40  	if c.entityLabel == "" {
    41  		panic("'entityName' must always be specified when initializing crudConfig")
    42  	}
    43  
    44  	if c.endpoint == "" {
    45  		panic("'endpoint' must always be specified when initializing crudConfig")
    46  	}
    47  
    48  	// softer validations that consumers of this SDK can manipulate
    49  
    50  	// If `endpointParams` is specified in `crudConfig` (it is not nil), then it must contain at
    51  	// least a single non empty string parameter
    52  	// Such validation should prevent cases where some ID is not speficied upon function call.
    53  	// E.g.: endpointParams: []string{vdcId}, <--- vdcId comes from consumer of the SDK
    54  	// If the user specified empty `vdcId` - we'd validate this
    55  	for _, paramValue := range c.endpointParams {
    56  		if paramValue == "" {
    57  			return fmt.Errorf(`endpointParams were specified but they contain empty value "" for %s. %#v`,
    58  				c.entityLabel, c.endpointParams)
    59  		}
    60  	}
    61  
    62  	return nil
    63  }
    64  
    65  // createInnerEntity implements a common pattern for creating an entity throughout codebase
    66  // Parameters:
    67  // * `client` is a *Client
    68  // * `c` holds settings for performing API call
    69  // * `innerConfig` is the new entity type
    70  func createInnerEntity[I any](client *Client, c crudConfig, innerConfig *I) (*I, error) {
    71  	if err := c.validate(); err != nil {
    72  		return nil, err
    73  	}
    74  
    75  	apiVersion, err := client.getOpenApiHighestElevatedVersion(c.endpoint)
    76  	if err != nil {
    77  		return nil, fmt.Errorf("error getting API version for creating entity '%s': %s", c.entityLabel, err)
    78  	}
    79  
    80  	exactEndpoint, err := urlFromEndpoint(c.endpoint, c.endpointParams)
    81  	if err != nil {
    82  		return nil, fmt.Errorf("error building endpoint '%s' with given params '%s' for entity '%s': %s", c.endpoint, strings.Join(c.endpointParams, ","), c.entityLabel, err)
    83  	}
    84  
    85  	urlRef, err := client.OpenApiBuildEndpoint(exactEndpoint)
    86  	if err != nil {
    87  		return nil, fmt.Errorf("error building API endpoint for entity '%s' creation: %s", c.entityLabel, err)
    88  	}
    89  
    90  	createdInnerEntityConfig := new(I)
    91  	err = client.OpenApiPostItem(apiVersion, urlRef, c.queryParameters, innerConfig, createdInnerEntityConfig, c.additionalHeader)
    92  	if err != nil {
    93  		return nil, fmt.Errorf("error creating entity of type '%s': %s", c.entityLabel, err)
    94  	}
    95  
    96  	return createdInnerEntityConfig, nil
    97  }
    98  
    99  // updateInnerEntity implements a common pattern for updating entity throughout codebase
   100  // Parameters:
   101  // * `client` is a *Client
   102  // * `c` holds settings for performing API call
   103  // * `innerConfig` is the new entity type
   104  func updateInnerEntity[I any](client *Client, c crudConfig, innerConfig *I) (*I, error) {
   105  	// Discarding returned headers to better match return signature for most common cases
   106  	updatedInnerEntity, _, err := updateInnerEntityWithHeaders(client, c, innerConfig)
   107  	return updatedInnerEntity, err
   108  }
   109  
   110  // updateInnerEntityWithHeaders implements a common pattern for updating entity throughout codebase
   111  // Parameters:
   112  // * `client` is a *Client
   113  // * `c` holds settings for performing API call
   114  // * `innerConfig` is the new entity type
   115  func updateInnerEntityWithHeaders[I any](client *Client, c crudConfig, innerConfig *I) (*I, http.Header, error) {
   116  	if err := c.validate(); err != nil {
   117  		return nil, nil, err
   118  	}
   119  
   120  	apiVersion, err := client.getOpenApiHighestElevatedVersion(c.endpoint)
   121  	if err != nil {
   122  		return nil, nil, fmt.Errorf("error getting API version for updating entity '%s': %s", c.entityLabel, err)
   123  	}
   124  
   125  	exactEndpoint, err := urlFromEndpoint(c.endpoint, c.endpointParams)
   126  	if err != nil {
   127  		return nil, nil, fmt.Errorf("error building endpoint '%s' with given params '%s' for entity '%s': %s", c.endpoint, strings.Join(c.endpointParams, ","), c.entityLabel, err)
   128  	}
   129  
   130  	urlRef, err := client.OpenApiBuildEndpoint(exactEndpoint)
   131  	if err != nil {
   132  		return nil, nil, fmt.Errorf("error building API endpoint for entity '%s' update: %s", c.entityLabel, err)
   133  	}
   134  
   135  	updatedInnerEntityConfig := new(I)
   136  	headers, err := client.OpenApiPutItemAndGetHeaders(apiVersion, urlRef, c.queryParameters, innerConfig, updatedInnerEntityConfig, c.additionalHeader)
   137  	if err != nil {
   138  		return nil, nil, fmt.Errorf("error updating entity of type '%s': %s", c.entityLabel, err)
   139  	}
   140  
   141  	return updatedInnerEntityConfig, headers, nil
   142  }
   143  
   144  // getInnerEntity is an implementation for a common pattern in our code where we have to retrieve
   145  // outer entity (usually *types.XXXX) and does not need to be wrapped in an inner container entity.
   146  // Parameters:
   147  // * `client` is a *Client
   148  // * `c` holds settings for performing API call
   149  func getInnerEntity[I any](client *Client, c crudConfig) (*I, error) {
   150  	// Discarding returned headers to better match return signature for most common cases
   151  	innerEntity, _, err := getInnerEntityWithHeaders[I](client, c)
   152  	return innerEntity, err
   153  }
   154  
   155  // getInnerEntityWithHeaders is an implementation for a common pattern in our code where we have to retrieve
   156  // outer entity (usually *types.XXXX) and does not need to be wrapped in an inner container entity.
   157  // Parameters:
   158  // * `client` is a *Client
   159  // * `c` holds settings for performing API call
   160  func getInnerEntityWithHeaders[I any](client *Client, c crudConfig) (*I, http.Header, error) {
   161  	if err := c.validate(); err != nil {
   162  		return nil, nil, err
   163  	}
   164  
   165  	apiVersion, err := client.getOpenApiHighestElevatedVersion(c.endpoint)
   166  	if err != nil {
   167  		return nil, nil, fmt.Errorf("error getting API version for entity '%s': %s", c.entityLabel, err)
   168  	}
   169  
   170  	exactEndpoint, err := urlFromEndpoint(c.endpoint, c.endpointParams)
   171  	if err != nil {
   172  		return nil, nil, fmt.Errorf("error building endpoint '%s' with given params '%s' for entity '%s': %s", c.endpoint, strings.Join(c.endpointParams, ","), c.entityLabel, err)
   173  	}
   174  
   175  	urlRef, err := client.OpenApiBuildEndpoint(exactEndpoint)
   176  	if err != nil {
   177  		return nil, nil, fmt.Errorf("error building API endpoint for entity '%s': %s", c.entityLabel, err)
   178  	}
   179  
   180  	typeResponse := new(I)
   181  	headers, err := client.OpenApiGetItemAndHeaders(apiVersion, urlRef, c.queryParameters, typeResponse, c.additionalHeader)
   182  	if err != nil {
   183  		return nil, nil, fmt.Errorf("error retrieving entity of type '%s': %s", c.entityLabel, err)
   184  	}
   185  
   186  	return typeResponse, headers, nil
   187  }
   188  
   189  // getAllInnerEntities can be used to retrieve a slice of any inner entities in the OpenAPI
   190  // endpoints that are not nested in outer types
   191  //
   192  // Parameters:
   193  // * `client` is a *Client
   194  // * `c` holds settings for performing API call
   195  func getAllInnerEntities[I any](client *Client, c crudConfig) ([]*I, error) {
   196  	if err := c.validate(); err != nil {
   197  		return nil, err
   198  	}
   199  
   200  	apiVersion, err := client.getOpenApiHighestElevatedVersion(c.endpoint)
   201  	if err != nil {
   202  		return nil, fmt.Errorf("error getting API version for entity '%s': %s", c.entityLabel, err)
   203  	}
   204  
   205  	exactEndpoint, err := urlFromEndpoint(c.endpoint, c.endpointParams)
   206  	if err != nil {
   207  		return nil, fmt.Errorf("error building endpoint '%s' with given params '%s' for entity '%s': %s", c.endpoint, strings.Join(c.endpointParams, ","), c.entityLabel, err)
   208  	}
   209  
   210  	urlRef, err := client.OpenApiBuildEndpoint(exactEndpoint)
   211  	if err != nil {
   212  		return nil, fmt.Errorf("error building API endpoint for entity '%s': %s", c.entityLabel, err)
   213  	}
   214  
   215  	typeResponses := make([]*I, 0)
   216  	err = client.OpenApiGetAllItems(apiVersion, urlRef, c.queryParameters, &typeResponses, c.additionalHeader)
   217  	if err != nil {
   218  		return nil, fmt.Errorf("error retrieving all entities of type '%s': %s", c.entityLabel, err)
   219  	}
   220  
   221  	return typeResponses, nil
   222  }
   223  
   224  // deleteEntityById performs a common operation for OpenAPI endpoints that calls DELETE method for a
   225  // given endpoint.
   226  // Note. It does not use generics for the operation, but is held in this file with other CRUD entries
   227  // Parameters:
   228  // * `client` is a *Client
   229  // * `c` holds settings for performing API call
   230  func deleteEntityById(client *Client, c crudConfig) error {
   231  	if err := c.validate(); err != nil {
   232  		return err
   233  	}
   234  
   235  	apiVersion, err := client.getOpenApiHighestElevatedVersion(c.endpoint)
   236  	if err != nil {
   237  		return err
   238  	}
   239  
   240  	exactEndpoint, err := urlFromEndpoint(c.endpoint, c.endpointParams)
   241  	if err != nil {
   242  		return fmt.Errorf("error building endpoint '%s' with given params '%s' for entity '%s': %s", c.endpoint, strings.Join(c.endpointParams, ","), c.entityLabel, err)
   243  	}
   244  
   245  	urlRef, err := client.OpenApiBuildEndpoint(exactEndpoint)
   246  	if err != nil {
   247  		return err
   248  	}
   249  
   250  	err = client.OpenApiDeleteItem(apiVersion, urlRef, c.queryParameters, c.additionalHeader)
   251  
   252  	if err != nil {
   253  		return fmt.Errorf("error deleting %s: %s", c.entityLabel, err)
   254  	}
   255  
   256  	return nil
   257  }
   258  
   259  func urlFromEndpoint(endpoint string, endpointParams []string) (string, error) {
   260  	// Count how many '%s' placeholders exist in the 'endpoint'
   261  	placeholderCount := strings.Count(endpoint, "%s")
   262  
   263  	// Validation. At the very minimum all placeholders must have their replacements - otherwise it
   264  	// is an error as we never want to query an endpoint that still has placeholders '%s'
   265  	if len(endpointParams) < placeholderCount {
   266  		return "", fmt.Errorf("endpoint '%s' has unpopulated placeholders", endpoint)
   267  	}
   268  
   269  	// if there are no 'endpointParams' - exit with the same endpoint
   270  	if len(endpointParams) == 0 {
   271  		return endpoint, nil
   272  	}
   273  
   274  	// Loop over given endpointParams and replace placeholders at first. Afterwards - amend any
   275  	// additional parameters to the end of endpoint
   276  	for _, v := range endpointParams {
   277  		// If there are placeholders '%s' to replace in the endpoint itself - do it
   278  		if placeholderCount > 0 {
   279  			endpoint = strings.Replace(endpoint, "%s", v, 1)
   280  			placeholderCount = placeholderCount - 1
   281  			continue
   282  		}
   283  
   284  		endpoint = endpoint + v
   285  	}
   286  
   287  	return endpoint, nil
   288  }