Skip to content

Commit

Permalink
Merge pull request #4428 from hashicorp/support-retry-funcs
Browse files Browse the repository at this point in the history
Support caller-specified RetryFuncs
  • Loading branch information
manicminer authored Sep 23, 2024
2 parents 345b393 + 1e7f8e7 commit b4563a9
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ type Operation struct {
}

type Option struct {
// Type signals a special behavior for this Option.
// Data: this option specifies Request data, as described in ObjectDefinition, HeaderName, ODataFieldName and/or QueryStringName.
// ContentType: this option specifies a custom Content Type for the Request to be specified by the caller.
// RetryFunc: this option specifies a client.RequestRetryFunc that can be passed in.
Type string

// HeaderName is the name of the Http Header which this Option should be set into
// (e.g. `If-Match`, `x-ms-client-request-id`)
HeaderName *string `json:"headerName,omitempty"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,49 @@ import (
)

func mapSDKOperationOptionFromRepository(input repositoryModels.Option, knownData helpers.KnownData) (*sdkModels.SDKOperationOption, error) {
objectDefinition, err := mapSDKOperationOptionObjectDefinitionFromRepository(input.ObjectDefinition, knownData)
if err != nil {
return nil, fmt.Errorf("mapping the ObjectDefinition: %+v", err)
var objectDefinition sdkModels.SDKOperationOptionObjectDefinition

if input.Type == "Data" {
mapping, err := mapSDKOperationOptionObjectDefinitionFromRepository(input.ObjectDefinition, knownData)
if err != nil {
return nil, fmt.Errorf("mapping the ObjectDefinition: %+v", err)
}
objectDefinition = *mapping
}

return &sdkModels.SDKOperationOption{
Type: input.Type,
HeaderName: input.HeaderName,
ODataFieldName: input.ODataFieldName,
QueryStringName: input.QueryString,
ObjectDefinition: *objectDefinition,
ObjectDefinition: objectDefinition,
Required: input.Required,
}, nil
}

func mapSDKOperationOptionToRepository(fieldName string, input sdkModels.SDKOperationOption, knownData helpers.KnownData) (*repositoryModels.Option, error) {
objectDefinition, err := mapSDKOperationOptionObjectDefinitionToRepository(input.ObjectDefinition, knownData)
if err != nil {
return nil, fmt.Errorf("mapping the object definition: %+v", err)
optionType := sdkModels.SDKOperationOptionTypeData
if input.Type != "" {
optionType = input.Type
}

var objectDefinition repositoryModels.OptionObjectDefinition

if optionType == sdkModels.SDKOperationOptionTypeData {
mapping, err := mapSDKOperationOptionObjectDefinitionToRepository(input.ObjectDefinition, knownData)
if err != nil {
return nil, fmt.Errorf("mapping the object definition: %+v", err)
}
objectDefinition = *mapping
}

option := repositoryModels.Option{
Type: optionType,
HeaderName: input.HeaderName,
ODataFieldName: input.ODataFieldName,
QueryString: input.QueryStringName,
Field: fieldName,
ObjectDefinition: *objectDefinition,
ObjectDefinition: objectDefinition,
}

if !input.Required {
Expand Down
14 changes: 14 additions & 0 deletions tools/data-api-sdk/v1/models/sdk_operation_option.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,26 @@

package models

type SDKOperationOptionType = string

const (
SDKOperationOptionTypeData SDKOperationOptionType = "Data"
SDKOperationOptionTypeContentType SDKOperationOptionType = "ContentType"
SDKOperationOptionTypeRetryFunc SDKOperationOptionType = "RetryFunc"
)

// SDKOperationOption defines a QueryString or HTTP Header that can be specified for an
// Operation.
type SDKOperationOption struct {
// HeaderName specifies the name of the HTTP Header associated with this Option.
HeaderName *string `json:"headerName,omitempty"`

// Type signals a special behavior for this Option.
// Data: this option specifies Request data, as described in ObjectDefinition, HeaderName, ODataFieldName and/or QueryStringName.
// ContentType: this option specifies a custom Content Type for the Request to be specified by the caller.
// RetryFunc: this option specifies a client.RequestRetryFunc that can be passed in.
Type SDKOperationOptionType

// ODataFieldName specifies the name for the OData query string parameter associated with this Option.
ODataFieldName *string `json:"odataFieldName,omitempty"`

Expand Down
50 changes: 41 additions & 9 deletions tools/generator-go-sdk/internal/generator/templater_methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,19 +579,36 @@ func (c methodsPandoraTemplater) requestOptions() (*string, error) {
}
}

items := []string{
fmt.Sprintf("ContentType: %q", c.operation.ContentType),
fmt.Sprintf(`ExpectedStatusCodes: []int{
%s,
}`, strings.Join(expectedStatusCodes, ",\n\t\t\t")),
fmt.Sprintf("HttpMethod: http.Method%s", method),
fmt.Sprintf("Path: %s", path),
options := map[string]string{
"ContentType": fmt.Sprintf("%q", c.operation.ContentType),
"ExpectedStatusCodes": fmt.Sprintf("[]int{\n\t\t\t%s,\n}", strings.Join(expectedStatusCodes, ",\n\t\t\t")),
"HttpMethod": fmt.Sprintf("http.Method%s", method),
"Path": path,
}

if len(c.operation.Options) > 0 {
items = append(items, "OptionsObject: options")
options["OptionsObject"] = "options"

// Look for special options
for optionName, option := range c.operation.Options {
switch option.Type {
case models.SDKOperationOptionTypeContentType:
options["ContentType"] = fmt.Sprintf("options.%s", optionName)
break
case models.SDKOperationOptionTypeRetryFunc:
options["RetryFunc"] = fmt.Sprintf("options.%s", optionName)
break
}
}
}

if c.operation.FieldContainingPaginationDetails != nil {
items = append(items, fmt.Sprintf("Pager: &%sCustomPager{}", c.operationName))
options["Pager"] = fmt.Sprintf("&%sCustomPager{}", c.operationName)
}

items := make([]string, 0, len(options))
for key, value := range options {
items = append(items, fmt.Sprintf("%s: %s", key, value))
}
sort.Strings(items)

Expand Down Expand Up @@ -830,25 +847,40 @@ func (c methodsPandoraTemplater) optionsStruct(data GeneratorData) (*string, err
headerAssignments := make([]string, 0)

for optionName, option := range c.operation.Options {
// Handle special options
switch option.Type {
case models.SDKOperationOptionTypeContentType:
properties = append(properties, fmt.Sprintf("%s string", optionName))
continue
case models.SDKOperationOptionTypeRetryFunc:
properties = append(properties, fmt.Sprintf("%s client.RequestRetryFunc", optionName))
continue
}

optionType, err := helpers.GolangTypeForSDKOperationOptionObjectDefinition(option.ObjectDefinition)
if err != nil {
return nil, fmt.Errorf("determining golang type name for option %q's ObjectDefinition: %+v", optionName, err)
}

properties = append(properties, fmt.Sprintf("%s *%s", optionName, *optionType))

if option.ODataFieldName != nil {
value := fmt.Sprintf("*o.%s", *option.ODataFieldName)
if option.ObjectDefinition.Type == models.IntegerSDKOperationOptionObjectDefinitionType {
value = fmt.Sprintf("int(%s)", value)
}

odataAssignments = append(odataAssignments, fmt.Sprintf(`if o.%[1]s != nil {
out.%[2]s = %[3]s
}`, optionName, *option.ODataFieldName, value))
}

if option.HeaderName != nil {
headerAssignments = append(headerAssignments, fmt.Sprintf(`if o.%[1]s != nil {
out.Append("%[2]s", fmt.Sprintf("%%v", *o.%[1]s))
}`, optionName, *option.HeaderName))
}

if option.QueryStringName != nil {
queryStringAssignments = append(queryStringAssignments, fmt.Sprintf(`if o.%[1]s != nil {
out.Append("%[2]s", fmt.Sprintf("%%v", *o.%[1]s))
Expand Down
197 changes: 197 additions & 0 deletions tools/generator-go-sdk/internal/generator/templater_methods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -586,3 +586,200 @@ func (c pandaClient) ListCompleteMatchingPredicate(ctx context.Context, id Panda

assertTemplatedCodeMatches(t, expected, *actual)
}

func TestTemplateGetMethodWithRetryFuncOption(t *testing.T) {
input := GeneratorData{
baseClientPackage: "testclient",
packageName: "skinnyPandas",
serviceClientName: "pandaClient",
source: AccTestLicenceType,
resourceIds: map[string]models.ResourceID{
"PandaPop": {
ExampleValue: "LingLing",
},
},
}

actual, err := methodsPandoraTemplater{
operation: models.SDKOperation{
ContentType: "application/json",
ExpectedStatusCodes: []int{200},
Method: "GET",
Options: map[string]models.SDKOperationOption{
"TheRetryFunc": {
Type: models.SDKOperationOptionTypeRetryFunc,
},
},
ResourceIDName: stringPointer("PandaPop"),
ResponseObject: &models.SDKObjectDefinition{
Type: models.StringSDKObjectDefinitionType,
},
},
operationName: "Get",
}.immediateOperationTemplate(input)
if err != nil {
t.Fatalf("err %+v", err)
}

expected := `
type GetOperationResponse struct {
HttpResponse *http.Response
OData *odata.OData
Model *string
}
type GetOperationOptions struct {
TheRetryFunc client.RequestRetryFunc
}
func DefaultGetOperationOptions() GetOperationOptions {
return GetOperationOptions{}
}
func (o GetOperationOptions) ToHeaders() *client.Headers {
out := client.Headers{}
return &out
}
func (o GetOperationOptions) ToOData() *odata.Query {
out := odata.Query{}
return &out
}
func (o GetOperationOptions) ToQuery() *client.QueryParams {
out := client.QueryParams{}
return &out
}
// Get ...
func (c pandaClient) Get(ctx context.Context , id PandaPop, options GetOperationOptions) (result GetOperationResponse, err error) {
opts := client.RequestOptions{
ContentType: "application/json",
ExpectedStatusCodes: []int{
http.StatusOK,
},
HttpMethod: http.MethodGet,
OptionsObject: options,
Path: id.ID(),
RetryFunc: options.TheRetryFunc,
}
req, err := c.Client.NewRequest(ctx, opts)
if err != nil {
return
}
var resp *client.Response
resp, err = req.Execute(ctx)
if resp != nil {
result.OData = resp.OData
result.HttpResponse = resp.Response
}
if err != nil {
return
}
var model string
result.Model = &model
if err = resp.Unmarshal(result.Model); err != nil {
return
}
return
}
`
assertTemplatedCodeMatches(t, expected, *actual)
}

func TestTemplatePutMethodWithContentTypeOption(t *testing.T) {
input := GeneratorData{
baseClientPackage: "testclient",
packageName: "skinnyPandas",
serviceClientName: "pandaClient",
source: AccTestLicenceType,
resourceIds: map[string]models.ResourceID{
"PandaPop": {
ExampleValue: "LingLing",
},
},
}

actual, err := methodsPandoraTemplater{
operation: models.SDKOperation{
ContentType: "application/json",
ExpectedStatusCodes: []int{204},
Method: "PUT",
Options: map[string]models.SDKOperationOption{
"UploadContentType": {
Type: models.SDKOperationOptionTypeContentType,
},
},
ResourceIDName: stringPointer("PandaPop"),
},
operationName: "Put",
}.immediateOperationTemplate(input)
if err != nil {
t.Fatalf("err %+v", err)
}

expected := `
type PutOperationResponse struct {
HttpResponse *http.Response
OData *odata.OData
}
type PutOperationOptions struct {
UploadContentType string
}
func DefaultPutOperationOptions() PutOperationOptions {
return PutOperationOptions{}
}
func (o PutOperationOptions) ToHeaders() *client.Headers {
out := client.Headers{}
return &out
}
func (o PutOperationOptions) ToOData() *odata.Query {
out := odata.Query{}
return &out
}
func (o PutOperationOptions) ToQuery() *client.QueryParams {
out := client.QueryParams{}
return &out
}
// Put ...
func (c pandaClient) Put(ctx context.Context , id PandaPop, options PutOperationOptions) (result PutOperationResponse, err error) {
opts := client.RequestOptions{
ContentType: options.UploadContentType,
ExpectedStatusCodes: []int{
http.StatusNoContent,
},
HttpMethod: http.MethodPut,
OptionsObject: options,
Path: id.ID(),
}
req, err := c.Client.NewRequest(ctx, opts)
if err != nil {
return
}
var resp *client.Response
resp, err = req.Execute(ctx)
if resp != nil {
result.OData = resp.OData
result.HttpResponse = resp.Response
}
if err != nil {
return
}
return
}
`
assertTemplatedCodeMatches(t, expected, *actual)
}
Original file line number Diff line number Diff line change
Expand Up @@ -428,8 +428,9 @@ func (p pipelineForService) parseResources(resourceIds parser.ResourceIds, model
continue
}

// Binary payloads are handled by the SDK
if content.Schema.Value != nil && content.Schema.Value.Format == "binary" {
// Set the request type to Binary. When translating to Data API SDK types, we will also work in a
// ContentType option to be set by the caller.
requestType = pointer.To(parser.DataTypeBinary)
break
}
Expand Down
Loading

0 comments on commit b4563a9

Please sign in to comment.