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);
}
}