grokify/go-ringcentral-client

Upload file error `undefined: localVarFile`

Closed this issue · 1 comments

A undefined: localVarFile error is encountered when a required formData file is present in the request:

Compile-time Errors

../../client/api_call_handling_settings.go:646:5: undefined: localVarFile
../../client/api_call_handling_settings.go:647:28: undefined: localVarFile
../../client/api_call_handling_settings.go:649:22: undefined: localVarFile
../../client/api_call_handling_settings.go:650:3: undefined: localVarFile
../../client/api_glip.go:82:5: undefined: localVarFile
../../client/api_glip.go:83:28: undefined: localVarFile
../../client/api_glip.go:85:22: undefined: localVarFile
../../client/api_glip.go:86:3: undefined: localVarFile

Example Spec

          -
            name: "attachment"
            in: "formData"
            type: "file"
            required: true
            description: "The file to upload"

Example Generated Code

Here is some example generated code. The issue is that:

  1. attachment *os.File is present in the function definition. but attachment is never used.
  2. localVarFile is never instantiated in the generate code
type CreatePromptsOpts struct {
	Name optional.String
}

func (a *CallHandlingSettingsApiService) CreatePrompts(ctx context.Context, accountId string, attachment *os.File, localVarOptionals *CreatePromptsOpts) (PromptInfo, *http.Response, error) {
	var (
		localVarHttpMethod  = strings.ToUpper("Post")
		localVarPostBody    interface{}
		localVarFileName    string
		localVarFileBytes   []byte
		localVarReturnValue PromptInfo
	)

	// create path and map variables
	localVarPath := a.client.cfg.BasePath + "/restapi/v1.0/account/{accountId}/ivr-prompts"
	localVarPath = strings.Replace(localVarPath, "{"+"accountId"+"}", fmt.Sprintf("%v", accountId), -1)

	localVarHeaderParams := make(map[string]string)
	localVarQueryParams := url.Values{}
	localVarFormParams := url.Values{}

	// to determine the Content-Type header
	localVarHttpContentTypes := []string{"multipart/form-data"}

	// set Content-Type header
	localVarHttpContentType := selectHeaderContentType(localVarHttpContentTypes)
	if localVarHttpContentType != "" {
		localVarHeaderParams["Content-Type"] = localVarHttpContentType
	}

	// to determine the Accept header
	localVarHttpHeaderAccepts := []string{"application/json"}

	// set Accept header
	localVarHttpHeaderAccept := selectHeaderAccept(localVarHttpHeaderAccepts)
	if localVarHttpHeaderAccept != "" {
		localVarHeaderParams["Accept"] = localVarHttpHeaderAccept
	}
	if localVarFile != nil {
		fbs, _ := ioutil.ReadAll(localVarFile)
		localVarFileBytes = fbs
		localVarFileName = localVarFile.Name()
		localVarFile.Close()
	}
	if localVarOptionals != nil && localVarOptionals.Name.IsSet() {
		localVarFormParams.Add("name", parameterToString(localVarOptionals.Name.Value(), ""))
	}
	r, err := a.client.prepareRequest(ctx, localVarPath, localVarHttpMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFileName, localVarFileBytes)
	if err != nil {
		return localVarReturnValue, nil, err
	}

	localVarHttpResponse, err := a.client.callAPI(r)
	if err != nil || localVarHttpResponse == nil {
		return localVarReturnValue, localVarHttpResponse, err
	}

	localVarBody, err := ioutil.ReadAll(localVarHttpResponse.Body)
	localVarHttpResponse.Body.Close()
	if err != nil {
		return localVarReturnValue, localVarHttpResponse, err
	}

	if localVarHttpResponse.StatusCode < 300 {
		// If we succeed, return the data, otherwise pass on to decode error.
		err = a.client.decode(&localVarReturnValue, localVarBody, localVarHttpResponse.Header.Get("Content-Type"))
		if err == nil {
			return localVarReturnValue, localVarHttpResponse, err
		}
	}

	if localVarHttpResponse.StatusCode >= 300 {
		newErr := GenericOpenAPIError{
			body:  localVarBody,
			error: localVarHttpResponse.Status,
		}
		if localVarHttpResponse.StatusCode == 0 {
			var v PromptInfo
			err = a.client.decode(&v, localVarBody, localVarHttpResponse.Header.Get("Content-Type"))
			if err != nil {
				newErr.error = err.Error()
				return localVarReturnValue, localVarHttpResponse, newErr
			}
			newErr.model = v
			return localVarReturnValue, localVarHttpResponse, newErr
		}
		return localVarReturnValue, localVarHttpResponse, newErr
	}

	return localVarReturnValue, localVarHttpResponse, nil
}

Explanation of Error

  1. For an optional file, the api.mustache code will instantiate var localVarFile {{dataType}} which turns out to be var localVarFile *os.File
  2. There is no such instantiation when the parameter is required ({{#required}}).

Here's the api.mustache template excerpt:

{{#isFile}}
{{^required}}
	var localVarFile {{dataType}}
	if localVarOptionals != nil && localVarOptionals.{{vendorExtensions.x-exportParamName}}.IsSet() {
		localVarFileOk := false
		localVarFile, localVarFileOk = localVarOptionals.{{vendorExtensions.x-exportParamName}}.Value().({{dataType}})
		if !localVarFileOk {
				return {{#returnType}}localVarReturnValue, {{/returnType}}nil, reportError("{{paramName}} should be {{dataType}}")
		}
	}
{{/required}}
	if localVarFile != nil {
		fbs, _ := ioutil.ReadAll(localVarFile)
		localVarFileBytes = fbs
		localVarFileName = localVarFile.Name()
		localVarFile.Close()
	}

Desired Code

The variable should be instantiated and set to the parameter passed in the function call:

localVarFile := attachment

Proposed Fix

  1. Process required file params before optional file params similar to method function.
  2. Loop through all {{allParams}} like in function definition in the event there's more than one required param
  3. Identify the parameter that is both {{#required}} and {{#isFile}}
  4. Write localVarFile := {{paramName}}. The data type will be implicit here and set to the same {{{dataType}}} used in the function definition, which is *os.File.

Here is the diff:

 {{#isFile}}
+{{#required}}
+    localVarFile := {{paramName}}
+{{/required}}

Known Limitations

This approach can only handle a single file, but it seems like the Go generator can only handle a single file in general since APIClient.prepareRequest() takes a single pair of localVarFileName and localVarFileBytes. A future enhancement could support multiple files.

Fixed in f434cfb