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