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

davidzollo 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 0bd42d8d91 [Improve][API] Allow exclusive/bundled options to coexist 
with optional value constraints (#11022)
0bd42d8d91 is described below

commit 0bd42d8d91b5e6f50cfe3c64fb01c46bd521c66a
Author: zhiwei.niu <[email protected]>
AuthorDate: Tue Jun 9 15:42:57 2026 +0800

    [Improve][API] Allow exclusive/bundled options to coexist with optional 
value constraints (#11022)
---
 .../api/configuration/util/OptionRule.java         |  31 +++-
 .../configuration/util/ConfigValidatorTest.java    |  71 +++++++++
 .../api/configuration/util/OptionRuleTest.java     | 168 +++++++++++++++++++++
 3 files changed, 266 insertions(+), 4 deletions(-)

diff --git 
a/seatunnel-api/src/main/java/org/apache/seatunnel/api/configuration/util/OptionRule.java
 
b/seatunnel-api/src/main/java/org/apache/seatunnel/api/configuration/util/OptionRule.java
index 4f91864c39..4153042350 100644
--- 
a/seatunnel-api/src/main/java/org/apache/seatunnel/api/configuration/util/OptionRule.java
+++ 
b/seatunnel-api/src/main/java/org/apache/seatunnel/api/configuration/util/OptionRule.java
@@ -19,6 +19,8 @@ package org.apache.seatunnel.api.configuration.util;
 
 import org.apache.seatunnel.api.configuration.Option;
 
+import lombok.AllArgsConstructor;
+import lombok.Getter;
 import lombok.NonNull;
 
 import java.util.ArrayList;
@@ -181,7 +183,7 @@ public class OptionRule {
          */
         public Builder optional(@NonNull Option<?>... options) {
             for (Option<?> option : options) {
-                verifyOptionOptionsDuplicate(option, "OptionsOption");
+                verifyOptionOptionsDuplicate(option, 
CurrentOptionType.OPTIONS_OPTION.getType());
             }
             this.optionalOptions.addAll(Arrays.asList(options));
             return this;
@@ -345,7 +347,8 @@ public class OptionRule {
                 @NonNull Option<?> option,
                 @NonNull Condition<?> condition1,
                 @NonNull Condition<?>... conditions) {
-            verifyOptionOptionsDuplicate(option, "OptionsOption");
+            verifyOptionOptionsDuplicate(
+                    option, 
CurrentOptionType.OPTIONS_OPTION_WITH_CONSTRAINT.getType());
             this.optionalOptions.add(option);
             this.valueConstraints.add(condition1);
             Collections.addAll(this.valueConstraints, conditions);
@@ -358,8 +361,10 @@ public class OptionRule {
                 @NonNull Option<?> option2,
                 @NonNull Condition<?> condition1,
                 @NonNull Condition<?>... conditions) {
-            verifyOptionOptionsDuplicate(option1, "OptionsOption");
-            verifyOptionOptionsDuplicate(option2, "OptionsOption");
+            verifyOptionOptionsDuplicate(
+                    option1, 
CurrentOptionType.OPTIONS_OPTION_WITH_CONSTRAINT.getType());
+            verifyOptionOptionsDuplicate(
+                    option2, 
CurrentOptionType.OPTIONS_OPTION_WITH_CONSTRAINT.getType());
             this.optionalOptions.add(option1);
             this.optionalOptions.add(option2);
             this.valueConstraints.add(condition1);
@@ -544,8 +549,17 @@ public class OptionRule {
                 @NonNull Option<?> option, @NonNull String currentOptionType) {
             verifyDuplicateWithOptionOptions(option, currentOptionType);
 
+            boolean hasOptionsOptionWithConstraint =
+                    
CurrentOptionType.OPTIONS_OPTION_WITH_CONSTRAINT.type.equals(currentOptionType);
             requiredOptions.forEach(
                     requiredOption -> {
+                        if (hasOptionsOptionWithConstraint
+                                && (requiredOption
+                                                instanceof 
RequiredOption.ExclusiveRequiredOptions
+                                        || requiredOption
+                                                instanceof 
RequiredOption.BundledRequiredOptions)) {
+                            return;
+                        }
                         if (requiredOption.getOptions().contains(option)) {
                             throw new OptionValidationException(
                                     String.format(
@@ -573,4 +587,13 @@ public class OptionRule {
             }
         }
     }
+
+    @Getter
+    @AllArgsConstructor
+    private enum CurrentOptionType {
+        OPTIONS_OPTION("OptionsOption"),
+        OPTIONS_OPTION_WITH_CONSTRAINT("OptionsOptionWithConstraint");
+
+        private final String type;
+    }
 }
diff --git 
a/seatunnel-api/src/test/java/org/apache/seatunnel/api/configuration/util/ConfigValidatorTest.java
 
b/seatunnel-api/src/test/java/org/apache/seatunnel/api/configuration/util/ConfigValidatorTest.java
index ef9536ea41..fec4b5e05b 100644
--- 
a/seatunnel-api/src/test/java/org/apache/seatunnel/api/configuration/util/ConfigValidatorTest.java
+++ 
b/seatunnel-api/src/test/java/org/apache/seatunnel/api/configuration/util/ConfigValidatorTest.java
@@ -2716,4 +2716,75 @@ public class ConfigValidatorTest {
         config.put(MAP_OPTION.key(), props);
         Assertions.assertDoesNotThrow(() -> validate(config, rule));
     }
+
+    @Test
+    public void testExclusiveWithOptionalValueConstraint() {
+        OptionRule rule =
+                OptionRule.builder()
+                        .exclusive(TEST_TOPIC_PATTERN, TEST_TOPIC)
+                        .optional(TEST_TOPIC_PATTERN, 
Conditions.notBlank(TEST_TOPIC_PATTERN))
+                        .optional(TEST_TOPIC, notEmpty(TEST_TOPIC))
+                        .build();
+
+        // neither present -> fails exclusive check
+        Map<String, Object> config1 = new HashMap<>();
+        assertThrows(OptionValidationException.class, () -> validate(config1, 
rule));
+
+        // one present with valid value -> pass
+        Map<String, Object> config2 = new HashMap<>();
+        config2.put(TEST_TOPIC_PATTERN.key(), "pattern.*");
+        Assertions.assertDoesNotThrow(() -> validate(config2, rule));
+
+        // one present with empty value -> fails value constraint
+        Map<String, Object> config3 = new HashMap<>();
+        config3.put(TEST_TOPIC_PATTERN.key(), "");
+        assertThrows(OptionValidationException.class, () -> validate(config3, 
rule));
+
+        // both present -> fails exclusive check
+        Map<String, Object> config4 = new HashMap<>();
+        config4.put(TEST_TOPIC_PATTERN.key(), "pattern.*");
+        config4.put(TEST_TOPIC.key(), Arrays.asList("t1", "t2"));
+        assertThrows(OptionValidationException.class, () -> validate(config4, 
rule));
+
+        // list option present with empty list -> fails value constraint
+        Map<String, Object> config5 = new HashMap<>();
+        config5.put(TEST_TOPIC.key(), Collections.emptyList());
+        assertThrows(OptionValidationException.class, () -> validate(config5, 
rule));
+
+        // list option present with valid list -> pass
+        Map<String, Object> config6 = new HashMap<>();
+        config6.put(TEST_TOPIC.key(), Arrays.asList("topic1"));
+        Assertions.assertDoesNotThrow(() -> validate(config6, rule));
+    }
+
+    @Test
+    public void testBundledWithOptionalValueConstraint() {
+        OptionRule rule =
+                OptionRule.builder()
+                        .bundled(TEST_TOPIC_PATTERN, TEST_TOPIC)
+                        .optional(TEST_TOPIC_PATTERN, 
Conditions.notBlank(TEST_TOPIC_PATTERN))
+                        .optional(TEST_TOPIC, notEmpty(TEST_TOPIC))
+                        .build();
+
+        // neither present -> pass (bundled options are optional as a group)
+        Map<String, Object> config1 = new HashMap<>();
+        Assertions.assertDoesNotThrow(() -> validate(config1, rule));
+
+        // both present with valid values -> pass
+        Map<String, Object> config2 = new HashMap<>();
+        config2.put(TEST_TOPIC_PATTERN.key(), "pattern.*");
+        config2.put(TEST_TOPIC.key(), Collections.singletonList("t1"));
+        Assertions.assertDoesNotThrow(() -> validate(config2, rule));
+
+        // only one present -> fails bundled check
+        Map<String, Object> config3 = new HashMap<>();
+        config3.put(TEST_TOPIC_PATTERN.key(), "pattern.*");
+        assertThrows(OptionValidationException.class, () -> validate(config3, 
rule));
+
+        // both present but one has empty value -> fails value constraint
+        Map<String, Object> config4 = new HashMap<>();
+        config4.put(TEST_TOPIC_PATTERN.key(), "pattern.*");
+        config4.put(TEST_TOPIC.key(), Collections.emptyList());
+        assertThrows(OptionValidationException.class, () -> validate(config4, 
rule));
+    }
 }
diff --git 
a/seatunnel-api/src/test/java/org/apache/seatunnel/api/configuration/util/OptionRuleTest.java
 
b/seatunnel-api/src/test/java/org/apache/seatunnel/api/configuration/util/OptionRuleTest.java
index cde6058209..1c3db5bdfc 100644
--- 
a/seatunnel-api/src/test/java/org/apache/seatunnel/api/configuration/util/OptionRuleTest.java
+++ 
b/seatunnel-api/src/test/java/org/apache/seatunnel/api/configuration/util/OptionRuleTest.java
@@ -182,6 +182,174 @@ public class OptionRuleTest {
         assertEquals(
                 "ErrorCode:[API-02], ErrorDescription:[Option item validate 
failed] - ConditionalRequiredOptions 'option.timestamp' duplicate in 
ExclusiveRequiredOptions options.",
                 assertThrows(OptionValidationException.class, 
executable).getMessage());
+
+        // test exclusive options can be paired with optional value constraints
+        OptionRule exclusiveWithOptional =
+                OptionRule.builder()
+                        .exclusive(TEST_TOPIC_PATTERN, TEST_TOPIC)
+                        .optional(TEST_TOPIC_PATTERN, 
Conditions.notBlank(TEST_TOPIC_PATTERN))
+                        .optional(TEST_TOPIC, Conditions.notEmpty(TEST_TOPIC))
+                        .build();
+        assertEquals(1, exclusiveWithOptional.getRequiredOptions().size());
+        assertEquals(2, exclusiveWithOptional.getOptionalOptions().size());
+        assertEquals(2, exclusiveWithOptional.getValueConstraints().size());
+
+        // test bundled options can be paired with optional value constraints
+        OptionRule bundledWithOptional =
+                OptionRule.builder()
+                        .bundled(TEST_TOPIC_PATTERN, TEST_TOPIC)
+                        .optional(TEST_TOPIC_PATTERN, 
Conditions.notBlank(TEST_TOPIC_PATTERN))
+                        .optional(TEST_TOPIC, Conditions.notEmpty(TEST_TOPIC))
+                        .build();
+        assertEquals(1, bundledWithOptional.getRequiredOptions().size());
+        assertEquals(2, bundledWithOptional.getOptionalOptions().size());
+        assertEquals(2, bundledWithOptional.getValueConstraints().size());
+
+        // test required options still cannot be paired with optional (no 
condition)
+        executable = () -> 
OptionRule.builder().required(TEST_PORTS).optional(TEST_PORTS).build();
+        assertThrows(OptionValidationException.class, executable);
+
+        // test required options still cannot be paired with optional (with 
condition)
+        executable =
+                () ->
+                        OptionRule.builder()
+                                .required(TEST_PORTS)
+                                .optional(TEST_PORTS, 
Conditions.notEmpty(TEST_PORTS))
+                                .build();
+        assertThrows(OptionValidationException.class, executable);
+
+        // test duplicate optional declaration still fails
+        executable =
+                () ->
+                        OptionRule.builder()
+                                .optional(TEST_TOPIC_PATTERN)
+                                .optional(
+                                        TEST_TOPIC_PATTERN, 
Conditions.notBlank(TEST_TOPIC_PATTERN))
+                                .build();
+        assertThrows(OptionValidationException.class, executable);
+
+        // test exclusive + duplicate optional value constraint still fails
+        executable =
+                () ->
+                        OptionRule.builder()
+                                .exclusive(TEST_TOPIC_PATTERN, TEST_TOPIC)
+                                .optional(
+                                        TEST_TOPIC_PATTERN, 
Conditions.notBlank(TEST_TOPIC_PATTERN))
+                                .optional(
+                                        TEST_TOPIC_PATTERN, 
Conditions.notBlank(TEST_TOPIC_PATTERN))
+                                .build();
+        assertThrows(OptionValidationException.class, executable);
+
+        // test exclusive + bundled with same key still fails
+        executable =
+                () ->
+                        OptionRule.builder()
+                                .exclusive(TEST_TOPIC_PATTERN, TEST_TOPIC)
+                                .bundled(TEST_TOPIC_PATTERN, TEST_NUM)
+                                .build();
+        assertThrows(OptionValidationException.class, executable);
+
+        // test bundled + exclusive with same key still fails
+        executable =
+                () ->
+                        OptionRule.builder()
+                                .bundled(TEST_TOPIC_PATTERN, TEST_TOPIC)
+                                .exclusive(TEST_TOPIC_PATTERN, TEST_NUM)
+                                .build();
+        assertThrows(OptionValidationException.class, executable);
+
+        // test exclusive + required with same key fails
+        executable =
+                () ->
+                        OptionRule.builder()
+                                .exclusive(TEST_TOPIC_PATTERN, TEST_TOPIC)
+                                .required(TEST_TOPIC_PATTERN)
+                                .build();
+        assertThrows(OptionValidationException.class, executable);
+
+        // test bundled + required with same key fails
+        executable =
+                () ->
+                        OptionRule.builder()
+                                .bundled(TEST_TOPIC_PATTERN, TEST_TOPIC)
+                                .required(TEST_TOPIC_PATTERN)
+                                .build();
+        assertThrows(OptionValidationException.class, executable);
+
+        // test required + exclusive with same key fails
+        executable =
+                () ->
+                        OptionRule.builder()
+                                .required(TEST_TOPIC_PATTERN)
+                                .exclusive(TEST_TOPIC_PATTERN, TEST_TOPIC)
+                                .build();
+        assertThrows(OptionValidationException.class, executable);
+
+        // test required + bundled with same key fails
+        executable =
+                () ->
+                        OptionRule.builder()
+                                .required(TEST_TOPIC_PATTERN)
+                                .bundled(TEST_TOPIC_PATTERN, TEST_TOPIC)
+                                .build();
+        assertThrows(OptionValidationException.class, executable);
+
+        // test optional(no condition) + exclusive with same key fails
+        executable =
+                () ->
+                        OptionRule.builder()
+                                .optional(TEST_TOPIC_PATTERN)
+                                .exclusive(TEST_TOPIC_PATTERN, TEST_TOPIC)
+                                .build();
+        assertThrows(OptionValidationException.class, executable);
+
+        // test optional(no condition) + bundled with same key fails
+        executable =
+                () ->
+                        OptionRule.builder()
+                                .optional(TEST_TOPIC_PATTERN)
+                                .bundled(TEST_TOPIC_PATTERN, TEST_TOPIC)
+                                .build();
+        assertThrows(OptionValidationException.class, executable);
+
+        // test optional(no condition) + required with same key fails
+        executable =
+                () ->
+                        OptionRule.builder()
+                                .optional(TEST_TOPIC_PATTERN)
+                                .required(TEST_TOPIC_PATTERN)
+                                .build();
+        assertThrows(OptionValidationException.class, executable);
+
+        // test exclusive -> optional(no condition) with same key fails
+        executable =
+                () ->
+                        OptionRule.builder()
+                                .exclusive(TEST_TOPIC_PATTERN, TEST_TOPIC)
+                                .optional(TEST_TOPIC_PATTERN)
+                                .build();
+        assertThrows(OptionValidationException.class, executable);
+
+        // test bundled -> optional(no condition) with same key fails
+        executable =
+                () ->
+                        OptionRule.builder()
+                                .bundled(TEST_TOPIC_PATTERN, TEST_TOPIC)
+                                .optional(TEST_TOPIC_PATTERN)
+                                .build();
+        assertThrows(OptionValidationException.class, executable);
+
+        // test bundled + duplicate optional value constraint still fails
+        executable =
+                () ->
+                        OptionRule.builder()
+                                .bundled(TEST_TOPIC_PATTERN, TEST_TOPIC)
+                                .optional(
+                                        TEST_TOPIC_PATTERN, 
Conditions.notBlank(TEST_TOPIC_PATTERN))
+                                .optional(
+                                        TEST_TOPIC_PATTERN, 
Conditions.notBlank(TEST_TOPIC_PATTERN))
+                                .build();
+        assertThrows(OptionValidationException.class, executable);
     }
 
     @Test

Reply via email to