feat: Add Firestore as Source (#786)

Firestore is a NoSQL document database built for automatic scaling, high
performance, and ease of application development. It's a fully managed,
serverless database that supports mobile, web, and server development.

This change adds Firestore as a source in toolbox
This commit is contained in:
trehanshakuntG
2025-07-18 11:42:07 +05:30
committed by GitHub
parent 2083ba5048
commit 2bb790e4f8
30 changed files with 4414 additions and 0 deletions

View File

@@ -425,6 +425,26 @@ steps:
"Valkey" \
valkey \
valkey
- id: "firestore"
name: golang:1
waitFor: ["compile-test-binary"]
entrypoint: /bin/bash
env:
- "GOPATH=/gopath"
- "FIRESTORE_PROJECT=$PROJECT_ID"
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
secretEnv: ["CLIENT_ID"]
volumes:
- name: "go"
path: "/gopath"
args:
- -c
- |
.ci/test_with_coverage.sh \
"Firestore" \
firestore \
firestore
availableSecrets:

View File

@@ -52,6 +52,12 @@ import (
_ "github.com/googleapis/genai-toolbox/internal/tools/bigtable"
_ "github.com/googleapis/genai-toolbox/internal/tools/couchbase"
_ "github.com/googleapis/genai-toolbox/internal/tools/dgraph"
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoredeletedocuments"
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoregetdocuments"
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoregetrules"
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestorelistcollections"
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestorequerycollection"
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestorevalidaterules"
_ "github.com/googleapis/genai-toolbox/internal/tools/http"
_ "github.com/googleapis/genai-toolbox/internal/tools/mssql/mssqlexecutesql"
_ "github.com/googleapis/genai-toolbox/internal/tools/mssql/mssqlsql"
@@ -77,6 +83,7 @@ import (
_ "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlpg"
_ "github.com/googleapis/genai-toolbox/internal/sources/couchbase"
_ "github.com/googleapis/genai-toolbox/internal/sources/dgraph"
_ "github.com/googleapis/genai-toolbox/internal/sources/firestore"
_ "github.com/googleapis/genai-toolbox/internal/sources/http"
_ "github.com/googleapis/genai-toolbox/internal/sources/mssql"
_ "github.com/googleapis/genai-toolbox/internal/sources/mysql"

View File

@@ -1166,6 +1166,7 @@ func TestPrebuiltTools(t *testing.T) {
cloudsqlpg_config, _ := prebuiltconfigs.Get("cloud-sql-postgres")
cloudsqlmysql_config, _ := prebuiltconfigs.Get("cloud-sql-mysql")
cloudsqlmssql_config, _ := prebuiltconfigs.Get("cloud-sql-mssql")
firestoreconfig, _ := prebuiltconfigs.Get("firestore")
postgresconfig, _ := prebuiltconfigs.Get("postgres")
spanner_config, _ := prebuiltconfigs.Get("spanner")
spannerpg_config, _ := prebuiltconfigs.Get("spanner-postgres")
@@ -1228,6 +1229,16 @@ func TestPrebuiltTools(t *testing.T) {
},
},
},
{
name: "firestore prebuilt tools",
in: firestoreconfig,
wantToolset: server.ToolsetConfigs{
"firestore-database-tools": tools.ToolsetConfig{
Name: "firestore-database-tools",
ToolNames: []string{"firestore-get-documents", "firestore-list-collections", "firestore-delete-documents", "firestore-query-collection", "firestore-get-rules", "firestore-validate-rules"},
},
},
},
{
name: "postgres prebuilt tools",
in: postgresconfig,

View File

@@ -0,0 +1,70 @@
---
title: "Firestore"
type: docs
weight: 1
description: >
Firestore is a NoSQL document database built for automatic scaling, high performance, and ease of application development. It's a fully managed, serverless database that supports mobile, web, and server development.
---
# Firestore Source
[Firestore][firestore-docs] is a NoSQL document database built for automatic
scaling, high performance, and ease of application development. While the
Firestore interface has many of the same features as traditional databases,
as a NoSQL database it differs from them in the way it describes relationships
between data objects.
If you are new to Firestore, you can [create a database and learn the
basics][firestore-quickstart].
[firestore-docs]: https://cloud.google.com/firestore/docs
[firestore-quickstart]: https://cloud.google.com/firestore/docs/quickstart-servers
## Requirements
### IAM Permissions
Firestore uses [Identity and Access Management (IAM)][iam-overview] to control
user and group access to Firestore resources. Toolbox will use your [Application
Default Credentials (ADC)][adc] to authorize and authenticate when interacting
with [Firestore][firestore-docs].
In addition to [setting the ADC for your server][set-adc], you need to ensure
the IAM identity has been given the correct IAM permissions for accessing
Firestore. Common roles include:
- `roles/datastore.user` - Read and write access to Firestore
- `roles/datastore.viewer` - Read-only access to Firestore
See [Firestore access control][firestore-iam] for more information on
applying IAM permissions and roles to an identity.
[iam-overview]: https://cloud.google.com/firestore/docs/security/iam
[adc]: https://cloud.google.com/docs/authentication#adc
[set-adc]: https://cloud.google.com/docs/authentication/provide-credentials-adc
[firestore-iam]: https://cloud.google.com/firestore/docs/security/iam
### Database Selection
Firestore allows you to create multiple databases within a single project. Each
database is isolated from the others and has its own set of documents and
collections. If you don't specify a database in your configuration, the default
database named `(default)` will be used.
## Example
```yaml
sources:
my-firestore-source:
kind: "firestore"
project: "my-project-id"
# database: "my-database" # Optional, defaults to "(default)"
```
## Reference
| **field** | **type** | **required** | **description** |
|-----------|:--------:|:------------:|----------------------------------------------------------------------------------------------------------|
| kind | string | true | Must be "firestore". |
| project | string | true | Id of the GCP project that contains the Firestore database (e.g. "my-project-id"). |
| database | string | false | Name of the Firestore database to connect to. Defaults to "(default)" if not specified. |

View File

@@ -0,0 +1,7 @@
---
title: "Firestore"
type: docs
weight: 1
description: >
Tools that work with Firestore Sources.
---

View File

@@ -0,0 +1,38 @@
---
title: "firestore-delete-documents"
type: docs
weight: 1
description: >
A "firestore-delete-documents" tool deletes multiple documents from Firestore by their paths.
aliases:
- /resources/tools/firestore-delete-documents
---
## About
A `firestore-delete-documents` tool deletes multiple documents from Firestore by their paths.
It's compatible with the following sources:
- [firestore](../sources/firestore.md)
`firestore-delete-documents` takes one input parameter `documentPaths` which is an array of
document paths to delete. The tool uses Firestore's BulkWriter for efficient batch deletion
and returns the success status for each document.
## Example
```yaml
tools:
delete_user_documents:
kind: firestore-delete-documents
source: my-firestore-source
description: Use this tool to delete multiple documents from Firestore.
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
| kind | string | true | Must be "firestore-delete-documents". |
| source | string | true | Name of the Firestore source to delete documents from. |
| description | string | true | Description of the tool that is passed to the LLM. |

View File

@@ -0,0 +1,38 @@
---
title: "firestore-get-documents"
type: docs
weight: 1
description: >
A "firestore-get-documents" tool retrieves multiple documents from Firestore by their paths.
aliases:
- /resources/tools/firestore-get-documents
---
## About
A `firestore-get-documents` tool retrieves multiple documents from Firestore by their paths.
It's compatible with the following sources:
- [firestore](../sources/firestore.md)
`firestore-get-documents` takes one input parameter `documentPaths` which is an array of
document paths, and returns the documents' data along with metadata such as existence status,
creation time, update time, and read time.
## Example
```yaml
tools:
get_user_documents:
kind: firestore-get-documents
source: my-firestore-source
description: Use this tool to retrieve multiple documents from Firestore.
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
| kind | string | true | Must be "firestore-get-documents". |
| source | string | true | Name of the Firestore source to retrieve documents from. |
| description | string | true | Description of the tool that is passed to the LLM. |

View File

@@ -0,0 +1,37 @@
---
title: "firestore-get-rules"
type: docs
weight: 1
description: >
A "firestore-get-rules" tool retrieves the active Firestore security rules for the current project.
aliases:
- /resources/tools/firestore-get-rules
---
## About
A `firestore-get-rules` tool retrieves the active [Firestore security rules](https://firebase.google.com/docs/firestore/security/get-started) for the current project.
It's compatible with the following sources:
- [firestore](../sources/firestore.md)
`firestore-get-rules` takes no input parameters and returns the security rules content along with metadata
such as the ruleset name, and timestamps.
## Example
```yaml
tools:
get_firestore_rules:
kind: firestore-get-rules
source: my-firestore-source
description: Use this tool to retrieve the active Firestore security rules.
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
| kind | string | true | Must be "firestore-get-rules". |
| source | string | true | Name of the Firestore source to retrieve rules from. |
| description | string | true | Description of the tool that is passed to the LLM. |

View File

@@ -0,0 +1,38 @@
---
title: "firestore-list-collections"
type: docs
weight: 1
description: >
A "firestore-list-collections" tool lists collections in Firestore, either at the root level or as subcollections of a document.
aliases:
- /resources/tools/firestore-list-collections
---
## About
A `firestore-list-collections` tool lists [collections](https://firebase.google.com/docs/firestore/data-model#collections) in Firestore, either at the root level or as [subcollections](https://firebase.google.com/docs/firestore/data-model#subcollections) of a specific document.
It's compatible with the following sources:
- [firestore](../sources/firestore.md)
`firestore-list-collections` takes an optional `parentPath` parameter to specify a document
path. If provided, it lists all subcollections of that document. If not provided, it lists
all root-level collections in the database.
## Example
```yaml
tools:
list_firestore_collections:
kind: firestore-list-collections
source: my-firestore-source
description: Use this tool to list collections in Firestore.
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
| kind | string | true | Must be "firestore-list-collections". |
| source | string | true | Name of the Firestore source to list collections from. |
| description | string | true | Description of the tool that is passed to the LLM. |

View File

@@ -0,0 +1,198 @@
# firestore-query-collection
The `firestore-query-collection` tool allows you to query Firestore collections with filters, ordering, and limit capabilities.
## Configuration
To use this tool, you need to configure it in your YAML configuration file:
```yaml
sources:
my-firestore:
kind: firestore
config:
project: my-gcp-project
database: "(default)"
tools:
query_collection:
kind: firestore-query-collection
source: my-firestore
description: Query Firestore collections with advanced filtering
```
## Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `collectionPath` | string | Yes | - | The path to the Firestore collection to query |
| `filters` | array | No | - | Array of filter objects (as JSON strings) to apply to the query |
| `orderBy` | string | No | - | JSON string specifying field and direction to order results |
| `limit` | integer | No | 100 | Maximum number of documents to return |
| `analyzeQuery` | boolean | No | false | If true, returns query explain metrics including execution statistics |
### Filter Format
Each filter in the `filters` array should be a JSON string with the following structure:
```json
{
"field": "fieldName",
"op": "operator",
"value": "compareValue"
}
```
Supported operators:
- `<` - Less than
- `<=` - Less than or equal to
- `>` - Greater than
- `>=` - Greater than or equal to
- `==` - Equal to
- `!=` - Not equal to
- `array-contains` - Array contains a specific value
- `array-contains-any` - Array contains any of the specified values
- `in` - Field value is in the specified array
- `not-in` - Field value is not in the specified array
Value types supported:
- String: `"value": "text"`
- Number: `"value": 123` or `"value": 45.67`
- Boolean: `"value": true` or `"value": false`
- Array: `"value": ["item1", "item2"]` (for `in`, `not-in`, `array-contains-any` operators)
### OrderBy Format
The `orderBy` parameter should be a JSON string with the following structure:
```json
{
"field": "fieldName",
"direction": "ASCENDING"
}
```
Direction values:
- `ASCENDING`
- `DESCENDING`
## Example Usage
### Query with filters
```json
{
"collectionPath": "users",
"filters": [
"{\"field\": \"age\", \"op\": \">\", \"value\": 18}",
"{\"field\": \"status\", \"op\": \"==\", \"value\": \"active\"}"
],
"orderBy": "{\"field\": \"createdAt\", \"direction\": \"DESCENDING\"}",
"limit": 50
}
```
### Query with array contains filter
```json
{
"collectionPath": "products",
"filters": [
"{\"field\": \"categories\", \"op\": \"array-contains\", \"value\": \"electronics\"}",
"{\"field\": \"price\", \"op\": \"<\", \"value\": 1000}"
],
"orderBy": "{\"field\": \"price\", \"direction\": \"ASCENDING\"}",
"limit": 20
}
```
### Query with IN operator
```json
{
"collectionPath": "orders",
"filters": [
"{\"field\": \"status\", \"op\": \"in\", \"value\": [\"pending\", \"processing\"]}"
],
"limit": 100
}
```
### Query with explain metrics
```json
{
"collectionPath": "users",
"filters": [
"{\"field\": \"age\", \"op\": \">=\", \"value\": 21}",
"{\"field\": \"active\", \"op\": \"==\", \"value\": true}"
],
"orderBy": "{\"field\": \"lastLogin\", \"direction\": \"DESCENDING\"}",
"limit": 25,
"analyzeQuery": true
}
```
## Response Format
### Standard Response (analyzeQuery = false)
The tool returns an array of documents, where each document includes:
```json
{
"id": "documentId",
"path": "collection/documentId",
"data": {
// Document fields
},
"createTime": "2025-01-07T12:00:00Z",
"updateTime": "2025-01-07T12:00:00Z",
"readTime": "2025-01-07T12:00:00Z"
}
```
### Response with Query Analysis (analyzeQuery = true)
When `analyzeQuery` is set to true, the tool returns a single object containing documents and explain metrics:
```json
{
"documents": [
// Array of document objects as shown above
],
"explainMetrics": {
"planSummary": {
"indexesUsed": [
{
"query_scope": "Collection",
"properties": "(field ASC, __name__ ASC)"
}
]
},
"executionStats": {
"resultsReturned": 50,
"readOperations": 50,
"executionDuration": "120ms",
"debugStats": {
"indexes_entries_scanned": "1000",
"documents_scanned": "50",
"billing_details": {
"documents_billable": "50",
"index_entries_billable": "1000",
"min_query_cost": "0"
}
}
}
}
}
```
## Error Handling
The tool will return errors for:
- Invalid collection path
- Malformed filter JSON
- Unsupported operators
- Query execution failures
- Invalid orderBy format

View File

@@ -0,0 +1,115 @@
---
title: firestore-validate-rules
weight: 6
date: 2025-01-07
---
## Overview
The `firestore-validate-rules` tool validates Firestore security rules syntax and semantic correctness without deploying them. It provides detailed error reporting with source positions and code snippets.
## Configuration
```yaml
tools:
firestore-validate-rules:
kind: firestore-validate-rules
source: <firestore-source-name>
description: "Checks the provided Firestore Rules source for syntax and validation errors"
```
## Authentication
This tool requires authentication if the source requires authentication.
## Parameters
| Parameter | Type | Required | Description |
|-----------|--------|----------|-------------|
| source | string | Yes | The Firestore Rules source code to validate |
## Response
The tool returns a `ValidationResult` object containing:
```json
{
"valid": boolean, // Whether the rules are valid
"issueCount": number, // Number of issues found
"formattedIssues": string, // Human-readable formatted issues
"rawIssues": [ // Array of raw issue objects
{
"sourcePosition": {
"fileName": string,
"line": number,
"column": number,
"currentOffset": number,
"endOffset": number
},
"description": string,
"severity": string // e.g., "ERROR", "WARNING"
}
]
}
```
## Example Usage
### Validate simple rules
```json
{
"source": "rules_version = '2';\nservice cloud.firestore {\n match /databases/{database}/documents {\n match /{document=**} {\n allow read, write: if true;\n }\n }\n}"
}
```
### Example response for valid rules
```json
{
"valid": true,
"issueCount": 0,
"formattedIssues": "✓ No errors detected. Rules are valid."
}
```
### Example response with errors
```json
{
"valid": false,
"issueCount": 1,
"formattedIssues": "Found 1 issue(s) in rules source:\n\nERROR: Unexpected token ';' [Ln 4, Col 32]\n```\n allow read, write: if true;;\n ^\n```",
"rawIssues": [
{
"sourcePosition": {
"line": 4,
"column": 32,
"currentOffset": 105,
"endOffset": 106
},
"description": "Unexpected token ';'",
"severity": "ERROR"
}
]
}
```
## Error Handling
The tool will return errors for:
- Missing or empty `source` parameter
- API errors when calling the Firebase Rules service
- Network connectivity issues
## Use Cases
1. **Pre-deployment validation**: Validate rules before deploying to production
2. **CI/CD integration**: Integrate rules validation into your build pipeline
3. **Development workflow**: Quickly check rules syntax while developing
4. **Error debugging**: Get detailed error locations with code snippets
## Related Tools
- [firestore-get-rules]({{< ref "firestore-get-rules" >}}): Retrieve current active rules
- [firestore-query-collection]({{< ref "firestore-query-collection" >}}): Test rules by querying collections

1
go.mod
View File

@@ -9,6 +9,7 @@ require (
cloud.google.com/go/bigquery v1.69.0
cloud.google.com/go/bigtable v1.38.0
cloud.google.com/go/cloudsqlconn v1.17.3
cloud.google.com/go/firestore v1.18.0
cloud.google.com/go/spanner v1.83.0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.29.0

2
go.sum
View File

@@ -293,6 +293,8 @@ cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLY
cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs=
cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg=
cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE=
cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s=
cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU=
cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk=
cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg=
cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY=

View File

@@ -28,6 +28,7 @@ func TestLoadPrebuiltToolYAMLs(t *testing.T) {
"cloud-sql-mssql",
"cloud-sql-mysql",
"cloud-sql-postgres",
"firestore",
"postgres",
"spanner-postgres",
"spanner",
@@ -68,6 +69,7 @@ func TestGetPrebuiltTool(t *testing.T) {
cloudsqlpg_config, _ := Get("cloud-sql-postgres")
cloudsqlmysql_config, _ := Get("cloud-sql-mysql")
cloudsqlmssql_config, _ := Get("cloud-sql-mssql")
firestoreconfig, _ := Get("firestore")
postgresconfig, _ := Get("postgres")
spanner_config, _ := Get("spanner")
spannerpg_config, _ := Get("spanner-postgres")
@@ -86,6 +88,9 @@ func TestGetPrebuiltTool(t *testing.T) {
if len(cloudsqlmssql_config) <= 0 {
t.Fatalf("unexpected error: could not fetch cloud sql mssql prebuilt tools yaml")
}
if len(firestoreconfig) <= 0 {
t.Fatalf("unexpected error: could not fetch firestore prebuilt tools yaml")
}
if len(postgresconfig) <= 0 {
t.Fatalf("unexpected error: could not fetch postgres prebuilt tools yaml")
}

View File

@@ -0,0 +1,42 @@
sources:
firestore-source:
kind: firestore
project: ${FIRESTORE_PROJECT}
database: ${FIRESTORE_DATABASE} # Optional, defaults to "(default)" if not specified
tools:
firestore-get-documents:
kind: firestore-get-documents
source: firestore-source
description: Gets multiple documents from Firestore by their paths
firestore-list-collections:
kind: firestore-list-collections
source: firestore-source
description: List Firestore collections for a given parent path
firestore-delete-documents:
kind: firestore-delete-documents
source: firestore-source
description: Delete multiple documents from Firestore
firestore-query-collection:
kind: firestore-query-collection
source: firestore-source
description: |
Retrieves one or more Firestore documents from a collection in a database in the current project by a collection with a full document path.
Use this if you know the exact path of a collection and the filtering clause you would like for the document.
firestore-get-rules:
kind: firestore-get-rules
source: firestore-source
description: Retrieves the active Firestore security rules for the current project
firestore-validate-rules:
kind: firestore-validate-rules
source: firestore-source
description: Checks the provided Firestore Rules source for syntax and validation errors. Provide the source code to validate.
toolsets:
firestore-database-tools:
- firestore-get-documents
- firestore-list-collections
- firestore-delete-documents
- firestore-query-collection
- firestore-get-rules
- firestore-validate-rules

View File

@@ -0,0 +1,153 @@
// Copyright 2025 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 firestore
import (
"context"
"fmt"
"cloud.google.com/go/firestore"
"github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/util"
"go.opentelemetry.io/otel/trace"
"google.golang.org/api/firebaserules/v1"
"google.golang.org/api/option"
)
const SourceKind string = "firestore"
// validate interface
var _ sources.SourceConfig = Config{}
func init() {
if !sources.Register(SourceKind, newConfig) {
panic(fmt.Sprintf("source kind %q already registered", SourceKind))
}
}
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, error) {
actual := Config{Name: name}
if err := decoder.DecodeContext(ctx, &actual); err != nil {
return nil, err
}
return actual, nil
}
type Config struct {
// Firestore configs
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
Project string `yaml:"project" validate:"required"`
Database string `yaml:"database"` // Optional, defaults to "(default)"
}
func (r Config) SourceConfigKind() string {
// Returns Firestore source kind
return SourceKind
}
func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) {
// Initializes a Firestore source
client, err := initFirestoreConnection(ctx, tracer, r.Name, r.Project, r.Database)
if err != nil {
return nil, err
}
// Initialize Firebase Rules client
rulesClient, err := initFirebaseRulesConnection(ctx, r.Project)
if err != nil {
return nil, fmt.Errorf("failed to initialize Firebase Rules client: %w", err)
}
s := &Source{
Name: r.Name,
Kind: SourceKind,
Client: client,
RulesClient: rulesClient,
ProjectId: r.Project,
}
return s, nil
}
var _ sources.Source = &Source{}
type Source struct {
// Firestore struct with client
Name string `yaml:"name"`
Kind string `yaml:"kind"`
Client *firestore.Client
RulesClient *firebaserules.Service
ProjectId string `yaml:"projectId"`
}
func (s *Source) SourceKind() string {
// Returns Firestore source kind
return SourceKind
}
func (s *Source) FirestoreClient() *firestore.Client {
return s.Client
}
func (s *Source) FirebaseRulesClient() *firebaserules.Service {
return s.RulesClient
}
func (s *Source) GetProjectId() string {
return s.ProjectId
}
func initFirestoreConnection(
ctx context.Context,
tracer trace.Tracer,
name string,
project string,
database string,
) (*firestore.Client, error) {
ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name)
defer span.End()
userAgent, err := util.UserAgentFromContext(ctx)
if err != nil {
return nil, err
}
// If database is not specified, use the default database
if database == "" {
database = "(default)"
}
// Create the Firestore client
client, err := firestore.NewClientWithDatabase(ctx, project, database, option.WithUserAgent(userAgent))
if err != nil {
return nil, fmt.Errorf("failed to create Firestore client for project %q and database %q: %w", project, database, err)
}
return client, nil
}
func initFirebaseRulesConnection(
ctx context.Context,
project string,
) (*firebaserules.Service, error) {
// Create the Firebase Rules client
rulesClient, err := firebaserules.NewService(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create Firebase Rules client for project %q: %w", project, err)
}
return rulesClient, nil
}

View File

@@ -0,0 +1,130 @@
// Copyright 2025 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 firestore_test
import (
"testing"
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/googleapis/genai-toolbox/internal/sources/firestore"
"github.com/googleapis/genai-toolbox/internal/testutils"
)
func TestParseFromYamlFirestore(t *testing.T) {
tcs := []struct {
desc string
in string
want server.SourceConfigs
}{
{
desc: "basic example with default database",
in: `
sources:
my-firestore:
kind: firestore
project: my-project
`,
want: server.SourceConfigs{
"my-firestore": firestore.Config{
Name: "my-firestore",
Kind: firestore.SourceKind,
Project: "my-project",
Database: "",
},
},
},
{
desc: "with custom database",
in: `
sources:
my-firestore:
kind: firestore
project: my-project
database: my-database
`,
want: server.SourceConfigs{
"my-firestore": firestore.Config{
Name: "my-firestore",
Kind: firestore.SourceKind,
Project: "my-project",
Database: "my-database",
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := struct {
Sources server.SourceConfigs `yaml:"sources"`
}{}
// Parse contents
err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if !cmp.Equal(tc.want, got.Sources) {
t.Fatalf("incorrect parse: want %v, got %v", tc.want, got.Sources)
}
})
}
}
func TestFailParseFromYamlFirestore(t *testing.T) {
tcs := []struct {
desc string
in string
err string
}{
{
desc: "extra field",
in: `
sources:
my-firestore:
kind: firestore
project: my-project
foo: bar
`,
err: "unable to parse source \"my-firestore\" as \"firestore\": [1:1] unknown field \"foo\"\n> 1 | foo: bar\n ^\n 2 | kind: firestore\n 3 | project: my-project",
},
{
desc: "missing required field",
in: `
sources:
my-firestore:
kind: firestore
database: my-database
`,
err: "unable to parse source \"my-firestore\" as \"firestore\": Key: 'Config.Project' Error:Field validation for 'Project' failed on the 'required' tag",
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := struct {
Sources server.SourceConfigs `yaml:"sources"`
}{}
// Parse contents
err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got)
if err == nil {
t.Fatalf("expect parsing to fail")
}
errStr := err.Error()
if errStr != tc.err {
t.Fatalf("unexpected error: got %q, want %q", errStr, tc.err)
}
})
}
}

View File

@@ -0,0 +1,194 @@
// Copyright 2025 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 firestoredeletedocuments
import (
"context"
"fmt"
firestoreapi "cloud.google.com/go/firestore"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/sources"
firestoreds "github.com/googleapis/genai-toolbox/internal/sources/firestore"
"github.com/googleapis/genai-toolbox/internal/tools"
)
const kind string = "firestore-delete-documents"
const documentPathsKey string = "documentPaths"
func init() {
if !tools.Register(kind, newConfig) {
panic(fmt.Sprintf("tool kind %q already registered", kind))
}
}
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 {
FirestoreClient() *firestoreapi.Client
}
// validate compatible sources are still compatible
var _ compatibleSource = &firestoreds.Source{}
var compatibleSources = [...]string{firestoreds.SourceKind}
type Config struct {
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
Source string `yaml:"source" validate:"required"`
Description string `yaml:"description" validate:"required"`
AuthRequired []string `yaml:"authRequired"`
}
// validate interface
var _ tools.ToolConfig = Config{}
func (cfg Config) ToolConfigKind() string {
return kind
}
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
// verify source exists
rawS, ok := srcs[cfg.Source]
if !ok {
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
}
// verify the source is compatible
s, ok := rawS.(compatibleSource)
if !ok {
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
}
documentPathsParameter := tools.NewArrayParameter(documentPathsKey, "Array of document paths to delete from Firestore.", tools.NewStringParameter("item", "Document path"))
parameters := tools.Parameters{documentPathsParameter}
mcpManifest := tools.McpManifest{
Name: cfg.Name,
Description: cfg.Description,
InputSchema: parameters.McpManifest(),
}
// finish tool setup
t := Tool{
Name: cfg.Name,
Kind: kind,
Parameters: parameters,
AuthRequired: cfg.AuthRequired,
Client: s.FirestoreClient(),
manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired},
mcpManifest: mcpManifest,
}
return t, nil
}
// validate interface
var _ tools.Tool = Tool{}
type Tool struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
AuthRequired []string `yaml:"authRequired"`
Parameters tools.Parameters `yaml:"parameters"`
Client *firestoreapi.Client
manifest tools.Manifest
mcpManifest tools.McpManifest
}
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
mapParams := params.AsMap()
documentPathsRaw, ok := mapParams[documentPathsKey].([]any)
if !ok {
return nil, fmt.Errorf("invalid or missing '%s' parameter; expected an array", documentPathsKey)
}
if len(documentPathsRaw) == 0 {
return nil, fmt.Errorf("'%s' parameter cannot be empty", documentPathsKey)
}
// Use ConvertAnySliceToTyped to convert the slice
typedSlice, err := tools.ConvertAnySliceToTyped(documentPathsRaw, "string")
if err != nil {
return nil, fmt.Errorf("failed to convert document paths: %w", err)
}
documentPaths, ok := typedSlice.([]string)
if !ok {
return nil, fmt.Errorf("unexpected type conversion error for document paths")
}
// Create a BulkWriter to handle multiple deletions efficiently
bulkWriter := t.Client.BulkWriter(ctx)
// Keep track of jobs for each document
jobs := make([]*firestoreapi.BulkWriterJob, len(documentPaths))
// Add all delete operations to the BulkWriter
for i, path := range documentPaths {
docRef := t.Client.Doc(path)
job, err := bulkWriter.Delete(docRef)
if err != nil {
return nil, fmt.Errorf("failed to add delete operation for document %q: %w", path, err)
}
jobs[i] = job
}
// End the BulkWriter to execute all operations
bulkWriter.End()
// Collect results
results := make([]any, len(documentPaths))
for i, job := range jobs {
docData := make(map[string]any)
docData["path"] = documentPaths[i]
// Wait for the job to complete and get the result
_, err := job.Results()
if err != nil {
docData["success"] = false
docData["error"] = err.Error()
} else {
docData["success"] = true
}
results[i] = docData
}
return results, nil
}
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
return tools.ParseParams(t.Parameters, data, claims)
}
func (t Tool) Manifest() tools.Manifest {
return t.manifest
}
func (t Tool) McpManifest() tools.McpManifest {
return t.mcpManifest
}
func (t Tool) Authorized(verifiedAuthServices []string) bool {
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
}

View File

@@ -0,0 +1,156 @@
// Copyright 2025 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 firestoredeletedocuments_test
import (
"testing"
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoredeletedocuments"
)
func TestParseFromYamlFirestoreDeleteDocuments(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: `
tools:
delete_docs_tool:
kind: firestore-delete-documents
source: my-firestore-instance
description: Delete documents from Firestore by paths
`,
want: server.ToolConfigs{
"delete_docs_tool": firestoredeletedocuments.Config{
Name: "delete_docs_tool",
Kind: "firestore-delete-documents",
Source: "my-firestore-instance",
Description: "Delete documents from Firestore by paths",
AuthRequired: []string{},
},
},
},
{
desc: "with auth requirements",
in: `
tools:
secure_delete_docs:
kind: firestore-delete-documents
source: prod-firestore
description: Delete documents with authentication
authRequired:
- google-auth-service
- api-key-service
`,
want: server.ToolConfigs{
"secure_delete_docs": firestoredeletedocuments.Config{
Name: "secure_delete_docs",
Kind: "firestore-delete-documents",
Source: "prod-firestore",
Description: "Delete documents with authentication",
AuthRequired: []string{"google-auth-service", "api-key-service"},
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
// Parse contents
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
t.Fatalf("incorrect parse: diff %v", diff)
}
})
}
}
func TestParseFromYamlMultipleTools(t *testing.T) {
ctx, err := testutils.ContextWithNewLogger()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
in := `
tools:
delete_user_docs:
kind: firestore-delete-documents
source: users-firestore
description: Delete user documents
authRequired:
- user-auth
delete_product_docs:
kind: firestore-delete-documents
source: products-firestore
description: Delete product documents
delete_order_docs:
kind: firestore-delete-documents
source: orders-firestore
description: Delete order documents
authRequired:
- user-auth
- admin-auth
`
want := server.ToolConfigs{
"delete_user_docs": firestoredeletedocuments.Config{
Name: "delete_user_docs",
Kind: "firestore-delete-documents",
Source: "users-firestore",
Description: "Delete user documents",
AuthRequired: []string{"user-auth"},
},
"delete_product_docs": firestoredeletedocuments.Config{
Name: "delete_product_docs",
Kind: "firestore-delete-documents",
Source: "products-firestore",
Description: "Delete product documents",
AuthRequired: []string{},
},
"delete_order_docs": firestoredeletedocuments.Config{
Name: "delete_order_docs",
Kind: "firestore-delete-documents",
Source: "orders-firestore",
Description: "Delete order documents",
AuthRequired: []string{"user-auth", "admin-auth"},
},
}
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
// Parse contents
err = yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if diff := cmp.Diff(want, got.Tools); diff != "" {
t.Fatalf("incorrect parse: diff %v", diff)
}
}

View File

@@ -0,0 +1,186 @@
// Copyright 2025 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 firestoregetdocuments
import (
"context"
"fmt"
firestoreapi "cloud.google.com/go/firestore"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/sources"
firestoreds "github.com/googleapis/genai-toolbox/internal/sources/firestore"
"github.com/googleapis/genai-toolbox/internal/tools"
)
const kind string = "firestore-get-documents"
const documentPathsKey string = "documentPaths"
func init() {
if !tools.Register(kind, newConfig) {
panic(fmt.Sprintf("tool kind %q already registered", kind))
}
}
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 {
FirestoreClient() *firestoreapi.Client
}
// validate compatible sources are still compatible
var _ compatibleSource = &firestoreds.Source{}
var compatibleSources = [...]string{firestoreds.SourceKind}
type Config struct {
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
Source string `yaml:"source" validate:"required"`
Description string `yaml:"description" validate:"required"`
AuthRequired []string `yaml:"authRequired"`
}
// validate interface
var _ tools.ToolConfig = Config{}
func (cfg Config) ToolConfigKind() string {
return kind
}
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
// verify source exists
rawS, ok := srcs[cfg.Source]
if !ok {
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
}
// verify the source is compatible
s, ok := rawS.(compatibleSource)
if !ok {
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
}
documentPathsParameter := tools.NewArrayParameter(documentPathsKey, "Array of document paths to retrieve from Firestore.", tools.NewStringParameter("item", "Document path"))
parameters := tools.Parameters{documentPathsParameter}
mcpManifest := tools.McpManifest{
Name: cfg.Name,
Description: cfg.Description,
InputSchema: parameters.McpManifest(),
}
// finish tool setup
t := Tool{
Name: cfg.Name,
Kind: kind,
Parameters: parameters,
AuthRequired: cfg.AuthRequired,
Client: s.FirestoreClient(),
manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired},
mcpManifest: mcpManifest,
}
return t, nil
}
// validate interface
var _ tools.Tool = Tool{}
type Tool struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
AuthRequired []string `yaml:"authRequired"`
Parameters tools.Parameters `yaml:"parameters"`
Client *firestoreapi.Client
manifest tools.Manifest
mcpManifest tools.McpManifest
}
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
mapParams := params.AsMap()
documentPathsRaw, ok := mapParams[documentPathsKey].([]any)
if !ok {
return nil, fmt.Errorf("invalid or missing '%s' parameter; expected an array", documentPathsKey)
}
if len(documentPathsRaw) == 0 {
return nil, fmt.Errorf("'%s' parameter cannot be empty", documentPathsKey)
}
// Use ConvertAnySliceToTyped to convert the slice
typedSlice, err := tools.ConvertAnySliceToTyped(documentPathsRaw, "string")
if err != nil {
return nil, fmt.Errorf("failed to convert document paths: %w", err)
}
documentPaths, ok := typedSlice.([]string)
if !ok {
return nil, fmt.Errorf("unexpected type conversion error for document paths")
}
// Create document references from paths
docRefs := make([]*firestoreapi.DocumentRef, len(documentPaths))
for i, path := range documentPaths {
docRefs[i] = t.Client.Doc(path)
}
// Get all documents
snapshots, err := t.Client.GetAll(ctx, docRefs)
if err != nil {
return nil, fmt.Errorf("failed to get documents: %w", err)
}
// Convert snapshots to response data
results := make([]any, len(snapshots))
for i, snapshot := range snapshots {
docData := make(map[string]any)
docData["path"] = documentPaths[i]
docData["exists"] = snapshot.Exists()
if snapshot.Exists() {
docData["data"] = snapshot.Data()
docData["createTime"] = snapshot.CreateTime
docData["updateTime"] = snapshot.UpdateTime
docData["readTime"] = snapshot.ReadTime
}
results[i] = docData
}
return results, nil
}
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
return tools.ParseParams(t.Parameters, data, claims)
}
func (t Tool) Manifest() tools.Manifest {
return t.manifest
}
func (t Tool) McpManifest() tools.McpManifest {
return t.mcpManifest
}
func (t Tool) Authorized(verifiedAuthServices []string) bool {
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
}

View File

@@ -0,0 +1,156 @@
// Copyright 2025 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 firestoregetdocuments_test
import (
"testing"
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoregetdocuments"
)
func TestParseFromYamlFirestoreGetDocuments(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: `
tools:
get_docs_tool:
kind: firestore-get-documents
source: my-firestore-instance
description: Retrieve documents from Firestore by paths
`,
want: server.ToolConfigs{
"get_docs_tool": firestoregetdocuments.Config{
Name: "get_docs_tool",
Kind: "firestore-get-documents",
Source: "my-firestore-instance",
Description: "Retrieve documents from Firestore by paths",
AuthRequired: []string{},
},
},
},
{
desc: "with auth requirements",
in: `
tools:
secure_get_docs:
kind: firestore-get-documents
source: prod-firestore
description: Get documents with authentication
authRequired:
- google-auth-service
- api-key-service
`,
want: server.ToolConfigs{
"secure_get_docs": firestoregetdocuments.Config{
Name: "secure_get_docs",
Kind: "firestore-get-documents",
Source: "prod-firestore",
Description: "Get documents with authentication",
AuthRequired: []string{"google-auth-service", "api-key-service"},
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
// Parse contents
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
t.Fatalf("incorrect parse: diff %v", diff)
}
})
}
}
func TestParseFromYamlMultipleTools(t *testing.T) {
ctx, err := testutils.ContextWithNewLogger()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
in := `
tools:
get_user_docs:
kind: firestore-get-documents
source: users-firestore
description: Get user documents
authRequired:
- user-auth
get_product_docs:
kind: firestore-get-documents
source: products-firestore
description: Get product documents
get_order_docs:
kind: firestore-get-documents
source: orders-firestore
description: Get order documents
authRequired:
- user-auth
- admin-auth
`
want := server.ToolConfigs{
"get_user_docs": firestoregetdocuments.Config{
Name: "get_user_docs",
Kind: "firestore-get-documents",
Source: "users-firestore",
Description: "Get user documents",
AuthRequired: []string{"user-auth"},
},
"get_product_docs": firestoregetdocuments.Config{
Name: "get_product_docs",
Kind: "firestore-get-documents",
Source: "products-firestore",
Description: "Get product documents",
AuthRequired: []string{},
},
"get_order_docs": firestoregetdocuments.Config{
Name: "get_order_docs",
Kind: "firestore-get-documents",
Source: "orders-firestore",
Description: "Get order documents",
AuthRequired: []string{"user-auth", "admin-auth"},
},
}
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
// Parse contents
err = yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if diff := cmp.Diff(want, got.Tools); diff != "" {
t.Fatalf("incorrect parse: diff %v", diff)
}
}

View File

@@ -0,0 +1,159 @@
// Copyright 2025 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 firestoregetrules
import (
"context"
"fmt"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/sources"
firestoreds "github.com/googleapis/genai-toolbox/internal/sources/firestore"
"github.com/googleapis/genai-toolbox/internal/tools"
"google.golang.org/api/firebaserules/v1"
)
const kind string = "firestore-get-rules"
func init() {
if !tools.Register(kind, newConfig) {
panic(fmt.Sprintf("tool kind %q already registered", kind))
}
}
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 {
FirebaseRulesClient() *firebaserules.Service
GetProjectId() string
}
// validate compatible sources are still compatible
var _ compatibleSource = &firestoreds.Source{}
var compatibleSources = [...]string{firestoreds.SourceKind}
type Config struct {
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
Source string `yaml:"source" validate:"required"`
Description string `yaml:"description" validate:"required"`
AuthRequired []string `yaml:"authRequired"`
}
// validate interface
var _ tools.ToolConfig = Config{}
func (cfg Config) ToolConfigKind() string {
return kind
}
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
// verify source exists
rawS, ok := srcs[cfg.Source]
if !ok {
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
}
// verify the source is compatible
s, ok := rawS.(compatibleSource)
if !ok {
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
}
// No parameters needed for this tool
parameters := tools.Parameters{}
mcpManifest := tools.McpManifest{
Name: cfg.Name,
Description: cfg.Description,
InputSchema: parameters.McpManifest(),
}
// finish tool setup
t := Tool{
Name: cfg.Name,
Kind: kind,
Parameters: parameters,
AuthRequired: cfg.AuthRequired,
RulesClient: s.FirebaseRulesClient(),
ProjectId: s.GetProjectId(),
manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired},
mcpManifest: mcpManifest,
}
return t, nil
}
// validate interface
var _ tools.Tool = Tool{}
type Tool struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
AuthRequired []string `yaml:"authRequired"`
Parameters tools.Parameters `yaml:"parameters"`
RulesClient *firebaserules.Service
ProjectId string
manifest tools.Manifest
mcpManifest tools.McpManifest
}
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
// Get the latest release for Firestore
releaseName := fmt.Sprintf("projects/%s/releases/cloud.firestore", t.ProjectId)
release, err := t.RulesClient.Projects.Releases.Get(releaseName).Context(ctx).Do()
if err != nil {
return nil, fmt.Errorf("failed to get latest Firestore release: %w", err)
}
if release.RulesetName == "" {
return nil, fmt.Errorf("no active Firestore rules were found in project '%s'", t.ProjectId)
}
// Get the ruleset content
ruleset, err := t.RulesClient.Projects.Rulesets.Get(release.RulesetName).Context(ctx).Do()
if err != nil {
return nil, fmt.Errorf("failed to get ruleset content: %w", err)
}
if ruleset.Source == nil || len(ruleset.Source.Files) == 0 {
return nil, fmt.Errorf("no rules files found in ruleset")
}
return ruleset, nil
}
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
return tools.ParseParams(t.Parameters, data, claims)
}
func (t Tool) Manifest() tools.Manifest {
return t.manifest
}
func (t Tool) McpManifest() tools.McpManifest {
return t.mcpManifest
}
func (t Tool) Authorized(verifiedAuthServices []string) bool {
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
}

View File

@@ -0,0 +1,156 @@
// Copyright 2025 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 firestoregetrules_test
import (
"testing"
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoregetrules"
)
func TestParseFromYamlFirestoreGetRules(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: `
tools:
get_rules_tool:
kind: firestore-get-rules
source: my-firestore-instance
description: Retrieves the active Firestore security rules for the current project
`,
want: server.ToolConfigs{
"get_rules_tool": firestoregetrules.Config{
Name: "get_rules_tool",
Kind: "firestore-get-rules",
Source: "my-firestore-instance",
Description: "Retrieves the active Firestore security rules for the current project",
AuthRequired: []string{},
},
},
},
{
desc: "with auth requirements",
in: `
tools:
secure_get_rules:
kind: firestore-get-rules
source: prod-firestore
description: Get Firestore security rules with authentication
authRequired:
- google-auth-service
- admin-service
`,
want: server.ToolConfigs{
"secure_get_rules": firestoregetrules.Config{
Name: "secure_get_rules",
Kind: "firestore-get-rules",
Source: "prod-firestore",
Description: "Get Firestore security rules with authentication",
AuthRequired: []string{"google-auth-service", "admin-service"},
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
// Parse contents
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
t.Fatalf("incorrect parse: diff %v", diff)
}
})
}
}
func TestParseFromYamlMultipleTools(t *testing.T) {
ctx, err := testutils.ContextWithNewLogger()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
in := `
tools:
get_dev_rules:
kind: firestore-get-rules
source: dev-firestore
description: Get development Firestore rules
authRequired:
- dev-auth
get_staging_rules:
kind: firestore-get-rules
source: staging-firestore
description: Get staging Firestore rules
get_prod_rules:
kind: firestore-get-rules
source: prod-firestore
description: Get production Firestore rules
authRequired:
- prod-auth
- admin-auth
`
want := server.ToolConfigs{
"get_dev_rules": firestoregetrules.Config{
Name: "get_dev_rules",
Kind: "firestore-get-rules",
Source: "dev-firestore",
Description: "Get development Firestore rules",
AuthRequired: []string{"dev-auth"},
},
"get_staging_rules": firestoregetrules.Config{
Name: "get_staging_rules",
Kind: "firestore-get-rules",
Source: "staging-firestore",
Description: "Get staging Firestore rules",
AuthRequired: []string{},
},
"get_prod_rules": firestoregetrules.Config{
Name: "get_prod_rules",
Kind: "firestore-get-rules",
Source: "prod-firestore",
Description: "Get production Firestore rules",
AuthRequired: []string{"prod-auth", "admin-auth"},
},
}
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
// Parse contents
err = yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if diff := cmp.Diff(want, got.Tools); diff != "" {
t.Fatalf("incorrect parse: diff %v", diff)
}
}

View File

@@ -0,0 +1,175 @@
// Copyright 2025 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 firestorelistcollections
import (
"context"
"fmt"
firestoreapi "cloud.google.com/go/firestore"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/sources"
firestoreds "github.com/googleapis/genai-toolbox/internal/sources/firestore"
"github.com/googleapis/genai-toolbox/internal/tools"
)
const kind string = "firestore-list-collections"
const parentPathKey string = "parentPath"
func init() {
if !tools.Register(kind, newConfig) {
panic(fmt.Sprintf("tool kind %q already registered", kind))
}
}
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 {
FirestoreClient() *firestoreapi.Client
}
// validate compatible sources are still compatible
var _ compatibleSource = &firestoreds.Source{}
var compatibleSources = [...]string{firestoreds.SourceKind}
type Config struct {
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
Source string `yaml:"source" validate:"required"`
Description string `yaml:"description" validate:"required"`
AuthRequired []string `yaml:"authRequired"`
}
// validate interface
var _ tools.ToolConfig = Config{}
func (cfg Config) ToolConfigKind() string {
return kind
}
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
// verify source exists
rawS, ok := srcs[cfg.Source]
if !ok {
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
}
// verify the source is compatible
s, ok := rawS.(compatibleSource)
if !ok {
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
}
emptyString := ""
parentPathParameter := tools.NewStringParameterWithDefault(parentPathKey, emptyString, "Parent document path to list subcollections from. If not provided, lists root collections.")
parameters := tools.Parameters{parentPathParameter}
mcpManifest := tools.McpManifest{
Name: cfg.Name,
Description: cfg.Description,
InputSchema: parameters.McpManifest(),
}
// finish tool setup
t := Tool{
Name: cfg.Name,
Kind: kind,
Parameters: parameters,
AuthRequired: cfg.AuthRequired,
Client: s.FirestoreClient(),
manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired},
mcpManifest: mcpManifest,
}
return t, nil
}
// validate interface
var _ tools.Tool = Tool{}
type Tool struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
AuthRequired []string `yaml:"authRequired"`
Parameters tools.Parameters `yaml:"parameters"`
Client *firestoreapi.Client
manifest tools.Manifest
mcpManifest tools.McpManifest
}
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
mapParams := params.AsMap()
var collectionRefs []*firestoreapi.CollectionRef
var err error
// Check if parentPath is provided
parentPath, hasParent := mapParams[parentPathKey].(string)
if hasParent && parentPath != "" {
// List subcollections of the specified document
docRef := t.Client.Doc(parentPath)
collectionRefs, err = docRef.Collections(ctx).GetAll()
if err != nil {
return nil, fmt.Errorf("failed to list subcollections of document %q: %w", parentPath, err)
}
} else {
// List root collections
collectionRefs, err = t.Client.Collections(ctx).GetAll()
if err != nil {
return nil, fmt.Errorf("failed to list root collections: %w", err)
}
}
// Convert collection references to response data
results := make([]any, len(collectionRefs))
for i, collRef := range collectionRefs {
collData := make(map[string]any)
collData["id"] = collRef.ID
collData["path"] = collRef.Path
// If this is a subcollection, include parent information
if collRef.Parent != nil {
collData["parent"] = collRef.Parent.Path
}
results[i] = collData
}
return results, nil
}
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
return tools.ParseParams(t.Parameters, data, claims)
}
func (t Tool) Manifest() tools.Manifest {
return t.manifest
}
func (t Tool) McpManifest() tools.McpManifest {
return t.mcpManifest
}
func (t Tool) Authorized(verifiedAuthServices []string) bool {
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
}

View File

@@ -0,0 +1,156 @@
// Copyright 2025 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 firestorelistcollections_test
import (
"testing"
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/internal/tools/firestore/firestorelistcollections"
)
func TestParseFromYamlFirestoreListCollections(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: `
tools:
list_collections_tool:
kind: firestore-list-collections
source: my-firestore-instance
description: List collections in Firestore
`,
want: server.ToolConfigs{
"list_collections_tool": firestorelistcollections.Config{
Name: "list_collections_tool",
Kind: "firestore-list-collections",
Source: "my-firestore-instance",
Description: "List collections in Firestore",
AuthRequired: []string{},
},
},
},
{
desc: "with auth requirements",
in: `
tools:
secure_list_collections:
kind: firestore-list-collections
source: prod-firestore
description: List collections with authentication
authRequired:
- google-auth-service
- api-key-service
`,
want: server.ToolConfigs{
"secure_list_collections": firestorelistcollections.Config{
Name: "secure_list_collections",
Kind: "firestore-list-collections",
Source: "prod-firestore",
Description: "List collections with authentication",
AuthRequired: []string{"google-auth-service", "api-key-service"},
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
// Parse contents
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
t.Fatalf("incorrect parse: diff %v", diff)
}
})
}
}
func TestParseFromYamlMultipleTools(t *testing.T) {
ctx, err := testutils.ContextWithNewLogger()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
in := `
tools:
list_user_collections:
kind: firestore-list-collections
source: users-firestore
description: List user-related collections
authRequired:
- user-auth
list_product_collections:
kind: firestore-list-collections
source: products-firestore
description: List product-related collections
list_admin_collections:
kind: firestore-list-collections
source: admin-firestore
description: List administrative collections
authRequired:
- user-auth
- admin-auth
`
want := server.ToolConfigs{
"list_user_collections": firestorelistcollections.Config{
Name: "list_user_collections",
Kind: "firestore-list-collections",
Source: "users-firestore",
Description: "List user-related collections",
AuthRequired: []string{"user-auth"},
},
"list_product_collections": firestorelistcollections.Config{
Name: "list_product_collections",
Kind: "firestore-list-collections",
Source: "products-firestore",
Description: "List product-related collections",
AuthRequired: []string{},
},
"list_admin_collections": firestorelistcollections.Config{
Name: "list_admin_collections",
Kind: "firestore-list-collections",
Source: "admin-firestore",
Description: "List administrative collections",
AuthRequired: []string{"user-auth", "admin-auth"},
},
}
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
// Parse contents
err = yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if diff := cmp.Diff(want, got.Tools); diff != "" {
t.Fatalf("incorrect parse: diff %v", diff)
}
}

View File

@@ -0,0 +1,529 @@
// Copyright 2025 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 firestorequerycollection
import (
"context"
"encoding/json"
"fmt"
"strings"
firestoreapi "cloud.google.com/go/firestore"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/sources"
firestoreds "github.com/googleapis/genai-toolbox/internal/sources/firestore"
"github.com/googleapis/genai-toolbox/internal/tools"
)
// Constants for tool configuration
const (
kind = "firestore-query-collection"
defaultLimit = 100
defaultAnalyze = false
maxFilterLength = 100 // Maximum filters to prevent abuse
)
// Parameter keys
const (
collectionPathKey = "collectionPath"
filtersKey = "filters"
orderByKey = "orderBy"
limitKey = "limit"
analyzeQueryKey = "analyzeQuery"
)
// Firestore operators
var validOperators = map[string]bool{
"<": true,
"<=": true,
">": true,
">=": true,
"==": true,
"!=": true,
"array-contains": true,
"array-contains-any": true,
"in": true,
"not-in": true,
}
// Error messages
const (
errMissingCollectionPath = "invalid or missing '%s' parameter"
errInvalidFilters = "invalid '%s' parameter; expected an array"
errFilterNotString = "filter at index %d is not a string"
errFilterParseFailed = "failed to parse filter at index %d: %w"
errInvalidOperator = "unsupported operator: %s. Valid operators are: %v"
errMissingFilterValue = "no value specified for filter on field '%s'"
errOrderByParseFailed = "failed to parse orderBy: %w"
errQueryExecutionFailed = "failed to execute query: %w"
errTooManyFilters = "too many filters provided: %d (maximum: %d)"
)
func init() {
if !tools.Register(kind, newConfig) {
panic(fmt.Sprintf("tool kind %q already registered", kind))
}
}
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
}
// compatibleSource defines the interface for sources that can provide a Firestore client
type compatibleSource interface {
FirestoreClient() *firestoreapi.Client
}
// validate compatible sources are still compatible
var _ compatibleSource = &firestoreds.Source{}
var compatibleSources = [...]string{firestoreds.SourceKind}
// Config represents the configuration for the Firestore query collection tool
type Config struct {
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
Source string `yaml:"source" validate:"required"`
Description string `yaml:"description" validate:"required"`
AuthRequired []string `yaml:"authRequired"`
}
// validate interface
var _ tools.ToolConfig = Config{}
// ToolConfigKind returns the kind of tool configuration
func (cfg Config) ToolConfigKind() string {
return kind
}
// Initialize creates a new Tool instance from the configuration
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
// verify source exists
rawS, ok := srcs[cfg.Source]
if !ok {
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
}
// verify the source is compatible
s, ok := rawS.(compatibleSource)
if !ok {
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
}
// Create parameters
parameters := createParameters()
mcpManifest := tools.McpManifest{
Name: cfg.Name,
Description: cfg.Description,
InputSchema: parameters.McpManifest(),
}
// finish tool setup
t := Tool{
Name: cfg.Name,
Kind: kind,
Parameters: parameters,
AuthRequired: cfg.AuthRequired,
Client: s.FirestoreClient(),
manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired},
mcpManifest: mcpManifest,
}
return t, nil
}
// createParameters creates the parameter definitions for the tool
func createParameters() tools.Parameters {
collectionPathParameter := tools.NewStringParameter(
collectionPathKey,
"The path to the Firestore collection to query",
)
filtersDescription := `Array of filter objects to apply to the query. Each filter is a JSON string with:
- field: The field name to filter on
- op: The operator to use ("<", "<=", ">", ">=", "==", "!=", "array-contains", "array-contains-any", "in", "not-in")
- value: The value to compare against (can be string, number, boolean, or array)
Example: {"field": "age", "op": ">", "value": 18}`
filtersParameter := tools.NewArrayParameter(
filtersKey,
filtersDescription,
tools.NewStringParameter("item", "JSON string representation of a filter object"),
)
orderByParameter := tools.NewStringParameter(
orderByKey,
"JSON string specifying the field and direction to order by (e.g., {\"field\": \"name\", \"direction\": \"ASCENDING\"}). Leave empty if not specified",
)
limitParameter := tools.NewIntParameterWithDefault(
limitKey,
defaultLimit,
"The maximum number of documents to return",
)
analyzeQueryParameter := tools.NewBooleanParameterWithDefault(
analyzeQueryKey,
defaultAnalyze,
"If true, returns query explain metrics including execution statistics",
)
return tools.Parameters{
collectionPathParameter,
filtersParameter,
orderByParameter,
limitParameter,
analyzeQueryParameter,
}
}
// validate interface
var _ tools.Tool = Tool{}
// Tool represents the Firestore query collection tool
type Tool struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
AuthRequired []string `yaml:"authRequired"`
Parameters tools.Parameters `yaml:"parameters"`
Client *firestoreapi.Client
manifest tools.Manifest
mcpManifest tools.McpManifest
}
// FilterConfig represents a filter for the query
type FilterConfig struct {
Field string `json:"field"`
Op string `json:"op"`
Value interface{} `json:"value"`
}
// Validate checks if the filter configuration is valid
func (f *FilterConfig) Validate() error {
if f.Field == "" {
return fmt.Errorf("filter field cannot be empty")
}
if !validOperators[f.Op] {
ops := make([]string, 0, len(validOperators))
for op := range validOperators {
ops = append(ops, op)
}
return fmt.Errorf(errInvalidOperator, f.Op, ops)
}
if f.Value == nil {
return fmt.Errorf(errMissingFilterValue, f.Field)
}
return nil
}
// OrderByConfig represents ordering configuration
type OrderByConfig struct {
Field string `json:"field"`
Direction string `json:"direction"`
}
// GetDirection returns the Firestore direction constant
func (o *OrderByConfig) GetDirection() firestoreapi.Direction {
if strings.EqualFold(o.Direction, "DESCENDING") {
return firestoreapi.Desc
}
return firestoreapi.Asc
}
// QueryResult represents a document result from the query
type QueryResult struct {
ID string `json:"id"`
Path string `json:"path"`
Data map[string]any `json:"data"`
CreateTime interface{} `json:"createTime,omitempty"`
UpdateTime interface{} `json:"updateTime,omitempty"`
ReadTime interface{} `json:"readTime,omitempty"`
}
// QueryResponse represents the full response including optional metrics
type QueryResponse struct {
Documents []QueryResult `json:"documents"`
ExplainMetrics map[string]any `json:"explainMetrics,omitempty"`
}
// Invoke executes the Firestore query based on the provided parameters
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
// Parse parameters
queryParams, err := t.parseQueryParameters(params)
if err != nil {
return nil, err
}
// Build the query
query, err := t.buildQuery(queryParams)
if err != nil {
return nil, err
}
// Execute the query and return results
return t.executeQuery(ctx, query, queryParams.AnalyzeQuery)
}
// queryParameters holds all parsed query parameters
type queryParameters struct {
CollectionPath string
Filters []FilterConfig
OrderBy *OrderByConfig
Limit int
AnalyzeQuery bool
}
// parseQueryParameters extracts and validates parameters from the input
func (t Tool) parseQueryParameters(params tools.ParamValues) (*queryParameters, error) {
mapParams := params.AsMap()
// Get collection path
collectionPath, ok := mapParams[collectionPathKey].(string)
if !ok || collectionPath == "" {
return nil, fmt.Errorf(errMissingCollectionPath, collectionPathKey)
}
result := &queryParameters{
CollectionPath: collectionPath,
Limit: defaultLimit,
AnalyzeQuery: defaultAnalyze,
}
// Parse filters
if filtersRaw, ok := mapParams[filtersKey]; ok && filtersRaw != nil {
filters, err := t.parseFilters(filtersRaw)
if err != nil {
return nil, err
}
result.Filters = filters
}
// Parse orderBy
if orderByRaw, ok := mapParams[orderByKey]; ok && orderByRaw != nil {
orderBy, err := t.parseOrderBy(orderByRaw)
if err != nil {
return nil, err
}
result.OrderBy = orderBy
}
// Parse limit
if limit, ok := mapParams[limitKey].(int); ok {
result.Limit = limit
}
// Parse analyze
if analyze, ok := mapParams[analyzeQueryKey].(bool); ok {
result.AnalyzeQuery = analyze
}
return result, nil
}
// parseFilters parses and validates filter configurations
func (t Tool) parseFilters(filtersRaw interface{}) ([]FilterConfig, error) {
filters, ok := filtersRaw.([]any)
if !ok {
return nil, fmt.Errorf(errInvalidFilters, filtersKey)
}
if len(filters) > maxFilterLength {
return nil, fmt.Errorf(errTooManyFilters, len(filters), maxFilterLength)
}
result := make([]FilterConfig, 0, len(filters))
for i, filterRaw := range filters {
filterJSON, ok := filterRaw.(string)
if !ok {
return nil, fmt.Errorf(errFilterNotString, i)
}
var filter FilterConfig
if err := json.Unmarshal([]byte(filterJSON), &filter); err != nil {
return nil, fmt.Errorf(errFilterParseFailed, i, err)
}
if err := filter.Validate(); err != nil {
return nil, fmt.Errorf("filter at index %d is invalid: %w", i, err)
}
result = append(result, filter)
}
return result, nil
}
// parseOrderBy parses the orderBy configuration
func (t Tool) parseOrderBy(orderByRaw interface{}) (*OrderByConfig, error) {
orderByJSON, ok := orderByRaw.(string)
if !ok || orderByJSON == "" {
return nil, nil
}
var orderBy OrderByConfig
if err := json.Unmarshal([]byte(orderByJSON), &orderBy); err != nil {
return nil, fmt.Errorf(errOrderByParseFailed, err)
}
if orderBy.Field == "" {
return nil, nil
}
return &orderBy, nil
}
// buildQuery constructs the Firestore query from parameters
func (t Tool) buildQuery(params *queryParameters) (*firestoreapi.Query, error) {
collection := t.Client.Collection(params.CollectionPath)
query := collection.Query
// Apply filters
if len(params.Filters) > 0 {
filterConditions := make([]firestoreapi.EntityFilter, 0, len(params.Filters))
for _, filter := range params.Filters {
filterConditions = append(filterConditions, firestoreapi.PropertyFilter{
Path: filter.Field,
Operator: filter.Op,
Value: filter.Value,
})
}
query = query.WhereEntity(firestoreapi.AndFilter{
Filters: filterConditions,
})
}
// Apply ordering
if params.OrderBy != nil {
query = query.OrderBy(params.OrderBy.Field, params.OrderBy.GetDirection())
}
// Apply limit
query = query.Limit(params.Limit)
// Apply analyze options
if params.AnalyzeQuery {
query = query.WithRunOptions(firestoreapi.ExplainOptions{
Analyze: true,
})
}
return &query, nil
}
// executeQuery runs the query and formats the results
func (t Tool) executeQuery(ctx context.Context, query *firestoreapi.Query, analyzeQuery bool) (any, error) {
docIterator := query.Documents(ctx)
docs, err := docIterator.GetAll()
if err != nil {
return nil, fmt.Errorf(errQueryExecutionFailed, err)
}
// Convert results to structured format
results := make([]QueryResult, len(docs))
for i, doc := range docs {
results[i] = QueryResult{
ID: doc.Ref.ID,
Path: doc.Ref.Path,
Data: doc.Data(),
CreateTime: doc.CreateTime,
UpdateTime: doc.UpdateTime,
ReadTime: doc.ReadTime,
}
}
// Return with explain metrics if requested
if analyzeQuery {
explainMetrics, err := t.getExplainMetrics(docIterator)
if err == nil && explainMetrics != nil {
response := QueryResponse{
Documents: results,
ExplainMetrics: explainMetrics,
}
return response, nil
}
}
// Return just the documents
resultsAny := make([]any, len(results))
for i, r := range results {
resultsAny[i] = r
}
return resultsAny, nil
}
// getExplainMetrics extracts explain metrics from the query iterator
func (t Tool) getExplainMetrics(docIterator *firestoreapi.DocumentIterator) (map[string]any, error) {
explainMetrics, err := docIterator.ExplainMetrics()
if err != nil || explainMetrics == nil {
return nil, err
}
metricsData := make(map[string]any)
// Add plan summary if available
if explainMetrics.PlanSummary != nil {
planSummary := make(map[string]any)
planSummary["indexesUsed"] = explainMetrics.PlanSummary.IndexesUsed
metricsData["planSummary"] = planSummary
}
// Add execution stats if available
if explainMetrics.ExecutionStats != nil {
executionStats := make(map[string]any)
executionStats["resultsReturned"] = explainMetrics.ExecutionStats.ResultsReturned
executionStats["readOperations"] = explainMetrics.ExecutionStats.ReadOperations
if explainMetrics.ExecutionStats.ExecutionDuration != nil {
executionStats["executionDuration"] = explainMetrics.ExecutionStats.ExecutionDuration.String()
}
if explainMetrics.ExecutionStats.DebugStats != nil {
executionStats["debugStats"] = *explainMetrics.ExecutionStats.DebugStats
}
metricsData["executionStats"] = executionStats
}
return metricsData, nil
}
// ParseParams parses and validates input parameters
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
return tools.ParseParams(t.Parameters, data, claims)
}
// Manifest returns the tool manifest
func (t Tool) Manifest() tools.Manifest {
return t.manifest
}
// McpManifest returns the MCP manifest
func (t Tool) McpManifest() tools.McpManifest {
return t.mcpManifest
}
// Authorized checks if the tool is authorized based on verified auth services
func (t Tool) Authorized(verifiedAuthServices []string) bool {
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
}

View File

@@ -0,0 +1,156 @@
// Copyright 2025 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 firestorequerycollection_test
import (
"testing"
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/internal/tools/firestore/firestorequerycollection"
)
func TestParseFromYamlFirestoreQueryCollection(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: `
tools:
query_users_tool:
kind: firestore-query-collection
source: my-firestore-instance
description: Query users collection with filters and ordering
`,
want: server.ToolConfigs{
"query_users_tool": firestorequerycollection.Config{
Name: "query_users_tool",
Kind: "firestore-query-collection",
Source: "my-firestore-instance",
Description: "Query users collection with filters and ordering",
AuthRequired: []string{},
},
},
},
{
desc: "with auth requirements",
in: `
tools:
secure_query_tool:
kind: firestore-query-collection
source: prod-firestore
description: Query collections with authentication
authRequired:
- google-auth-service
- api-key-service
`,
want: server.ToolConfigs{
"secure_query_tool": firestorequerycollection.Config{
Name: "secure_query_tool",
Kind: "firestore-query-collection",
Source: "prod-firestore",
Description: "Query collections with authentication",
AuthRequired: []string{"google-auth-service", "api-key-service"},
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
// Parse contents
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
t.Fatalf("incorrect parse: diff %v", diff)
}
})
}
}
func TestParseFromYamlMultipleTools(t *testing.T) {
ctx, err := testutils.ContextWithNewLogger()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
in := `
tools:
query_users:
kind: firestore-query-collection
source: users-firestore
description: Query user documents with filtering
authRequired:
- user-auth
query_products:
kind: firestore-query-collection
source: products-firestore
description: Query product catalog
query_orders:
kind: firestore-query-collection
source: orders-firestore
description: Query customer orders with complex filters
authRequired:
- user-auth
- admin-auth
`
want := server.ToolConfigs{
"query_users": firestorequerycollection.Config{
Name: "query_users",
Kind: "firestore-query-collection",
Source: "users-firestore",
Description: "Query user documents with filtering",
AuthRequired: []string{"user-auth"},
},
"query_products": firestorequerycollection.Config{
Name: "query_products",
Kind: "firestore-query-collection",
Source: "products-firestore",
Description: "Query product catalog",
AuthRequired: []string{},
},
"query_orders": firestorequerycollection.Config{
Name: "query_orders",
Kind: "firestore-query-collection",
Source: "orders-firestore",
Description: "Query customer orders with complex filters",
AuthRequired: []string{"user-auth", "admin-auth"},
},
}
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
// Parse contents
err = yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if diff := cmp.Diff(want, got.Tools); diff != "" {
t.Fatalf("incorrect parse: diff %v", diff)
}
}

View File

@@ -0,0 +1,292 @@
// Copyright 2025 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 firestorevalidaterules
import (
"context"
"fmt"
"strings"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/sources"
firestoreds "github.com/googleapis/genai-toolbox/internal/sources/firestore"
"github.com/googleapis/genai-toolbox/internal/tools"
"google.golang.org/api/firebaserules/v1"
)
const kind string = "firestore-validate-rules"
// Parameter keys
const (
sourceKey = "source"
)
func init() {
if !tools.Register(kind, newConfig) {
panic(fmt.Sprintf("tool kind %q already registered", kind))
}
}
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 {
FirebaseRulesClient() *firebaserules.Service
GetProjectId() string
}
// validate compatible sources are still compatible
var _ compatibleSource = &firestoreds.Source{}
var compatibleSources = [...]string{firestoreds.SourceKind}
type Config struct {
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
Source string `yaml:"source" validate:"required"`
Description string `yaml:"description" validate:"required"`
AuthRequired []string `yaml:"authRequired"`
}
// validate interface
var _ tools.ToolConfig = Config{}
func (cfg Config) ToolConfigKind() string {
return kind
}
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
// verify source exists
rawS, ok := srcs[cfg.Source]
if !ok {
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
}
// verify the source is compatible
s, ok := rawS.(compatibleSource)
if !ok {
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
}
// Create parameters
parameters := createParameters()
mcpManifest := tools.McpManifest{
Name: cfg.Name,
Description: cfg.Description,
InputSchema: parameters.McpManifest(),
}
// finish tool setup
t := Tool{
Name: cfg.Name,
Kind: kind,
Parameters: parameters,
AuthRequired: cfg.AuthRequired,
RulesClient: s.FirebaseRulesClient(),
ProjectId: s.GetProjectId(),
manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired},
mcpManifest: mcpManifest,
}
return t, nil
}
// createParameters creates the parameter definitions for the tool
func createParameters() tools.Parameters {
sourceParameter := tools.NewStringParameter(
sourceKey,
"The Firestore Rules source code to validate",
)
return tools.Parameters{sourceParameter}
}
// validate interface
var _ tools.Tool = Tool{}
type Tool struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
AuthRequired []string `yaml:"authRequired"`
Parameters tools.Parameters `yaml:"parameters"`
RulesClient *firebaserules.Service
ProjectId string
manifest tools.Manifest
mcpManifest tools.McpManifest
}
// Issue represents a validation issue in the rules
type Issue struct {
SourcePosition SourcePosition `json:"sourcePosition"`
Description string `json:"description"`
Severity string `json:"severity"`
}
// SourcePosition represents the location of an issue in the source
type SourcePosition struct {
FileName string `json:"fileName,omitempty"`
Line int64 `json:"line"` // 1-based
Column int64 `json:"column"` // 1-based
CurrentOffset int64 `json:"currentOffset"` // 0-based, inclusive start
EndOffset int64 `json:"endOffset"` // 0-based, exclusive end
}
// ValidationResult represents the result of rules validation
type ValidationResult struct {
Valid bool `json:"valid"`
IssueCount int `json:"issueCount"`
FormattedIssues string `json:"formattedIssues,omitempty"`
RawIssues []Issue `json:"rawIssues,omitempty"`
}
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
mapParams := params.AsMap()
// Get source parameter
source, ok := mapParams[sourceKey].(string)
if !ok || source == ""{
return nil, fmt.Errorf("invalid or missing '%s' parameter", sourceKey)
}
// Create test request
testRequest := &firebaserules.TestRulesetRequest{
Source: &firebaserules.Source{
Files: []*firebaserules.File{
{
Name: "firestore.rules",
Content: source,
},
},
},
// We don't need test cases for validation only
TestSuite: &firebaserules.TestSuite{
TestCases: []*firebaserules.TestCase{},
},
}
// Call the test API
projectName := fmt.Sprintf("projects/%s", t.ProjectId)
response, err := t.RulesClient.Projects.Test(projectName, testRequest).Context(ctx).Do()
if err != nil {
return nil, fmt.Errorf("failed to validate rules: %w", err)
}
// Process the response
result := t.processValidationResponse(response, source)
return result, nil
}
func (t Tool) processValidationResponse(response *firebaserules.TestRulesetResponse, source string) ValidationResult {
if len(response.Issues) == 0 {
return ValidationResult{
Valid: true,
IssueCount: 0,
FormattedIssues: "✓ No errors detected. Rules are valid.",
}
}
// Convert issues to our format
issues := make([]Issue, len(response.Issues))
for i, issue := range response.Issues {
issues[i] = Issue{
Description: issue.Description,
Severity: issue.Severity,
SourcePosition: SourcePosition{
FileName: issue.SourcePosition.FileName,
Line: issue.SourcePosition.Line,
Column: issue.SourcePosition.Column,
CurrentOffset: issue.SourcePosition.CurrentOffset,
EndOffset: issue.SourcePosition.EndOffset,
},
}
}
// Format issues
formattedIssues := t.formatRulesetIssues(issues, source)
return ValidationResult{
Valid: false,
IssueCount: len(issues),
FormattedIssues: formattedIssues,
RawIssues: issues,
}
}
// formatRulesetIssues formats validation issues into a human-readable string with code snippets
func (t Tool) formatRulesetIssues(issues []Issue, rulesSource string) string {
sourceLines := strings.Split(rulesSource, "\n")
var formattedOutput []string
formattedOutput = append(formattedOutput, fmt.Sprintf("Found %d issue(s) in rules source:\n", len(issues)))
for _, issue := range issues {
issueString := fmt.Sprintf("%s: %s [Ln %d, Col %d]",
issue.Severity,
issue.Description,
issue.SourcePosition.Line,
issue.SourcePosition.Column)
if issue.SourcePosition.Line > 0 {
lineIndex := int(issue.SourcePosition.Line - 1) // 0-based index
if lineIndex >= 0 && lineIndex < len(sourceLines) {
errorLine := sourceLines[lineIndex]
issueString += fmt.Sprintf("\n```\n%s", errorLine)
// Add carets if we have column and offset information
if issue.SourcePosition.Column > 0 &&
issue.SourcePosition.CurrentOffset >= 0 &&
issue.SourcePosition.EndOffset > issue.SourcePosition.CurrentOffset {
startColumn := int(issue.SourcePosition.Column - 1) // 0-based
errorTokenLength := int(issue.SourcePosition.EndOffset - issue.SourcePosition.CurrentOffset)
if startColumn >= 0 && errorTokenLength > 0 && startColumn <= len(errorLine) {
padding := strings.Repeat(" ", startColumn)
carets := strings.Repeat("^", errorTokenLength)
issueString += fmt.Sprintf("\n%s%s", padding, carets)
}
}
issueString += "\n```"
}
}
formattedOutput = append(formattedOutput, issueString)
}
return strings.Join(formattedOutput, "\n\n")
}
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
return tools.ParseParams(t.Parameters, data, claims)
}
func (t Tool) Manifest() tools.Manifest {
return t.manifest
}
func (t Tool) McpManifest() tools.McpManifest {
return t.mcpManifest
}
func (t Tool) Authorized(verifiedAuthServices []string) bool {
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
}

View File

@@ -0,0 +1,156 @@
// Copyright 2025 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 firestorevalidaterules_test
import (
"testing"
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/internal/tools/firestore/firestorevalidaterules"
)
func TestParseFromYamlFirestoreValidateRules(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: `
tools:
validate_rules_tool:
kind: firestore-validate-rules
source: my-firestore-instance
description: Validate Firestore security rules
`,
want: server.ToolConfigs{
"validate_rules_tool": firestorevalidaterules.Config{
Name: "validate_rules_tool",
Kind: "firestore-validate-rules",
Source: "my-firestore-instance",
Description: "Validate Firestore security rules",
AuthRequired: []string{},
},
},
},
{
desc: "with auth requirements",
in: `
tools:
secure_validate_rules:
kind: firestore-validate-rules
source: prod-firestore
description: Validate rules with authentication
authRequired:
- google-auth-service
- api-key-service
`,
want: server.ToolConfigs{
"secure_validate_rules": firestorevalidaterules.Config{
Name: "secure_validate_rules",
Kind: "firestore-validate-rules",
Source: "prod-firestore",
Description: "Validate rules with authentication",
AuthRequired: []string{"google-auth-service", "api-key-service"},
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
// Parse contents
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
t.Fatalf("incorrect parse: diff %v", diff)
}
})
}
}
func TestParseFromYamlMultipleTools(t *testing.T) {
ctx, err := testutils.ContextWithNewLogger()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
in := `
tools:
validate_dev_rules:
kind: firestore-validate-rules
source: dev-firestore
description: Validate development environment rules
authRequired:
- dev-auth
validate_staging_rules:
kind: firestore-validate-rules
source: staging-firestore
description: Validate staging environment rules
validate_prod_rules:
kind: firestore-validate-rules
source: prod-firestore
description: Validate production environment rules
authRequired:
- prod-auth
- admin-auth
`
want := server.ToolConfigs{
"validate_dev_rules": firestorevalidaterules.Config{
Name: "validate_dev_rules",
Kind: "firestore-validate-rules",
Source: "dev-firestore",
Description: "Validate development environment rules",
AuthRequired: []string{"dev-auth"},
},
"validate_staging_rules": firestorevalidaterules.Config{
Name: "validate_staging_rules",
Kind: "firestore-validate-rules",
Source: "staging-firestore",
Description: "Validate staging environment rules",
AuthRequired: []string{},
},
"validate_prod_rules": firestorevalidaterules.Config{
Name: "validate_prod_rules",
Kind: "firestore-validate-rules",
Source: "prod-firestore",
Description: "Validate production environment rules",
AuthRequired: []string{"prod-auth", "admin-auth"},
},
}
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
// Parse contents
err = yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if diff := cmp.Diff(want, got.Tools); diff != "" {
t.Fatalf("incorrect parse: diff %v", diff)
}
}

File diff suppressed because it is too large Load Diff