Skip to content

Commit a533f0f

Browse files
committed
feat(tools/looker): Enable Get All Lookml Tests tool for Looker
1 parent d135891 commit a533f0f

4 files changed

Lines changed: 291 additions & 0 deletions

File tree

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ import (
134134
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerdeleteprojectfile"
135135
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerdevmode"
136136
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergenerateembedurl"
137+
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetalllookmltests"
137138
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetconnectiondatabases"
138139
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetconnections"
139140
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetconnectionschemas"

internal/prebuiltconfigs/tools/looker.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,11 @@ tools:
10411041
A JSON array of objects, where each object represents a column and contains details
10421042
such as `table_name`, `column_name`, `data_type`, and `is_nullable`.
10431043
1044+
get_all_lookml_tests:
1045+
kind: looker-get-all-lookml-tests
1046+
source: looker-source
1047+
description: |
1048+
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.
10441049
10451050
toolsets:
10461051
looker_tools:
@@ -1077,3 +1082,4 @@ toolsets:
10771082
- get_connection_databases
10781083
- get_connection_tables
10791084
- get_connection_table_columns
1085+
- get_all_lookml_tests
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package lookergetalllookmltests
15+
16+
import (
17+
"context"
18+
"fmt"
19+
20+
yaml "github.com/goccy/go-yaml"
21+
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
22+
"github.com/googleapis/genai-toolbox/internal/sources"
23+
"github.com/googleapis/genai-toolbox/internal/tools"
24+
"github.com/googleapis/genai-toolbox/internal/util/parameters"
25+
26+
"github.com/looker-open-source/sdk-codegen/go/rtl"
27+
v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4"
28+
)
29+
30+
const resourceType string = "looker-get-all-lookml-tests"
31+
32+
func init() {
33+
if !tools.Register(resourceType, newConfig) {
34+
panic(fmt.Sprintf("tool type %q already registered", resourceType))
35+
}
36+
}
37+
38+
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
39+
actual := Config{Name: name}
40+
if err := decoder.DecodeContext(ctx, &actual); err != nil {
41+
return nil, err
42+
}
43+
return actual, nil
44+
}
45+
46+
type compatibleSource interface {
47+
UseClientAuthorization() bool
48+
GetAuthTokenHeaderName() string
49+
LookerApiSettings() *rtl.ApiSettings
50+
GetLookerSDK(string) (*v4.LookerSDK, error)
51+
}
52+
53+
type Config struct {
54+
Name string `yaml:"name" validate:"required"`
55+
Type string `yaml:"type" validate:"required"`
56+
Source string `yaml:"source" validate:"required"`
57+
Description string `yaml:"description" validate:"required"`
58+
AuthRequired []string `yaml:"authRequired"`
59+
Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"`
60+
}
61+
62+
// validate interface
63+
var _ tools.ToolConfig = Config{}
64+
65+
func (cfg Config) ToolConfigType() string {
66+
return resourceType
67+
}
68+
69+
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
70+
projectIdParameter := parameters.NewStringParameter("project_id", "The id of the project to retrieve LookML tests for.")
71+
fileIdParameter := parameters.NewStringParameterWithRequired("file_id", "Optional id of the file to filter tests by.", false)
72+
params := parameters.Parameters{projectIdParameter, fileIdParameter}
73+
74+
annotations := cfg.Annotations
75+
if annotations == nil {
76+
readOnlyHint := true
77+
annotations = &tools.ToolAnnotations{
78+
ReadOnlyHint: &readOnlyHint,
79+
}
80+
}
81+
82+
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations)
83+
84+
// finish tool setup
85+
return Tool{
86+
Config: cfg,
87+
Parameters: params,
88+
manifest: tools.Manifest{
89+
Description: cfg.Description,
90+
Parameters: params.Manifest(),
91+
AuthRequired: cfg.AuthRequired,
92+
},
93+
mcpManifest: mcpManifest,
94+
}, nil
95+
}
96+
97+
// validate interface
98+
var _ tools.Tool = Tool{}
99+
100+
type Tool struct {
101+
Config
102+
Parameters parameters.Parameters `yaml:"parameters"`
103+
manifest tools.Manifest
104+
mcpManifest tools.McpManifest
105+
}
106+
107+
func (t Tool) ToConfig() tools.ToolConfig {
108+
return t.Config
109+
}
110+
111+
func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, error) {
112+
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
113+
if err != nil {
114+
return nil, err
115+
}
116+
117+
sdk, err := source.GetLookerSDK(string(accessToken))
118+
if err != nil {
119+
return nil, fmt.Errorf("error getting sdk: %w", err)
120+
}
121+
122+
mapParams := params.AsMap()
123+
projectId, ok := mapParams["project_id"].(string)
124+
if !ok {
125+
return nil, fmt.Errorf("'project_id' must be a string, got %T", mapParams["project_id"])
126+
}
127+
128+
var fileId string
129+
if val, ok := mapParams["file_id"].(string); ok {
130+
fileId = val
131+
}
132+
133+
resp, err := sdk.AllLookmlTests(projectId, fileId, source.LookerApiSettings())
134+
if err != nil {
135+
return nil, fmt.Errorf("error retrieving lookml tests: %w", err)
136+
}
137+
138+
return resp, nil
139+
}
140+
141+
func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) {
142+
return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil)
143+
}
144+
145+
func (t Tool) Manifest() tools.Manifest {
146+
return t.manifest
147+
}
148+
149+
func (t Tool) McpManifest() tools.McpManifest {
150+
return t.mcpManifest
151+
}
152+
153+
func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) {
154+
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
155+
if err != nil {
156+
return false, err
157+
}
158+
return source.UseClientAuthorization(), nil
159+
}
160+
161+
func (t Tool) Authorized(verifiedAuthServices []string) bool {
162+
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
163+
}
164+
165+
func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) {
166+
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
167+
if err != nil {
168+
return "", err
169+
}
170+
return source.GetAuthTokenHeaderName(), nil
171+
}
172+
173+
func (t Tool) GetParameters() parameters.Parameters {
174+
return t.Parameters
175+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package lookergetalllookmltests_test
16+
17+
import (
18+
"strings"
19+
"testing"
20+
21+
"github.com/google/go-cmp/cmp"
22+
"github.com/googleapis/genai-toolbox/internal/server"
23+
"github.com/googleapis/genai-toolbox/internal/testutils"
24+
lkr "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetalllookmltests"
25+
)
26+
27+
func TestParseFromYamlLookerGetAllLookmlTests(t *testing.T) {
28+
ctx, err := testutils.ContextWithNewLogger()
29+
if err != nil {
30+
t.Fatalf("unexpected error: %s", err)
31+
}
32+
tcs := []struct {
33+
desc string
34+
in string
35+
want server.ToolConfigs
36+
}{
37+
{
38+
desc: "basic example",
39+
in: `
40+
kind: tools
41+
name: example_tool
42+
type: looker-get-all-lookml-tests
43+
source: my-instance
44+
description: some description
45+
`,
46+
want: server.ToolConfigs{
47+
"example_tool": lkr.Config{
48+
Name: "example_tool",
49+
Type: "looker-get-all-lookml-tests",
50+
Source: "my-instance",
51+
Description: "some description",
52+
AuthRequired: []string{},
53+
},
54+
},
55+
},
56+
}
57+
for _, tc := range tcs {
58+
t.Run(tc.desc, func(t *testing.T) {
59+
// Parse contents
60+
_, _, _, got, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in))
61+
if err != nil {
62+
t.Fatalf("unable to unmarshal: %s", err)
63+
}
64+
if diff := cmp.Diff(tc.want, got); diff != "" {
65+
t.Fatalf("incorrect parse: diff %v", diff)
66+
}
67+
})
68+
}
69+
70+
}
71+
72+
func TestFailParseFromYamlLookerGetAllLookmlTests(t *testing.T) {
73+
ctx, err := testutils.ContextWithNewLogger()
74+
if err != nil {
75+
t.Fatalf("unexpected error: %s", err)
76+
}
77+
tcs := []struct {
78+
desc string
79+
in string
80+
err string
81+
}{
82+
{
83+
desc: "Invalid method",
84+
in: `
85+
kind: tools
86+
name: example_tool
87+
type: looker-get-all-lookml-tests
88+
source: my-instance
89+
method: GOT
90+
description: some description
91+
`,
92+
err: "error unmarshaling tools: unable to parse tool \"example_tool\" as type \"looker-get-all-lookml-tests\": [3:1] unknown field \"method\"",
93+
},
94+
}
95+
for _, tc := range tcs {
96+
t.Run(tc.desc, func(t *testing.T) {
97+
// Parse contents
98+
_, _, _, _, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in))
99+
if err == nil {
100+
t.Fatalf("expect parsing to fail")
101+
}
102+
errStr := err.Error()
103+
if !strings.Contains(errStr, tc.err) {
104+
t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err)
105+
}
106+
})
107+
}
108+
109+
}

0 commit comments

Comments
 (0)