diff --git a/tools/data-api-repository/repository/internal/models/operations.go b/tools/data-api-repository/repository/internal/models/operations.go index bbd0e31114c..2fa8f97551c 100644 --- a/tools/data-api-repository/repository/internal/models/operations.go +++ b/tools/data-api-repository/repository/internal/models/operations.go @@ -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"` diff --git a/tools/data-api-repository/repository/internal/transforms/sdk_operation_option.go b/tools/data-api-repository/repository/internal/transforms/sdk_operation_option.go index 5af0009d20f..56e2f0aba13 100644 --- a/tools/data-api-repository/repository/internal/transforms/sdk_operation_option.go +++ b/tools/data-api-repository/repository/internal/transforms/sdk_operation_option.go @@ -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 { diff --git a/tools/data-api-sdk/v1/models/sdk_operation_option.go b/tools/data-api-sdk/v1/models/sdk_operation_option.go index 9219a169ac9..86362258ac7 100644 --- a/tools/data-api-sdk/v1/models/sdk_operation_option.go +++ b/tools/data-api-sdk/v1/models/sdk_operation_option.go @@ -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"` diff --git a/tools/generator-go-sdk/internal/generator/templater_methods.go b/tools/generator-go-sdk/internal/generator/templater_methods.go index 60669b2f5c7..ba73d2c4abc 100644 --- a/tools/generator-go-sdk/internal/generator/templater_methods.go +++ b/tools/generator-go-sdk/internal/generator/templater_methods.go @@ -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) @@ -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)) diff --git a/tools/generator-go-sdk/internal/generator/templater_methods_test.go b/tools/generator-go-sdk/internal/generator/templater_methods_test.go index f733d4bf580..a292e52569f 100644 --- a/tools/generator-go-sdk/internal/generator/templater_methods_test.go +++ b/tools/generator-go-sdk/internal/generator/templater_methods_test.go @@ -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) +} diff --git a/tools/importer-msgraph-metadata/internal/pipeline/task_parse_resources.go b/tools/importer-msgraph-metadata/internal/pipeline/task_parse_resources.go index fa734b63d28..e8282f64f05 100644 --- a/tools/importer-msgraph-metadata/internal/pipeline/task_parse_resources.go +++ b/tools/importer-msgraph-metadata/internal/pipeline/task_parse_resources.go @@ -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 } diff --git a/tools/importer-msgraph-metadata/internal/pipeline/task_translate_service.go b/tools/importer-msgraph-metadata/internal/pipeline/task_translate_service.go index 6bf6d0f7609..993cc6da860 100644 --- a/tools/importer-msgraph-metadata/internal/pipeline/task_translate_service.go +++ b/tools/importer-msgraph-metadata/internal/pipeline/task_translate_service.go @@ -57,6 +57,23 @@ func (p pipelineForService) translateServiceToDataApiSdkTypes() (*sdkModels.Serv resourceIdName = &operation.ResourceId.Name } + options := map[string]sdkModels.SDKOperationOption{ + // All operations can specify the odata.metadata Accept parameter + "Metadata": { + Type: sdkModels.SDKOperationOptionTypeData, + ODataFieldName: pointer.To("Metadata"), + ObjectDefinition: sdkModels.SDKOperationOptionObjectDefinition{ + ReferenceName: pointer.To("odata.Metadata"), + Type: sdkModels.ReferenceSDKOperationOptionObjectDefinitionType, + }, + }, + + // All operations can accept a custom RetryFunc + "RetryFunc": { + Type: sdkModels.SDKOperationOptionTypeRetryFunc, + }, + } + var requestObject *sdkModels.SDKObjectDefinition if operation.RequestModelName != nil { @@ -86,22 +103,21 @@ func (p pipelineForService) translateServiceToDataApiSdkTypes() (*sdkModels.Serv } } else if operation.RequestType != nil { requestObject = &sdkModels.SDKObjectDefinition{ + // This is a regular type, i.e. not a model or typed constant Type: operation.RequestType.DataApiSdkObjectDefinitionType(), } - } - options := map[string]sdkModels.SDKOperationOption{ - "Metadata": { - ODataFieldName: pointer.To("Metadata"), - ObjectDefinition: sdkModels.SDKOperationOptionObjectDefinition{ - ReferenceName: pointer.To("odata.Metadata"), - Type: sdkModels.ReferenceSDKOperationOptionObjectDefinitionType, - }, - }, + if *operation.RequestType == parser.DataTypeBinary { + // Add a ContentType option so the caller can specify the media type for the request body + options["ContentType"] = sdkModels.SDKOperationOption{ + Type: sdkModels.SDKOperationOptionTypeContentType, + } + } } if operation.RequestHeaders != nil { for _, header := range *operation.RequestHeaders { + // Some operations support the ConsistencyLevel header, this is structured via the odata package if strings.EqualFold(header.Name, "ConsistencyLevel") { options[normalize.CleanName(header.Name)] = sdkModels.SDKOperationOption{ ODataFieldName: &header.Name, @@ -189,7 +205,7 @@ func (p pipelineForService) translateServiceToDataApiSdkTypes() (*sdkModels.Serv } case "Skip", "Top": - // Don't set here, we handle this implicitly in the SDK for list operations + // Don't set here, we handle this implicitly for list operations default: objectDefinition, err := param.DataApiSdkObjectDefinition() @@ -211,6 +227,7 @@ func (p pipelineForService) translateServiceToDataApiSdkTypes() (*sdkModels.Serv } } + // Allow the caller to control paging for list operations if operation.Type == parser.OperationTypeList { options["Top"] = sdkModels.SDKOperationOption{ ODataFieldName: pointer.To("Top"),