diff --git a/.codegen/impl.go.tmpl b/.codegen/impl.go.tmpl index 8336e0e67..811111567 100644 --- a/.codegen/impl.go.tmpl +++ b/.codegen/impl.go.tmpl @@ -9,6 +9,7 @@ import ( "io" "net/http" "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/httpclient" {{range .ImportedPackages}} "github.com/databricks/databricks-sdk-go/service/{{.}}"{{end}} ) @@ -21,9 +22,7 @@ type {{.CamelName}}Impl struct { {{range .Methods}} func (a *{{.Service.CamelName}}Impl) {{.PascalName}}(ctx context.Context{{if .Request}}, request {{.Request.PascalName}}{{end}}) {{ template "response-type" . }} { {{- template "response-var" . }} - path := {{if .PathParts -}} - fmt.Sprintf("{{range .PathParts}}{{.Prefix}}{{if or .Field .IsAccountId}}%v{{end}}{{ end }}"{{ range .PathParts }}{{if .Field}}, request.{{.Field.PascalName}}{{ else if .IsAccountId }}, a.client.ConfiguredAccountID(){{end}}{{ end }}) - {{- else}}"{{.Path}}"{{end}} + path := {{ template "path" . }} {{ template "make-header" . }} err := a.client.Do(ctx, http.Method{{.TitleVerb}}, path, headers, {{ template "request-param" . }}, {{if .Response}}&{{.Response.CamelName}}{{else}}nil{{end}}) return {{ template "response" . }} @@ -31,6 +30,21 @@ func (a *{{.Service.CamelName}}Impl) {{.PascalName}}(ctx context.Context{{if .Re {{end -}} {{end}} +{{- define "path" -}} +{{- if .PathParts -}} + fmt.Sprintf("{{range .PathParts -}} + {{.Prefix}}{{if or .Field .IsAccountId}}%v{{end}}{{ end }}" + {{- range .PathParts -}} + {{- if and .Field .Field.IsPathMultiSegment}}, httpclient.EncodeMultiSegmentPathParameter(request.{{.Field.PascalName}}) + {{- else if .Field}}, request.{{.Field.PascalName}} + {{- else if .IsAccountId }}, a.client.ConfiguredAccountID() + {{- end -}} + {{- end -}}) +{{- else -}} + "{{.Path}}" +{{- end -}} +{{- end -}} + {{ define "request-param" -}} {{ if or (and .Request (or (eq .Verb "GET") (eq .Verb "DELETE") (eq .Verb "HEAD"))) (and .Operation .Operation.RequestBody) -}} request{{ if .RequestBodyField }}.{{.RequestBodyField.PascalName}}{{end}} diff --git a/httpclient/request.go b/httpclient/request.go index 803e699c7..633b9e8b8 100644 --- a/httpclient/request.go +++ b/httpclient/request.go @@ -109,6 +109,19 @@ func makeQueryString(data interface{}) (string, error) { return "", fmt.Errorf("unsupported query string data: %#v", data) } +func EncodeMultiSegmentPathParameter(p string) string { + segments := strings.Split(p, "/") + b := strings.Builder{} + b.Grow(len(p)) + for i, s := range segments { + if i > 0 { + b.WriteString("/") + } + b.WriteString(url.PathEscape(s)) + } + return b.String() +} + func makeRequestBody(method string, requestURL *string, data interface{}) (common.RequestBody, error) { if data == nil { return common.RequestBody{}, nil diff --git a/httpclient/request_test.go b/httpclient/request_test.go index af3688374..4d838abe8 100644 --- a/httpclient/request_test.go +++ b/httpclient/request_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" ) @@ -118,3 +119,10 @@ func TestWithTokenSource(t *testing.T) { require.NoError(t, err) require.Equal(t, "awesome token", buf.String()) } + +func TestEncodeMultiSegmentPathParameter(t *testing.T) { + // Slashes should not be encoded. + assert.Equal(t, "a/b/c", EncodeMultiSegmentPathParameter("a/b/c")) + // # and ? should be encoded. + assert.Equal(t, "a%23b%3Fc", EncodeMultiSegmentPathParameter("a#b?c")) +} diff --git a/internal/files_test.go b/internal/files_test.go index d80285cf3..b42bc69a5 100644 --- a/internal/files_test.go +++ b/internal/files_test.go @@ -29,10 +29,10 @@ func (buf hashable) Hash() uint32 { return h.Sum32() } -func TestAccUCUploadAndDownloadFilesAPI(t *testing.T) { +func TestUcAccFilesUploadAndDownload(t *testing.T) { ctx, w, volume := setupUCVolume(t) - filePath := RandomName("/Volumes/" + volume.CatalogName + "/" + volume.SchemaName + "/" + volume.Name + "/files-") + filePath := RandomName("/Volumes/" + volume.CatalogName + "/" + volume.SchemaName + "/" + volume.Name + "/files-with-?-and-#-") err := w.Files.Upload(ctx, files.UploadRequest{ FilePath: filePath, Contents: io.NopCloser(strings.NewReader("abcd")), @@ -57,7 +57,7 @@ func TestAccUCUploadAndDownloadFilesAPI(t *testing.T) { assert.Equal(t, "abcd", string(contents)) } -func TestAccUCDeleteFile(t *testing.T) { +func TestUcAccFilesDelete(t *testing.T) { ctx, w, volume := setupUCVolume(t) filePath := RandomName("/Volumes/" + volume.CatalogName + "/" + volume.SchemaName + "/" + volume.Name + "/file-") @@ -67,7 +67,7 @@ func TestAccUCDeleteFile(t *testing.T) { require.NoError(t, err) } -func TestAccUCGetMetadata(t *testing.T) { +func TestUcAccFilesGetMetadata(t *testing.T) { ctx, w, volume := setupUCVolume(t) filePath := RandomName("/Volumes/" + volume.CatalogName + "/" + volume.SchemaName + "/" + volume.Name + "/file-") @@ -81,14 +81,14 @@ func TestAccUCGetMetadata(t *testing.T) { assert.NotEmpty(t, metadata.LastModified) } -func TestAccUCCreateDirectory(t *testing.T) { +func TestUcAccFilesCreateDirectory(t *testing.T) { ctx, w, volume := setupUCVolume(t) directoryPath := RandomName("/Volumes/" + volume.CatalogName + "/" + volume.SchemaName + "/" + volume.Name + "/directory-") require.NoError(t, createDirectory(t, ctx, w, directoryPath)) } -func TestAccUCListDirectoryContents(t *testing.T) { +func TestUcAccFilesListDirectoryContents(t *testing.T) { ctx, w, volume := setupUCVolume(t) directoryPath := RandomName("/Volumes/" + volume.CatalogName + "/" + volume.SchemaName + "/" + volume.Name + "/directory-") @@ -102,7 +102,7 @@ func TestAccUCListDirectoryContents(t *testing.T) { assert.Len(t, response, 3) } -func TestAccUCDeleteDirectory(t *testing.T) { +func TestUcAccFilesDeleteDirectory(t *testing.T) { ctx, w, volume := setupUCVolume(t) directoryPath := RandomName("/Volumes/" + volume.CatalogName + "/" + volume.SchemaName + "/" + volume.Name + "/directory-") @@ -112,7 +112,7 @@ func TestAccUCDeleteDirectory(t *testing.T) { assert.NoError(t, err) } -func TestAccUCGetDirectoryMetadata(t *testing.T) { +func TestUcAccFilesGetDirectoryMetadata(t *testing.T) { ctx, w, volume := setupUCVolume(t) directoryPath := RandomName("/Volumes/" + volume.CatalogName + "/" + volume.SchemaName + "/" + volume.Name + "/directory-") diff --git a/openapi/code/entity.go b/openapi/code/entity.go index a19ae6a84..54f80db98 100644 --- a/openapi/code/entity.go +++ b/openapi/code/entity.go @@ -10,14 +10,15 @@ import ( // Field of a Type (Entity) type Field struct { Named - Required bool - Entity *Entity - Of *Entity - IsJson bool - IsPath bool - IsQuery bool - IsHeader bool - Schema *openapi.Schema + Required bool + Entity *Entity + Of *Entity + IsJson bool + IsPath bool + IsPathMultiSegment bool + IsQuery bool + IsHeader bool + Schema *openapi.Schema } func (f *Field) IsOptionalObject() bool { diff --git a/openapi/code/service.go b/openapi/code/service.go index de63634ce..49f8504ff 100644 --- a/openapi/code/service.go +++ b/openapi/code/service.go @@ -149,11 +149,12 @@ func (svc *Service) getDescription(param openapi.Parameter) string { func (svc *Service) paramToField(op *openapi.Operation, param openapi.Parameter) *Field { named := Named{param.Name, svc.getDescription(param)} return &Field{ - Named: named, - Required: param.Required, - IsPath: param.In == "path", - IsQuery: param.In == "query", - IsHeader: param.In == "header", + Named: named, + Required: param.Required, + IsPath: param.In == "path", + IsPathMultiSegment: param.MultiSegment, + IsQuery: param.In == "query", + IsHeader: param.In == "header", Entity: svc.Package.schemaToEntity(param.Schema, []string{ op.Name(), named.PascalName(), @@ -254,6 +255,7 @@ func (svc *Service) addParams(request *Entity, op *openapi.Operation, params []o field = param } field.IsPath = param.IsPath + field.IsPathMultiSegment = param.IsPathMultiSegment field.IsQuery = param.IsQuery field.IsHeader = param.IsHeader request.fields[param.Name] = field diff --git a/service/files/impl.go b/service/files/impl.go index a4559b786..23b25c450 100755 --- a/service/files/impl.go +++ b/service/files/impl.go @@ -8,6 +8,7 @@ import ( "net/http" "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/httpclient" ) // unexported type that holds implementations of just Dbfs API methods @@ -119,7 +120,7 @@ type filesImpl struct { func (a *filesImpl) CreateDirectory(ctx context.Context, request CreateDirectoryRequest) error { var createDirectoryResponse CreateDirectoryResponse - path := fmt.Sprintf("/api/2.0/fs/directories%v", request.DirectoryPath) + path := fmt.Sprintf("/api/2.0/fs/directories%v", httpclient.EncodeMultiSegmentPathParameter(request.DirectoryPath)) headers := make(map[string]string) err := a.client.Do(ctx, http.MethodPut, path, headers, nil, &createDirectoryResponse) return err @@ -127,7 +128,7 @@ func (a *filesImpl) CreateDirectory(ctx context.Context, request CreateDirectory func (a *filesImpl) Delete(ctx context.Context, request DeleteFileRequest) error { var deleteResponse DeleteResponse - path := fmt.Sprintf("/api/2.0/fs/files%v", request.FilePath) + path := fmt.Sprintf("/api/2.0/fs/files%v", httpclient.EncodeMultiSegmentPathParameter(request.FilePath)) headers := make(map[string]string) err := a.client.Do(ctx, http.MethodDelete, path, headers, request, &deleteResponse) return err @@ -135,7 +136,7 @@ func (a *filesImpl) Delete(ctx context.Context, request DeleteFileRequest) error func (a *filesImpl) DeleteDirectory(ctx context.Context, request DeleteDirectoryRequest) error { var deleteDirectoryResponse DeleteDirectoryResponse - path := fmt.Sprintf("/api/2.0/fs/directories%v", request.DirectoryPath) + path := fmt.Sprintf("/api/2.0/fs/directories%v", httpclient.EncodeMultiSegmentPathParameter(request.DirectoryPath)) headers := make(map[string]string) err := a.client.Do(ctx, http.MethodDelete, path, headers, request, &deleteDirectoryResponse) return err @@ -143,7 +144,7 @@ func (a *filesImpl) DeleteDirectory(ctx context.Context, request DeleteDirectory func (a *filesImpl) Download(ctx context.Context, request DownloadRequest) (*DownloadResponse, error) { var downloadResponse DownloadResponse - path := fmt.Sprintf("/api/2.0/fs/files%v", request.FilePath) + path := fmt.Sprintf("/api/2.0/fs/files%v", httpclient.EncodeMultiSegmentPathParameter(request.FilePath)) headers := make(map[string]string) headers["Accept"] = "application/octet-stream" err := a.client.Do(ctx, http.MethodGet, path, headers, request, &downloadResponse) @@ -152,7 +153,7 @@ func (a *filesImpl) Download(ctx context.Context, request DownloadRequest) (*Dow func (a *filesImpl) GetDirectoryMetadata(ctx context.Context, request GetDirectoryMetadataRequest) error { var getDirectoryMetadataResponse GetDirectoryMetadataResponse - path := fmt.Sprintf("/api/2.0/fs/directories%v", request.DirectoryPath) + path := fmt.Sprintf("/api/2.0/fs/directories%v", httpclient.EncodeMultiSegmentPathParameter(request.DirectoryPath)) headers := make(map[string]string) err := a.client.Do(ctx, http.MethodHead, path, headers, request, &getDirectoryMetadataResponse) return err @@ -160,7 +161,7 @@ func (a *filesImpl) GetDirectoryMetadata(ctx context.Context, request GetDirecto func (a *filesImpl) GetMetadata(ctx context.Context, request GetMetadataRequest) (*GetMetadataResponse, error) { var getMetadataResponse GetMetadataResponse - path := fmt.Sprintf("/api/2.0/fs/files%v", request.FilePath) + path := fmt.Sprintf("/api/2.0/fs/files%v", httpclient.EncodeMultiSegmentPathParameter(request.FilePath)) headers := make(map[string]string) err := a.client.Do(ctx, http.MethodHead, path, headers, request, &getMetadataResponse) return &getMetadataResponse, err @@ -168,7 +169,7 @@ func (a *filesImpl) GetMetadata(ctx context.Context, request GetMetadataRequest) func (a *filesImpl) ListDirectoryContents(ctx context.Context, request ListDirectoryContentsRequest) (*ListDirectoryResponse, error) { var listDirectoryResponse ListDirectoryResponse - path := fmt.Sprintf("/api/2.0/fs/directories%v", request.DirectoryPath) + path := fmt.Sprintf("/api/2.0/fs/directories%v", httpclient.EncodeMultiSegmentPathParameter(request.DirectoryPath)) headers := make(map[string]string) headers["Accept"] = "application/json" err := a.client.Do(ctx, http.MethodGet, path, headers, request, &listDirectoryResponse) @@ -177,7 +178,7 @@ func (a *filesImpl) ListDirectoryContents(ctx context.Context, request ListDirec func (a *filesImpl) Upload(ctx context.Context, request UploadRequest) error { var uploadResponse UploadResponse - path := fmt.Sprintf("/api/2.0/fs/files%v", request.FilePath) + path := fmt.Sprintf("/api/2.0/fs/files%v", httpclient.EncodeMultiSegmentPathParameter(request.FilePath)) headers := make(map[string]string) headers["Content-Type"] = "application/octet-stream" err := a.client.Do(ctx, http.MethodPut, path, headers, request.Contents, &uploadResponse) diff --git a/service/iam/model.go b/service/iam/model.go index fe0fc94e3..896f4e6d9 100755 --- a/service/iam/model.go +++ b/service/iam/model.go @@ -329,7 +329,7 @@ type Group struct { Groups []ComplexValue `json:"groups,omitempty"` // Databricks group ID - Id string `json:"id,omitempty"` + Id string `json:"id,omitempty" url:"-"` Members []ComplexValue `json:"members,omitempty"` // Container for the group identifier. Workspace local versus account. diff --git a/service/sql/model.go b/service/sql/model.go index f69dd35f3..702b8d1af 100755 --- a/service/sql/model.go +++ b/service/sql/model.go @@ -287,7 +287,6 @@ func (s ChannelInfo) MarshalJSON() ([]byte, error) { return marshal.Marshal(s) } -// Name of the channel type ChannelName string const ChannelNameChannelNameCurrent ChannelName = `CHANNEL_NAME_CURRENT`