diff --git a/httpclient/fixtures/name.go b/httpclient/fixtures/name.go new file mode 100644 index 000000000..05ac93a7c --- /dev/null +++ b/httpclient/fixtures/name.go @@ -0,0 +1,112 @@ +package fixtures + +import ( + "strings" + "unicode" +) + +// These methods are taken from the OpenAPI code generator as it has been moved +// to a separate repository. + +// Return the value of cond evaluated at the nearest letter to index i in name. +// dir determines the direction of search: if true, search forwards, if false, +// search backwards. +func search(name string, cond func(rune) bool, dir bool, i int) bool { + nameLen := len(name) + incr := 1 + if !dir { + incr = -1 + } + for j := i; j >= 0 && j < nameLen; j += incr { + if unicode.IsLetter(rune(name[j])) { + return cond(rune(name[j])) + } + } + return false +} + +// Return the value of cond evaluated on the rune at index i in name. If that +// rune is not a letter, search in both directions for the nearest letter and +// return the result of cond on those letters. +func checkCondAtNearestLetters(name string, cond func(rune) bool, i int) bool { + r := rune(name[i]) + + if unicode.IsLetter(r) { + return cond(r) + } + return search(name, cond, true, i) && search(name, cond, false, i) +} + +// emulate positive lookaheads from JVM regex: +// (?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|([-_\s]) +// and convert all words to lower case + +func splitASCII(name string) (w []string) { + var current []rune + nameLen := len(name) + var isPrevUpper, isCurrentUpper, isNextLower, isNextUpper, isNotLastChar bool + // we do assume here that all named entities are strictly ASCII + for i := 0; i < nameLen; i++ { + r := rune(name[i]) + if r == '$' { + // we're naming language literals, $ is usually not allowed + continue + } + // if the current rune is a digit, check the neighboring runes to + // determine whether to treat this one as upper-case. + isCurrentUpper = checkCondAtNearestLetters(name, unicode.IsUpper, i) + r = unicode.ToLower(r) + isNextLower = false + isNextUpper = false + isNotLastChar = i+1 < nameLen + if isNotLastChar { + isNextLower = checkCondAtNearestLetters(name, unicode.IsLower, i+1) + isNextUpper = checkCondAtNearestLetters(name, unicode.IsUpper, i+1) + } + split, before, after := false, false, true + // At the end of a string of capital letters (e.g. HTML[P]arser). + if isPrevUpper && isCurrentUpper && isNextLower && isNotLastChar { + // (?<=[A-Z])(?=[A-Z][a-z]) + split = true + before = false + after = true + } + // At the end of a camel- or pascal-case word (e.g. htm[l]Parser). + if !isCurrentUpper && isNextUpper { + // (?<=[a-z])(?=[A-Z]) + split = true + before = true + after = false + } + if !unicode.IsLetter(r) && !unicode.IsNumber(r) { + // ([-_\s]) + split = true + before = false + after = false + } + if before { + current = append(current, r) + } + if split && len(current) > 0 { + w = append(w, string(current)) + current = []rune{} + } + if after { + current = append(current, r) + } + isPrevUpper = isCurrentUpper + } + if len(current) > 0 { + w = append(w, string(current)) + } + return w +} + +// PascalName creates NamesLikesThis +func pascalName(name string) string { + var sb strings.Builder + for _, w := range splitASCII(name) { + sb.WriteString(strings.Title(w)) + } + return sb.String() +} diff --git a/httpclient/fixtures/stub.go b/httpclient/fixtures/stub.go index 7c213bf03..11b474133 100644 --- a/httpclient/fixtures/stub.go +++ b/httpclient/fixtures/stub.go @@ -6,8 +6,6 @@ import ( "fmt" "net/http" "net/url" - - "github.com/databricks/databricks-sdk-go/openapi/code" ) func resourceFromRequest(req *http.Request) string { @@ -57,7 +55,7 @@ func bodyStub(req *http.Request) (string, error) { // which is not something i'm willing to write on my weekend expectedRequest += "ExpectedRequest: XXX {\n" for key, value := range receivedRequest { - camel := (&code.Named{Name: key}).PascalName() + camel := pascalName(key) expectedRequest += fmt.Sprintf("\t\t\t%s: %#v,\n", camel, value) } expectedRequest += "\t\t},\n" diff --git a/openapi/code/entity.go b/openapi/code/entity.go deleted file mode 100644 index a61917b9d..000000000 --- a/openapi/code/entity.go +++ /dev/null @@ -1,356 +0,0 @@ -package code - -import ( - "fmt" - "sort" - - "github.com/databricks/databricks-sdk-go/openapi" -) - -// Field of a Type (Entity) -type Field struct { - Named - Required bool - Entity *Entity - Of *Entity - IsJson bool - IsPath bool - IsQuery bool - Schema *openapi.Schema -} - -func (f *Field) IsOptionalObject() bool { - return f.Entity != nil && !f.Required && (f.Entity.IsObject() || f.Entity.IsExternal()) -} - -// IsPrivatePreview flags object being in private preview. -func (f *Field) IsPrivatePreview() bool { - return f.Schema != nil && isPrivatePreview(&f.Schema.Node) -} - -// IsPublicPreview flags object being in public preview. -func (f *Field) IsPublicPreview() bool { - return f.Schema != nil && isPublicPreview(&f.Schema.Node) -} - -// IsRequestBodyField indicates a field which can only be set as part of request body -// There are some fields, such as PipelineId for example, which can be both used in JSON and -// as path parameters. In code generation we handle path and request body parameters separately -// by making path parameters always required. Thus, we don't need to consider such fields -// as request body fields anymore. -func (f *Field) IsRequestBodyField() bool { - return f.IsJson && !f.IsPath && !f.IsQuery -} - -// Call the provided callback on this field and any nested fields in its entity, -// recursively. -func (f *Field) Traverse(fn func(*Field)) { - fn(f) - if f.Entity != nil && len(f.Entity.fields) > 0 { - for _, field := range f.Entity.fields { - field.Traverse(fn) - } - } -} - -type EnumEntry struct { - Named - Entity *Entity - // SCIM API has schema specifiers - Content string -} - -// Entity represents a Type -type Entity struct { - Named - Package *Package - enum map[string]EnumEntry - ArrayValue *Entity - MapValue *Entity - IsInt bool - IsInt64 bool - IsFloat64 bool - IsBool bool - IsString bool - IsByteStream bool - IsEmpty bool - - // this field does not have a concrete type - IsAny bool - - // this field is computed on the platform side - IsComputed bool - - // if entity has required fields, this is the order of them - RequiredOrder []string - fields map[string]*Field - - // Schema references the OpenAPI schema this entity was created from. - Schema *openapi.Schema -} - -// Whether the Entity contains a basic GoLang type which is not required -func (e *Entity) ShouldIncludeForceSendFields() bool { - for _, field := range e.fields { - fieldType := field.Entity - if !field.Required && fieldType.IsBasicGoLangType() { - return true - } - } - return false -} - -// Whether this entity represents a basic GoLang type -func (e *Entity) IsBasicGoLangType() bool { - return e.IsBool || e.IsInt64 || e.IsInt || e.IsFloat64 || e.IsString -} - -// FullName includes package name and untransformed name of the entity -func (e *Entity) FullName() string { - return fmt.Sprintf("%s.%s", e.Package.FullName(), e.PascalName()) -} - -// PascalName overrides parent implementation by appending List -// suffix for unnamed list types -func (e *Entity) PascalName() string { - if e.Name == "" && e.ArrayValue != nil { - return e.ArrayValue.PascalName() + "List" - } - return e.Named.PascalName() -} - -// CamelName overrides parent implementation by appending List -// suffix for unnamed list types -func (e *Entity) CamelName() string { - if e.Name == "" && e.ArrayValue != nil { - return e.ArrayValue.CamelName() + "List" - } - return e.Named.CamelName() -} - -// Field gets field representation by name or nil -func (e *Entity) Field(name string) *Field { - if e == nil { - // nil received: entity is not present - return nil - } - field, ok := e.fields[name] - if !ok { - return nil - } - field.Of = e - return field -} - -// Given a list of field names, return the list of *Field objects which result -// from following the path of fields in the entity. -func (e *Entity) GetUnderlyingFields(path []string) ([]*Field, error) { - if len(path) == 0 { - return nil, fmt.Errorf("empty path is not allowed (entity: %s)", e.FullName()) - } - if len(path) == 1 { - return []*Field{e.Field(path[0])}, nil - } - field := e.Field(path[0]) - if field == nil { - return nil, fmt.Errorf("field %s not found in entity %s", path[0], e.FullName()) - } - rest, err := field.Entity.GetUnderlyingFields(path[1:]) - if err != nil { - return nil, err - } - return append([]*Field{field}, rest...), nil -} - -// IsObject returns true if entity is not a Mpa and has more than zero fields -func (e *Entity) IsObject() bool { - return e.MapValue == nil && len(e.fields) > 0 -} - -// IsExternal returns true if entity is declared in external package and -// has to be imported from it -func (e *Entity) IsExternal() bool { - return e.Package != nil && len(e.Package.types) == 0 -} - -func (e *Entity) RequiredFields() (fields []*Field) { - for _, r := range e.RequiredOrder { - v := e.fields[r] - v.Of = e - fields = append(fields, v) - } - - // Path fields should always be first in required arguments order. - // We use stable sort to male sure we preserve the path arguments order - sort.SliceStable(fields, func(a, b int) bool { - return fields[a].IsPath && !fields[b].IsPath - }) - return -} - -func (e *Entity) RequiredPathFields() (fields []*Field) { - for _, r := range e.RequiredOrder { - v := e.fields[r] - if !v.IsPath { - continue - } - v.Of = e - fields = append(fields, v) - } - return -} - -func (e *Entity) RequiredRequestBodyFields() (fields []*Field) { - for _, r := range e.RequiredOrder { - v := e.fields[r] - if !v.IsRequestBodyField() { - continue - } - v.Of = e - fields = append(fields, v) - } - return -} - -func (e *Entity) NonRequiredFields() (fields []*Field) { - required := map[string]bool{} - for _, r := range e.RequiredOrder { - required[r] = true - } - for k, v := range e.fields { - if required[k] { - // handled in [Entity.RequiredFields] - continue - } - v.Of = e - fields = append(fields, v) - } - pascalNameSort(fields) - return -} - -// Fields returns sorted slice of field representations -func (e *Entity) Fields() (fields []*Field) { - for _, v := range e.fields { - v.Of = e - fields = append(fields, v) - } - pascalNameSort(fields) - return fields -} - -// HasQueryField returns true if any of the fields is from query -func (e *Entity) HasQueryField() bool { - for _, v := range e.fields { - if v.IsQuery { - return true - } - } - return false -} - -// HasJsonField returns true if any of the fields is in the body -func (e *Entity) HasJsonField() bool { - for _, v := range e.fields { - if v.IsJson { - return true - } - } - return false -} - -// Enum returns all entries for enum entities -func (e *Entity) Enum() (enum []EnumEntry) { - for _, v := range e.enum { - enum = append(enum, v) - } - sort.Slice(enum, func(i, j int) bool { - return enum[i].Name < enum[j].Name - }) - return enum -} - -func (e *Entity) IsPrimitive() bool { - return e.IsNumber() || e.IsBool || e.IsString || len(e.enum) > 0 -} - -// IsNumber returns true if field is numeric -func (e *Entity) IsNumber() bool { - return e.IsInt64 || e.IsInt || e.IsFloat64 -} - -func (e *Entity) IsOnlyPrimitiveFields() bool { - for _, v := range e.fields { - if !v.Entity.IsPrimitive() { - return false - } - } - return true -} - -func (e *Entity) IsAllRequiredFieldsPrimitive() bool { - for _, v := range e.RequiredFields() { - if !v.Entity.IsPrimitive() { - return false - } - } - return true -} - -func (e *Entity) HasRequiredPathFields() bool { - return len(e.RequiredPathFields()) > 0 -} - -func (e *Entity) HasRequiredRequestBodyFields() bool { - return len(e.RequiredRequestBodyFields()) > 0 -} - -// IsPrivatePreview flags object being in private preview. -func (e *Entity) IsPrivatePreview() bool { - return e.Schema != nil && isPrivatePreview(&e.Schema.Node) -} - -// IsPublicPreview flags object being in public preview. -func (e *Entity) IsPublicPreview() bool { - return e.Schema != nil && isPublicPreview(&e.Schema.Node) -} - -func (e *Entity) IsRequest() bool { - for _, svc := range e.Package.services { - for _, m := range svc.methods { - if m.Request == e { - return true - } - } - } - return false -} - -func (e *Entity) IsResponse() bool { - for _, svc := range e.Package.services { - for _, m := range svc.methods { - if m.Response == e { - return true - } - } - } - return false -} - -func (e *Entity) IsReferred() bool { - for _, t := range e.Package.types { - for _, f := range t.fields { - if f.Entity == e { - return true - } - } - } - return false -} - -func (e *Entity) Traverse(fn func(*Entity)) { - fn(e) - for _, f := range e.fields { - f.Entity.Traverse(fn) - } -} diff --git a/openapi/code/errors.go b/openapi/code/errors.go deleted file mode 100644 index 183f29ea1..000000000 --- a/openapi/code/errors.go +++ /dev/null @@ -1,71 +0,0 @@ -package code - -import "github.com/databricks/databricks-sdk-go/openapi" - -type ExceptionType struct { - Named - StatusCode int - Inherit *Named -} - -func (et *ExceptionType) FullName() string { - return et.Name -} - -type ErrorMappingRule struct { - Named - StatusCode int - ErrorCode string -} - -func (b *Batch) ErrorStatusCodeMapping() (rules []ErrorMappingRule) { - for _, em := range openapi.ErrorStatusCodeMapping { - rules = append(rules, ErrorMappingRule{ - StatusCode: em.StatusCode, - Named: Named{ - Name: em.ErrorCode, - }, - }) - } - return rules -} - -func (b *Batch) ErrorCodeMapping() (rules []ErrorMappingRule) { - for _, em := range openapi.ErrorCodeMapping { - rules = append(rules, ErrorMappingRule{ - ErrorCode: em.ErrorCode, - Named: Named{ - Name: em.ErrorCode, - }, - }) - } - return rules -} - -func (b *Batch) ExceptionTypes() []*ExceptionType { - statusCodeMapping := map[int]*Named{} - exceptionTypes := []*ExceptionType{} - for _, em := range openapi.ErrorStatusCodeMapping { - statusCodeMapping[em.StatusCode] = &Named{ - Name: em.ErrorCode, - } - exceptionTypes = append(exceptionTypes, &ExceptionType{ - Named: Named{ - Name: em.ErrorCode, - Description: em.Description, - }, - StatusCode: em.StatusCode, - }) - } - for _, em := range openapi.ErrorCodeMapping { - exceptionTypes = append(exceptionTypes, &ExceptionType{ - Named: Named{ - Name: em.ErrorCode, - Description: em.Description, - }, - StatusCode: em.StatusCode, - Inherit: statusCodeMapping[em.StatusCode], - }) - } - return exceptionTypes -} diff --git a/openapi/code/load.go b/openapi/code/load.go deleted file mode 100644 index 75af8d5af..000000000 --- a/openapi/code/load.go +++ /dev/null @@ -1,122 +0,0 @@ -package code - -import ( - "context" - "fmt" - "os" - "sort" - "strings" - - "github.com/databricks/databricks-sdk-go/openapi" -) - -type Batch struct { - packages map[string]*Package -} - -// NewFromFile loads OpenAPI specification from file -func NewFromFile(ctx context.Context, name string) (*Batch, error) { - f, err := os.Open(name) - if err != nil { - return nil, fmt.Errorf("no %s file: %w", name, err) - } - defer f.Close() - spec, err := openapi.NewFromReader(f) - if err != nil { - return nil, fmt.Errorf("spec from %s: %w", name, err) - } - return NewFromSpec(ctx, spec) -} - -// NewFromSpec converts OpenAPI spec to intermediate representation -func NewFromSpec(ctx context.Context, spec *openapi.Specification) (*Batch, error) { - batch := Batch{ - packages: map[string]*Package{}, - } - for _, tag := range spec.Tags { - pkg, ok := batch.packages[tag.Package] - if !ok { - pkg = &Package{ - Named: Named{tag.Package, ""}, - Components: spec.Components, - services: map[string]*Service{}, - types: map[string]*Entity{}, - } - batch.packages[tag.Package] = pkg - } - err := pkg.Load(ctx, spec, tag) - if err != nil { - return nil, fmt.Errorf("fail to load %s: %w", tag.Name, err) - } - } - // add some packages at least some description - for _, pkg := range batch.packages { - if len(pkg.services) > 1 { - continue - } - // we know that we have just one service - for _, svc := range pkg.services { - pkg.Description = svc.Summary() - } - } - return &batch, nil -} - -func (b *Batch) FullName() string { - return "all" -} - -// Packages returns sorted slice of packages -func (b *Batch) Packages() (pkgs []*Package) { - for _, pkg := range b.packages { - pkgs = append(pkgs, pkg) - } - // add some determinism into code generation for globally stable order in - // files like for workspaces/accounts clinets. - fullNameSort(pkgs) - return pkgs -} - -// Pkgs returns sorted slice of packages -func (b *Batch) Types() (types []*Entity) { - for _, pkg := range b.packages { - types = append(types, pkg.Types()...) - } - // add some determinism into code generation for globally stable order in - // files like api.go and/or {{.Package.Name}}.py and clients. - fullNameSort(types) - return types -} - -// Pkgs returns sorted slice of packages -func (b *Batch) Services() (services []*Service) { - for _, pkg := range b.packages { - services = append(services, pkg.Services()...) - } - // we'll have more and more account level equivalents of APIs that are - // currently workspace-level. In the AccountsClient we're striping - // the `Account` prefix, so that naming and ordering is more logical. - // this requires us to do the proper sorting of services. E.g. - // - // - Groups: scim.NewAccountGroups(apiClient), - // - ServicePrincipals: scim.NewAccountServicePrincipals(apiClient), - // - Users: scim.NewAccountUsers(apiClient), - // - // more services may follow this pattern in the future. - norm := func(name string) string { - if !strings.HasPrefix(name, "Account") { - return name - } - // sorting-only rename: AccountGroups -> GroupsAccount - return name[7:] + "Account" - } - // add some determinism into code generation for globally stable order in - // files like api.go and/or {{.Package.Name}}.py and clients. - sort.Slice(services, func(a, b int) bool { - // not using .FullName() here, as in "batch" context - // services have to be sorted globally, not only within a package. - // alternatively we may think on adding .ReverseFullName() to sort on. - return norm(services[a].Name) < norm(services[b].Name) - }) - return services -} diff --git a/openapi/code/load_test.go b/openapi/code/load_test.go deleted file mode 100644 index b8b524aa3..000000000 --- a/openapi/code/load_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package code - -import ( - "context" - "fmt" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestBasic(t *testing.T) { - ctx := context.Background() - batch, err := NewFromFile(ctx, "../testdata/spec.json") - require.NoError(t, err) - - require.Len(t, batch.Packages(), 1) - require.Len(t, batch.Services(), 1) - require.Len(t, batch.Types(), 14) - commands, ok := batch.packages["commands"] - require.True(t, ok) - - assert.Equal(t, "commands", commands.FullName()) - - ce, ok := commands.services["CommandExecution"] - require.True(t, ok) - assert.Equal(t, "commands.CommandExecution", ce.FullName()) - assert.Equal(t, "commandExecution", ce.CamelName()) - assert.Equal(t, "CommandExecution", ce.PascalName()) - assert.Equal(t, commands, ce.Package) - - methods := ce.Methods() - assert.Equal(t, 6, len(methods)) - - execute := methods[5] - assert.Equal(t, "execute", execute.Name) - assert.Equal(t, "Execute", execute.PascalName()) - assert.Equal(t, "Post", execute.TitleVerb()) - assert.Nil(t, execute.Shortcut()) - - wait := execute.Wait() - require.NotNil(t, wait) - binding := wait.Binding() - assert.Equal(t, 3, len(binding)) - - assert.Equal(t, "finished", wait.Success()[0].CamelName()) - assert.Equal(t, "CommandStatus", wait.Failure()[0].Entity.PascalName()) - assert.Equal(t, 0, len(wait.MessagePath())) - - types := commands.Types() - assert.Equal(t, 14, len(types)) - - command := types[1] - assert.Equal(t, "Command", command.PascalName()) - assert.Equal(t, "command", command.CamelName()) - assert.Equal(t, "commands.Command", command.FullName()) - assert.Equal(t, 4, len(command.Fields())) - - assert.Nil(t, command.Field("nothing")) - - language := command.Field("language") - assert.NotNil(t, language) - assert.False(t, language.IsOptionalObject()) - assert.Equal(t, 3, len(language.Entity.Enum())) -} - -// This test is used for debugging purposes -func TestMethodsReport(t *testing.T) { - t.SkipNow() - ctx := context.Background() - home, _ := os.UserHomeDir() - batch, err := NewFromFile(ctx, filepath.Join(home, - "universe/bazel-bin/openapi/all-internal.json")) - assert.NoError(t, err) - - for _, pkg := range batch.Packages() { - for _, svc := range pkg.Services() { - singleService := strings.EqualFold(pkg.PascalName(), svc.PascalName()) - serviceSingularKebab := svc.Singular().KebabName() - for _, m := range svc.Methods() { - var fields []string - if m.Request != nil { - for _, f := range m.Request.Fields() { - flag := fmt.Sprintf("--%s", f.KebabName()) - if f.Entity.IsObject() || f.Entity.MapValue != nil { - flag = fmt.Sprintf("%%%s", flag) - } - fields = append(fields, flag) - } - } - methodWithoutService := (&Named{ - Name: strings.ReplaceAll( - strings.ReplaceAll( - m.KebabName(), svc.KebabName(), ""), - serviceSingularKebab, ""), - }).KebabName() - println(fmt.Sprintf("| %s | %s | %s | %s | %v | %s | %s |", - pkg.KebabName(), - svc.KebabName(), - m.KebabName(), - methodWithoutService, - singleService, - m.Operation.Crud, - strings.Join(fields, ", "), - )) - } - } - } - - assert.Equal(t, len(batch.packages), 1) -} diff --git a/openapi/code/method.go b/openapi/code/method.go deleted file mode 100644 index 3cf7ba212..000000000 --- a/openapi/code/method.go +++ /dev/null @@ -1,443 +0,0 @@ -package code - -import ( - "fmt" - "regexp" - "strings" - - "github.com/databricks/databricks-sdk-go/openapi" -) - -// Method represents service RPC -type Method struct { - Named - Service *Service - // HTTP method name - Verb string - // Full API Path, including /api/2.x prefix - Path string - // Slice of path params, e.g. permissions/{type}/{id} - PathParts []PathPart - // Request type representation - Request *Entity - // Response type representation - Response *Entity - EmptyResponseName Named - - // The style of the request, either "rpc" or "rest". See the documentation on - // Operation for more details. - PathStyle openapi.PathStyle - - // For list APIs, the path of fields in the response entity to follow to get - // the resource ID. - IdFieldPath []*Field - - // For list APIs, the path of fields in the response entity to follow to get - // the user-friendly name of the resource. - NameFieldPath []*Field - - // If not nil, the field in the request and reponse entities that should be - // mapped to the request/response body. - RequestBodyField *Field - ResponseBodyField *Field - - // Expected content type of the request and response - FixedRequestHeaders map[string]string - - wait *openapi.Wait - pagination *openapi.Pagination - Operation *openapi.Operation - shortcut bool -} - -// Shortcut holds definition of "shortcut" methods, that are generated for -// methods with request entities only with required fields. -type Shortcut struct { - Named - Params []Field - Method *Method -} - -// Pagination holds definition of result iteration type per specific RPC. -// Databricks as of now has a couple different types of pagination: -// - next_token/next_page_token + repeated field -// - offset/limit with zero-based offsets + repeated field -// - page/limit with 1-based pages + repeated field -// - repeated inline field -// - repeated field -type Pagination struct { - Offset *Field - Limit *Field - Results *Field - Entity *Entity - Token *Binding - Increment int -} - -// NamedIdMap depends on Pagination and is generated, when paginated item -// entity has Identifier and Name fields. End-users usually use this method for -// drop-downs or any other selectors. -type NamedIdMap struct { - Named - IdPath []*Field - NamePath []*Field - Entity *Entity - - // if List method returns []Item directly - // without generated iteration wrapper - Direct bool -} - -// PathPart represents required field, that is always part of the path -type PathPart struct { - Prefix string - Field *Field - IsAccountId bool -} - -var pathPairRE = regexp.MustCompile(`(?m)([^\{]+)(\{(\w+)\})?`) - -func (m *Method) pathParams() (params []Field) { - if len(m.PathParts) == 0 { - return nil - } - if !(m.Verb == "GET" || m.Verb == "DELETE") { - return nil - } - for _, part := range m.PathParts { - if part.Field == nil { - continue - } - params = append(params, *part.Field) - } - return params -} - -func (m *Method) allowShortcut() bool { - if m.shortcut { - return true - } - if m.PathStyle == openapi.PathStyleRpc { - return true - } - return false -} - -func (m *Method) rpcSingleFields() (params []Field) { - if !m.allowShortcut() { - return nil - } - if m.Request == nil { - return nil - } - if len(m.Request.fields) != 1 { - // TODO: explicitly annotate with x-databricks-shortcut - return nil - } - return []Field{*m.Request.Fields()[0]} -} - -func (m *Method) requestShortcutFields() []Field { - pathParams := m.pathParams() - rpcFields := m.rpcSingleFields() - if len(pathParams) == 0 && len(rpcFields) == 0 { - return nil - } - if len(pathParams) > 0 { - return pathParams - } - return rpcFields -} - -// Shortcut creates definition from path params and single-field request entities -func (m *Method) Shortcut() *Shortcut { - params := m.requestShortcutFields() - if len(params) == 0 { - return nil - } - nameParts := []string{} - for _, part := range params { - nameParts = append(nameParts, part.PascalName()) - } - name := fmt.Sprintf("%sBy%s", m.PascalName(), strings.Join(nameParts, "And")) - return &Shortcut{ - Named: Named{name, ""}, - Method: m, - Params: params, - } -} - -func (m *Method) IsCrudRead() bool { - return m.Operation.Crud == "read" -} - -func (m *Method) IsCrudCreate() bool { - return m.Operation.Crud == "create" -} - -func (m *Method) IsJsonOnly() bool { - return m.Operation.JsonOnly -} - -// MustUseJson indicates whether we have to use -// JSON input to set all required fields for request. -// If we can do so, it means we can only use JSON input passed via --json flag. -func (m *Method) MustUseJson() bool { - // method supports only JSON input - if m.IsJsonOnly() { - return true - } - - // if not all required fields are primitive, then fields must be provided in JSON - if m.Request != nil && !m.Request.IsAllRequiredFieldsPrimitive() { - return true - } - - return false -} - -// CanUseJson indicates whether the generated command supports --json flag. -// It happens when either method has to use JSON input or not all fields in request -// are primitives. Because such fields are not required, the command has not but -// should support JSON input. -func (m *Method) CanUseJson() bool { - return m.MustUseJson() || (m.Request != nil && m.Request.HasJsonField()) -} - -func (m *Method) HasIdentifierField() bool { - return len(m.IdFieldPath) > 0 -} - -func (m *Method) IdentifierField() *Field { - if !m.HasIdentifierField() { - return nil - } - return m.IdFieldPath[len(m.IdFieldPath)-1] -} - -func (m *Method) HasNameField() bool { - return len(m.NameFieldPath) > 0 -} - -// Wait returns definition for long-running operation -func (m *Method) Wait() *Wait { - if m.wait == nil { - return nil - } - // here it's easier to follow the snake_case, as success states are already - // in the CONSTANT_CASE and it's easier to convert from constant to snake, - // than from constant to camel or pascal. - name := m.Service.Singular().SnakeName() - success := strings.ToLower(strings.Join(m.wait.Success, "_or_")) - getStatus, ok := m.Service.methods[m.wait.Poll] - if !ok { - panic(fmt.Errorf("cannot find %s.%s", m.Service.Name, m.wait.Poll)) - } - name = fmt.Sprintf("wait_%s_%s_%s", getStatus.SnakeName(), name, success) - return &Wait{ - Named: Named{ - Name: name, - }, - Method: m, - } -} - -// Pagination returns definition for possibly multi-request result iterator -func (m *Method) Pagination() *Pagination { - if m.pagination == nil { - return nil - } - if m.Response.ArrayValue != nil { - // we assume that method already returns body-as-array - return nil - } - var token *Binding - if m.pagination.Token != nil { - token = &Binding{ // reuse the same datastructure as for waiters - PollField: m.Request.Field(m.pagination.Token.Request), - Bind: m.Response.Field(m.pagination.Token.Response), - } - } - offset := m.Request.Field(m.pagination.Offset) - limit := m.Request.Field(m.pagination.Limit) - results := m.Response.Field(m.pagination.Results) - if results == nil { - panic(fmt.Errorf("invalid results field '%v': %s", - m.pagination.Results, m.Operation.OperationId)) - } - entity := results.Entity.ArrayValue - increment := m.pagination.Increment - return &Pagination{ - Results: results, - Token: token, - Entity: entity, - Offset: offset, - Limit: limit, - Increment: increment, - } -} - -func (m *Method) paginationItem() *Entity { - if m.pagination == nil { - return nil - } - if m.Response.ArrayValue != nil { - // we assume that method already returns body-as-array - return m.Response.ArrayValue - } - p := m.Pagination() - return p.Entity -} - -func (m *Method) NeedsOffsetDedupe() bool { - p := m.Pagination() - return p.Offset != nil && m.HasIdentifierField() -} - -func (p *Pagination) MultiRequest() bool { - return p.Offset != nil || p.Token != nil -} - -// NamedIdMap returns name-to-id mapping retrieval definition for all -// entities of a type -func (m *Method) NamedIdMap() *NamedIdMap { - entity := m.paginationItem() - if entity == nil || !m.HasIdentifierField() || !m.HasNameField() { - return nil - } - namePath := m.NameFieldPath - nameParts := []string{entity.PascalName()} - for _, v := range namePath { - nameParts = append(nameParts, v.PascalName()) - } - nameParts = append(nameParts, "To") - nameParts = append(nameParts, m.IdentifierField().PascalName()) - nameParts = append(nameParts, "Map") - return &NamedIdMap{ - Named: Named{ - Name: strings.Join(nameParts, ""), - }, - IdPath: m.IdFieldPath, - NamePath: namePath, - Entity: entity, - Direct: m.Response.ArrayValue != nil, - } -} - -func (n *NamedIdMap) Id() *Field { - return n.IdPath[len(n.IdPath)-1] -} - -// GetByName returns entity from the same service with x-databricks-crud:read -func (m *Method) GetByName() *Entity { - n := m.NamedIdMap() - if n == nil { - return nil - } - potentialName := "GetBy" - for _, v := range n.NamePath { - potentialName += v.PascalName() - } - for _, other := range m.Service.methods { - shortcut := other.Shortcut() - if shortcut == nil { - continue - } - if shortcut.PascalName() == potentialName { - // we already have the shortcut - return nil - } - } - return n.Entity -} - -func (m *Method) CanHaveResponseBody() bool { - return m.TitleVerb() == "Get" || m.TitleVerb() == "Post" -} - -func (m *Method) TitleVerb() string { - return strings.Title(strings.ToLower(m.Verb)) -} - -// IsPrivatePreview flags object being in private preview. -func (m *Method) IsPrivatePreview() bool { - return isPrivatePreview(&m.Operation.Node) -} - -// IsPublicPreview flags object being in public preview. -func (m *Method) IsPublicPreview() bool { - return isPublicPreview(&m.Operation.Node) -} - -func (m *Method) AsFlat() *Named { - if m.PascalName() == "CreateOboToken" { - return &m.Named - } - methodWords := m.Named.splitASCII() - svc := m.Service.Named - - remap := map[string]string{ - "ModelRegistry": "Models", - "Libraries": "ClusterLibraries", - "PolicyFamilies": "ClusterPolicyFamilies", - "Workspace": "Notebooks", // or WorkspaceObjects - "OAuthEnrollment": "OauthEnrollment", - "CurrentUser": "", - } - if replace, ok := remap[svc.PascalName()]; ok { - svc = Named{ - Name: replace, - } - } - - serviceWords := svc.splitASCII() - serviceSingularWords := svc.Singular().splitASCII() - - words := []string{} - if len(methodWords) == 1 && strings.ToLower(methodWords[0]) == "list" { - words = append(words, "list") - words = append(words, serviceWords...) - } else if methodWords[0] == "execute" { - words = append(words, methodWords[0]) - // command_execution.execute -> execute-command-execution - words = append(words, serviceWords[0]) - } else { - words = append(words, methodWords[0]) - words = append(words, serviceSingularWords...) - } - words = append(words, methodWords[1:]...) - // warehouses.get_workspace_warehouse_config -> get-warehouse-workspace-config - seen := map[string]bool{} - tmp := []string{} - for _, w := range words { - if seen[w] { - continue - } - tmp = append(tmp, w) - seen[w] = true - } - - return &Named{ - Name: strings.Join(tmp, "_"), - } -} - -func (m *Method) CmdletName(prefix string) string { - words := m.AsFlat().splitASCII() - noun := &Named{ - Name: strings.Join(words[1:], "_"), - } - verb := strings.Title(words[0]) - prefix = strings.Title(prefix) - return fmt.Sprintf("%s-%s%s", verb, prefix, noun.PascalName()) -} - -func (m *Method) IsRequestByteStream() bool { - contentType, ok := m.FixedRequestHeaders["Content-Type"] - return m.Request != nil && ok && contentType != string(openapi.MimeTypeJson) -} - -func (m *Method) IsResponseByteStream() bool { - accept, ok := m.FixedRequestHeaders["Accept"] - return m.Response != nil && ok && accept != string(openapi.MimeTypeJson) -} diff --git a/openapi/code/method_test.go b/openapi/code/method_test.go deleted file mode 100644 index b04e0bd71..000000000 --- a/openapi/code/method_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package code - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestFlatNames(t *testing.T) { - for _, v := range []struct { - service, method, flat string - }{ - {"secrets", "delete_acl", "delete_secret_acl"}, - {"secrets", "list_scopes", "list_secret_scopes"}, - {"workspace_conf", "get_status", "get_workspace_conf_status"}, - {"statement_execution", "execute_statement", "execute_statement"}, - {"statement_execution", "get_statement_result_chunk_n", "get_statement_execution_result_chunk_n"}, - - // TBD: - {"current_user", "me", "me"}, - {"warehouses", "get_workspace_warehouse_config", "get_warehouse_workspace_config"}, - {"libraries", "install", "install_cluster_library"}, - {"policy_families", "get", "get_cluster_policy_family"}, - {"workspace", "get_status", "get_notebook_status"}, - {"model_registry", "create_comment", "create_model_comment"}, - {"token_management", "create_obo_token", "create_obo_token"}, - } { - m := &Method{ - Named: Named{ - Name: v.method, - }, - Service: &Service{ - Named: Named{ - Name: v.service, - }, - }, - } - assert.Equal(t, v.flat, m.AsFlat().SnakeName()) - } -} - -func TestCmdletNames(t *testing.T) { - for _, v := range []struct { - service, method, cmdlet string - }{ - {"secrets", "delete_acl", "Delete-DatabricksSecretAcl"}, - {"secrets", "list_scopes", "List-DatabricksSecretScopes"}, - {"workspace_conf", "get_status", "Get-DatabricksWorkspaceConfStatus"}, - - // TBD: - {"current_user", "me", "Me-Databricks"}, - {"warehouses", "get_workspace_warehouse_config", "Get-DatabricksWarehouseWorkspaceConfig"}, - {"libraries", "install", "Install-DatabricksClusterLibrary"}, - {"policy_families", "get", "Get-DatabricksClusterPolicyFamily"}, - {"workspace", "get_status", "Get-DatabricksNotebookStatus"}, - {"model_registry", "create_comment", "Create-DatabricksModelComment"}, - {"token_management", "create_obo_token", "Create-DatabricksOboToken"}, - } { - m := &Method{ - Named: Named{ - Name: v.method, - }, - Service: &Service{ - Named: Named{ - Name: v.service, - }, - }, - } - assert.Equal(t, v.cmdlet, m.CmdletName("Databricks")) - } -} diff --git a/openapi/code/named.go b/openapi/code/named.go deleted file mode 100644 index 53360a5f9..000000000 --- a/openapi/code/named.go +++ /dev/null @@ -1,347 +0,0 @@ -package code - -import ( - "fmt" - "regexp" - "sort" - "strings" - "unicode" -) - -var reservedWords = []string{ - "break", "default", "func", "interface", "select", "case", "defer", "go", - "map", "struct", "chan", "else", "goto", "switch", "const", "fallthrough", - "if", "range", "type", "continue", "for", "import", "return", "var", - "append", "bool", "byte", "iota", "len", "make", "new", "package", -} - -// Named holds common methods for identifying and describing things -type Named struct { - Name string - Description string -} - -func (n *Named) IsNameReserved() bool { - for _, v := range reservedWords { - if n.CamelName() == v { - return true - } - } - return false -} - -func (n *Named) isNamePlural() bool { - if n.Name == "" { - return false - } - return n.Name[len(n.Name)-1] == 's' -} - -// simplified ruleset for singularizing multiples -var singularizers = []*regexTransform{ - // branches -> branch - newRegexTransform("(s|ss|sh|ch|x|z)es$", "$1"), - - // policies -> policy - newRegexTransform("([bcdfghjklmnpqrstvwxz])ies$", "${1}y"), - - // permissions -> permission - newRegexTransform("([a-z])s$", "$1"), -} - -var singularExceptions = map[string]string{ - "dbfs": "dbfs", - "warehouses": "warehouse", - "databricks": "databricks", -} - -type regexTransform struct { - Search *regexp.Regexp - Replace string -} - -func newRegexTransform(search, replace string) *regexTransform { - return ®exTransform{ - Search: regexp.MustCompile(search), - Replace: replace, - } -} - -func (t *regexTransform) Transform(src string) string { - return t.Search.ReplaceAllString(src, t.Replace) -} - -func (n *Named) Singular() *Named { - if !n.isNamePlural() { - return n - } - exception, ok := singularExceptions[strings.ToLower(n.Name)] - if ok { - return &Named{ - Name: exception, - Description: n.Description, - } - } - for _, t := range singularizers { - after := t.Transform(n.Name) - if after == n.Name { - continue - } - return &Named{ - Name: after, - Description: n.Description, - } - } - return n -} - -// Return the value of cond evaluated at the nearest letter to index i in name. -// dir determines the direction of search: if true, search forwards, if false, -// search backwards. -func (n *Named) search(name string, cond func(rune) bool, dir bool, i int) bool { - nameLen := len(name) - incr := 1 - if !dir { - incr = -1 - } - for j := i; j >= 0 && j < nameLen; j += incr { - if unicode.IsLetter(rune(name[j])) { - return cond(rune(name[j])) - } - } - return false -} - -// Return the value of cond evaluated on the rune at index i in name. If that -// rune is not a letter, search in both directions for the nearest letter and -// return the result of cond on those letters. -func (n *Named) checkCondAtNearestLetters(name string, cond func(rune) bool, i int) bool { - r := rune(name[i]) - - if unicode.IsLetter(r) { - return cond(r) - } - return n.search(name, cond, true, i) && n.search(name, cond, false, i) -} - -// emulate positive lookaheads from JVM regex: -// (?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|([-_\s]) -// and convert all words to lower case - -func (n *Named) splitASCII() (w []string) { - var current []rune - var name = n.Name - nameLen := len(name) - var isPrevUpper, isCurrentUpper, isNextLower, isNextUpper, isNotLastChar bool - // we do assume here that all named entities are strictly ASCII - for i := 0; i < nameLen; i++ { - r := rune(name[i]) - if r == '$' { - // we're naming language literals, $ is usually not allowed - continue - } - // if the current rune is a digit, check the neighboring runes to - // determine whether to treat this one as upper-case. - isCurrentUpper = n.checkCondAtNearestLetters(name, unicode.IsUpper, i) - r = unicode.ToLower(r) - isNextLower = false - isNextUpper = false - isNotLastChar = i+1 < nameLen - if isNotLastChar { - isNextLower = n.checkCondAtNearestLetters(name, unicode.IsLower, i+1) - isNextUpper = n.checkCondAtNearestLetters(name, unicode.IsUpper, i+1) - } - split, before, after := false, false, true - // At the end of a string of capital letters (e.g. HTML[P]arser). - if isPrevUpper && isCurrentUpper && isNextLower && isNotLastChar { - // (?<=[A-Z])(?=[A-Z][a-z]) - split = true - before = false - after = true - } - // At the end of a camel- or pascal-case word (e.g. htm[l]Parser). - if !isCurrentUpper && isNextUpper { - // (?<=[a-z])(?=[A-Z]) - split = true - before = true - after = false - } - if !unicode.IsLetter(r) && !unicode.IsNumber(r) { - // ([-_\s]) - split = true - before = false - after = false - } - if before { - current = append(current, r) - } - if split && len(current) > 0 { - w = append(w, string(current)) - current = []rune{} - } - if after { - current = append(current, r) - } - isPrevUpper = isCurrentUpper - } - if len(current) > 0 { - w = append(w, string(current)) - } - return w -} - -// PascalName creates NamesLikesThis -func (n *Named) PascalName() string { - var sb strings.Builder - for _, w := range n.splitASCII() { - sb.WriteString(strings.Title(w)) - } - return sb.String() -} - -// TitleName creates Names Likes This -func (n *Named) TitleName() string { - return strings.Title(strings.Join(n.splitASCII(), " ")) -} - -// CamelName creates namesLikesThis -func (n *Named) CamelName() string { - if n.Name == "_" { - return "_" - } - cc := n.PascalName() - return strings.ToLower(cc[0:1]) + cc[1:] -} - -// SnakeName creates names_like_this -func (n *Named) SnakeName() string { - if n.Name == "_" { - return "_" - } - return strings.Join(n.splitASCII(), "_") -} - -// ConstantName creates NAMES_LIKE_THIS -func (n *Named) ConstantName() string { - return strings.ToUpper(n.SnakeName()) -} - -// KebabName creates names-like-this -func (n *Named) KebabName() string { - return strings.Join(n.splitASCII(), "-") -} - -// AbbrName returns `nlt` for `namesLikeThis` -func (n *Named) AbbrName() string { - var abbr []byte - for _, v := range n.splitASCII() { - abbr = append(abbr, v[0]) - } - return string(abbr) -} - -// TrimPrefix returns *Named, but with a prefix trimmed from CamelName() -// -// Example: -// -// (&Named{Name: "AccountMetastoreAssigment"}).TrimPrefix("account").CamelName() == "metastoreAssignment" -func (n *Named) TrimPrefix(prefix string) *Named { - return &Named{ - Name: strings.TrimPrefix(n.CamelName(), prefix), - Description: n.Description, - } -} - -func (n *Named) HasComment() bool { - return n.Description != "" -} - -func (n *Named) sentences() []string { - if n.Description == "" { - return []string{} - } - norm := whitespace.ReplaceAllString(n.Description, " ") - trimmed := strings.TrimSpace(norm) - return strings.Split(trimmed, ". ") -} - -// Summary gets the first sentence from the description. Always ends in a dot. -func (n *Named) Summary() string { - sentences := n.sentences() - if len(sentences) > 0 { - return strings.TrimSuffix(sentences[0], ".") + "." - } - return "" -} - -// match markdown links, ignoring new lines -var markdownLink = regexp.MustCompile(`\[([^\]]+)\]\(([^\)]+)\)`) - -func (n *Named) DescriptionWithoutSummary() string { - sentences := n.sentences() - if len(sentences) > 1 { - return strings.Join(sentences[1:], ". ") - } - return "" -} - -// Comment formats description into language-specific comment multi-line strings -func (n *Named) Comment(prefix string, maxLen int) string { - if n.Description == "" { - return "" - } - trimmed := strings.TrimSpace(n.Description) - // collect links, which later be sorted - links := map[string]string{} - // safe to iterate and update, as match slice is a snapshot - for _, m := range markdownLink.FindAllStringSubmatch(trimmed, -1) { - label := strings.TrimSpace(m[1]) - link := strings.TrimSpace(m[2]) - if !strings.HasPrefix(link, "http") { - // this condition is here until OpenAPI spec normalizes all links - continue - } - // simplify logic by overriding links in case of duplicates. - // this will also lead us to alphabetically sort links in the bottom, - // instead of always following the order they appear in the comment. - // luckily, this doesn't happen often. - links[label] = link - // replace [test](url) with [text] - trimmed = strings.ReplaceAll(trimmed, m[0], fmt.Sprintf("[%s]", label)) - } - linksInBottom := []string{} - for k, v := range links { - link := fmt.Sprintf("[%s]: %s", k, v) - linksInBottom = append(linksInBottom, link) - } - sort.Strings(linksInBottom) - // fix new-line characters - trimmed = strings.ReplaceAll(trimmed, "\\n", "\n") - description := strings.ReplaceAll(trimmed, "\n\n", " __BLANK__ ") - var lines []string - currentLine := strings.Builder{} - for _, v := range whitespace.Split(description, -1) { - if v == "__BLANK__" { - lines = append(lines, currentLine.String()) - lines = append(lines, "") - currentLine.Reset() - continue - } - if len(prefix)+currentLine.Len()+len(v)+1 > maxLen { - lines = append(lines, currentLine.String()) - currentLine.Reset() - } - if currentLine.Len() > 0 { - currentLine.WriteRune(' ') - } - currentLine.WriteString(v) - } - if currentLine.Len() > 0 { - lines = append(lines, currentLine.String()) - currentLine.Reset() - } - if len(linksInBottom) > 0 { - lines = append(append(lines, ""), linksInBottom...) - } - return strings.TrimLeft(prefix, "\t ") + strings.Join(lines, "\n"+prefix) -} diff --git a/openapi/code/named_sort.go b/openapi/code/named_sort.go deleted file mode 100644 index 2682cf6b8..000000000 --- a/openapi/code/named_sort.go +++ /dev/null @@ -1,25 +0,0 @@ -package code - -import "sort" - -// github.com/databricks/databricks-sdk-go/httpclient/fixtures stub generator uses Named.PascalName() method to come up with -// the best possible field name for generated copy-pastable stubs, though, when this library is attempted to be used together -// with github.com/spf13/viper, we immediately get the following error related to a change in -// golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e as: -// .../entity.go:185:32: type func(a *Field, b *Field) bool of func(a, b *Field) bool {…} does not match inferred type -// ... func(a *Field, b *Field) int for func(a E, b E) int, -// because github.com/spf13/viper v0.17+ transitively depends on golang.org/x/exp v0.0.0-20230905200255-921286631fa9 - -// sorts slice predictably by NamesLikeThis -func pascalNameSort[E interface{ PascalName() string }](things []E) { - sort.Slice(things, func(i, j int) bool { - return things[i].PascalName() < things[j].PascalName() - }) -} - -// sorts slice predictably by package_names_and.ClassNamesLikeThis -func fullNameSort[E interface{ FullName() string }](things []E) { - sort.Slice(things, func(i, j int) bool { - return things[i].FullName() < things[j].FullName() - }) -} diff --git a/openapi/code/named_test.go b/openapi/code/named_test.go deleted file mode 100644 index 8716fa0e0..000000000 --- a/openapi/code/named_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package code - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestCommentFromDescription(t *testing.T) { - n := Named{Description: `A data lakehouse unifies the best of data - warehouses and data lakes in one simple platform to handle all your - data, analytics and AI use cases.\nIt's built on an open and reliable - data foundation that efficiently handles all data types and applies - one common security and governance approach across all of your data - and cloud platforms.`} - assert.Equal(t, strings.TrimLeft(` - // A data lakehouse unifies the best of data warehouses and data lakes in - // one simple platform to handle all your data, analytics and AI use cases. - // It's built on an open and reliable data foundation that efficiently - // handles all data types and applies one common security and governance - // approach across all of your data and cloud platforms.`, - "\n\t "), n.Comment(" // ", 80)) -} - -func TestCommentFromDescriptionWithSummaryAndBlankLines(t *testing.T) { - n := Named{Description: strings.Join([]string{ - "Databricks Lakehouse", - "", - `A data lakehouse unifies the best of data warehouses and data lakes - in one simple platform to handle all your data, analytics and AI use - cases.`, - "", - `It's built on an open and reliable data foundation that efficiently - handles all data types and applies one common security and governance - approach across all of your data and cloud platforms.`, - }, "\n")} - assert.Equal(t, strings.TrimLeft(` - // Databricks Lakehouse - // - // A data lakehouse unifies the best of data warehouses and data lakes in - // one simple platform to handle all your data, analytics and AI use cases. - // - // It's built on an open and reliable data foundation that efficiently - // handles all data types and applies one common security and governance - // approach across all of your data and cloud platforms.`, - "\n\t "), n.Comment(" // ", 80)) -} - -func TestSentencesFromDescription(t *testing.T) { - n := Named{Description: strings.Join([]string{ - "Databricks Lakehouse.", - "", - `A data lakehouse unifies the best of data warehouses and data lakes - in one simple platform to handle all your data, analytics and AI use - cases.`, - }, "\n")} - assert.Equal(t, "Databricks Lakehouse.", n.Summary()) - assert.Equal(t, "A data lakehouse unifies the best of data "+ - "warehouses and data lakes in one simple platform to "+ - "handle all your data, analytics and AI use cases.", - n.DescriptionWithoutSummary()) - - assert.Equal(t, "", (&Named{}).Summary()) - assert.Equal(t, "", (&Named{}).DescriptionWithoutSummary()) - assert.Equal(t, "Foo.", (&Named{Description: "Foo"}).Summary()) - assert.Equal(t, "", (&Named{Description: "Foo"}).DescriptionWithoutSummary()) -} - -func TestCommentWithLinks(t *testing.T) { - n := Named{Description: `This is an [example](https://example.com) of - linking to other web pages. Follows this - [convention](https://tip.golang.org/doc/comment#links).`} - assert.Equal(t, strings.TrimLeft(` - // This is an [example] of linking to other web pages. Follows this - // [convention]. - // - // [convention]: https://tip.golang.org/doc/comment#links - // [example]: https://example.com`, - "\n\t "), n.Comment(" // ", 80)) -} - -func TestNonConflictingCamelNames(t *testing.T) { - n := Named{Name: "Import"} - assert.True(t, n.IsNameReserved()) -} - -func TestNamedDecamel(t *testing.T) { - for in, out := range map[string][]string{ - "BigHTMLParser": {"big", "html", "parser"}, - "parseHTML": {"parse", "html"}, - "parse HTML": {"parse", "html"}, - "parse-HTML": {"parse", "html"}, - "parse--HTML": {"parse", "html"}, - "parse_HTML": {"parse", "html"}, - "parseHTMLNow": {"parse", "html", "now"}, - "parseHtml": {"parse", "html"}, - "ParseHtml": {"parse", "html"}, - "clusterID": {"cluster", "id"}, - "positionX": {"position", "x"}, - "parseHtmlNow": {"parse", "html", "now"}, - "HTMLParser": {"html", "parser"}, - "BigO": {"big", "o"}, - "OCaml": {"o", "caml"}, - "K8S_FAILURE": {"k8s", "failure"}, - "k8sFailure": {"k8s", "failure"}, - "i18nFailure": {"i18n", "failure"}, - "Patch:Request": {"patch", "request"}, - "urn:ietf:params:scim:api:messages:2.0:PatchOp": {"urn", "ietf", "params", "scim", "api", "messages", "2", "0", "patch", "op"}, - } { - assert.Equal(t, out, (&Named{Name: in}).splitASCII(), in) - } -} - -func TestNamedTransforms(t *testing.T) { - n := Named{Name: "bigBrownFOX"} - for actual, expected := range map[string]string{ - n.CamelName(): "bigBrownFox", - n.PascalName(): "BigBrownFox", - n.ConstantName(): "BIG_BROWN_FOX", - n.SnakeName(): "big_brown_fox", - n.KebabName(): "big-brown-fox", - n.TitleName(): "Big Brown Fox", - n.AbbrName(): "bbf", - } { - assert.Equal(t, expected, actual) - } -} - -func TestNamedSingular(t *testing.T) { - for in, out := range map[string]string{ - "buses": "bus", - "boxes": "box", - "branches": "branch", - "blitzes": "blitz", - "cluster-policies": "cluster-policy", - "clusters": "cluster", - "dbfs": "dbfs", - "alerts": "alert", - "dashboards": "dashboard", - "data-sources": "data-source", - "dbsql-permissions": "dbsql-permission", - "queries": "query", - "delta-pipelines": "delta-pipeline", - "repos": "repo", - "metastores": "metastore", - "tables": "table", - "workspace": "workspace", - "warehouses": "warehouse", - } { - n := &Named{Name: in} - assert.Equal(t, out, n.Singular().Name) - } -} diff --git a/openapi/code/package.go b/openapi/code/package.go deleted file mode 100644 index 0b603bcec..000000000 --- a/openapi/code/package.go +++ /dev/null @@ -1,389 +0,0 @@ -// Package holds higher-level abstractions on top of OpenAPI that are used -// to generate code via text/template for Databricks SDK in different languages. -package code - -import ( - "context" - "fmt" - "regexp" - "sort" - "strings" - - "golang.org/x/exp/slices" - - "github.com/databricks/databricks-sdk-go/logger" - "github.com/databricks/databricks-sdk-go/openapi" -) - -// Package represents a service package, which contains entities and interfaces -// that are relevant to a single service -type Package struct { - Named - Components *openapi.Components - services map[string]*Service - types map[string]*Entity - emptyTypes []*Named - extImports map[string]*Entity -} - -// FullName just returns pacakge name -func (pkg *Package) FullName() string { - return pkg.CamelName() -} - -// Services returns sorted slice of services -func (pkg *Package) Services() (types []*Service) { - for _, v := range pkg.services { - types = append(types, v) - } - pascalNameSort(types) - return types -} - -// MainService returns a Service that matches Package name -func (pkg *Package) MainService() *Service { - for _, svc := range pkg.services { - if !svc.MatchesPackageName() { - continue - } - return svc - } - return nil -} - -// Types returns sorted slice of types -func (pkg *Package) Types() (types []*Entity) { - for _, v := range pkg.types { - types = append(types, v) - } - pascalNameSort(types) - return types -} - -// EmptyTypes returns sorted list of types without fields -func (pkg *Package) EmptyTypes() (types []*Named) { - types = append(types, pkg.emptyTypes...) - pascalNameSort(types) - return types -} - -// HasPagination returns try if any service within this package has result -// iteration -func (pkg *Package) HasPagination() bool { - for _, v := range pkg.services { - if v.HasPagination() { - return true - } - } - return false -} - -func (pkg *Package) ImportedEntities() (res []*Entity) { - for _, e := range pkg.extImports { - res = append(res, e) - } - fullNameSort(res) - return -} - -func (pkg *Package) ImportedPackages() (res []string) { - tmp := map[string]bool{} - for _, e := range pkg.extImports { - tmp[e.Package.Name] = true - } - for name := range tmp { - res = append(res, name) - } - sort.Strings(res) - return -} - -func (pkg *Package) schemaToEntity(s *openapi.Schema, path []string, hasName bool) *Entity { - if s.IsRef() { - pair := strings.Split(s.Component(), ".") - if len(pair) == 2 && pair[0] != pkg.Name { - schemaPackage := pair[0] - schemaType := pair[1] - if pkg.extImports == nil { - pkg.extImports = map[string]*Entity{} - } - known, ok := pkg.extImports[s.Component()] - if ok { - return known - } - // referred entity is declared in another package - pkg.extImports[s.Component()] = &Entity{ - Named: Named{ - Name: schemaType, - }, - Package: &Package{ - Named: Named{ - Name: schemaPackage, - }, - }, - } - return pkg.extImports[s.Component()] - } - // if schema is src, load it to this package - src := pkg.Components.Schemas.Resolve(s) - if src == nil { - return nil - } - component := pkg.localComponent(&s.Node) - return pkg.definedEntity(component, *src) - } - e := &Entity{ - Named: Named{ - Description: s.Description, - }, - Schema: s, - Package: pkg, - enum: map[string]EnumEntry{}, - } - // pull embedded types up, if they can be defined at package level - if s.IsDefinable() && !hasName { - // TODO: log message or panic when overrides a type - e.Named.Name = strings.Join(path, "") - pkg.define(e) - } - e.IsEmpty = s.IsEmpty() - e.IsAny = s.IsAny || s.Type == "object" && s.IsEmpty() - e.IsComputed = s.IsComputed - e.RequiredOrder = s.Required - // enum - if len(s.Enum) != 0 { - return pkg.makeEnum(e, s, path) - } - // object - if len(s.Properties) != 0 { - return pkg.makeObject(e, s, path) - } - // array - if s.ArrayValue != nil { - e.ArrayValue = pkg.schemaToEntity(s.ArrayValue, append(path, "Item"), false) - return e - } - // map - if s.MapValue != nil { - e.MapValue = pkg.schemaToEntity(s.MapValue, path, hasName) - return e - } - e.IsBool = s.Type == "boolean" || s.Type == "bool" - e.IsString = s.Type == "string" - e.IsInt64 = s.Type == "integer" && s.Format == "int64" - e.IsFloat64 = s.Type == "number" && s.Format == "double" - e.IsInt = s.Type == "integer" || s.Type == "int" - return e -} - -// makeObject converts OpenAPI Schema into type representation -func (pkg *Package) makeObject(e *Entity, s *openapi.Schema, path []string) *Entity { - e.fields = map[string]*Field{} - required := map[string]bool{} - for _, v := range s.Required { - required[v] = true - } - for k, v := range s.Properties { - if v.Description == "" && v.IsRef() { - vv := pkg.Components.Schemas.Resolve(v) - if vv != nil { - v.Description = (*vv).Description - } - } - named := Named{k, v.Description} - e.fields[k] = &Field{ - Named: named, - Entity: pkg.schemaToEntity(v, append(path, named.PascalName()), false), - Required: required[k], - Schema: v, - IsJson: true, - } - } - pkg.updateType(e) - return e -} - -var whitespace = regexp.MustCompile(`\s+`) - -func (pkg *Package) makeEnum(e *Entity, s *openapi.Schema, path []string) *Entity { - for idx, content := range s.Enum { - name := content - if len(s.AliasEnum) == len(s.Enum) { - name = s.AliasEnum[idx] - } - description := s.EnumDescriptions[content] - e.enum[content] = EnumEntry{ - Named: Named{name, description}, - Entity: e, - Content: content, - } - } - return e -} - -func (pkg *Package) localComponent(n *openapi.Node) string { - component := n.Component() - if strings.HasPrefix(component, pkg.Name+".") { - component = strings.ReplaceAll(component, pkg.Name+".", "") - } - return component -} - -func (pkg *Package) definedEntity(name string, s *openapi.Schema) *Entity { - if s == nil || s.IsEmpty() { - entity := &Entity{ - Named: Named{ - Name: name, - Description: "", - }, - IsEmpty: true, - } - return pkg.define(entity) - } - - e := pkg.schemaToEntity(s, []string{name}, true) - if e == nil { - // gets here when responses are objects with no properties - return nil - } - if e.ArrayValue != nil { - return e - } - if e.Name == "" { - e.Named = Named{name, s.Description} - } - return pkg.define(e) -} - -func (pkg *Package) define(entity *Entity) *Entity { - if entity.IsEmpty { - if slices.Contains(pkg.emptyTypes, &entity.Named) { - //panic(fmt.Sprintf("%s is already defined", entity.Name)) - return entity - } - pkg.emptyTypes = append(pkg.emptyTypes, &entity.Named) - return entity - } - _, defined := pkg.types[entity.Name] - if defined { - //panic(fmt.Sprintf("%s is already defined", entity.Name)) - return entity - } - if entity.Package == nil { - entity.Package = pkg - } - pkg.types[entity.Name] = entity - return entity -} - -func (pkg *Package) updateType(entity *Entity) { - e, defined := pkg.types[entity.Name] - if !defined { - return - } - for k, v := range entity.fields { - e.fields[k] = v - } -} - -// HasPathParams returns true if any service has methods that rely on path params -func (pkg *Package) HasPathParams() bool { - for _, s := range pkg.services { - for _, m := range s.methods { - if len(m.PathParts) > 0 { - return true - } - } - } - return false -} - -// HasWaits returns true if any service has methods with long-running operations -func (pkg *Package) HasWaits() bool { - for _, s := range pkg.services { - for _, m := range s.methods { - if m.wait != nil { - return true - } - } - } - return false -} - -// Load takes OpenAPI specification and loads a service model -func (pkg *Package) Load(ctx context.Context, spec *openapi.Specification, tag openapi.Tag) error { - for k, v := range spec.Components.Schemas { - split := strings.Split(k, ".") - if split[0] != pkg.Name { - continue - } - pkg.definedEntity(split[1], *v) - } - for prefix, path := range spec.Paths { - for verb, op := range path.Verbs() { - if !op.HasTag(tag.Name) { - continue - } - logger.Infof(ctx, "pkg.Load %s %s", verb, prefix) - svc, ok := pkg.services[tag.Service] - if !ok { - svc = &Service{ - Package: pkg, - IsAccounts: tag.IsAccounts, - PathStyle: tag.PathStyle, - methods: map[string]*Method{}, - Named: Named{ - Name: tag.Service, - Description: tag.Description, - }, - tag: &tag, - } - pkg.services[tag.Service] = svc - } - params := []openapi.Parameter{} - seenParams := map[string]bool{} - for _, list := range [][]openapi.Parameter{path.Parameters, op.Parameters} { - for _, v := range list { - param, err := pkg.resolveParam(&v) - if err != nil { - return fmt.Errorf("could not resolve parameter %s for %s %s. This could be due to a problem in the definition of this parameter. If using $ref, ensure that $ref is used inside the 'schema' keyword", v.Name, verb, prefix) - } - if param == nil { - return nil - } - if param.In == "header" { - continue - } - // do not propagate common path parameter to account-level APIs - if svc.IsAccounts && param.In == "path" && param.Name == "account_id" { - continue - } - if seenParams[param.Name] { - continue - } - if prefix == "/api/2.0/workspace/export" && param.Name == "direct_download" { - // prevent changing the response content type via request parameter - // https://github.com/databricks/databricks-sdk-py/issues/104 - continue - } - params = append(params, *param) - seenParams[param.Name] = true - } - } - method := svc.newMethod(verb, prefix, params, op) - svc.methods[method.Name] = method - } - } - return nil -} - -func (pkg *Package) resolveParam(v *openapi.Parameter) (param *openapi.Parameter, err error) { - defer func() { - r := recover() - if r != nil { - err = fmt.Errorf("panic: %v", r) - } - }() - param = *pkg.Components.Parameters.Resolve(v) - return -} diff --git a/openapi/code/preview.go b/openapi/code/preview.go deleted file mode 100644 index 0be333b36..000000000 --- a/openapi/code/preview.go +++ /dev/null @@ -1,15 +0,0 @@ -package code - -import ( - "strings" - - "github.com/databricks/databricks-sdk-go/openapi" -) - -func isPrivatePreview(n *openapi.Node) bool { - return strings.ToLower(n.Preview) == "private" -} - -func isPublicPreview(n *openapi.Node) bool { - return strings.ToLower(n.Preview) == "public" -} diff --git a/openapi/code/service.go b/openapi/code/service.go deleted file mode 100644 index 2810fa0ec..000000000 --- a/openapi/code/service.go +++ /dev/null @@ -1,501 +0,0 @@ -package code - -import ( - "fmt" - "path/filepath" - "strings" - - "github.com/databricks/databricks-sdk-go/openapi" -) - -// Service represents specific Databricks API -type Service struct { - Named - PathStyle openapi.PathStyle - IsAccounts bool - Package *Package - methods map[string]*Method - ByPathParamsMethods []*Shortcut - tag *openapi.Tag -} - -// FullName holds package name and service name -func (svc *Service) FullName() string { - return fmt.Sprintf("%s.%s", svc.Package.FullName(), svc.PascalName()) -} - -// MatchesPackageName if package name and service name are the same, -// e.g. `clusters` package & `Clusters` service -func (svc *Service) MatchesPackageName() bool { - return strings.ToLower(svc.Name) == svc.Package.Name -} - -// Methods returns sorted slice of methods -func (svc *Service) Methods() (methods []*Method) { - for _, v := range svc.methods { - methods = append(methods, v) - } - pascalNameSort(methods) - return methods -} - -// List returns a method annotated with x-databricks-crud:list -func (svc *Service) List() *Method { - for _, v := range svc.methods { - if v.Operation.Crud == "list" { - return v - } - } - return nil -} - -// List returns a method annotated with x-databricks-crud:create -func (svc *Service) Create() *Method { - for _, v := range svc.methods { - if v.Operation.Crud == "create" { - return v - } - } - return nil -} - -// List returns a method annotated with x-databricks-crud:read -func (svc *Service) Read() *Method { - for _, v := range svc.methods { - if v.Operation.Crud == "read" { - return v - } - } - return nil -} - -// List returns a method annotated with x-databricks-crud:update -func (svc *Service) Update() *Method { - for _, v := range svc.methods { - if v.Operation.Crud == "update" { - return v - } - } - return nil -} - -// List returns a method annotated with x-databricks-crud:delete -func (svc *Service) Delete() *Method { - for _, v := range svc.methods { - if v.Operation.Crud == "delete" { - return v - } - } - return nil -} - -// HasPagination returns true if any method has result iteration -func (svc *Service) HasPagination() bool { - for _, v := range svc.methods { - p := v.pagination - if p == nil { - continue - } - if p.Offset != "" || p.Token != nil { - return true - } - } - return false -} - -func (svc *Service) getDescription(param openapi.Parameter) string { - if param.Description != "" { - return param.Description - } - if param.Schema != nil { - return param.Schema.Description - } - return "" -} - -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", - Entity: svc.Package.schemaToEntity(param.Schema, []string{ - op.Name(), - named.PascalName(), - }, false), - } -} - -var crudNames = map[string]bool{ - "create": true, - "read": true, - "get": true, - "update": true, - "replace": true, - "delete": true, - "list": true, - "restore": true, -} - -// Returns the schema representing a request body for a given operation, along with the mime type. -// For requests whose mime type is not application/json, the request body is always a byte stream. -func (svc *Service) getBaseSchemaAndMimeType(body *openapi.Body) (*openapi.Schema, openapi.MimeType) { - mimeType, mediaType := body.MimeTypeAndMediaType() - schema := mediaType.GetSchema() - if mimeType.IsByteStream() { - schema = &openapi.Schema{ - Type: "object", - Properties: map[string]*openapi.Schema{ - openapi.MediaTypeNonJsonBodyFieldName: schema, - }, - } - } - return schema, mimeType -} - -func (svc *Service) updateEntityTypeFromMimeType(entity *Entity, mimeType openapi.MimeType) { - if !mimeType.IsByteStream() { - return - } - // For request or response bodies that are not application/json, the body - // is modeled by a byte stream. - entity.IsByteStream = true - entity.IsEmpty = false - entity.IsAny = false -} - -// Construct the base request entity for a given operation. For requests whose -// mime type is not application/json, the request body is always a byte stream. -// For requests whose mime type is application/json, the request body consists -// of the top-level fields of the request object as defined in the OpenAPI spec. -// Additionally, for non-application/json requests, the request body is nested -// into a field named "contents". -func (svc *Service) newMethodEntity(op *openapi.Operation) (*Entity, openapi.MimeType, *Field) { - if op.RequestBody == nil { - return &Entity{fields: map[string]*Field{}}, "", nil - } - requestSchema, mimeType := svc.getBaseSchemaAndMimeType(op.RequestBody) - res := svc.Package.schemaToEntity(requestSchema, []string{op.Name()}, true) - if res == nil { - panic(fmt.Errorf("%s request body is nil", op.OperationId)) - } - - var bodyField *Field - if mimeType.IsByteStream() { - bodyField = res.fields[openapi.MediaTypeNonJsonBodyFieldName] - } - - // This next block of code is needed to make up for shortcomings in - // schemaToEntity. That function (and the Entity structure) assumes that all - // entities are modeled by JSON objects. Later, we should change Entity - // and schemaToEntity to be more tolerant of non-JSON entities; for now, we - // put this hack in place to make things work. - if mimeType.IsByteStream() { - for _, v := range res.fields { - v.IsJson = false - } - svc.updateEntityTypeFromMimeType(bodyField.Entity, mimeType) - } - - return res, mimeType, bodyField -} - -func (svc *Service) addParams(request *Entity, op *openapi.Operation, params []openapi.Parameter) { - for _, v := range params { - if v.In == "header" { - continue - } - param := svc.paramToField(op, v) - if param == nil { - continue - } - field, exists := request.fields[param.Name] - if !exists { - field = param - } - field.IsPath = param.IsPath - field.IsQuery = param.IsQuery - request.fields[param.Name] = field - if param.Required { - var alreadyRequired bool - for _, v := range request.RequiredOrder { - if v == param.Name { - alreadyRequired = true - break - } - } - if !alreadyRequired { - // TODO: figure out what to do with entity+param requests - request.RequiredOrder = append(request.RequiredOrder, param.Name) - } - } - if field.IsQuery { - // recursively update field entity and sub entities with IsQuery = true - // this should be safe as paramToField() should recursively create - // all needed sub-entities - field.Traverse( - func(f *Field) { - f.IsQuery = true - }) - } - } - // IsQuery may have been set on some fields, so the request entity and - // sub-entities need to be updated - request.Traverse( - func(e *Entity) { - svc.Package.updateType(e) - }) -} - -// The body param must be added after all other params so that it appears in the -// correct position in shortcut methods. -func (svc *Service) addBodyParamIfNeeded(request *Entity, mimeType openapi.MimeType) { - if mimeType.IsByteStream() { - request.RequiredOrder = append(request.RequiredOrder, openapi.MediaTypeNonJsonBodyFieldName) - } -} - -// Use heuristics to construct a "good" name for the request entity, as the name -// was not provided by the original OpenAPI spec. -func (svc *Service) nameAndDefineRequest(request *Entity, op *openapi.Operation) { - if request.Name != "" { - panic(fmt.Sprintf("request entity already has a name: %s", request.Name)) - } - - // If the operation defines a request type name, use it. - if op.RequestTypeName != "" { - request.Name = op.RequestTypeName - } else { - // Otherwise, synthesize a request type name. - singularServiceName := svc.Singular().PascalName() - notExplicit := !strings.Contains(op.Name(), singularServiceName) - if op.Name() == "list" && notExplicit { - request.Name = op.Name() + svc.Name + "Request" - } else if crudNames[strings.ToLower(op.Name())] { - request.Name = op.Name() + singularServiceName + "Request" - } else { - request.Name = op.Name() + "Request" - } - if svc.Package.Name == "scim" { - request.Name = strings.ReplaceAll(request.Name, "Account", "") - } - } - - request.Description = op.Summary - svc.Package.define(request) -} - -// Constructs the request object metadata for a method. This consists of -// -// 1. the request entity (i.e. the parameters and/or body) -// 2. the request MIME type -// 3. the field pointing to the request body (for non-JSON requests) -// -// The request entity includes fields for every parameter in the request (path, -// query, and body). If the request is defined anonymously (i.e. it is not -// refactored into a named type), the name for the request is constructed from -// the operation name and service name. -func (svc *Service) newRequest(params []openapi.Parameter, op *openapi.Operation) (*Entity, openapi.MimeType, *Field) { - if op.RequestBody == nil && len(params) == 0 { - return nil, "", nil - } - request, mimeType, bodyField := svc.newMethodEntity(op) - if request.fields == nil && request.MapValue == nil { - return nil, "", nil - } - svc.addParams(request, op, params) - svc.addBodyParamIfNeeded(request, mimeType) - if request.Name == "" { - svc.nameAndDefineRequest(request, op) - } - return request, mimeType, bodyField -} - -func (svc *Service) newResponse(op *openapi.Operation) (*Entity, openapi.MimeType, *Field, Named) { - body := op.SuccessResponseBody(svc.Package.Components) - schema, mimeType := svc.getBaseSchemaAndMimeType(body) - name := op.Name() - response := svc.Package.definedEntity(name+"Response", schema) - var bodyField *Field - if mimeType.IsByteStream() { - bodyField = response.fields[openapi.MediaTypeNonJsonBodyFieldName] - } - - // This next block of code is needed to make up for shortcomings in - // schemaToEntity. That function (and the Entity structure) assumes that all - // entities are modeled by JSON objects. Later, we should change Entity - // and schemaToEntity to be more tolerant of non-JSON entities; for now, we - // put this hack in place to make things work. - if mimeType.IsByteStream() { - svc.updateEntityTypeFromMimeType(bodyField.Entity, mimeType) - for _, v := range response.fields { - v.IsJson = false - } - } - - var emptyResponse Named - if response != nil && response.IsEmpty { - emptyResponse = response.Named - response = nil - } - return response, mimeType, bodyField, emptyResponse -} - -func (svc *Service) paramPath(path string, request *Entity, params []openapi.Parameter) (parts []PathPart) { - var pathParams int - for _, v := range params { - if v.In == "path" { - pathParams++ - } - } - if svc.IsAccounts && pathParams == 0 { - // account-level services do always have `/accounts/2.0` in path - pathParams++ - } - if pathParams == 0 { - return - } - for _, v := range pathPairRE.FindAllStringSubmatch(path, -1) { - prefix := v[1] - name := v[3] - if svc.IsAccounts && name == "account_id" { - parts = append(parts, PathPart{prefix, nil, true}) - continue - } - if request == nil { - // e.g. POST /api/2.0/accounts/{account_id}/budget - parts = append(parts, PathPart{prefix, nil, false}) - continue - } - field, ok := request.fields[name] - if !ok { - parts = append(parts, PathPart{prefix, nil, false}) - continue - } - parts = append(parts, PathPart{prefix, field, false}) - } - return -} - -func (svc *Service) getPathStyle(op *openapi.Operation) openapi.PathStyle { - if op.PathStyle != "" { - return op.PathStyle - } - if svc.PathStyle != "" { - return svc.PathStyle - } - return openapi.PathStyleRest -} - -func (svc *Service) newMethod(verb, path string, params []openapi.Parameter, op *openapi.Operation) *Method { - methodName := op.Name() - request, reqMimeType, reqBodyField := svc.newRequest(params, op) - response, respMimeType, respBodyField, emptyResponse := svc.newResponse(op) - requestStyle := svc.getPathStyle(op) - if requestStyle == openapi.PathStyleRpc { - methodName = filepath.Base(path) - } - description := op.Description - summary := strings.TrimSpace(op.Summary) - // merge summary into description - if summary != "" { - // add a dot to the end of the summary, so that it could be extracted - // in templated with [*Named.Summary], separating from the rest of - // description. - if !strings.HasSuffix(summary, ".") { - summary += "." - } - description = fmt.Sprintf("%s\n\n%s", summary, description) - } - - var nameFieldPath, idFieldPath []*Field - respEntity := getPaginationEntity(response, op.Pagination) - if op.HasNameField() && respEntity != nil { - nameField, err := respEntity.GetUnderlyingFields(op.NameField) - if err != nil { - panic(fmt.Errorf("[%s] could not find name field %q: %w", op.OperationId, op.NameField, err)) - } - nameFieldPath = nameField - } - if op.HasIdentifierField() && respEntity != nil { - idField, err := respEntity.GetUnderlyingFields(op.IdField) - if err != nil { - panic(fmt.Errorf("[%s] could not find id field %q: %w", op.OperationId, op.IdField, err)) - } - idFieldPath = idField - } - headers := map[string]string{} - if reqMimeType != "" { - headers["Content-Type"] = string(reqMimeType) - } - if respMimeType != "" { - headers["Accept"] = string(respMimeType) - } - return &Method{ - Named: Named{methodName, description}, - Service: svc, - Verb: strings.ToUpper(verb), - Path: path, - Request: request, - PathParts: svc.paramPath(path, request, params), - Response: response, - EmptyResponseName: emptyResponse, - PathStyle: requestStyle, - NameFieldPath: nameFieldPath, - IdFieldPath: idFieldPath, - RequestBodyField: reqBodyField, - ResponseBodyField: respBodyField, - FixedRequestHeaders: headers, - wait: op.Wait, - Operation: op, - pagination: op.Pagination, - shortcut: op.Shortcut, - } -} - -func (svc *Service) HasWaits() bool { - for _, v := range svc.methods { - if v.wait != nil { - return true - } - } - return false -} - -func (svc *Service) Waits() (waits []*Wait) { - seen := map[string]bool{} - for _, m := range svc.methods { - if m.wait == nil { - continue - } - wait := m.Wait() - if seen[wait.Name] { - continue - } - waits = append(waits, wait) - seen[wait.Name] = true - } - pascalNameSort(waits) - return waits -} - -// IsPrivatePreview flags object being in private preview. -func (svc *Service) IsPrivatePreview() bool { - return isPrivatePreview(&svc.tag.Node) -} - -// IsPublicPreview flags object being in public preview. -func (svc *Service) IsPublicPreview() bool { - return isPublicPreview(&svc.tag.Node) -} - -func getPaginationEntity(entity *Entity, pagination *openapi.Pagination) *Entity { - if pagination == nil { - return nil - } - if pagination.Inline { - return entity.ArrayValue - } - return entity.Field(pagination.Results).Entity.ArrayValue -} diff --git a/openapi/code/tmpl_util_funcs.go b/openapi/code/tmpl_util_funcs.go deleted file mode 100644 index 551d075ba..000000000 --- a/openapi/code/tmpl_util_funcs.go +++ /dev/null @@ -1,86 +0,0 @@ -package code - -import ( - "errors" - "fmt" - "reflect" - "regexp" - "strings" - "text/template" -) - -var ErrSkipThisFile = errors.New("skip generating this file") - -var alphanumRE = regexp.MustCompile(`^\w*$`) - -var HelperFuncs = template.FuncMap{ - "notLast": func(idx int, a interface{}) bool { - return idx+1 != reflect.ValueOf(a).Len() - }, - "contains": strings.Contains, - "lower": strings.ToLower, - "lowerFirst": func(s string) string { - return strings.ToLower(s[0:1]) + s[1:] - }, - "trimPrefix": func(right, left string) string { - return strings.TrimPrefix(left, right) - }, - "trimSuffix": func(right, left string) string { - return strings.TrimSuffix(left, right) - }, - "replaceAll": func(from, to, str string) string { - return strings.ReplaceAll(str, from, to) - }, - "without": func(left, right string) string { - return strings.ReplaceAll(right, left, "") - }, - "skipThisFile": func() error { - // error is rendered as string in the resulting file, so we must panic, - // so that we handle this error in [gen.Pass[T].File] gracefully - // via errors.Is(err, code.ErrSkipThisFile) - panic(ErrSkipThisFile) - }, - "alphanumOnly": func(in []*Field) (out []*Field) { - for _, v := range in { - if !alphanumRE.MatchString(v.Name) { - continue - } - out = append(out, v) - } - return out - }, - "list": func(l ...any) []any { - return l - }, - "in": func(haystack []any, needle string) bool { - for _, v := range haystack { - if needle == fmt.Sprint(v) { - return true - } - } - return false - }, - "dict": func(args ...any) map[string]any { - if len(args)%2 != 0 { - panic("number of arguments to dict is not even") - } - result := map[string]any{} - for i := 0; i < len(args); i += 2 { - k := fmt.Sprint(args[i]) - v := args[i+1] - result[k] = v - } - return result - }, - "getOrDefault": func(dict map[string]any, key string, def any) any { - v, ok := dict[key] - if ok { - return v - } - return def - }, - "fmt": fmt.Sprintf, - "concat": func(v ...string) string { - return strings.Join(v, "") - }, -} diff --git a/openapi/code/wait.go b/openapi/code/wait.go deleted file mode 100644 index 62d25c5d1..000000000 --- a/openapi/code/wait.go +++ /dev/null @@ -1,180 +0,0 @@ -package code - -import ( - "fmt" - "sort" -) - -// Wait represents a long-running operation, that requires multiple RPC calls -type Wait struct { - Named - // represents a method that triggers the start of the long-running operation - Method *Method -} - -// Binding connects fields in generated code across multiple requests -type Binding struct { - // Polling method request field - PollField *Field - - // Wrapped method either response or request body field - Bind *Field - - // Is wrapped method response used? - IsResponseBind bool -} - -// reasonable default timeout for the most of long-running operations -const defaultLongRunningTimeout = 20 - -// Timeout returns timeout in minutes, defaulting to 20 -func (w *Wait) Timeout() int { - t := w.Method.Operation.Wait.Timeout - if t == 0 { - return defaultLongRunningTimeout - } - return t -} - -// Binding returns a slice of request and response connections -func (w *Wait) Binding() (binding []Binding) { - poll := w.Poll() - if w.Method.wait.Binding != nil { - for pollRequestField, b := range w.Method.wait.Binding { - var bind *Field - if b.Response != "" { - bind = w.Method.Response.Field(b.Response) - } else { - bind = w.Method.Request.Field(b.Request) - } - binding = append(binding, Binding{ - PollField: poll.Request.Field(pollRequestField), - Bind: bind, - IsResponseBind: b.Response != "", - }) - } - // ensure generated code is deterministic - // Java SDK relies on bind parameter order. - sort.Slice(binding, func(a, b int) bool { - return binding[a].PollField.Name < binding[b].PollField.Name - }) - } else { - responseBind := true - bind := w.Method.wait.Bind - entity := w.Method.Response - if entity == nil { - entity = w.Method.Request - responseBind = false - } - pollField := poll.Request.Field(bind) - if pollField == nil { - panic(fmt.Errorf("cannot bind response field: %s", bind)) - } - binding = append(binding, Binding{ - PollField: pollField, - Bind: entity.Field(bind), - IsResponseBind: responseBind, - }) - } - return binding -} - -// ForceBindRequest is a workaround for Jobs#RepairRun, -// that does not send run_id in response -func (w *Wait) ForceBindRequest() bool { - if w.Method.Response == nil { - return false - } - binding := w.Binding() - if len(binding) == 1 && !binding[0].IsResponseBind { - return true - } - return false -} - -// Poll returns method definition for checking the state of -// the long running operation -func (w *Wait) Poll() *Method { - getStatus, ok := w.Method.Service.methods[w.Method.wait.Poll] - if !ok { - return nil - } - return getStatus -} - -// Success holds the successful end-states of the operation -func (w *Wait) Success() (match []EnumEntry) { - enum := w.enum() - for _, v := range w.Method.wait.Success { - match = append(match, enum[v]) - } - return match -} - -// Failure holds the failed end-states of the operation -func (w *Wait) Failure() (match []EnumEntry) { - enum := w.enum() - for _, v := range w.Method.wait.Failure { - match = append(match, enum[v]) - } - return match -} - -func (w *Wait) enum() map[string]EnumEntry { - statusPath := w.StatusPath() - statusField := statusPath[len(statusPath)-1] - return statusField.Entity.enum -} - -// StatusPath holds the path to the field of polled entity, -// that holds current state of the long-running operation -func (w *Wait) StatusPath() (path []*Field) { - pollMethod := w.Poll() - pathToStatus := w.Method.wait.Field - current := pollMethod.Response - for { - fieldName := pathToStatus[0] - pathToStatus = pathToStatus[1:] - field := current.Field(fieldName) - path = append(path, field) - current = field.Entity - if len(pathToStatus) == 0 { - break - } - } - return path -} - -// MessagePath holds the path to the field of polled entity, -// that can tell about current inner status of the long-running operation -func (w *Wait) MessagePath() (path []*Field) { - pollMethod := w.Poll() - current := pollMethod.Response - for _, fieldName := range w.Method.wait.Message { - field := current.Field(fieldName) - path = append(path, field) - current = field.Entity - } - return path -} - -func (w *Wait) Status() *Field { - path := w.StatusPath() - if path == nil { - // unreachable - return nil - } - return path[len(path)-1] -} - -func (w *Wait) ComplexMessagePath() bool { - return len(w.Method.wait.Message) > 1 -} - -func (w *Wait) MessagePathHead() *Field { - path := w.MessagePath() - if len(path) == 0 { - panic("message path is empty for " + w.Method.Operation.OperationId) - } - return path[0] -} diff --git a/openapi/errors.go b/openapi/errors.go deleted file mode 100644 index 4f4635e48..000000000 --- a/openapi/errors.go +++ /dev/null @@ -1,33 +0,0 @@ -package openapi - -type ErrorMappingRule struct { - StatusCode int `json:"status_code"` - ErrorCode string `json:"error_code"` - Description string `json:"description"` -} - -var ErrorStatusCodeMapping = []ErrorMappingRule{ - {400, "BAD_REQUEST", "the request is invalid"}, - {401, "UNAUTHENTICATED", "the request does not have valid authentication (AuthN) credentials for the operation"}, - {403, "PERMISSION_DENIED", "the caller does not have permission to execute the specified operation"}, - {404, "NOT_FOUND", "the operation was performed on a resource that does not exist"}, - {409, "RESOURCE_CONFLICT", "maps to all HTTP 409 (Conflict) responses"}, - {429, "TOO_MANY_REQUESTS", "maps to HTTP code: 429 Too Many Requests"}, - {499, "CANCELLED", "the operation was explicitly canceled by the caller"}, - {500, "INTERNAL_ERROR", "some invariants expected by the underlying system have been broken"}, - {501, "NOT_IMPLEMENTED", "the operation is not implemented or is not supported/enabled in this service"}, - {503, "TEMPORARILY_UNAVAILABLE", "the service is currently unavailable"}, - {504, "DEADLINE_EXCEEDED", "the deadline expired before the operation could complete"}, -} - -var ErrorCodeMapping = []ErrorMappingRule{ - {400, "INVALID_PARAMETER_VALUE", "supplied value for a parameter was invalid"}, - {404, "RESOURCE_DOES_NOT_EXIST", "operation was performed on a resource that does not exist"}, - {409, "ABORTED", "the operation was aborted, typically due to a concurrency issue such as a sequencer check failure"}, - {409, "ALREADY_EXISTS", "operation was rejected due a conflict with an existing resource"}, - {409, "RESOURCE_ALREADY_EXISTS", "operation was rejected due a conflict with an existing resource"}, - {429, "RESOURCE_EXHAUSTED", "operation is rejected due to per-user rate limiting"}, - {429, "REQUEST_LIMIT_EXCEEDED", "cluster request was rejected because it would exceed a resource limit"}, - {500, "UNKNOWN", "this error is used as a fallback if the platform-side mapping is missing some reason"}, - {500, "DATA_LOSS", "unrecoverable data loss or corruption"}, -} diff --git a/openapi/extensions.go b/openapi/extensions.go deleted file mode 100644 index 047577f39..000000000 --- a/openapi/extensions.go +++ /dev/null @@ -1,31 +0,0 @@ -package openapi - -// Pagination is the Databricks OpenAPI Extension for retrieving -// lists of entities through multiple API calls -type Pagination struct { - Offset string `json:"offset,omitempty"` - Limit string `json:"limit,omitempty"` - Results string `json:"results,omitempty"` - Increment int `json:"increment,omitempty"` - Inline bool `json:"inline,omitempty"` - Token *Binding `json:"token,omitempty"` -} - -// Wait is the Databricks OpenAPI Extension for long-running result polling -type Wait struct { - Poll string `json:"poll"` - Bind string `json:"bind"` - BindResponse string `json:"bindResponse,omitempty"` - Binding map[string]Binding `json:"binding,omitempty"` - Field []string `json:"field"` - Message []string `json:"message"` - Success []string `json:"success"` - Failure []string `json:"failure"` - Timeout int `json:"timeout,omitempty"` -} - -// Binding is a relationship between request and/or response -type Binding struct { - Request string `json:"request,omitempty"` - Response string `json:"response,omitempty"` -} diff --git a/openapi/gen/main.go b/openapi/gen/main.go deleted file mode 100644 index b1a2233e1..000000000 --- a/openapi/gen/main.go +++ /dev/null @@ -1,67 +0,0 @@ -// Usage: openapi-codegen -package main - -import ( - "context" - "flag" - "fmt" - "os" - "path" - - "github.com/databricks/databricks-sdk-go/openapi/code" - "github.com/databricks/databricks-sdk-go/openapi/generator" - "github.com/databricks/databricks-sdk-go/openapi/render" - "github.com/databricks/databricks-sdk-go/openapi/roll" -) - -var c Context - -func main() { - ctx := context.Background() - cfg, err := render.Config() - if err != nil { - fmt.Printf("WARN: %s\n\n", err) - } - workDir, _ := os.Getwd() - flag.StringVar(&c.Spec, "spec", cfg.Spec, "location of the spec file") - flag.StringVar(&c.GoSDK, "gosdk", cfg.GoSDK, "location of the Go SDK") - flag.StringVar(&c.Target, "target", workDir, "path to directory with .codegen.json") - flag.BoolVar(&c.DryRun, "dry-run", false, "print to stdout instead of real files") - flag.Parse() - if c.Spec == "" { - println("USAGE: go run openapi/gen/main.go -spec /path/to/spec.json") - flag.PrintDefaults() - os.Exit(1) - } - err = c.Run(ctx) - if err != nil { - fmt.Printf("ERROR: %s\n\n", err) - os.Exit(1) - } -} - -type Context struct { - Spec string - GoSDK string - Target string - DryRun bool -} - -func (c *Context) Run(ctx context.Context) error { - spec, err := code.NewFromFile(ctx, c.Spec) - if err != nil { - return fmt.Errorf("spec: %w", err) - } - var suite *roll.Suite - if c.GoSDK != "" { - suite, err = roll.NewSuite(path.Join(c.GoSDK, "internal")) - if err != nil { - return fmt.Errorf("examples: %w", err) - } - } - gen, err := generator.NewGenerator(c.Target) - if err != nil { - return fmt.Errorf("config: %w", err) - } - return gen.Apply(ctx, spec, suite) -} diff --git a/openapi/generator/config.go b/openapi/generator/config.go deleted file mode 100644 index 964308231..000000000 --- a/openapi/generator/config.go +++ /dev/null @@ -1,176 +0,0 @@ -package generator - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - "sort" - - "github.com/databricks/databricks-sdk-go/openapi/code" - "github.com/databricks/databricks-sdk-go/openapi/render" - "github.com/databricks/databricks-sdk-go/openapi/roll" -) - -type Toolchain struct { - Required []string `json:"required"` - PreSetup []string `json:"pre_setup,omitempty"` - PrependPath string `json:"prepend_path,omitempty"` - Setup []string `json:"setup,omitempty"` - PostGenerate []string `json:"post_generate,omitempty"` -} - -type Generator struct { - Formatter string `json:"formatter"` - - // TemplateLibraries is a list of files containing go template definitions - // that are reused in different stages of codegen. E.g. the "type" template - // is needed in both the "types" and "services" stages. - TemplateLibraries []string `json:"template_libraries,omitempty"` - - // We can generate SDKs in three modes: Packages, Types, Services - // E.g. Go is Package-focused and Java is Types+Services - Packages map[string]string `json:"packages,omitempty"` - Types map[string]string `json:"types,omitempty"` - Services map[string]string `json:"services,omitempty"` - ExceptionTypes map[string]string `json:"exception_types,omitempty"` - Batch map[string]string `json:"batch,omitempty"` - - // special case for usage example templates, that are generated - // from Go SDK integration tests - Examples map[string]string `json:"examples,omitempty"` - Samples map[string]string `json:"samples,omitempty"` - - // version bumps - Version map[string]string `json:"version,omitempty"` - - // code generation toolchain configuration - Toolchain *Toolchain `json:"toolchain,omitempty"` - - dir string -} - -func NewGenerator(target string) (*Generator, error) { - f, err := os.Open(fmt.Sprintf("%s/.codegen.json", target)) - if err != nil { - return nil, fmt.Errorf("no .codegen.json file in %s: %w", target, err) - } - defer f.Close() - raw, err := io.ReadAll(f) - if err != nil { - return nil, fmt.Errorf("read all: %w", err) - } - var c Generator - err = json.Unmarshal(raw, &c) - if err != nil { - return nil, fmt.Errorf(".codegen.json: %w", err) - } - c.dir = target - return &c, nil -} - -func (c *Generator) Apply(ctx context.Context, batch *code.Batch, suite *roll.Suite) error { - if suite != nil { - err := suite.OptimizeWithApiSpec(batch) - if err != nil { - return fmt.Errorf("optimize examples: %w", err) - } - } - var filenames []string - if c.Batch != nil { - pass := render.NewPass(c.dir, []render.Named{batch}, c.Batch, c.TemplateLibraries) - err := pass.Run(ctx) - if err != nil { - return fmt.Errorf("batch: %w", err) - } - filenames = append(filenames, pass.Filenames...) - } - if c.Packages != nil { - pass := render.NewPass(c.dir, batch.Packages(), c.Packages, c.TemplateLibraries) - err := pass.Run(ctx) - if err != nil { - return fmt.Errorf("packages: %w", err) - } - filenames = append(filenames, pass.Filenames...) - } - if c.Services != nil { - pass := render.NewPass(c.dir, batch.Services(), c.Services, c.TemplateLibraries) - err := pass.Run(ctx) - if err != nil { - return fmt.Errorf("services: %w", err) - } - filenames = append(filenames, pass.Filenames...) - } - if c.Types != nil { - pass := render.NewPass(c.dir, batch.Types(), c.Types, c.TemplateLibraries) - err := pass.Run(ctx) - if err != nil { - return fmt.Errorf("types: %w", err) - } - filenames = append(filenames, pass.Filenames...) - } - if c.ExceptionTypes != nil { - pass := render.NewPass(c.dir, batch.ExceptionTypes(), c.ExceptionTypes, c.TemplateLibraries) - err := pass.Run(ctx) - if err != nil { - return fmt.Errorf("exception types: %w", err) - } - filenames = append(filenames, pass.Filenames...) - } - if c.Examples != nil && suite != nil { - pass := render.NewPass(c.dir, suite.ServicesExamples(), c.Examples, c.TemplateLibraries) - err := pass.Run(ctx) - if err != nil { - return fmt.Errorf("examples: %w", err) - } - filenames = append(filenames, pass.Filenames...) - } - if c.Samples != nil && suite != nil { - pass := render.NewPass(c.dir, suite.Samples(), c.Samples, c.TemplateLibraries) - err := pass.Run(ctx) - if err != nil { - return fmt.Errorf("examples: %w", err) - } - filenames = append(filenames, pass.Filenames...) - } - - mockDir := filepath.Join(c.dir, "experimental", "mocks", "service") - mockFilenames := []string{} - info, err := os.Stat(mockDir) - if err == nil && info.IsDir() { - err := filepath.Walk(mockDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() { - relPath, err := filepath.Rel(c.dir, path) - if err != nil { - return err - } - mockFilenames = append(mockFilenames, relPath) - } - return nil - }) - if err != nil { - return fmt.Errorf("mocks: %w", err) - } - filenames = append(filenames, mockFilenames...) - } - - err = render.Formatter(ctx, c.dir, filenames, c.Formatter) - if err != nil { - return err - } - sort.Strings(filenames) - sb := bytes.NewBuffer([]byte{}) - for _, v := range filenames { - // service/*/api.go linguist-generated=true - sb.WriteString(v) - sb.WriteString(" linguist-generated=true\n") - } - genMetaFile := fmt.Sprintf("%s/.gitattributes", c.dir) - return os.WriteFile(genMetaFile, sb.Bytes(), 0o755) -} diff --git a/openapi/model.go b/openapi/model.go deleted file mode 100644 index 18709af5e..000000000 --- a/openapi/model.go +++ /dev/null @@ -1,342 +0,0 @@ -package openapi - -import ( - "encoding/json" - "fmt" - "io" - "reflect" - "strings" -) - -type Node struct { - Description string `json:"description,omitempty"` - Preview string `json:"x-databricks-preview,omitempty"` - Ref string `json:"$ref,omitempty"` -} - -// IsRef flags object being a reference to a component -func (n *Node) IsRef() bool { - return n.Ref != "" -} - -// Components is the basename of the reference path. Usually a class name -func (n *Node) Component() string { - s := strings.Split(n.Ref, "/") - return s[len(s)-1] -} - -func NewFromReader(r io.Reader) (*Specification, error) { - raw, err := io.ReadAll(r) - if err != nil { - return nil, fmt.Errorf("cannot read openapi spec: %w", err) - } - var spec Specification - err = json.Unmarshal(raw, &spec) - if err != nil { - return nil, fmt.Errorf("cannot parse openapi spec: %w", err) - } - return &spec, nil -} - -type Specification struct { - Node - Paths map[string]Path `json:"paths"` - Components *Components `json:"components"` - Tags []Tag `json:"tags"` -} - -type PathStyle string - -const ( - // PathStyleRpc indicates that the endpoint is an RPC-style endpoint. - // The endpoint path is an action, and the entity to act on is specified - // in the request body. - PathStyleRpc PathStyle = "rpc" - - // PathStyleRest indicates that the endpoint is a REST-style endpoint. - // The endpoint path is a resource, and the operation to perform on the - // resource is specified in the HTTP method. - PathStyleRest PathStyle = "rest" -) - -func (r *PathStyle) UnmarshalJSON(data []byte) error { - var s string - err := json.Unmarshal(data, &s) - if err != nil { - return fmt.Errorf("cannot unmarshal RequestStyle: %w", err) - } - switch s { - case "rpc", "rest": - *r = PathStyle(s) - default: - return fmt.Errorf("invalid RequestStyle: %s", s) - } - return nil -} - -type Tag struct { - Node - Package string `json:"x-databricks-package"` - PathStyle PathStyle `json:"x-databricks-path-style"` - Service string `json:"x-databricks-service"` - IsAccounts bool `json:"x-databricks-is-accounts"` - Name string `json:"name"` -} - -type Path struct { - Node - Parameters []Parameter `json:"parameters,omitempty"` - Get *Operation `json:"get,omitempty"` - Post *Operation `json:"post,omitempty"` - Put *Operation `json:"put,omitempty"` - Patch *Operation `json:"patch,omitempty"` - Delete *Operation `json:"delete,omitempty"` -} - -// Verbs returns a map of HTTP methods for a Path -func (path *Path) Verbs() map[string]*Operation { - m := map[string]*Operation{} - if path.Get != nil { - m["GET"] = path.Get - } - if path.Post != nil { - m["POST"] = path.Post - } - if path.Put != nil { - m["PUT"] = path.Put - } - if path.Patch != nil { - m["PATCH"] = path.Patch - } - if path.Delete != nil { - m["DELETE"] = path.Delete - } - return m -} - -type fieldPath []string - -func (fp fieldPath) String() string { - return strings.Join(fp, ".") -} - -// Operation is the equivalent of method -type Operation struct { - Node - Wait *Wait `json:"x-databricks-wait,omitempty"` - Pagination *Pagination `json:"x-databricks-pagination,omitempty"` - Shortcut bool `json:"x-databricks-shortcut,omitempty"` - Crud string `json:"x-databricks-crud,omitempty"` - JsonOnly bool `json:"x-databricks-cli-json-only,omitempty"` - - // The x-databricks-path-style field indicates whether the operation has a - // RESTful path style or a RPC style. When specified, this overrides the - // service-level setting. Valid values are "rest" and "rpc". "rest" means - // that the operation has a RESTful path style, i.e. the path represents - // a resource and the HTTP method represents an action on the resource. - // "rpc" means that the operation has a RPC style, i.e. the path represents - // an action and the request body represents the resource. - PathStyle PathStyle `json:"x-databricks-path-style,omitempty"` - - // The x-databricks-request-type-name field defines the name to use for - // the request type in the generated client. This may be specified only - // if the operation does NOT have a request body, thus only uses a request - // type to encapsulate path and query parameters. - RequestTypeName string `json:"x-databricks-request-type-name,omitempty"` - - // For list APIs, the path to the field in the response entity that contains - // the resource ID. - IdField fieldPath `json:"x-databricks-id,omitempty"` - - // For list APIs, the path to the field in the response entity that contains - // the user-friendly name of the resource. - NameField fieldPath `json:"x-databricks-name,omitempty"` - - Summary string `json:"summary,omitempty"` - OperationId string `json:"operationId"` - Tags []string `json:"tags"` - Parameters []Parameter `json:"parameters,omitempty"` - Responses map[string]*Body `json:"responses"` - RequestBody *Body `json:"requestBody,omitempty"` -} - -// Name is picking the last element of . string, -// that is coming in as part of Databricks OpenAPI spec. -func (o *Operation) Name() string { - split := strings.Split(o.OperationId, ".") - if len(split) == 2 { - return split[1] - } - return o.OperationId -} - -func (o *Operation) HasTag(tag string) bool { - for _, v := range o.Tags { - if v == tag { - return true - } - } - return false -} - -func (o *Operation) SuccessResponseBody(c *Components) *Body { - for _, v := range []string{"200", "201"} { - response, ok := o.Responses[v] - if ok { - return (*c.Responses.Resolve(response)) - } - } - return nil -} - -func (o *Operation) HasNameField() bool { - return len(o.NameField) > 0 -} - -func (o *Operation) HasIdentifierField() bool { - return len(o.IdField) > 0 -} - -type node interface { - IsRef() bool - Component() string -} - -type refs[T node] map[string]*T - -func (c refs[T]) Resolve(item T) *T { - if reflect.ValueOf(item).IsNil() { - return nil - } - if !item.IsRef() { - return &item - } - return c[item.Component()] -} - -type Components struct { - Node - Parameters refs[*Parameter] `json:"parameters,omitempty"` - Responses refs[*Body] `json:"responses,omitempty"` - Schemas refs[*Schema] `json:"schemas,omitempty"` -} - -type Schema struct { - Node - IsComputed bool `json:"x-databricks-computed,omitempty"` - IsAny bool `json:"x-databricks-any,omitempty"` - Type string `json:"type,omitempty"` - Enum []string `json:"enum,omitempty"` - AliasEnum []string `json:"x-databricks-alias-enum,omitempty"` - EnumDescriptions map[string]string `json:"x-databricks-enum-descriptions,omitempty"` - Default any `json:"default,omitempty"` - Example any `json:"example,omitempty"` - Format string `json:"format,omitempty"` - Required []string `json:"required,omitempty"` - Properties map[string]*Schema `json:"properties,omitempty"` - ArrayValue *Schema `json:"items,omitempty"` - MapValue *Schema `json:"additionalProperties,omitempty"` -} - -func (s *Schema) IsEnum() bool { - return len(s.Enum) != 0 -} - -func (s *Schema) IsObject() bool { - return len(s.Properties) != 0 -} - -// IsDefinable states that type could be translated into a valid top-level type -// in Go, Python, Java, Scala, and JavaScript -func (s *Schema) IsDefinable() bool { - return s.IsObject() || s.IsEnum() -} - -func (s *Schema) IsMap() bool { - return s.MapValue != nil -} - -func (s *Schema) IsArray() bool { - return s.ArrayValue != nil -} - -func (s *Schema) IsEmpty() bool { - if s.IsMap() { - return false - } - if s.IsArray() { - return false - } - if s.IsObject() { - return false - } - if s.IsRef() { - return false - } - if s.Type == "object" || s.Type == "" { - return true - } - return false -} - -type Parameter struct { - Node - Required bool `json:"required,omitempty"` - In string `json:"in,omitempty"` - Name string `json:"name,omitempty"` - MultiSegment bool `json:"x-databricks-multi-segment,omitempty"` - Schema *Schema `json:"schema,omitempty"` -} - -type Body struct { - Node - Required bool `json:"required,omitempty"` - Content map[string]MediaType `json:"content,omitempty"` -} - -type MimeType string - -const ( - MimeTypeJson MimeType = "application/json" - MimeTypeOctetStream MimeType = "application/octet-stream" - MimeTypeTextPlain MimeType = "text/plain" -) - -// IsByteStream returns true if the body should be modeled as a byte stream. -// Today, we only support application/json and application/octet-stream, and non -// application/json entities are all modeled as byte streams. -func (m MimeType) IsByteStream() bool { - return m != "" && m != MimeTypeJson -} - -var allowedMimeTypes = []MimeType{ - MimeTypeJson, - MimeTypeOctetStream, - MimeTypeTextPlain, -} - -func (b *Body) MimeTypeAndMediaType() (MimeType, *MediaType) { - if b == nil || b.Content == nil { - return "", nil - } - for _, m := range allowedMimeTypes { - if mediaType, ok := b.Content[string(m)]; ok { - return m, &mediaType - } - } - return "", nil -} - -type MediaType struct { - Node - Schema *Schema `json:"schema,omitempty"` -} - -const MediaTypeNonJsonBodyFieldName = "contents" - -func (m *MediaType) GetSchema() *Schema { - if m == nil { - return nil - } - return m.Schema -} diff --git a/openapi/model_test.go b/openapi/model_test.go deleted file mode 100644 index e4b7d9b31..000000000 --- a/openapi/model_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package openapi - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLoadFromJson(t *testing.T) { - f, err := os.Open("testdata/spec.json") - assert.NoError(t, err) - spec, err := NewFromReader(f) - assert.NoError(t, err) - assert.Equal(t, "Command Execution", spec.Tags[0].Name) -} diff --git a/openapi/render/render.go b/openapi/render/render.go deleted file mode 100644 index 950fd7c55..000000000 --- a/openapi/render/render.go +++ /dev/null @@ -1,172 +0,0 @@ -package render - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "io/fs" - "os" - "os/exec" - "path" - "path/filepath" - "strings" - "text/template" - - "github.com/databricks/databricks-sdk-go/logger" - "github.com/databricks/databricks-sdk-go/openapi/code" -) - -type toolConfig struct { - Spec string `json:"spec"` - GoSDK string `json:"gosdk,omitempty"` -} - -func Config() (toolConfig, error) { - home, err := os.UserHomeDir() - if err != nil { - return toolConfig{}, fmt.Errorf("home: %w", err) - } - loc := filepath.Join(home, ".openapi-codegen.json") - f, err := os.Open(loc) - if err != nil { - return toolConfig{}, fmt.Errorf("open %s: %w", loc, err) - } - raw, err := io.ReadAll(f) - if err != nil { - return toolConfig{}, fmt.Errorf("read all: %w", err) - } - var cfg toolConfig - err = json.Unmarshal(raw, &cfg) - if err != nil { - return cfg, fmt.Errorf("parse %s: %w", loc, err) - } - return cfg, nil -} - -func NewPass[T Named](target string, items []T, fileset map[string]string, libs []string) *pass[T] { - var tmpls []string - newFileset := map[string]string{} - for filename, v := range fileset { - filename = filepath.Join(target, filename) - tmpls = append(tmpls, filename) - newFileset[filepath.Base(filename)] = v - } - for _, lib := range libs { - tmpls = append(tmpls, filepath.Join(target, lib)) - } - t := template.New("codegen").Funcs(code.HelperFuncs) - t = t.Funcs(template.FuncMap{ - "load": func(tmpl string) (string, error) { - _, err := t.ParseFiles(tmpl) - return "", err - }, - }) - return &pass[T]{ - Items: items, - target: target, - fileset: newFileset, - tmpl: template.Must(t.ParseFiles(tmpls...)), - } -} - -type Named interface { - FullName() string -} - -type pass[T Named] struct { - Items []T - target string - tmpl *template.Template - fileset map[string]string - Filenames []string -} - -func (p *pass[T]) Run(ctx context.Context) error { - for _, item := range p.Items { - name := item.FullName() - logger.Infof(ctx, "Processing: %s\n", name) - for k, v := range p.fileset { - err := p.File(item, k, v) - if err != nil { - return fmt.Errorf("%s: %w", item.FullName(), err) - } - } - } - return nil -} - -func (p *pass[T]) File(item T, contentTRef, nameT string) error { - nt, err := template.New("filename").Parse(nameT) - if err != nil { - return fmt.Errorf("parse %s: %w", nameT, err) - } - var contents strings.Builder - err = p.tmpl.ExecuteTemplate(&contents, contentTRef, &item) - if errors.Is(err, code.ErrSkipThisFile) { - // special case for CLI generation with `{{skipThisFile}}` - return nil - } - if err != nil { - return fmt.Errorf("exec %s: %w", contentTRef, err) - } - var childFilename strings.Builder - err = nt.Execute(&childFilename, item) - if err != nil { - return fmt.Errorf("exec %s: %w", nameT, err) - } - p.Filenames = append(p.Filenames, childFilename.String()) - if nameT == "stdout" { - // print something, usually instructions for any manual work - println(contents.String()) - return nil - } - targetFilename := filepath.Join(p.target, childFilename.String()) - _, err = os.Stat(targetFilename) - if errors.Is(err, fs.ErrNotExist) { - err = os.MkdirAll(path.Dir(targetFilename), 0o755) - if err != nil { - return fmt.Errorf("cannot create parent folders: %w", err) - } - } - file, err := os.OpenFile(targetFilename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755) - if err != nil { - return fmt.Errorf("open %s: %w", targetFilename, err) - } - _, err = file.WriteString(contents.String()) - if err != nil { - return fmt.Errorf("write %s: %w", targetFilename, err) - } - return file.Close() -} - -func Formatter(ctx context.Context, target string, filenames []string, formatSpec string) error { - for _, formatter := range strings.Split(formatSpec, "&&") { - formatter = strings.TrimSpace(formatter) - logger.Infof(ctx, "Formatting: %s\n", formatter) - - formatter = strings.ReplaceAll(formatter, "$FILENAMES", - strings.Join(filenames, " ")) - split := strings.Split(formatter, " ") - - // create pipe to forward stdout and stderr to same fd, - // so that it's clear why formatter failed. - reader, writer := io.Pipe() - out := bytes.NewBuffer([]byte{}) - go io.Copy(out, reader) - defer reader.Close() - defer writer.Close() - - cmd := exec.Command(split[0], split[1:]...) - cmd.Dir = target - cmd.Stdout = writer - cmd.Stderr = writer - err := cmd.Run() - if err != nil { - return fmt.Errorf("%s:\n%s", formatter, out.Bytes()) - } - } - return nil -} diff --git a/openapi/roll/ast.go b/openapi/roll/ast.go deleted file mode 100644 index 07f326a71..000000000 --- a/openapi/roll/ast.go +++ /dev/null @@ -1,339 +0,0 @@ -package roll - -import ( - "strings" - - "github.com/databricks/databricks-sdk-go/openapi/code" -) - -type expression interface { - Type() string -} - -type traversable interface { - Traverse(func(expression)) -} - -type binaryExpr struct { - Left, Right expression - Op string -} - -func (b *binaryExpr) Traverse(cb func(expression)) { - cb(b.Left) - if t, ok := b.Left.(traversable); ok { - t.Traverse(cb) - } - cb(b.Right) - if t, ok := b.Right.(traversable); ok { - t.Traverse(cb) - } -} - -func (b *binaryExpr) Type() string { - return "binary" -} - -type indexExpr struct { - Left, Right expression -} - -func (i *indexExpr) Traverse(cb func(expression)) { - cb(i.Left) - if t, ok := i.Left.(traversable); ok { - t.Traverse(cb) - } - cb(i.Right) - if t, ok := i.Right.(traversable); ok { - t.Traverse(cb) - } -} - -func (i *indexExpr) Type() string { - return "index" -} - -// type boolean struct { -// Value bool -// } - -// func (b *boolean) Type() string { -// return "boolean" -// } - -type literal struct { - Value string -} - -func (l *literal) Type() string { - return "literal" -} - -type heredoc struct { - Value string -} - -func (l *heredoc) Type() string { - return "heredoc" -} - -type lookup struct { - X expression - Field *code.Named -} - -func (l *lookup) Variable() string { - switch x := l.X.(type) { - case *variable: - return x.Name - default: - return "" - } -} - -func (l *lookup) Traverse(cb func(expression)) { - cb(l.X) - if t, ok := l.X.(traversable); ok { - t.Traverse(cb) - } -} - -func (l *lookup) Type() string { - return "lookup" -} - -type variable struct { - code.Named -} - -func (v *variable) Type() string { - return "variable" -} - -type entity struct { - code.Named - Package string - FieldValues []*fieldValue - IsPointer bool -} - -func (e *entity) Traverse(cb func(expression)) { - for _, v := range e.FieldValues { - cb(v.Value) - if t, ok := v.Value.(traversable); ok { - t.Traverse(cb) - } - } -} - -func (e *entity) Type() string { - return "entity" -} - -type mapKV struct { - Key expression - Value expression -} - -type mapLiteral struct { - KeyType string - ValueType string - Pairs []mapKV -} - -func (e *mapLiteral) Type() string { - return "map" -} - -func (e *mapLiteral) Traverse(cb func(expression)) { - for _, v := range e.Pairs { - if t, ok := v.Key.(traversable); ok { - t.Traverse(cb) - } - if t, ok := v.Value.(traversable); ok { - t.Traverse(cb) - } - } -} - -type array struct { - code.Named - Package string - Values []expression -} - -func (a *array) Traverse(cb func(expression)) { - for _, v := range a.Values { - cb(v) - if t, ok := v.(traversable); ok { - t.Traverse(cb) - } - } -} - -func (a *array) Type() string { - return "array" -} - -type fieldValue struct { - code.Named - Value expression -} - -type call struct { - code.Named - IsAccount bool - Service *code.Named - Assign *code.Named - Args []expression - - // ID to avoid duplicates. alternative could be hashing, - // but implementation would grow more complex than needed. - id int - - // hint about the call creating an entity behind the variable - creates string -} - -func (c *call) IsDependentOn(other *call) bool { - if other.Assign == nil { - return false - } - result := []bool{false} - c.Traverse(func(e expression) { - v, ok := e.(*variable) - if !ok { - return - } - if other.Assign.CamelName() == v.CamelName() { - result[0] = true - return - } - }) - return result[0] -} - -func (c *call) IsWait() bool { - return strings.HasSuffix(c.Name, "AndWait") -} - -func (c *call) Request() (fv []*fieldValue) { - if strings.Contains(c.Name, "By") { - // E.g. DeleteByJobId, DeleteByClusterIdAndWait - firstSplit := strings.SplitN(c.Name, "By", 2) - // And is used to separate field names, but some methods end with AndWait - joinedFields := strings.TrimSuffix(firstSplit[1], "AndWait") - fields := strings.Split(joinedFields, "And") - for i, name := range fields { - fv = append(fv, &fieldValue{ - Named: code.Named{ - Name: name, - }, - Value: c.Args[i], - }) - } - return fv - } - if len(c.Args) == 0 { - return fv - } - e, ok := c.Args[0].(*entity) - if !ok { - return fv - } - return e.FieldValues -} - -func (c *call) Original() *code.Named { - name := c.CamelName() - name = strings.Split(name, "By")[0] - name = strings.TrimSuffix(name, "AndWait") - name = strings.TrimSuffix(name, "All") - return &code.Named{ - Name: name, - } -} - -func (c *call) OriginalName() string { - camel := c.CamelName() - camel = strings.Split(camel, "By")[0] - camel = strings.TrimSuffix(camel, "AndWait") - camel = strings.TrimSuffix(camel, "All") - return camel -} - -func (c *call) HasVariable(camelName string) bool { - // this assumes that v is already camelCased - tmp := []int{0} - c.Traverse(func(e expression) { - v, ok := e.(*variable) - if !ok { - return - } - if v.CamelName() == camelName { - tmp[0]++ - } - }) - return tmp[0] > 0 -} - -func (c *call) Traverse(cb func(expression)) { - for _, v := range c.Args { - cb(v) - if t, ok := v.(traversable); ok { - t.Traverse(cb) - } - } -} - -func (c *call) Type() string { - return "call" -} - -type initVar struct { - code.Named - Value expression -} - -type example struct { - code.Named - // TODO: add Method and Service - IsAccount bool - Calls []*call - Cleanup []*call - Asserts []expression - Init []*initVar - scope map[string]expression -} - -func (ex *example) FullName() string { - return ex.Name -} - -func (ex *example) findCall(svcCamelName, methodCamelName string) *call { - for _, v := range ex.Calls { - if v.Service == nil { - continue - } - if v.Service.CamelName() != svcCamelName { - continue - } - if v.OriginalName() != methodCamelName { - continue - } - return v - } - for _, v := range ex.Cleanup { - if v.Service == nil { - continue - } - if v.Service.CamelName() != svcCamelName { - continue - } - if v.OriginalName() != methodCamelName { - continue - } - return v - } - return nil -} diff --git a/openapi/roll/optimize.go b/openapi/roll/optimize.go deleted file mode 100644 index 64083519d..000000000 --- a/openapi/roll/optimize.go +++ /dev/null @@ -1,155 +0,0 @@ -package roll - -import ( - "strings" - - "github.com/databricks/databricks-sdk-go/openapi/code" -) - -type enum struct { - code.Named - Package string - Entity *code.Named - Content string -} - -func (e *enum) Type() string { - return "enum" -} - -func (s *Suite) OptimizeWithApiSpec(b *code.Batch) error { - o := &suiteOptimizer{b} - for _, e := range s.examples { - err := o.optimizeExample(e) - if err != nil { - return err - } - } - return nil -} - -type suiteOptimizer struct { - *code.Batch -} - -func (s *suiteOptimizer) optimizeExample(e *example) error { - for _, c := range e.Calls { - err := s.optimizeCall(c) - if err != nil { - return err - } - } - for _, c := range e.Cleanup { - err := s.optimizeCall(c) - if err != nil { - return err - } - } - return nil -} - -func (s *suiteOptimizer) optimizeCall(c *call) error { - for i := range c.Args { - err := s.optimizeExpression(&c.Args[i]) - if err != nil { - return err - } - } - return nil -} - -func (s *suiteOptimizer) enumConstant(l *lookup) expression { - potentialPackage := l.Variable() - potentialEnumValue := l.Field.PascalName() - for _, pkg := range s.Packages() { - if pkg.Name != potentialPackage { - continue - } - for _, v := range pkg.Types() { - if v.Schema != nil && !v.Schema.IsEnum() { - continue - } - prefix := v.PascalName() - if !strings.HasPrefix(potentialEnumValue, prefix) { - continue - } - value := strings.TrimPrefix(potentialEnumValue, prefix) - for _, e := range v.Enum() { - if e.PascalName() != value { - continue - } - return &enum{ - Package: potentialPackage, - Entity: &code.Named{ - Name: prefix, - }, - Named: code.Named{ - Name: e.Content, - }, - Content: e.Content, - } - } - } - } - return nil -} - -func (s *suiteOptimizer) optimizeExpression(e *expression) (err error) { - switch x := (*e).(type) { - case *call: - return s.optimizeCall(x) - case *lookup: - enumConstant := s.enumConstant(x) - if enumConstant != nil { - *e = enumConstant - return - } - return s.optimizeExpression(&x.X) - case *binaryExpr: - err = s.optimizeExpression(&x.Left) - if err != nil { - return err - } - err = s.optimizeExpression(&x.Right) - if err != nil { - return err - } - case *indexExpr: - err = s.optimizeExpression(&x.Left) - if err != nil { - return err - } - err = s.optimizeExpression(&x.Right) - if err != nil { - return err - } - case *entity: - for i := range x.FieldValues { - err = s.optimizeExpression(&x.FieldValues[i].Value) - if err != nil { - return err - } - } - case *mapLiteral: - for i := range x.Pairs { - err = s.optimizeExpression(&x.Pairs[i].Key) - if err != nil { - return err - } - err = s.optimizeExpression(&x.Pairs[i].Value) - if err != nil { - return err - } - } - case *array: - for i := range x.Values { - err = s.optimizeExpression(&x.Values[i]) - if err != nil { - return err - } - } - default: - return nil - } - return nil -} diff --git a/openapi/roll/tool.go b/openapi/roll/tool.go deleted file mode 100644 index ac93f8af0..000000000 --- a/openapi/roll/tool.go +++ /dev/null @@ -1,773 +0,0 @@ -package roll - -import ( - "fmt" - "go/ast" - "go/parser" - "go/token" - "os" - "path/filepath" - "strings" - - "github.com/databricks/databricks-sdk-go/openapi/code" - "github.com/databricks/databricks-sdk-go/service/compute" - "golang.org/x/exp/slices" -) - -func NewSuite(dirname string) (*Suite, error) { - fset := token.NewFileSet() - s := &Suite{ - fset: fset, - ServiceToPackage: map[string]string{}, - } - err := filepath.WalkDir(dirname, func(path string, info os.DirEntry, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - if strings.HasSuffix(path, "acceptance_test.go") { - // not transpilable - return nil - } - if strings.HasSuffix(path, "files_test.go") { - // not transpilable - return nil - } - if strings.HasSuffix(path, "workspaceconf_test.go") { - // not transpilable - return nil - } - file, err := parser.ParseFile(fset, path, nil, parser.ParseComments) - if err != nil { - return err - } - s.expectExamples(file) - return nil - }) - if err != nil { - return nil, err - } - err = s.parsePackages(dirname+"/../workspace_client.go", "WorkspaceClient") - if err != nil { - return nil, err - } - err = s.parsePackages(dirname+"/../account_client.go", "AccountClient") - if err != nil { - return nil, err - } - return s, nil -} - -type Suite struct { - fset *token.FileSet - ServiceToPackage map[string]string - examples []*example - counter int -} - -func (s *Suite) parsePackages(filename, client string) error { - file, err := parser.ParseFile(s.fset, filename, nil, 0) - if err != nil { - return err - } - spec, ok := file.Scope.Objects[client].Decl.(*ast.TypeSpec) - if !ok { - s.explainAndPanic("type spec", file.Scope.Objects[client].Decl) - } - structType, ok := spec.Type.(*ast.StructType) - if !ok { - s.explainAndPanic("struct type", spec.Type) - } - for _, f := range structType.Fields.List { - fieldName := f.Names[0].Name - selectorExpr, ok := f.Type.(*ast.SelectorExpr) - if !ok { - continue - } - apiName := selectorExpr.Sel.Name - if !strings.HasSuffix(apiName, "Interface") { - continue - } - s.ServiceToPackage[fieldName] = s.expectIdent(selectorExpr.X) - } - return nil -} - -func (s *Suite) FullName() string { - return "suite" -} - -type methodRef struct { - Pacakge, Service, Method string -} - -func (s *Suite) Methods() []methodRef { - found := map[methodRef]bool{} - for _, ex := range s.examples { - for _, v := range ex.Calls { - if v.Service == nil { - continue - } - if strings.HasSuffix(v.PascalName(), "IdMap") { - continue - } - found[methodRef{ - Pacakge: s.ServiceToPackage[v.Service.PascalName()], - Service: v.Service.CamelName(), - Method: v.OriginalName(), - }] = true - } - } - methods := []methodRef{} - for k := range found { - methods = append(methods, k) - } - slices.SortFunc(methods, func(a, b methodRef) bool { - return a.Service < b.Service && a.Method < b.Method - }) - return methods -} - -func (s *Suite) Samples() (out []*sample) { - for _, v := range s.Methods() { - out = append(out, s.usageSamples(v.Service, v.Method)...) - } - return out -} - -type serviceExample struct { - code.Named - Suite *Suite - Package string - Samples []*sample -} - -func (s *serviceExample) FullName() string { - return fmt.Sprintf("%s.%s", s.Package, s.PascalName()) -} - -func (s *Suite) ServicesExamples() (out []*serviceExample) { - samples := s.Samples() - for svc, pkg := range s.ServiceToPackage { - se := &serviceExample{ - Named: code.Named{ - Name: svc, - }, - Package: pkg, - Suite: s, - } - for _, v := range samples { - if v.Service.PascalName() != se.PascalName() { - continue - } - se.Samples = append(se.Samples, v) - } - slices.SortFunc(se.Samples, func(a, b *sample) bool { - return a.FullName() < b.FullName() - }) - out = append(out, se) - } - return out -} - -type sample struct { - example - Package string - Service *code.Named - Method *code.Named - Suite *Suite -} - -func (sa *sample) FullName() string { - return fmt.Sprintf("%s.%s", sa.Service.PascalName(), sa.Method.CamelName()) -} - -func (s *Suite) usageSamples(svc, mth string) (out []*sample) { - svcName := &code.Named{ - Name: svc, - } - methodName := &code.Named{ - Name: mth, - } - for _, ex := range s.examples { - c := ex.findCall(svcName.CamelName(), methodName.CamelName()) - if c == nil { - continue - } - sa := &sample{ - example: example{ - Named: ex.Named, - IsAccount: ex.IsAccount, - }, - Service: svcName, - Method: methodName, - Package: s.ServiceToPackage[svcName.PascalName()], - Suite: s, - } - out = append(out, sa) - variablesUsed := []string{} - queue := []expression{c} - added := map[string]bool{} - callIds := map[int]bool{} - for len(queue) > 0 { - current := queue[0] - queue = queue[1:] - switch x := current.(type) { - case *call: - if callIds[x.id] { - continue - } - if x.Assign != nil && x.Assign.Name != "_" { - variablesUsed = append(variablesUsed, x.Assign.CamelName()) - // call methods that may actually create an entity. - // executed before we append to sa.Calls, as we reverse - // the slice in the end - for _, v := range ex.Calls { - if v.creates != x.Assign.CamelName() { - continue - } - if callIds[v.id] { - continue - } - // put at the front of the queue - queue = append([]expression{v}, queue...) - } - } - x.IsAccount = ex.IsAccount - sa.Calls = append(sa.Calls, x) - callIds[x.id] = true - x.Traverse(func(e expression) { - v, ok := e.(*variable) - if !ok { - return - } - if added[v.CamelName()] { - // don't add the same variable twice - return - } - found, ok := ex.scope[v.CamelName()] - if ok { - queue = append(queue, found) - added[v.CamelName()] = true - return - } - for _, iv := range ex.Init { - if iv.CamelName() != v.CamelName() { - continue - } - sa.Init = append(sa.Init, iv) - return - } - }) - default: - panic("unsupported") - } - } - for _, v := range variablesUsed { - // TODO: also include ex.Asserts - for _, c := range ex.Cleanup { - if !c.HasVariable(v) { - continue - } - if callIds[c.id] { - continue - } - c.Traverse(func(e expression) { - v, ok := e.(*variable) - if !ok { - return - } - if added[v.CamelName()] { - // don't add the same variable twice - return - } - found, ok := ex.scope[v.CamelName()] - if !ok { - return - } - assignCall, ok := found.(*call) - if !ok { - // ideally, we could do multiple optimization passes - // to discover used variables also in cleanups, but - // we're not doing it for now. - return - } - if callIds[assignCall.id] { - return - } - sa.Calls = append(sa.Calls, assignCall) - callIds[assignCall.id] = true - }) - c.IsAccount = ex.IsAccount - sa.Cleanup = append(sa.Cleanup, c) - callIds[c.id] = true - } - } - slices.SortFunc[*call](sa.Calls, func(a, b *call) bool { - x := a.IsDependentOn(b) - return x - }) - reverse(sa.Calls) - reverse(sa.Cleanup) - } - return out -} - -func reverse[T any](s []T) { - for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { - s[i], s[j] = s[j], s[i] - } -} - -func (s *Suite) assignedNames(a *ast.AssignStmt) (names []string) { - for _, v := range a.Lhs { - ident, ok := v.(*ast.Ident) - if !ok { - continue - } - names = append(names, ident.Name) - } - return -} - -func (s *Suite) expectExamples(file *ast.File) { - for _, v := range file.Decls { - fn, ok := v.(*ast.FuncDecl) - if !ok { - continue - } - fnName := fn.Name.Name - if !strings.HasPrefix(fnName, "TestAcc") && - !strings.HasPrefix(fnName, "TestMws") && - !strings.HasPrefix(fnName, "TestUc") { - continue - } - if strings.HasSuffix(fnName, "NoTranspile") { - // Tests with NoTranspile suffix are too expensive to automatically - // translate, we just ignore them. - continue - } - s.examples = append(s.examples, s.expectFn(fn, file)) - } -} - -func (s *Suite) expectFn(fn *ast.FuncDecl, file *ast.File) *example { - testName := fn.Name.Name - testName = strings.TrimPrefix(testName, "TestAcc") - testName = strings.TrimPrefix(testName, "TestUcAcc") - testName = strings.TrimPrefix(testName, "TestMwsAcc") - ex := &example{ - Named: code.Named{ - Name: testName, - }, - scope: map[string]expression{}, - } - lastPos := fn.Pos() - hint := "" - for _, v := range fn.Body.List { - for _, cmt := range file.Comments { - if cmt.End() < lastPos { - continue - } - if cmt.Pos() > v.Pos() { - continue - } - // figure out comment hint exactly above the given statement - hint = strings.TrimSpace(cmt.Text()) - break - } - switch stmt := v.(type) { - case *ast.AssignStmt: - s.expectAssign(ex, stmt, hint) - case *ast.DeferStmt: - ex.Cleanup = append(ex.Cleanup, s.expectCall(stmt.Call)) - case *ast.ExprStmt: - s.expectExprStmt(ex, stmt) - } - // store the end of the last statement to figure out the next - // statement comment. - lastPos = v.End() - hint = "" - } - return ex -} - -var ignoreFns = map[string]bool{ - "SkipNow": true, - "NoError": true, - "Lock": true, - "Errorf": true, - "Skipf": true, - "Log": true, -} - -func (s *Suite) expectCleanup(ex *example, ce *ast.CallExpr) bool { - se, ok := ce.Fun.(*ast.SelectorExpr) - if !ok || se.Sel.Name != "Cleanup" { - return false - } - if len(ce.Args) == 0 { - return false - } - inlineFn, ok := ce.Args[0].(*ast.FuncLit) - if !ok { - return false - } - for _, v := range inlineFn.Body.List { - assign, ok := v.(*ast.AssignStmt) - if !ok { - continue - } - c := s.expectAssignCall(assign) - if c == nil { - continue - } - ex.Cleanup = append(ex.Cleanup, c) - } - return true -} - -func (s *Suite) expectExprStmt(ex *example, stmt *ast.ExprStmt) { - ce, ok := stmt.X.(*ast.CallExpr) - if !ok { - return - } - if s.expectCleanup(ex, ce) { - return - } - c := s.expectCall(stmt.X) - if ignoreFns[c.Name] { - return - } - assertions := map[string]string{ - "EqualError": "equalError", - "Contains": "contains", - "GreaterOrEqual": ">=", - "Greater": ">", - "Equal": "==", - "NotEqual": "!=", - } - op, ok := assertions[c.Name] - if ok { - ex.Asserts = append(ex.Asserts, &binaryExpr{ - Left: c.Args[0], - Op: op, - Right: c.Args[1], - }) - } else if c.Name == "NotEmpty" { - // TODO: replace code occurences with assert.True - ex.Asserts = append(ex.Asserts, &binaryExpr{ - Left: c.Args[0], - Op: "notEmpty", - Right: nil, - }) - } else if c.Name == "True" { - ex.Asserts = append(ex.Asserts, c.Args[0]) - } else { - s.explainAndPanic("known assertion", c) - } -} - -func (s *Suite) expectAssign(ex *example, stmt *ast.AssignStmt, hint string) { - names := s.assignedNames(stmt) - if len(names) == 2 && names[0] == "ctx" { - // w - workspace, a - account - ex.IsAccount = names[1] == "a" - return - } - switch x := stmt.Rhs[0].(type) { - case *ast.CallExpr: - c := s.expectAssignCall(stmt) - if c == nil { - return - } - if c.Assign != nil && c.Assign.Name != "_" { - ex.scope[c.Assign.CamelName()] = c - } - if strings.HasPrefix(hint, "creates ") { - c.creates = strings.TrimPrefix(hint, "creates ") - } - ex.Calls = append(ex.Calls, c) - case *ast.BasicLit: - lit := s.expectPrimitive(x) - ex.Init = append(ex.Init, &initVar{ - Named: code.Named{ - Name: names[0], - }, - Value: lit, - }) - } -} - -func (s *Suite) expectAssignCall(stmt *ast.AssignStmt) *call { - names := s.assignedNames(stmt) - c := s.expectCall(stmt.Rhs[0]) - if len(names) == 1 && names[0] != "err" { - c.Assign = &code.Named{ - Name: names[0], - } - } - if len(names) == 2 && names[1] == "err" { - c.Assign = &code.Named{ - Name: names[0], - } - } - return c -} - -func (s *Suite) expectIdent(e ast.Expr) string { - ident, ok := e.(*ast.Ident) - if !ok { - s.explainAndPanic("ident", e) - return "" - } - return ident.Name -} - -func (s *Suite) expectEntity(t *ast.SelectorExpr, cl *ast.CompositeLit) *entity { - ent := &entity{} - ent.Name = t.Sel.Name - ent.Package = s.expectIdent(t.X) - ent.FieldValues = s.expectFieldValues(cl.Elts) - return ent -} - -func (s *Suite) expectFieldValues(exprs []ast.Expr) (fvs []*fieldValue) { - for _, v := range exprs { - switch kv := v.(type) { - case *ast.KeyValueExpr: - fvs = append(fvs, &fieldValue{ - Named: code.Named{ - Name: s.expectIdent(kv.Key), - }, - Value: s.expectExpr(kv.Value), - }) - default: - s.explainAndPanic("field value", v) - return - } - } - return fvs -} - -func (s *Suite) expectArray(t *ast.ArrayType, cl *ast.CompositeLit) *array { - arr := &array{} - switch elt := t.Elt.(type) { - case *ast.SelectorExpr: - arr.Package = s.expectIdent(elt.X) - arr.Name = s.expectIdent(elt.Sel) - case *ast.Ident: - arr.Name = s.expectIdent(elt) - default: - s.explainAndPanic("array element", elt) - return nil - } - for _, v := range cl.Elts { - switch item := v.(type) { - case *ast.CompositeLit: - arr.Values = append(arr.Values, &entity{ - Named: code.Named{ - Name: arr.Name, - }, - Package: arr.Package, - FieldValues: s.expectFieldValues(item.Elts), - }) - default: - arr.Values = append(arr.Values, s.expectExpr(v)) - } - } - return arr -} - -func (s *Suite) expectMap(t *ast.MapType, cl *ast.CompositeLit) *mapLiteral { - m := &mapLiteral{ - KeyType: s.expectIdent(t.Key), - ValueType: s.expectIdent(t.Value), - } - for _, v := range cl.Elts { - kv, ok := v.(*ast.KeyValueExpr) - if !ok { - s.explainAndPanic("key value expr", v) - } - m.Pairs = append(m.Pairs, mapKV{ - Key: s.expectExpr(kv.Key), - Value: s.expectExpr(kv.Value), - }) - } - return m -} - -func (s *Suite) expectPrimitive(x *ast.BasicLit) expression { - // we directly translate literal values - if x.Value[0] == '`' { - txt := x.Value[1 : len(x.Value)-1] - return &heredoc{ - Value: compute.TrimLeadingWhitespace(txt), - } - } - return &literal{x.Value} -} - -func (s *Suite) expectCompositeLiteral(x *ast.CompositeLit) expression { - switch t := x.Type.(type) { - case *ast.SelectorExpr: - return s.expectEntity(t, x) - case *ast.ArrayType: - return s.expectArray(t, x) - case *ast.MapType: - return s.expectMap(t, x) - default: - s.explainAndPanic("composite lit type", t) - return nil - } -} - -func (s *Suite) expectExpr(e ast.Expr) expression { - switch x := e.(type) { - case *ast.BasicLit: - return s.expectPrimitive(x) - case *ast.CompositeLit: - return s.expectCompositeLiteral(x) - case *ast.UnaryExpr: - if x.Op != token.AND { - s.explainAndPanic("only references to composite literals are supported", x) - } - y, ok := x.X.(*ast.CompositeLit) - if !ok { - s.explainAndPanic("composite lit", x.X) - } - e, ok := s.expectCompositeLiteral(y).(*entity) - if !ok { - s.explainAndPanic("entity", x) - } - e.IsPointer = true - return e - case *ast.SelectorExpr: - return s.expectLookup(x) - case *ast.Ident: - return &variable{ - Named: code.Named{ - Name: x.Name, - }, - } - case *ast.CallExpr: - return s.expectCall(x) - case *ast.BinaryExpr: - return &binaryExpr{ - Left: s.expectExpr(x.X), - Right: s.expectExpr(x.Y), - Op: x.Op.String(), - } - case *ast.IndexExpr: - return &indexExpr{ - Left: s.expectExpr(x.X), - Right: s.expectExpr(x.Index), - } - default: - s.explainAndPanic("expr", e) - return nil - } -} - -func (s *Suite) explainAndPanic(expected string, x any) any { - ast.Print(s.fset, x) - panic("expected " + expected) -} - -func (s *Suite) expectLookup(se *ast.SelectorExpr) *lookup { - return &lookup{ - X: s.expectExpr(se.X), - Field: &code.Named{ - Name: s.expectIdent(se.Sel), - }, - } -} - -func (s *Suite) inlineRetryExpression(e *ast.CallExpr) *ast.CallExpr { - t, ok := e.Fun.(*ast.SelectorExpr) - if !ok { - return e - } - name := t.Sel.Name - if name != "Run" && name != "Wait" { - return e - } - retryConstructor, ok := t.X.(*ast.CallExpr) - if !ok { - return e - } - retryIndexExpr, ok := retryConstructor.Fun.(*ast.IndexExpr) - if !ok { - return e - } - retrySelector, ok := retryIndexExpr.X.(*ast.SelectorExpr) - if !ok { - return e - } - if retrySelector.X.(*ast.Ident).Name != "retries" { - return e - } - - // inline the retry expression - retryFunc, ok := e.Args[1].(*ast.FuncLit) - if !ok { - s.explainAndPanic("function literal", e.Args[1]) - } - retStmt, ok := retryFunc.Body.List[len(retryFunc.Body.List)-1].(*ast.ReturnStmt) - if !ok { - s.explainAndPanic("return statement", retryFunc.Body.List[len(retryFunc.Body.List)-1]) - } - inlinedCallExpr, ok := retStmt.Results[0].(*ast.CallExpr) - if !ok { - s.explainAndPanic("call expression", retStmt.Results[0]) - } - return inlinedCallExpr -} - -func (s *Suite) expectCall(e ast.Expr) *call { - ce, ok := e.(*ast.CallExpr) - if !ok { - s.explainAndPanic("call expr", e) - return nil - } - s.counter++ - c := &call{ - id: s.counter, - } - // If the call is actually a retry, we inline the returned value here - ce = s.inlineRetryExpression(ce) - switch t := ce.Fun.(type) { - case *ast.Ident: - c.Name = t.Name - case *ast.SelectorExpr: - c.Name = t.Sel.Name - switch se := t.X.(type) { - case *ast.SelectorExpr: - c.Service = &code.Named{ - Name: se.Sel.Name, - } - case *ast.Ident: - c.Service = &code.Named{ - Name: se.Name, - } - default: - s.explainAndPanic("selector expr", se) - return nil - } - } - if c.Service != nil && c.Service.Name == "fmt" { - // fmt.Sprintf is not a service - c.Service = nil - } - for _, v := range ce.Args { - arg := s.expectExpr(v) - if x, ok := arg.(*variable); ok && (x.Name == "ctx" || x.Name == "t") { - // context.Context is irrelevant in other languages than Go - continue - } - c.Args = append(c.Args, arg) - } - return c -} diff --git a/openapi/roll/tool_test.go b/openapi/roll/tool_test.go deleted file mode 100644 index fb7f97356..000000000 --- a/openapi/roll/tool_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package roll - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/databricks/databricks-sdk-go/openapi/code" - "github.com/databricks/databricks-sdk-go/openapi/render" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestLoadsFolder(t *testing.T) { - // ../../internal is the folder with integration tests - s, err := NewSuite("../../internal") - require.NoError(t, err) - - methods := s.Methods() - assert.True(t, len(methods) > 1) - - examples := s.ServicesExamples() - assert.True(t, len(examples) > 1) - - for _, v := range examples { - for _, sa := range v.Samples { - for _, ca := range sa.Calls { - // verify no panic - _ = ca.Original() - - // verify no panic - _ = ca.Request() - } - } - } -} - -func TestOptimize(t *testing.T) { - t.Skip() - ctx := context.Background() - home, _ := os.UserHomeDir() - batch, err := code.NewFromFile(ctx, filepath.Join(home, - "universe/bazel-bin/openapi/all-internal.json")) - require.NoError(t, err) - - s, err := NewSuite("../../internal") - require.NoError(t, err) - - err = s.OptimizeWithApiSpec(batch) - require.NoError(t, err) -} - -func TestRegenerateExamples(t *testing.T) { - t.Skip() // temporary measure - ctx := context.Background() - s, err := NewSuite("../../internal") - require.NoError(t, err) - - target := "../.." - pass := render.NewPass(target, s.ServicesExamples(), map[string]string{ - ".codegen/examples_test.go.tmpl": "service/{{.Package}}/{{.SnakeName}}_usage_test.go", - }, []string{}) - err = pass.Run(ctx) - assert.NoError(t, err) - - err = render.Formatter(ctx, target, pass.Filenames, "go fmt ./... && go run golang.org/x/tools/cmd/goimports@latest -w $FILENAMES") - assert.NoError(t, err) -} - -func TestRegeneratePythonExamples(t *testing.T) { - t.Skip() - ctx := context.Background() - s, err := NewSuite("../../internal") - require.NoError(t, err) - - target := "../../../databricks-sdk-py" - pass := render.NewPass(target, s.Samples(), map[string]string{ - ".codegen/example.py.tmpl": "examples/{{.Service.SnakeName}}/{{.Method.SnakeName}}_{{.SnakeName}}.py", - }, []string{}) - err = pass.Run(ctx) - assert.NoError(t, err) - - err = render.Formatter(ctx, target, pass.Filenames, "yapf -pri $FILENAMES && autoflake -i $FILENAMES && isort $FILENAMES") - assert.NoError(t, err) -} diff --git a/openapi/testdata/spec.json b/openapi/testdata/spec.json deleted file mode 100644 index 366deee47..000000000 --- a/openapi/testdata/spec.json +++ /dev/null @@ -1,468 +0,0 @@ -{ - "openapi": "3.0.0", - "tags": [ - { - "name": "Command Execution", - "x-databricks-package": "commands", - "x-databricks-service": "CommandExecution" - } - ], - "paths": { - "/api/1.2/commands/cancel": { - "post": { - "operationId": "CommandExecution.cancel", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CancelCommand" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "Status was returned successfully." - } - }, - "summary": "Cancel a command", - "tags": [ - "Command Execution" - ], - "x-databricks-wait": { - "bind": "commandId", - "failure": [ - "Error" - ], - "field": [ - "status" - ], - "message": [ - "results", - "cause" - ], - "poll": "commandStatus", - "success": [ - "Cancelled" - ] - } - } - }, - "/api/1.2/commands/execute": { - "post": { - "operationId": "CommandExecution.execute", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Command" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Created" - } - } - }, - "description": "Status was returned successfully." - } - }, - "summary": "Run a command", - "tags": [ - "Command Execution" - ], - "x-databricks-wait": { - "binding": { - "clusterId": { - "request": "clusterId" - }, - "commandId": { - "response": "id" - }, - "contextId": { - "request": "contextId" - } - }, - "failure": [ - "Cancelled", - "Cancelling" - ], - "field": [ - "status" - ], - "poll": "commandStatus", - "success": [ - "Finished", - "Error" - ] - } - } - }, - "/api/1.2/commands/status": { - "get": { - "operationId": "CommandExecution.commandStatus", - "parameters": [ - { - "in": "query", - "name": "clusterId", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "contextId", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "commandId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CommandStatusResponse" - } - } - }, - "description": "Status was returned successfully." - } - }, - "summary": "Get information about a command", - "tags": [ - "Command Execution" - ] - } - }, - "/api/1.2/contexts/create": { - "post": { - "operationId": "CommandExecution.create", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateContext" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Created" - } - } - }, - "description": "Status was returned successfully." - } - }, - "summary": "Create an execution context", - "tags": [ - "Command Execution" - ], - "x-databricks-wait": { - "binding": { - "clusterId": { - "request": "clusterId" - }, - "contextId": { - "response": "id" - } - }, - "failure": [ - "Error" - ], - "field": [ - "status" - ], - "poll": "contextStatus", - "success": [ - "Running" - ] - } - } - }, - "/api/1.2/contexts/destroy": { - "post": { - "operationId": "CommandExecution.destroy", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DestroyContext" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "Status was returned successfully." - } - }, - "summary": "Delete an execution context", - "tags": [ - "Command Execution" - ] - } - }, - "/api/1.2/contexts/status": { - "get": { - "operationId": "CommandExecution.contextStatus", - "parameters": [ - { - "in": "query", - "name": "clusterId", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "contextId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ContextStatusResponse" - } - } - }, - "description": "Status was returned successfully." - } - }, - "summary": "Get information about an execution context", - "tags": [ - "Command Execution" - ] - } - } - }, - "components": { - "schemas": { - "CancelCommand": { - "properties": { - "clusterId": { - "type": "string" - }, - "commandId": { - "type": "string" - }, - "contextId": { - "type": "string" - } - }, - "type": "object" - }, - "Command": { - "properties": { - "clusterId": { - "description": "Running cluster id", - "type": "string" - }, - "command": { - "description": "Executable code", - "type": "string" - }, - "contextId": { - "description": "Running context id", - "type": "string" - }, - "language": { - "$ref": "#/components/schemas/Language" - } - }, - "type": "object" - }, - "CommandStatus": { - "enum": [ - "Cancelled", - "Cancelling", - "Error", - "Finished", - "Queued", - "Running" - ], - "type": "string" - }, - "CommandStatusResponse": { - "properties": { - "id": { - "type": "string" - }, - "results": { - "$ref": "#/components/schemas/Results" - }, - "status": { - "$ref": "#/components/schemas/CommandStatus" - } - }, - "type": "object" - }, - "ContextStatus": { - "enum": [ - "Running", - "Pending", - "Error" - ], - "type": "string" - }, - "ContextStatusResponse": { - "properties": { - "id": { - "type": "string" - }, - "status": { - "$ref": "#/components/schemas/ContextStatus" - } - }, - "type": "object" - }, - "CreateContext": { - "properties": { - "clusterId": { - "description": "Running cluster id", - "type": "string" - }, - "language": { - "$ref": "#/components/schemas/Language" - } - }, - "type": "object" - }, - "Created": { - "properties": { - "id": { - "type": "string" - } - }, - "type": "object" - }, - "DestroyContext": { - "properties": { - "clusterId": { - "type": "string" - }, - "contextId": { - "type": "string" - } - }, - "required": [ - "clusterId", - "contextId" - ], - "type": "object" - }, - "Language": { - "enum": [ - "python", - "scala", - "sql" - ], - "type": "string" - }, - "ResultType": { - "enum": [ - "error", - "image", - "images", - "table", - "text" - ], - "type": "string" - }, - "Results": { - "properties": { - "cause": { - "description": "The cause of the error", - "type": "string" - }, - "data": { - "type": "object", - "x-databricks-any": true - }, - "fileName": { - "description": "The image filename", - "type": "string" - }, - "fileNames": { - "items": { - "type": "string" - }, - "type": "array" - }, - "isJsonSchema": { - "description": "true if a JSON schema is returned instead of a string representation of the Hive type.", - "type": "boolean" - }, - "pos": { - "description": "internal field used by SDK", - "type": "integer" - }, - "resultType": { - "$ref": "#/components/schemas/ResultType" - }, - "schema": { - "description": "The table schema", - "items": { - "items": { - "type": "object", - "x-databricks-any": true - }, - "type": "array" - }, - "type": "array" - }, - "summary": { - "description": "The summary of the error", - "type": "string" - }, - "truncated": { - "description": "true if partial results are returned.", - "type": "boolean" - } - }, - "type": "object" - } - } - } -} \ No newline at end of file