diff --git a/cmd/internal/imports.go b/cmd/internal/imports.go index a0dac175e400..a3f0640ae861 100644 --- a/cmd/internal/imports.go +++ b/cmd/internal/imports.go @@ -107,6 +107,7 @@ import ( _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerconversationalanalytics" _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercreateprojectdirectory" _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercreateprojectfile" + _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercreateviewfromtable" _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerdeleteprojectdirectory" _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerdeleteprojectfile" _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerdevmode" @@ -120,6 +121,7 @@ import ( _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetdimensions" _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetexplores" _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetfilters" + _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetlookmltests" _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetlooks" _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetmeasures" _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetmodels" @@ -138,6 +140,7 @@ import ( _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerqueryurl" _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrundashboard" _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrunlook" + _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrunlookmltests" _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerupdateprojectfile" _ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookervalidateproject" _ "github.com/googleapis/genai-toolbox/internal/tools/mindsdb/mindsdbexecutesql" diff --git a/cmd/internal/tools_file_test.go b/cmd/internal/tools_file_test.go index 31e61392402b..5e445e2935db 100644 --- a/cmd/internal/tools_file_test.go +++ b/cmd/internal/tools_file_test.go @@ -1778,7 +1778,7 @@ func TestPrebuiltTools(t *testing.T) { wantToolset: server.ToolsetConfigs{ "looker_tools": tools.ToolsetConfig{ Name: "looker_tools", - ToolNames: []string{"get_models", "get_explores", "get_dimensions", "get_measures", "get_filters", "get_parameters", "query", "query_sql", "query_url", "get_looks", "run_look", "make_look", "get_dashboards", "run_dashboard", "make_dashboard", "add_dashboard_element", "add_dashboard_filter", "generate_embed_url", "health_pulse", "health_analyze", "health_vacuum", "dev_mode", "get_projects", "get_project_files", "get_project_file", "create_project_file", "update_project_file", "delete_project_file", "get_project_directories", "create_project_directory", "delete_project_directory", "validate_project", "get_connections", "get_connection_schemas", "get_connection_databases", "get_connection_tables", "get_connection_table_columns"}, + ToolNames: []string{"get_models", "get_explores", "get_dimensions", "get_measures", "get_filters", "get_parameters", "query", "query_sql", "query_url", "get_looks", "run_look", "make_look", "get_dashboards", "run_dashboard", "make_dashboard", "add_dashboard_element", "add_dashboard_filter", "generate_embed_url", "health_pulse", "health_analyze", "health_vacuum", "dev_mode", "get_projects", "get_project_files", "get_project_file", "create_project_file", "update_project_file", "delete_project_file", "get_project_directories", "create_project_directory", "delete_project_directory", "validate_project", "get_connections", "get_connection_schemas", "get_connection_databases", "get_connection_tables", "get_connection_table_columns", "get_lookml_tests", "run_lookml_tests", "create_view_from_table"}, }, }, }, diff --git a/docs/en/reference/prebuilt-tools.md b/docs/en/reference/prebuilt-tools.md index 5330d1735d48..4e36ebfa2c41 100644 --- a/docs/en/reference/prebuilt-tools.md +++ b/docs/en/reference/prebuilt-tools.md @@ -552,6 +552,9 @@ See [Usage Examples](../reference/cli.md#examples). * `get_connection_databases`: Get the available databases in a connection. * `get_connection_tables`: Get the available tables in a connection. * `get_connection_table_columns`: Get the available columns for a table. + * `get_lookml_tests`: Retrieves a list of available LookML tests for a project. + * `run_lookml_tests`: Executes specific LookML tests within a project. + * `create_view_from_table`: Generates boilerplate LookML views directly from the database schema. ## Looker Conversational Analytics diff --git a/docs/en/resources/tools/looker/looker-create-view-from-table.md b/docs/en/resources/tools/looker/looker-create-view-from-table.md new file mode 100644 index 000000000000..d40117826e89 --- /dev/null +++ b/docs/en/resources/tools/looker/looker-create-view-from-table.md @@ -0,0 +1,50 @@ +--- +title: "looker-create-view-from-table" +type: docs +weight: 1 +description: > + This tool generates boilerplate LookML views directly from the database schema. +aliases: +- /resources/tools/looker-create-view-from-table +--- + +## About + +A "looker-create-view-from-table" tool triggers the automatic generation of LookML view files based on database tables. + +It's compatible with the following sources: + +- [looker](../../sources/looker.md) + +`looker-create-view-from-table` accepts project_id, connection, tables, and folder_name parameters. + +## Example + +```yaml +tools: + create_view_from_table: + kind: looker-create-view-from-table + source: looker-source + description: | + This tool generates boilerplate LookML views directly from the database schema. + It does not create model or explore files, only view files in the specified folder. + + Prerequisite: The Looker session must be in Development Mode. Use `dev_mode: true` first. + + Parameters: + - project_id (required): The unique ID of the LookML project. + - connection (required): The database connection name. + - tables (required): A list of objects to generate views for. Each object must contain `schema` and `table_name` (note: table names are case-sensitive). Optional fields include `primary_key`, `base_view`, and `columns` (array of objects with `column_name`). + - folder_name (optional): The folder to place the view files in (defaults to 'views/'). + + Output: + A confirmation message upon successful view generation, or an error message if the operation fails. +``` + +## Reference + +| **field** | **type** | **required** | **description** | +|-------------|:--------:|:------------:|----------------------------------------------------| +| kind | string | true | Must be "looker-create-view-from-table". | +| source | string | true | Name of the source Looker instance. | +| description | string | true | Description of the tool that is passed to the LLM. | diff --git a/docs/en/resources/tools/looker/looker-get-lookml-tests.md b/docs/en/resources/tools/looker/looker-get-lookml-tests.md new file mode 100644 index 000000000000..7148034ef2d1 --- /dev/null +++ b/docs/en/resources/tools/looker/looker-get-lookml-tests.md @@ -0,0 +1,53 @@ +--- +title: "looker-get-lookml-tests" +type: docs +weight: 1 +description: > + Returns a list of tests which can be run to validate a project's LookML code and/or the underlying data, optionally filtered by the file id. +aliases: +- /resources/tools/looker-get-lookml-tests +--- + +## About + +A "looker-get-lookml-tests" tool retrieves a list of available LookML tests for a project. + +It's compatible with the following sources: + +- [looker](../../sources/looker.md) + +`looker-get-lookml-tests` accepts project_id and file_id parameters. + +## Example + +```yaml +tools: + get_lookml_tests: + kind: looker-get-lookml-tests + source: looker-source + description: | + Returns a list of tests which can be run to validate a project's LookML code and/or the underlying data, optionally filtered by the file id. + + Prerequisite: The Looker session must be in Development Mode. Use `dev_mode: true` first. + + Parameters: + - project_id (required): The unique ID of the LookML project. + - file_id (optional): The ID of the file to filter tests by. This must be the complete file path from the project root (e.g., `models/my_model.model.lkml` or `views/my_view.view.lkml`). + + Output: + A JSON array of LookML test objects, each containing: + - model_name: The name of the model. + - name: The name of the test. + - explore_name: The name of the explore being tested. + - query_url_params: The query parameters used for the test. + - file: The file path where the test is defined. + - line: The line number where the test is defined. +``` + +## Reference + +| **field** | **type** | **required** | **description** | +|-------------|:--------:|:------------:|----------------------------------------------------| +| kind | string | true | Must be "looker-get-lookml-tests". | +| source | string | true | Name of the source Looker instance. | +| description | string | true | Description of the tool that is passed to the LLM. | diff --git a/docs/en/resources/tools/looker/looker-run-lookml-tests.md b/docs/en/resources/tools/looker/looker-run-lookml-tests.md new file mode 100644 index 000000000000..94c80c433b87 --- /dev/null +++ b/docs/en/resources/tools/looker/looker-run-lookml-tests.md @@ -0,0 +1,56 @@ +--- +title: "looker-run-lookml-tests" +type: docs +weight: 1 +description: > + This tool runs LookML tests in the project, filtered by file, test, and/or model. +aliases: +- /resources/tools/looker-run-lookml-tests +--- + +## About + +A "looker-run-lookml-tests" tool executes specific LookML tests within a project. + +It's compatible with the following sources: + +- [looker](../../sources/looker.md) + +`looker-run-lookml-tests` accepts project_id, file_id, test, and model parameters. + +## Example + +```yaml +tools: + run_lookml_tests: + kind: looker-run-lookml-tests + source: looker-source + description: | + This tool runs LookML tests in the project, filtered by file, test, and/or model. These filters work in conjunction (logical AND). + + Prerequisite: The Looker session must be in Development Mode. Use `dev_mode: true` first. + + Parameters: + - project_id (required): The unique ID of the project to run LookML tests for. + - file_id (optional): The ID of the file to run tests for. This must be the complete file path from the project root (e.g., `models/my_model.model.lkml` or `views/my_view.view.lkml`). + - test (optional): The name of the test to run. + - model (optional): The name of the model to run tests for. + + Output: + A JSON array containing the results of the executed tests, where each object includes: + - model_name: Name of the model tested. + - test_name: Name of the test. + - assertions_count: Total number of assertions in the test. + - assertions_failed: Number of assertions that failed. + - success: Boolean indicating if the test passed. + - errors: Array of error objects (if any), containing details like `message`, `file_path`, `line_number`, and `severity`. + - warnings: Array of warning messages (if any). +``` + +## Reference + +| **field** | **type** | **required** | **description** | +|-------------|:--------:|:------------:|----------------------------------------------------| +| kind | string | true | Must be "looker-run-lookml-tests". | +| source | string | true | Name of the source Looker instance. | +| description | string | true | Description of the tool that is passed to the LLM. | diff --git a/internal/prebuiltconfigs/tools/looker.yaml b/internal/prebuiltconfigs/tools/looker.yaml index 2f3f62f670eb..b751de822a3d 100644 --- a/internal/prebuiltconfigs/tools/looker.yaml +++ b/internal/prebuiltconfigs/tools/looker.yaml @@ -1098,6 +1098,68 @@ tools: A JSON array of objects, where each object represents a column and contains details such as `table_name`, `column_name`, `data_type`, and `is_nullable`. + get_lookml_tests: + kind: looker-get-lookml-tests + source: looker-source + description: | + Returns a list of tests which can be run to validate a project's LookML code and/or the underlying data, optionally filtered by the file id. + + Prerequisite: The Looker session must be in Development Mode. Use `dev_mode: true` first. + + Parameters: + - project_id (required): The unique ID of the LookML project. + - file_id (optional): The ID of the file to filter tests by. This must be the complete file path from the project root (e.g., `models/my_model.model.lkml` or `views/my_view.view.lkml`). + + Output: + A JSON array of LookML test objects, each containing: + - model_name: The name of the model. + - name: The name of the test. + - explore_name: The name of the explore being tested. + - query_url_params: The query parameters used for the test. + - file: The file path where the test is defined. + - line: The line number where the test is defined. + + run_lookml_tests: + kind: looker-run-lookml-tests + source: looker-source + description: | + This tool runs LookML tests in the project, filtered by file, test, and/or model. These filters work in conjunction (logical AND). + + Prerequisite: The Looker session must be in Development Mode. Use `dev_mode: true` first. + + Parameters: + - project_id (required): The unique ID of the project to run LookML tests for. + - file_id (optional): The ID of the file to run tests for. This must be the complete file path from the project root (e.g., `models/my_model.model.lkml` or `views/my_view.view.lkml`). + - test (optional): The name of the test to run. + - model (optional): The name of the model to run tests for. + + Output: + A JSON array containing the results of the executed tests, where each object includes: + - model_name: Name of the model tested. + - test_name: Name of the test. + - assertions_count: Total number of assertions in the test. + - assertions_failed: Number of assertions that failed. + - success: Boolean indicating if the test passed. + - errors: Array of error objects (if any), containing details like `message`, `file_path`, `line_number`, and `severity`. + - warnings: Array of warning messages (if any). + + create_view_from_table: + kind: looker-create-view-from-table + source: looker-source + description: | + This tool generates boilerplate LookML views directly from the database schema. + It does not create model or explore files, only view files in the specified folder. + + Prerequisite: The Looker session must be in Development Mode. Use `dev_mode: true` first. + + Parameters: + - project_id (required): The unique ID of the LookML project. + - connection (required): The database connection name. + - tables (required): A list of objects to generate views for. Each object must contain `schema` and `table_name` (note: table names are case-sensitive). Optional fields include `primary_key`, `base_view`, and `columns` (array of objects with `column_name`). + - folder_name (optional): The folder to place the view files in (defaults to 'views/'). + + Output: + A confirmation message upon successful view generation, or an error message if the operation fails. toolsets: looker_tools: @@ -1138,3 +1200,6 @@ toolsets: - get_connection_databases - get_connection_tables - get_connection_table_columns + - get_lookml_tests + - run_lookml_tests + - create_view_from_table diff --git a/internal/tools/looker/lookercommon/lookercommon.go b/internal/tools/looker/lookercommon/lookercommon.go index 1749a459bd0d..683b82c01cd9 100644 --- a/internal/tools/looker/lookercommon/lookercommon.go +++ b/internal/tools/looker/lookercommon/lookercommon.go @@ -330,3 +330,45 @@ func DeleteProjectDirectory(l *v4.LookerSDK, projectId string, directoryPath str path := fmt.Sprintf("/projects/%s/directories", url.PathEscape(projectId)) return l.AuthSession.Do(&result, "DELETE", "/4.0", path, query, nil, options) } + +type ProjectGeneratorColumn struct { + ColumnName string `json:"column_name"` +} + +type ProjectGeneratorTable struct { + Schema string `json:"schema"` + TableName string `json:"table_name"` + PrimaryKey *string `json:"primary_key,omitempty"` + BaseView *bool `json:"base_view,omitempty"` + Columns []ProjectGeneratorColumn `json:"columns,omitempty"` +} + +type ProjectGeneratorRequestBody struct { + Tables []ProjectGeneratorTable `json:"tables"` +} + +type ProjectGeneratorQueryParams struct { + Connection string `json:"connection"` + FileTypeForExplores string `json:"file_type_for_explores"` + FolderName string `json:"folder_name,omitempty"` +} + +func CreateViewsFromTables(ctx context.Context, l *v4.LookerSDK, projectId string, queryParams ProjectGeneratorQueryParams, reqBody ProjectGeneratorRequestBody, options *rtl.ApiSettings) error { + path := fmt.Sprintf("/projects/%s/generate", url.PathEscape(projectId)) + + // Construct query parameter map + query := map[string]any{ + "connection": queryParams.Connection, + "file_type_for_explores": queryParams.FileTypeForExplores, + "folder_name": queryParams.FolderName, + } + + // Pass the Tables slice directly as the body, not the wrapped struct. + // The API spec defines `tables` as `body_param ... array: true`, + // which means the body itself should be the array. + err := l.AuthSession.Do(nil, "POST", "/4.0", path, query, reqBody.Tables, options) + + logger, _ := util.LoggerFromContext(ctx) + logger.DebugContext(ctx, fmt.Sprintf("generating views with request: query=%v body=%v error=%v", query, reqBody.Tables, err)) + return err +} diff --git a/internal/tools/looker/lookercreateviewfromtable/lookercreateviewfromtable.go b/internal/tools/looker/lookercreateviewfromtable/lookercreateviewfromtable.go new file mode 100644 index 000000000000..08c6d75c15b1 --- /dev/null +++ b/internal/tools/looker/lookercreateviewfromtable/lookercreateviewfromtable.go @@ -0,0 +1,272 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package lookercreateviewfromtable + +import ( + "context" + "fmt" + "net/http" + + yaml "github.com/goccy/go-yaml" + "github.com/googleapis/genai-toolbox/internal/embeddingmodels" + "github.com/googleapis/genai-toolbox/internal/sources" + "github.com/googleapis/genai-toolbox/internal/tools" + "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon" + "github.com/googleapis/genai-toolbox/internal/util" + "github.com/googleapis/genai-toolbox/internal/util/parameters" + + "github.com/looker-open-source/sdk-codegen/go/rtl" + v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" +) + +const resourceType string = "looker-create-view-from-table" + +func init() { + if !tools.Register(resourceType, newConfig) { + panic(fmt.Sprintf("tool type %q already registered", resourceType)) + } +} + +func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { + actual := Config{Name: name} + if err := decoder.DecodeContext(ctx, &actual); err != nil { + return nil, err + } + return actual, nil +} + +type compatibleSource interface { + UseClientAuthorization() bool + GetAuthTokenHeaderName() string + LookerApiSettings() *rtl.ApiSettings + GetLookerSDK(string) (*v4.LookerSDK, error) +} + +type Config struct { + Name string `yaml:"name" validate:"required"` + Type string `yaml:"type" validate:"required"` + Source string `yaml:"source" validate:"required"` + Description string `yaml:"description" validate:"required"` + AuthRequired []string `yaml:"authRequired"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` +} + +// validate interface +var _ tools.ToolConfig = Config{} + +func (cfg Config) ToolConfigType() string { + return resourceType +} + +func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { + projectIdParameter := parameters.NewStringParameter("project_id", "The id of the project to create the view in.") + connectionParameter := parameters.NewStringParameter("connection", "The database connection name.") + + tableDef := parameters.NewMapParameter("table", "Table definition.", "") + tablesParameter := parameters.NewArrayParameter("tables", `The tables to generate views for. + Each item must be a map with: + - schema (string, required) + - table_name (string, required) + - primary_key (string, optional) + - base_view (boolean, optional) + - columns (array of objects, optional): Each object must have 'column_name' (string).`, tableDef) + + folderNameParameter := parameters.NewStringParameterWithDefault("folder_name", "views", "The folder to place the view files in (e.g., 'views').") + + params := parameters.Parameters{projectIdParameter, connectionParameter, tablesParameter, folderNameParameter} + + annotations := cfg.Annotations + if annotations == nil { + readOnlyHint := false + annotations = &tools.ToolAnnotations{ + ReadOnlyHint: &readOnlyHint, + } + } + + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations) + + // finish tool setup + return Tool{ + Config: cfg, + Parameters: params, + manifest: tools.Manifest{ + Description: cfg.Description, + Parameters: params.Manifest(), + AuthRequired: cfg.AuthRequired, + }, + mcpManifest: mcpManifest, + }, nil +} + +// validate interface +var _ tools.Tool = Tool{} + +type Tool struct { + Config + Parameters parameters.Parameters `yaml:"parameters"` + manifest tools.Manifest + mcpManifest tools.McpManifest +} + +func (t Tool) ToConfig() tools.ToolConfig { + return t.Config +} + +func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err) + } + + logger, err := util.LoggerFromContext(ctx) + if err != nil { + return nil, util.NewClientServerError(fmt.Sprintf("error getting logger from context: %s", err), http.StatusInternalServerError, err) + } + + sdk, err := source.GetLookerSDK(string(accessToken)) + if err != nil { + return nil, util.NewClientServerError(fmt.Sprintf("error getting sdk: %v", err), http.StatusInternalServerError, err) + } + + mapParams := params.AsMap() + projectId, ok := mapParams["project_id"].(string) + if !ok { + return nil, util.NewAgentError(fmt.Sprintf("'project_id' must be a string, got %T", mapParams["project_id"]), nil) + } + connection, ok := mapParams["connection"].(string) + if !ok { + return nil, util.NewAgentError(fmt.Sprintf("'connection' must be a string, got %T", mapParams["connection"]), nil) + } + folderName, ok := mapParams["folder_name"].(string) + if !ok { + return nil, util.NewAgentError(fmt.Sprintf("'folder_name' must be a string, got %T", mapParams["folder_name"]), nil) + } + + tablesSlice, ok := mapParams["tables"].([]any) + if !ok { + return nil, util.NewAgentError(fmt.Sprintf("'tables' must be an array, got %T", mapParams["tables"]), nil) + } + + logger.DebugContext(ctx, "generating views with request", "tables", tablesSlice) + + var generatorTables []lookercommon.ProjectGeneratorTable + for _, tRaw := range tablesSlice { + t, ok := tRaw.(map[string]any) + if !ok { + return nil, util.NewClientServerError(fmt.Sprintf("expected map in tables list, got %T", tRaw), http.StatusInternalServerError, nil) + } + + var schema, tableName string + var primaryKey *string + var baseView *bool + var columns []lookercommon.ProjectGeneratorColumn + + if s, ok := t["schema"].(string); ok { + schema = s + } + if tn, ok := t["table_name"].(string); ok { + tableName = tn + } + // Enforce required fields for map input + if schema == "" || tableName == "" { + return nil, util.NewClientServerError("schema and table_name are required in table map", http.StatusInternalServerError, nil) + } + + if pk, ok := t["primary_key"].(string); ok { + primaryKey = &pk + } + if bv, ok := t["base_view"].(bool); ok { + baseView = &bv + } + if colsRaw, ok := t["columns"].([]any); ok { + for _, cRaw := range colsRaw { + if cMap, ok := cRaw.(map[string]any); ok { + if cName, ok := cMap["column_name"].(string); ok { + columns = append(columns, lookercommon.ProjectGeneratorColumn{ColumnName: cName}) + } + } + } + } + + if tableName == "" { + continue // Skip invalid entries + } + + generatorTables = append(generatorTables, lookercommon.ProjectGeneratorTable{ + Schema: schema, + TableName: tableName, + PrimaryKey: primaryKey, + BaseView: baseView, + Columns: columns, + }) + } + + queryParams := lookercommon.ProjectGeneratorQueryParams{ + Connection: connection, + FileTypeForExplores: "none", + FolderName: folderName, + } + + reqBody := lookercommon.ProjectGeneratorRequestBody{ + Tables: generatorTables, + } + + logger.DebugContext(ctx, "generating views with request", "query", queryParams, "body", reqBody) + + err = lookercommon.CreateViewsFromTables(ctx, sdk, projectId, queryParams, reqBody, source.LookerApiSettings()) + if err != nil { + return nil, util.NewClientServerError(fmt.Sprintf("error generating views: %s", err), http.StatusInternalServerError, err) + } + + return map[string]string{ + "status": "success", + "message": fmt.Sprintf("Triggered view generation for project %s in folder %s", projectId, folderName), + }, nil +} + +func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { + return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil) +} + +func (t Tool) Manifest() tools.Manifest { + return t.manifest +} + +func (t Tool) McpManifest() tools.McpManifest { + return t.mcpManifest +} + +func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return false, err + } + return source.UseClientAuthorization(), nil +} + +func (t Tool) Authorized(verifiedAuthServices []string) bool { + return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) +} + +func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return "", err + } + return source.GetAuthTokenHeaderName(), nil +} + +func (t Tool) GetParameters() parameters.Parameters { + return t.Parameters +} diff --git a/internal/tools/looker/lookercreateviewfromtable/lookercreateviewfromtable_test.go b/internal/tools/looker/lookercreateviewfromtable/lookercreateviewfromtable_test.go new file mode 100644 index 000000000000..265345442f32 --- /dev/null +++ b/internal/tools/looker/lookercreateviewfromtable/lookercreateviewfromtable_test.go @@ -0,0 +1,109 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lookercreateviewfromtable_test + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/googleapis/genai-toolbox/internal/server" + "github.com/googleapis/genai-toolbox/internal/testutils" + lkr "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercreateviewfromtable" +) + +func TestParseFromYamlLookerCreateViewFromTable(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tcs := []struct { + desc string + in string + want server.ToolConfigs + }{ + { + desc: "basic example", + in: ` + kind: tools + name: example_tool + type: looker-create-view-from-table + source: my-instance + description: some description + `, + want: server.ToolConfigs{ + "example_tool": lkr.Config{ + Name: "example_tool", + Type: "looker-create-view-from-table", + Source: "my-instance", + Description: "some description", + AuthRequired: []string{}, + }, + }, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + // Parse contents + _, _, _, got, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("incorrect parse: diff %v", diff) + } + }) + } + +} + +func TestFailParseFromYamlLookerCreateViewFromTable(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tcs := []struct { + desc string + in string + err string + }{ + { + desc: "Invalid method", + in: ` + kind: tools + name: example_tool + type: looker-create-view-from-table + source: my-instance + method: GOT + description: some description + `, + err: "error unmarshaling tools: unable to parse tool \"example_tool\" as type \"looker-create-view-from-table\": [3:1] unknown field \"method\"", + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + // Parse contents + _, _, _, _, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err == nil { + t.Fatalf("expect parsing to fail") + } + errStr := err.Error() + if !strings.Contains(errStr, tc.err) { + t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err) + } + }) + } + +} diff --git a/internal/tools/looker/lookergetlookmltests/lookergetlookmltests.go b/internal/tools/looker/lookergetlookmltests/lookergetlookmltests.go new file mode 100644 index 000000000000..d1e9df65700d --- /dev/null +++ b/internal/tools/looker/lookergetlookmltests/lookergetlookmltests.go @@ -0,0 +1,178 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lookergetlookmltests + +import ( + "context" + "fmt" + "net/http" + + yaml "github.com/goccy/go-yaml" + "github.com/googleapis/genai-toolbox/internal/embeddingmodels" + "github.com/googleapis/genai-toolbox/internal/sources" + "github.com/googleapis/genai-toolbox/internal/tools" + "github.com/googleapis/genai-toolbox/internal/util" + "github.com/googleapis/genai-toolbox/internal/util/parameters" + + "github.com/looker-open-source/sdk-codegen/go/rtl" + v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" +) + +const resourceType string = "looker-get-lookml-tests" + +func init() { + if !tools.Register(resourceType, newConfig) { + panic(fmt.Sprintf("tool type %q already registered", resourceType)) + } +} + +func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { + actual := Config{Name: name} + if err := decoder.DecodeContext(ctx, &actual); err != nil { + return nil, err + } + return actual, nil +} + +type compatibleSource interface { + UseClientAuthorization() bool + GetAuthTokenHeaderName() string + LookerApiSettings() *rtl.ApiSettings + GetLookerSDK(string) (*v4.LookerSDK, error) +} + +type Config struct { + Name string `yaml:"name" validate:"required"` + Type string `yaml:"type" validate:"required"` + Source string `yaml:"source" validate:"required"` + Description string `yaml:"description" validate:"required"` + AuthRequired []string `yaml:"authRequired"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` +} + +// validate interface +var _ tools.ToolConfig = Config{} + +func (cfg Config) ToolConfigType() string { + return resourceType +} + +func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { + projectIdParameter := parameters.NewStringParameter("project_id", "The unique ID of the LookML project.") + fileIdParameter := parameters.NewStringParameterWithRequired("file_id", "Optional ID of the file to filter tests by. This must be the complete file path from the project root (e.g., 'models/my_model.model.lkml').", false) + params := parameters.Parameters{projectIdParameter, fileIdParameter} + + annotations := cfg.Annotations + if annotations == nil { + readOnlyHint := true + annotations = &tools.ToolAnnotations{ + ReadOnlyHint: &readOnlyHint, + } + } + + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations) + + // finish tool setup + return Tool{ + Config: cfg, + Parameters: params, + manifest: tools.Manifest{ + Description: cfg.Description, + Parameters: params.Manifest(), + AuthRequired: cfg.AuthRequired, + }, + mcpManifest: mcpManifest, + }, nil +} + +// validate interface +var _ tools.Tool = Tool{} + +type Tool struct { + Config + Parameters parameters.Parameters `yaml:"parameters"` + manifest tools.Manifest + mcpManifest tools.McpManifest +} + +func (t Tool) ToConfig() tools.ToolConfig { + return t.Config +} + +func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err) + } + + sdk, err := source.GetLookerSDK(string(accessToken)) + if err != nil { + return nil, util.NewClientServerError(fmt.Sprintf("error getting sdk: %v", err), http.StatusInternalServerError, err) + } + + mapParams := params.AsMap() + projectId, ok := mapParams["project_id"].(string) + if !ok { + return nil, util.NewAgentError(fmt.Sprintf("'project_id' must be a string, got %T", mapParams["project_id"]), nil) + } + + var fileId string + if val, ok := mapParams["file_id"].(string); ok { + fileId = val + } + + resp, err := sdk.AllLookmlTests(projectId, fileId, source.LookerApiSettings()) + if err != nil { + return nil, util.NewClientServerError(fmt.Sprintf("error retrieving lookml tests: %s", err), http.StatusInternalServerError, err) + } + + return resp, nil +} + +func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { + return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil) +} + +func (t Tool) Manifest() tools.Manifest { + return t.manifest +} + +func (t Tool) McpManifest() tools.McpManifest { + return t.mcpManifest +} + +func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return false, err + } + return source.UseClientAuthorization(), nil +} + +func (t Tool) Authorized(verifiedAuthServices []string) bool { + return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) +} + +func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return "", err + } + return source.GetAuthTokenHeaderName(), nil +} + +func (t Tool) GetParameters() parameters.Parameters { + return t.Parameters +} diff --git a/internal/tools/looker/lookergetlookmltests/lookergetlookmltests_test.go b/internal/tools/looker/lookergetlookmltests/lookergetlookmltests_test.go new file mode 100644 index 000000000000..e00b2bdac226 --- /dev/null +++ b/internal/tools/looker/lookergetlookmltests/lookergetlookmltests_test.go @@ -0,0 +1,109 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lookergetlookmltests_test + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/googleapis/genai-toolbox/internal/server" + "github.com/googleapis/genai-toolbox/internal/testutils" + lkr "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetlookmltests" +) + +func TestParseFromYamlLookerGetLookmlTests(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tcs := []struct { + desc string + in string + want server.ToolConfigs + }{ + { + desc: "basic example", + in: ` + kind: tools + name: example_tool + type: looker-get-lookml-tests + source: my-instance + description: some description + `, + want: server.ToolConfigs{ + "example_tool": lkr.Config{ + Name: "example_tool", + Type: "looker-get-lookml-tests", + Source: "my-instance", + Description: "some description", + AuthRequired: []string{}, + }, + }, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + // Parse contents + _, _, _, got, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("incorrect parse: diff %v", diff) + } + }) + } + +} + +func TestFailParseFromYamlLookerGetAllLookmlTests(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tcs := []struct { + desc string + in string + err string + }{ + { + desc: "Invalid method", + in: ` + kind: tools + name: example_tool + type: looker-get-lookml-tests + source: my-instance + method: GOT + description: some description + `, + err: "error unmarshaling tools: unable to parse tool \"example_tool\" as type \"looker-get-lookml-tests\": [3:1] unknown field \"method\"", + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + // Parse contents + _, _, _, _, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err == nil { + t.Fatalf("expect parsing to fail") + } + errStr := err.Error() + if !strings.Contains(errStr, tc.err) { + t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err) + } + }) + } + +} diff --git a/internal/tools/looker/lookerrunlookmltests/lookerrunlookmltests.go b/internal/tools/looker/lookerrunlookmltests/lookerrunlookmltests.go new file mode 100644 index 000000000000..43f242203a7d --- /dev/null +++ b/internal/tools/looker/lookerrunlookmltests/lookerrunlookmltests.go @@ -0,0 +1,199 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package lookerrunlookmltests + +import ( + "context" + "fmt" + "net/http" + + yaml "github.com/goccy/go-yaml" + "github.com/googleapis/genai-toolbox/internal/embeddingmodels" + "github.com/googleapis/genai-toolbox/internal/sources" + "github.com/googleapis/genai-toolbox/internal/tools" + "github.com/googleapis/genai-toolbox/internal/util" + "github.com/googleapis/genai-toolbox/internal/util/parameters" + + "github.com/looker-open-source/sdk-codegen/go/rtl" + v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" +) + +const resourceType string = "looker-run-lookml-tests" + +func init() { + if !tools.Register(resourceType, newConfig) { + panic(fmt.Sprintf("tool type %q already registered", resourceType)) + } +} + +func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { + actual := Config{Name: name} + if err := decoder.DecodeContext(ctx, &actual); err != nil { + return nil, err + } + return actual, nil +} + +type compatibleSource interface { + UseClientAuthorization() bool + GetAuthTokenHeaderName() string + LookerApiSettings() *rtl.ApiSettings + GetLookerSDK(string) (*v4.LookerSDK, error) +} + +type Config struct { + Name string `yaml:"name" validate:"required"` + Type string `yaml:"type" validate:"required"` + Source string `yaml:"source" validate:"required"` + Description string `yaml:"description" validate:"required"` + AuthRequired []string `yaml:"authRequired"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` +} + +// validate interface +var _ tools.ToolConfig = Config{} + +func (cfg Config) ToolConfigType() string { + return resourceType +} + +func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { + projectIdParameter := parameters.NewStringParameter("project_id", "The id of the project to run LookML tests for.") + fileIdParameter := parameters.NewStringParameterWithRequired("file_id", "Optional id of the file to run tests for.", false) + testParameter := parameters.NewStringParameterWithRequired("test", "Optional name of the test to run.", false) + modelParameter := parameters.NewStringParameterWithRequired("model", "Optional name of the model to run tests for.", false) + params := parameters.Parameters{projectIdParameter, fileIdParameter, testParameter, modelParameter} + + annotations := cfg.Annotations + if annotations == nil { + readOnlyHint := true + annotations = &tools.ToolAnnotations{ + ReadOnlyHint: &readOnlyHint, + } + } + + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations) + + // finish tool setup + return Tool{ + Config: cfg, + Parameters: params, + manifest: tools.Manifest{ + Description: cfg.Description, + Parameters: params.Manifest(), + AuthRequired: cfg.AuthRequired, + }, + mcpManifest: mcpManifest, + }, nil +} + +// validate interface +var _ tools.Tool = Tool{} + +type Tool struct { + Config + Parameters parameters.Parameters `yaml:"parameters"` + manifest tools.Manifest + mcpManifest tools.McpManifest +} + +func (t Tool) ToConfig() tools.ToolConfig { + return t.Config +} + +func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err) + } + + sdk, err := source.GetLookerSDK(string(accessToken)) + if err != nil { + return nil, util.NewClientServerError(fmt.Sprintf("error getting sdk: %v", err), http.StatusInternalServerError, err) + } + + mapParams := params.AsMap() + projectId, ok := mapParams["project_id"].(string) + if !ok { + return nil, util.NewAgentError(fmt.Sprintf("'project_id' must be a string, got %T", mapParams["project_id"]), nil) + } + + var fileId *string + if val, ok := mapParams["file_id"].(string); ok && val != "" { + fileId = &val + } + + var test *string + if val, ok := mapParams["test"].(string); ok && val != "" { + test = &val + } + + var model *string + if val, ok := mapParams["model"].(string); ok && val != "" { + model = &val + } + + req := v4.RequestRunLookmlTest{ + ProjectId: projectId, + FileId: fileId, + Test: test, + Model: model, + } + + resp, err := sdk.RunLookmlTest(req, source.LookerApiSettings()) + if err != nil { + return nil, util.NewClientServerError(fmt.Sprintf("error running lookml tests: %s", err), http.StatusInternalServerError, err) + } + + // Filter out pointer fields for better JSON marshaling in basic map if needed, + // but the SDK struct usually has JSON tags. + // Returning directly as it should marshal correctly. + return resp, nil +} + +func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { + return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil) +} + +func (t Tool) Manifest() tools.Manifest { + return t.manifest +} + +func (t Tool) McpManifest() tools.McpManifest { + return t.mcpManifest +} + +func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return false, err + } + return source.UseClientAuthorization(), nil +} + +func (t Tool) Authorized(verifiedAuthServices []string) bool { + return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) +} + +func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return "", err + } + return source.GetAuthTokenHeaderName(), nil +} + +func (t Tool) GetParameters() parameters.Parameters { + return t.Parameters +} diff --git a/internal/tools/looker/lookerrunlookmltests/lookerrunlookmltests_test.go b/internal/tools/looker/lookerrunlookmltests/lookerrunlookmltests_test.go new file mode 100644 index 000000000000..6352fb4257f9 --- /dev/null +++ b/internal/tools/looker/lookerrunlookmltests/lookerrunlookmltests_test.go @@ -0,0 +1,109 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lookerrunlookmltests_test + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/googleapis/genai-toolbox/internal/server" + "github.com/googleapis/genai-toolbox/internal/testutils" + lkr "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrunlookmltests" +) + +func TestParseFromYamlLookerRunLookmlTests(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tcs := []struct { + desc string + in string + want server.ToolConfigs + }{ + { + desc: "basic example", + in: ` + kind: tools + name: example_tool + type: looker-run-lookml-tests + source: my-instance + description: some description + `, + want: server.ToolConfigs{ + "example_tool": lkr.Config{ + Name: "example_tool", + Type: "looker-run-lookml-tests", + Source: "my-instance", + Description: "some description", + AuthRequired: []string{}, + }, + }, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + // Parse contents + _, _, _, got, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("incorrect parse: diff %v", diff) + } + }) + } + +} + +func TestFailParseFromYamlLookerRunLookmlTests(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + tcs := []struct { + desc string + in string + err string + }{ + { + desc: "Invalid method", + in: ` + kind: tools + name: example_tool + type: looker-run-lookml-tests + source: my-instance + method: GOT + description: some description + `, + err: "error unmarshaling tools: unable to parse tool \"example_tool\" as type \"looker-run-lookml-tests\": [3:1] unknown field \"method\"", + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + // Parse contents + _, _, _, _, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err == nil { + t.Fatalf("expect parsing to fail") + } + errStr := err.Error() + if !strings.Contains(errStr, tc.err) { + t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err) + } + }) + } + +} diff --git a/tests/looker/looker_integration_test.go b/tests/looker/looker_integration_test.go index 33688899626c..5c275333ed14 100644 --- a/tests/looker/looker_integration_test.go +++ b/tests/looker/looker_integration_test.go @@ -272,6 +272,21 @@ func TestLooker(t *testing.T) { "source": "my-instance", "description": "Simple tool to test end to end functionality.", }, + "get_lookml_tests": map[string]any{ + "type": "looker-get-lookml-tests", + "source": "my-instance", + "description": "Simple tool to test end to end functionality.", + }, + "run_lookml_tests": map[string]any{ + "type": "looker-run-lookml-tests", + "source": "my-instance", + "description": "Simple tool to test end to end functionality.", + }, + "create_view_from_table": map[string]any{ + "type": "looker-create-view-from-table", + "source": "my-instance", + "description": "Simple tool to test end to end functionality.", + }, }, } @@ -696,6 +711,115 @@ func TestLooker(t *testing.T) { }, }, ) + tests.RunToolGetTestByName(t, "get_lookml_tests", + map[string]any{ + "get_lookml_tests": map[string]any{ + "description": "Simple tool to test end to end functionality.", + "authRequired": []any{}, + "parameters": []any{ + map[string]any{ + "authSources": []any{}, + "description": "The unique ID of the LookML project.", + "name": "project_id", + "required": true, + "type": "string", + }, + map[string]any{ + "authSources": []any{}, + "description": "Optional ID of the file to filter tests by. This must be the complete file path from the project root (e.g., 'models/my_model.model.lkml').", + "name": "file_id", + "required": false, + "type": "string", + }, + }, + }, + }, + ) + tests.RunToolGetTestByName(t, "run_lookml_tests", + map[string]any{ + "run_lookml_tests": map[string]any{ + "description": "Simple tool to test end to end functionality.", + "authRequired": []any{}, + "parameters": []any{ + map[string]any{ + "authSources": []any{}, + "description": "The id of the project to run LookML tests for.", + "name": "project_id", + "required": true, + "type": "string", + }, + map[string]any{ + "authSources": []any{}, + "description": "Optional id of the file to run tests for.", + "name": "file_id", + "required": false, + "type": "string", + }, + map[string]any{ + "authSources": []any{}, + "description": "Optional name of the test to run.", + "name": "test", + "required": false, + "type": "string", + }, + map[string]any{ + "authSources": []any{}, + "description": "Optional name of the model to run tests for.", + "name": "model", + "required": false, + "type": "string", + }, + }, + }, + }, + ) + tests.RunToolGetTestByName(t, "create_view_from_table", + map[string]any{ + "create_view_from_table": map[string]any{ + "description": "Simple tool to test end to end functionality.", + "authRequired": []any{}, + "parameters": []any{ + map[string]any{ + "authSources": []any{}, + "description": "The id of the project to create the view in.", + "name": "project_id", + "required": true, + "type": "string", + }, + map[string]any{ + "authSources": []any{}, + "description": "The database connection name.", + "name": "connection", + "required": true, + "type": "string", + }, + map[string]any{ + "authSources": []any{}, + "description": "The tables to generate views for.\n\t\tEach item must be a map with:\n\t\t- schema (string, required)\n\t\t- table_name (string, required)\n\t\t- primary_key (string, optional)\n\t\t- base_view (boolean, optional)\n\t\t- columns (array of objects, optional): Each object must have 'column_name' (string).", + "items": map[string]any{ + "additionalProperties": true, + "authSources": []any{}, + "description": "Table definition.", + "name": "table", + "required": true, + "type": "object", + }, + "name": "tables", + "required": true, + "type": "array", + }, + map[string]any{ + "authSources": []any{}, + "default": "views", + "description": "The folder to place the view files in (e.g., 'views').", + "name": "folder_name", + "required": false, + "type": "string", + }, + }, + }, + }, + ) tests.RunToolGetTestByName(t, "get_looks", map[string]any{ "get_looks": map[string]any{ @@ -1768,17 +1892,27 @@ func TestLooker(t *testing.T) { tests.RunToolInvokeParametersTest(t, "delete_project_file", []byte(`{"project_id": "the_look", "file_path": "foo.view.lkml"}`), wantResult) wantResult = "Created" - tests.RunToolInvokeParametersTest(t, "create_project_directory", []byte(`{"project_id": "the_look", "directory_path": "foo_dir"}`), wantResult) + tests.RunToolInvokeParametersTest(t, "create_project_directory", []byte(`{"project_id": "the_look", "directory_path": "views"}`), wantResult) - wantResult = "foo_dir" + wantResult = "views" tests.RunToolInvokeParametersTest(t, "get_project_directories", []byte(`{"project_id": "the_look"}`), wantResult) + // Add test back when infrastructure for testing supports it. + // wantResult = "{\"status\": \"success\", \"message\": \"Triggered view generation for project the_look in folder views\"}" + // tests.RunToolInvokeParametersTest(t, "create_view_from_table", []byte(`{"project_id": "the_look", "connection": "thelook", "tables": [{"schema": "demo_db", "table_name": "Employees"}]}`), wantResult) + wantResult = "Deleted" - tests.RunToolInvokeParametersTest(t, "delete_project_directory", []byte(`{"project_id": "the_look", "directory_path": "foo_dir"}`), wantResult) + tests.RunToolInvokeParametersTest(t, "delete_project_directory", []byte(`{"project_id": "the_look", "directory_path": "views"}`), wantResult) wantResult = "\"errors\":[]" tests.RunToolInvokeParametersTest(t, "validate_project", []byte(`{"project_id": "the_look"}`), wantResult) + wantResult = "[]" + tests.RunToolInvokeParametersTest(t, "get_lookml_tests", []byte(`{"project_id": "the_look"}`), wantResult) + + wantResult = "[]" + tests.RunToolInvokeParametersTest(t, "run_lookml_tests", []byte(`{"project_id": "the_look"}`), wantResult) + wantResult = "production" tests.RunToolInvokeParametersTest(t, "dev_mode", []byte(`{"devMode": false}`), wantResult)