This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new d69f5a7848 [Improve][Connector-V2] Add declarative validation for 
Google Firestore sink (#12278)
d69f5a7848 is described below

commit d69f5a7848025e9de62e145814edf8e931fe736e
Author: Supidan <[email protected]>
AuthorDate: Sun Sep 13 07:14:54 2026 +0000

    [Improve][Connector-V2] Add declarative validation for Google Firestore 
sink (#12278)
    
    Co-authored-by: Supidan <[email protected]>
---
 docs/en/connectors/sink/GoogleFirestore.md         | 12 +--
 docs/zh/connectors/sink/GoogleFirestore.md         | 12 +--
 .../firestore/sink/FirestoreSinkFactory.java       |  7 +-
 .../google/firestore/FirestoreFactoryTest.java     | 91 +++++++++++++++++++++-
 4 files changed, 108 insertions(+), 14 deletions(-)

diff --git a/docs/en/connectors/sink/GoogleFirestore.md 
b/docs/en/connectors/sink/GoogleFirestore.md
index 38f7232107..645d482098 100644
--- a/docs/en/connectors/sink/GoogleFirestore.md
+++ b/docs/en/connectors/sink/GoogleFirestore.md
@@ -42,22 +42,22 @@ It can be downloaded via install-plugin.sh or from Maven 
central repository.
 
 | name           | type   | required | default value | description |
 |----------------|--------|----------|---------------|-------------|
-| project_id     | string | yes      | -             | Google Cloud project ID 
that owns the Firestore database. |
-| collection     | string | yes      | -             | Firestore collection 
name to write to. |
-| credentials    | string | no       | -             | Base64-encoded Google 
Cloud service account JSON. |
+| project_id     | string | yes      | -             | Nonblank Google Cloud 
project ID that owns the Firestore database. |
+| collection     | string | yes      | -             | Nonblank Firestore 
collection name to write to. |
+| credentials    | string | no       | -             | Base64-encoded Google 
Cloud service account JSON. If set, the value must not be blank. |
 | common-options |        | no       | -             | Sink common options. 
See [Sink Common Options](../common-options/sink-common-options.md). |
 
 ### project_id [string]
 
-The Google Cloud project ID that owns the Firestore database.
+The Google Cloud project ID that owns the Firestore database. This option is 
required and must not be blank.
 
 ### collection [string]
 
-The Firestore collection to write to. Each sink block writes to one collection.
+The Firestore collection to write to. This option is required and must not be 
blank. Each sink block writes to one collection.
 
 ### credentials [string]
 
-Base64-encoded Google Cloud service account JSON.
+Optional Base64-encoded Google Cloud service account JSON. If configured, the 
value must not be blank.
 
 If this option is not set, the connector uses Google Application Default 
Credentials. In that case, make sure `GOOGLE_APPLICATION_CREDENTIALS` points to 
the service account JSON file or the runtime environment already provides 
default credentials.
 
diff --git a/docs/zh/connectors/sink/GoogleFirestore.md 
b/docs/zh/connectors/sink/GoogleFirestore.md
index 2d948adddd..75d825f82a 100644
--- a/docs/zh/connectors/sink/GoogleFirestore.md
+++ b/docs/zh/connectors/sink/GoogleFirestore.md
@@ -40,22 +40,22 @@ GoogleFirestore Sink 用于将 SeaTunnel 数据写入 Google Cloud 
Firestore 集
 
 | 名称           | 类型   | 必填 | 默认值 | 说明 |
 |----------------|--------|------|--------|------|
-| project_id     | string | 是   | -      | Firestore 数据库所在的 Google Cloud 项目 
ID。 |
-| collection     | string | 是   | -      | 要写入的 Firestore 集合名称。 |
-| credentials    | string | 否   | -      | Base64 编码后的 Google Cloud 服务账号 JSON。 
|
+| project_id     | string | 是   | -      | Firestore 数据库所在的非空白 Google Cloud 项目 
ID。 |
+| collection     | string | 是   | -      | 要写入的非空白 Firestore 集合名称。 |
+| credentials    | string | 否   | -      | Base64 编码后的 Google Cloud 服务账号 
JSON;配置时不能为空白。 |
 | common-options |        | 否   | -      | Sink 通用选项,详见 [Sink 
通用选项](../common-options/sink-common-options.md)。 |
 
 ### project_id [string]
 
-Firestore 数据库所在的 Google Cloud 项目 ID。
+Firestore 数据库所在的 Google Cloud 项目 ID。此选项必填,且不能为空白。
 
 ### collection [string]
 
-要写入的 Firestore 集合名称。每个 sink 配置块写入一个集合。
+要写入的 Firestore 集合名称。此选项必填,且不能为空白。每个 sink 配置块写入一个集合。
 
 ### credentials [string]
 
-Base64 编码后的 Google Cloud 服务账号 JSON。
+可选的 Base64 编码 Google Cloud 服务账号 JSON。配置此选项时,值不能为空白。
 
 如果不配置该参数,连接器会使用 Google 应用默认凭证。此时需要确保 `GOOGLE_APPLICATION_CREDENTIALS` 指向服务账号 
JSON 文件,或者运行环境已经提供默认凭证。
 
diff --git 
a/seatunnel-connectors-v2/connector-google-firestore/src/main/java/org/apache/seatunnel/connectors/seatunnel/google/firestore/sink/FirestoreSinkFactory.java
 
b/seatunnel-connectors-v2/connector-google-firestore/src/main/java/org/apache/seatunnel/connectors/seatunnel/google/firestore/sink/FirestoreSinkFactory.java
index 76b3496800..0baf29aace 100644
--- 
a/seatunnel-connectors-v2/connector-google-firestore/src/main/java/org/apache/seatunnel/connectors/seatunnel/google/firestore/sink/FirestoreSinkFactory.java
+++ 
b/seatunnel-connectors-v2/connector-google-firestore/src/main/java/org/apache/seatunnel/connectors/seatunnel/google/firestore/sink/FirestoreSinkFactory.java
@@ -26,6 +26,7 @@ import 
org.apache.seatunnel.connectors.seatunnel.google.firestore.config.Firesto
 
 import com.google.auto.service.AutoService;
 
+import static org.apache.seatunnel.api.configuration.util.Conditions.notBlank;
 import static 
org.apache.seatunnel.connectors.seatunnel.google.firestore.config.FirestoreSinkOptions.COLLECTION;
 import static 
org.apache.seatunnel.connectors.seatunnel.google.firestore.config.FirestoreSinkOptions.CREDENTIALS;
 import static 
org.apache.seatunnel.connectors.seatunnel.google.firestore.config.FirestoreSinkOptions.PROJECT_ID;
@@ -40,7 +41,11 @@ public class FirestoreSinkFactory implements 
TableSinkFactory {
 
     @Override
     public OptionRule optionRule() {
-        return OptionRule.builder().required(PROJECT_ID, 
COLLECTION).optional(CREDENTIALS).build();
+        return OptionRule.builder()
+                .required(PROJECT_ID, notBlank(PROJECT_ID))
+                .required(COLLECTION, notBlank(COLLECTION))
+                .optional(CREDENTIALS, notBlank(CREDENTIALS))
+                .build();
     }
 
     @Override
diff --git 
a/seatunnel-connectors-v2/connector-google-firestore/src/test/java/org/apache/seatunnel/connectors/seatunnel/google/firestore/FirestoreFactoryTest.java
 
b/seatunnel-connectors-v2/connector-google-firestore/src/test/java/org/apache/seatunnel/connectors/seatunnel/google/firestore/FirestoreFactoryTest.java
index 5b41317ff8..f8b7993417 100644
--- 
a/seatunnel-connectors-v2/connector-google-firestore/src/test/java/org/apache/seatunnel/connectors/seatunnel/google/firestore/FirestoreFactoryTest.java
+++ 
b/seatunnel-connectors-v2/connector-google-firestore/src/test/java/org/apache/seatunnel/connectors/seatunnel/google/firestore/FirestoreFactoryTest.java
@@ -17,15 +17,104 @@
 
 package org.apache.seatunnel.connectors.seatunnel.google.firestore;
 
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.configuration.util.ConfigValidator;
+import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.configuration.util.OptionValidationException;
+import 
org.apache.seatunnel.connectors.seatunnel.google.firestore.config.FirestoreSinkOptions;
 import 
org.apache.seatunnel.connectors.seatunnel.google.firestore.sink.FirestoreSinkFactory;
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.HashMap;
+import java.util.Map;
 
 class FirestoreFactoryTest {
 
+    private final OptionRule optionRule = new 
FirestoreSinkFactory().optionRule();
+
     @Test
     void optionRule() {
-        Assertions.assertNotNull((new FirestoreSinkFactory()).optionRule());
+        Assertions.assertNotNull(optionRule);
+    }
+
+    @Test
+    void testValidConfigWithoutCredentials() {
+        Assertions.assertDoesNotThrow(() -> validate(validConfig()));
+    }
+
+    @Test
+    void testValidConfigWithCredentials() {
+        Map<String, Object> config = validConfig();
+        config.put(FirestoreSinkOptions.CREDENTIALS.key(), 
"encoded-credentials");
+
+        Assertions.assertDoesNotThrow(() -> validate(config));
+    }
+
+    @Test
+    void testMissingProjectIdRejected() {
+        Map<String, Object> config = validConfig();
+        config.remove(FirestoreSinkOptions.PROJECT_ID.key());
+
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(config));
+    }
+
+    @Test
+    void testMissingCollectionRejected() {
+        Map<String, Object> config = validConfig();
+        config.remove(FirestoreSinkOptions.COLLECTION.key());
+
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(config));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"", " ", "\t", "\n", "\r", " \t\r\n "})
+    void testBlankProjectIdRejected(String value) {
+        assertInvalidOption(FirestoreSinkOptions.PROJECT_ID.key(), value);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"", " ", "\t", "\n", "\r", " \t\r\n "})
+    void testBlankCollectionRejected(String value) {
+        assertInvalidOption(FirestoreSinkOptions.COLLECTION.key(), value);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"", " ", "\t", "\n", "\r", " \t\r\n "})
+    void testBlankCredentialsRejected(String value) {
+        assertInvalidOption(FirestoreSinkOptions.CREDENTIALS.key(), value);
+    }
+
+    @Test
+    void testUnknownOptionRejected() {
+        Map<String, Object> config = validConfig();
+        config.put("unknown_option", "value");
+
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(config));
+    }
+
+    private void assertInvalidOption(String key, String value) {
+        Map<String, Object> config = validConfig();
+        config.put(key, value);
+
+        OptionValidationException error =
+                Assertions.assertThrows(OptionValidationException.class, () -> 
validate(config));
+        Assertions.assertTrue(error.getMessage().contains(key), 
error.getMessage());
+    }
+
+    private Map<String, Object> validConfig() {
+        Map<String, Object> config = new HashMap<>();
+        config.put(FirestoreSinkOptions.PROJECT_ID.key(), "test-project");
+        config.put(FirestoreSinkOptions.COLLECTION.key(), "test-collection");
+        return config;
+    }
+
+    private void validate(Map<String, Object> config) {
+        ReadonlyConfig readonlyConfig = ReadonlyConfig.fromMap(config);
+        ConfigValidator.validateUnknownKeys(readonlyConfig, optionRule, 
"GoogleFirestoreSink");
+        ConfigValidator.of(readonlyConfig).validate(optionRule);
     }
 }

Reply via email to