diff --git a/docs/en/introduction/concepts/incompatible-changes.md b/docs/en/introduction/concepts/incompatible-changes.md index 6ea2dc2ac1d3..c0fe5c18240f 100644 --- a/docs/en/introduction/concepts/incompatible-changes.md +++ b/docs/en/introduction/concepts/incompatible-changes.md @@ -102,6 +102,17 @@ You need to check this document before you upgrade to related version. **Migration Guide**: If you are using custom datetime format patterns in `PARSEDATETIME`, `TO_DATE`, or `IS_DATE` functions, you must update your queries to use one of the supported patterns above. If your data uses a different format, you may need to preprocess the input data to match a supported format, or use string manipulation functions to transform the format before parsing. - DataValidator transform: In `row_error_handle_way = ROUTE_TO_TABLE` mode, the routed error row `table_id` now includes the upstream database/schema prefix (for example, `db1.ffp` / `db1.schema1.ffp` instead of `ffp`). +- **[BREAKING]** Several transform plugins now perform stricter submission-time config validation via declarative `OptionRule`. Configs that previously passed submission but failed at runtime will now be rejected at submission time with a descriptive `OptionValidationException`: + + | Transform | Newly Rejected Config | Previous Behavior | Migration | + |-----------|----------------------|-------------------|-----------| + | `DefineSinkType` | `columns` entries with null/empty `column` or `type` | Runtime NPE or undefined behavior | Ensure every entry has non-empty `column` and `type` fields | + | `DefineSinkType` | `columns` with duplicate column names | Silent override or runtime conflict | Remove duplicate column entries | + | `FieldEncrypt` | `max_field_length` set to ≤ 0 | Ignored or unexpected truncation | Set `max_field_length` to a positive integer, or remove the option to use the default | + | `DynamicCompile` | `compile_pattern = SOURCE_CODE` without a non-blank `source_code` | Runtime compilation failure | Provide `source_code` when using `SOURCE_CODE` pattern | + | `DynamicCompile` | `compile_pattern = ABSOLUTE_PATH` without a non-blank `absolute_path` | Runtime file-read failure | Provide `absolute_path` when using `ABSOLUTE_PATH` pattern | + + **Migration Guide**: Review your transform configs against the table above. If any of your existing configs match a "Newly Rejected" pattern, update them before upgrading. The error messages at submission time now clearly identify which option is invalid and why. - Adjusted SQL Transform date & time functions: - `DATEDIFF(, , 'MONTH')` now returns the total number of months between the two dates across years (for example, from `2023-01-01` to `2024-03-01` returns `14` instead of `15`). - `WEEK()` now returns the ISO week number directly (previous behavior added an extra `+1` to the ISO week value). diff --git a/docs/zh/introduction/concepts/incompatible-changes.md b/docs/zh/introduction/concepts/incompatible-changes.md index 8a6d7b0a5198..8c046178f267 100644 --- a/docs/zh/introduction/concepts/incompatible-changes.md +++ b/docs/zh/introduction/concepts/incompatible-changes.md @@ -98,6 +98,18 @@ **迁移指南**: 如果您在 `PARSEDATETIME`、`TO_DATE` 或 `IS_DATE` 函数中使用自定义日期时间格式模式,您必须更新查询以使用上述支持的模式之一。如果您的数据使用不同的格式,您可能需要预处理输入数据以匹配支持的格式,或使用字符串操作函数在解析之前转换格式。 - DataValidator 转换:当 `row_error_handle_way = ROUTE_TO_TABLE` 时,路由到错误表的行 `table_id` 现在会携带上游的 database/schema 前缀(例如从 `ffp` 变为 `db1.ffp` / `db1.schema1.ffp`)。 +- **[BREAKING]** 多个转换插件现在通过声明式 `OptionRule` 在提交时执行更严格的配置校验。以前在提交时能通过但运行时失败的配置,现在会在提交时被拒绝,并抛出描述清晰的 `OptionValidationException`: + + | 转换插件 | 新增拒绝的配置 | 以前的行为 | 迁移方式 | + |---------|--------------|-----------|---------| + | `DefineSinkType` | `columns` 条目中 `column` 或 `type` 为空 | 运行时 NPE 或未定义行为 | 确保每个条目都有非空的 `column` 和 `type` 字段 | + | `DefineSinkType` | `columns` 中存在重复列名 | 静默覆盖或运行时冲突 | 移除重复的列条目 | + | `FieldEncrypt` | `max_field_length` 设置为 ≤ 0 | 被忽略或产生意外截断 | 设置为正整数,或移除该选项以使用默认值 | + | `DynamicCompile` | `compile_pattern = SOURCE_CODE` 但 `source_code` 为空 | 运行时编译失败 | 使用 `SOURCE_CODE` 模式时提供 `source_code` | + | `DynamicCompile` | `compile_pattern = ABSOLUTE_PATH` 但 `absolute_path` 为空 | 运行时文件读取失败 | 使用 `ABSOLUTE_PATH` 模式时提供 `absolute_path` | + + **迁移指南**:升级前请对照上表检查您的转换配置。如果现有配置匹配了"新增拒绝的配置"中的情况,请在升级前修改。提交时的错误消息会清楚标明哪个选项无效及原因。 + ### 引擎行为变更 ### 依赖升级 diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/adaptsink/DefineSinkTypeTransformConfig.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/adaptsink/DefineSinkTypeTransformConfig.java index 06eda0e78372..fa37191e9656 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/adaptsink/DefineSinkTypeTransformConfig.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/adaptsink/DefineSinkTypeTransformConfig.java @@ -33,8 +33,6 @@ import java.util.Map; import java.util.stream.Collectors; -import static org.apache.seatunnel.shade.com.google.common.base.Preconditions.checkArgument; - @Data @AllArgsConstructor public class DefineSinkTypeTransformConfig implements Serializable { @@ -82,16 +80,6 @@ public static class TableTransforms implements Serializable { public static DefineSinkTypeTransformConfig of(ReadonlyConfig config) { List columns = config.get(COLUMNS); - - checkArgument(columns != null && !columns.isEmpty(), "The columns must be set"); - columns.forEach( - defineColumnType -> { - checkArgument( - defineColumnType.getColumn() != null, "The column name must be set"); - checkArgument( - defineColumnType.getType() != null, "The column type must be set"); - }); - return new DefineSinkTypeTransformConfig(columns); } } diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/adaptsink/DefineSinkTypeTransformFactory.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/adaptsink/DefineSinkTypeTransformFactory.java index e583ca2b4432..2f0d986b2216 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/adaptsink/DefineSinkTypeTransformFactory.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/adaptsink/DefineSinkTypeTransformFactory.java @@ -17,15 +17,24 @@ package org.apache.seatunnel.transform.adaptsink; +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.api.configuration.util.ConditionExtension; +import org.apache.seatunnel.api.configuration.util.Conditions; import org.apache.seatunnel.api.configuration.util.OptionRule; +import org.apache.seatunnel.api.configuration.util.OptionValidationException; import org.apache.seatunnel.api.table.connector.TableTransform; import org.apache.seatunnel.api.table.factory.Factory; import org.apache.seatunnel.api.table.factory.TableTransformFactory; import org.apache.seatunnel.api.table.factory.TableTransformFactoryContext; +import org.apache.seatunnel.transform.adaptsink.DefineSinkTypeTransformConfig.DefineColumnType; import org.apache.seatunnel.transform.common.TransformCommonOptions; import com.google.auto.service.AutoService; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + @AutoService(Factory.class) public class DefineSinkTypeTransformFactory implements TableTransformFactory { @Override @@ -36,7 +45,13 @@ public String factoryIdentifier() { @Override public OptionRule optionRule() { return OptionRule.builder() - .required(DefineSinkTypeTransformConfig.COLUMNS) + .required( + DefineSinkTypeTransformConfig.COLUMNS, + Conditions.notEmpty(DefineSinkTypeTransformConfig.COLUMNS) + .and( + Conditions.extension( + DefineSinkTypeTransformConfig.COLUMNS, + new ColumnsStructureValidator()))) .optional(TransformCommonOptions.MULTI_TABLES) .optional(TransformCommonOptions.TABLE_MATCH_REGEX) .build(); @@ -48,4 +63,39 @@ public TableTransform createTransform(TableTransformFactoryContext context) { new DefineSinkTypeMultiCatalogTransform( context.getCatalogTables(), context.getOptions()); } + + static class ColumnsStructureValidator implements ConditionExtension> { + @Override + public String description() { + return "each column entry must contain non-null 'column' and 'type'"; + } + + @Override + public boolean evaluate(ReadonlyConfig config, List value) + throws OptionValidationException { + if (value == null) { + return false; + } + Set seen = new HashSet<>(); + for (int i = 0; i < value.size(); i++) { + DefineColumnType entry = value.get(i); + if (entry.getColumn() == null || entry.getColumn().trim().isEmpty()) { + throw new OptionValidationException( + String.format( + "columns[%d]: 'column' name must not be null or empty", i)); + } + if (entry.getType() == null || entry.getType().trim().isEmpty()) { + throw new OptionValidationException( + String.format("columns[%d]: 'type' must not be null or empty", i)); + } + if (!seen.add(entry.getColumn())) { + throw new OptionValidationException( + String.format( + "columns[%d]: duplicate column name '%s'", + i, entry.getColumn())); + } + } + return true; + } + } } diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/copy/CopyFieldTransformFactory.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/copy/CopyFieldTransformFactory.java index 428623ff2e17..5de39766ffb1 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/copy/CopyFieldTransformFactory.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/copy/CopyFieldTransformFactory.java @@ -17,6 +17,9 @@ package org.apache.seatunnel.transform.copy; +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.api.configuration.util.ConditionExtension; +import org.apache.seatunnel.api.configuration.util.Conditions; import org.apache.seatunnel.api.configuration.util.OptionRule; import org.apache.seatunnel.api.table.connector.TableTransform; import org.apache.seatunnel.api.table.factory.Factory; @@ -36,8 +39,15 @@ public String factoryIdentifier() { @Override public OptionRule optionRule() { return OptionRule.builder() - .bundled(CopyTransformConfig.SRC_FIELD, CopyTransformConfig.DEST_FIELD) - .bundled(CopyTransformConfig.FIELDS) + .exclusive(CopyTransformConfig.FIELDS, CopyTransformConfig.SRC_FIELD) + .optional( + CopyTransformConfig.FIELDS, + Conditions.mapNotEmpty(CopyTransformConfig.FIELDS)) + .optional( + CopyTransformConfig.SRC_FIELD, + Conditions.extension( + CopyTransformConfig.SRC_FIELD, new RequireDestFieldValidator())) + .optional(CopyTransformConfig.DEST_FIELD) .optional(TransformCommonOptions.MULTI_TABLES) .optional(TransformCommonOptions.TABLE_MATCH_REGEX) .build(); @@ -49,4 +59,17 @@ public TableTransform createTransform(TableTransformFactoryContext context) { new CopyFieldMultiCatalogTransform( context.getCatalogTables(), context.getOptions()); } + + static class RequireDestFieldValidator implements ConditionExtension { + @Override + public String description() { + return "'dest_field' is required when 'src_field' is provided"; + } + + @Override + public boolean evaluate(ReadonlyConfig config, String value) { + String destField = config.get(CopyTransformConfig.DEST_FIELD); + return destField != null && !destField.trim().isEmpty(); + } + } } diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/dynamiccompile/DynamicCompileTransformFactory.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/dynamiccompile/DynamicCompileTransformFactory.java index 593e940cb621..02141fd0a985 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/dynamiccompile/DynamicCompileTransformFactory.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/dynamiccompile/DynamicCompileTransformFactory.java @@ -17,6 +17,7 @@ package org.apache.seatunnel.transform.dynamiccompile; +import org.apache.seatunnel.api.configuration.util.Conditions; import org.apache.seatunnel.api.configuration.util.OptionRule; import org.apache.seatunnel.api.table.connector.TableTransform; import org.apache.seatunnel.api.table.factory.Factory; @@ -36,17 +37,24 @@ public String factoryIdentifier() { @Override public OptionRule optionRule() { return OptionRule.builder() - .optional( - DynamicCompileTransformConfig.COMPILE_LANGUAGE, - DynamicCompileTransformConfig.COMPILE_PATTERN) + .required(DynamicCompileTransformConfig.COMPILE_LANGUAGE) + .optional(DynamicCompileTransformConfig.COMPILE_PATTERN) .conditional( DynamicCompileTransformConfig.COMPILE_PATTERN, CompilePattern.SOURCE_CODE, DynamicCompileTransformConfig.SOURCE_CODE) + .conditional( + DynamicCompileTransformConfig.COMPILE_PATTERN, + CompilePattern.SOURCE_CODE, + Conditions.notBlank(DynamicCompileTransformConfig.SOURCE_CODE)) .conditional( DynamicCompileTransformConfig.COMPILE_PATTERN, CompilePattern.ABSOLUTE_PATH, DynamicCompileTransformConfig.ABSOLUTE_PATH) + .conditional( + DynamicCompileTransformConfig.COMPILE_PATTERN, + CompilePattern.ABSOLUTE_PATH, + Conditions.notBlank(DynamicCompileTransformConfig.ABSOLUTE_PATH)) .optional(TransformCommonOptions.MULTI_TABLES) .optional(TransformCommonOptions.TABLE_MATCH_REGEX) .build(); diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformFactory.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformFactory.java index d12d4292a852..3c33a095ffff 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformFactory.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformFactory.java @@ -17,6 +17,7 @@ package org.apache.seatunnel.transform.encrypt; +import org.apache.seatunnel.api.configuration.util.Conditions; import org.apache.seatunnel.api.configuration.util.OptionRule; import org.apache.seatunnel.api.table.connector.TableTransform; import org.apache.seatunnel.api.table.factory.Factory; @@ -38,10 +39,17 @@ public String factoryIdentifier() { @Override public OptionRule optionRule() { return OptionRule.builder() - .required(FieldEncryptTransformConfig.FIELDS) - .required(FieldEncryptTransformConfig.KEY) + .required( + FieldEncryptTransformConfig.FIELDS, + Conditions.notEmpty(FieldEncryptTransformConfig.FIELDS)) + .required( + FieldEncryptTransformConfig.KEY, + Conditions.notBlank(FieldEncryptTransformConfig.KEY)) .optional(FieldEncryptTransformConfig.ALGORITHM) .optional(FieldEncryptTransformConfig.MODE) + .optional( + FieldEncryptTransformConfig.MAX_FIELD_LENGTH, + Conditions.greaterThan(FieldEncryptTransformConfig.MAX_FIELD_LENGTH, 0)) .optional(TransformCommonOptions.MULTI_TABLES) .optional(TransformCommonOptions.TABLE_MATCH_REGEX) .build(); diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/fieldmapper/FieldMapperTransformFactory.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/fieldmapper/FieldMapperTransformFactory.java index ade69dd3db89..8edb4419e3e9 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/fieldmapper/FieldMapperTransformFactory.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/fieldmapper/FieldMapperTransformFactory.java @@ -18,6 +18,7 @@ package org.apache.seatunnel.transform.fieldmapper; import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.api.configuration.util.Conditions; import org.apache.seatunnel.api.configuration.util.OptionRule; import org.apache.seatunnel.api.table.connector.TableTransform; import org.apache.seatunnel.api.table.factory.Factory; @@ -37,7 +38,9 @@ public String factoryIdentifier() { @Override public OptionRule optionRule() { return OptionRule.builder() - .optional(FieldMapperTransformConfig.FIELD_MAPPER) + .required( + FieldMapperTransformConfig.FIELD_MAPPER, + Conditions.mapNotEmpty(FieldMapperTransformConfig.FIELD_MAPPER)) .optional(TransformCommonOptions.MULTI_TABLES) .optional(TransformCommonOptions.TABLE_MATCH_REGEX) .build(); diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/filter/FilterFieldTransform.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/filter/FilterFieldTransform.java index 53b66af3d842..0fee01c378b8 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/filter/FilterFieldTransform.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/filter/FilterFieldTransform.java @@ -18,8 +18,6 @@ package org.apache.seatunnel.transform.filter; 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.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.Column; import org.apache.seatunnel.api.table.catalog.ConstraintKey; @@ -58,14 +56,6 @@ public FilterFieldTransform( SeaTunnelRowType seaTunnelRowType = catalogTable.getTableSchema().toPhysicalRowDataType(); includeFields = config.get(FilterFieldTransformConfig.INCLUDE_FIELDS); excludeFields = config.get(FilterFieldTransformConfig.EXCLUDE_FIELDS); - // exactly only one should be set - ConfigValidator.of(config) - .validate( - OptionRule.builder() - .exclusive( - FilterFieldTransformConfig.INCLUDE_FIELDS, - FilterFieldTransformConfig.EXCLUDE_FIELDS) - .build()); List canNotFoundFields = Stream.concat( Optional.ofNullable(includeFields).orElse(new ArrayList<>()) diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/filter/FilterFieldTransformFactory.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/filter/FilterFieldTransformFactory.java index f390f4842494..bdc273ed2758 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/filter/FilterFieldTransformFactory.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/filter/FilterFieldTransformFactory.java @@ -17,6 +17,7 @@ package org.apache.seatunnel.transform.filter; +import org.apache.seatunnel.api.configuration.util.Conditions; import org.apache.seatunnel.api.configuration.util.OptionRule; import org.apache.seatunnel.api.table.connector.TableTransform; import org.apache.seatunnel.api.table.factory.Factory; @@ -38,9 +39,15 @@ public String factoryIdentifier() { @Override public OptionRule optionRule() { return OptionRule.builder() - .optional( + .exclusive( FilterFieldTransformConfig.INCLUDE_FIELDS, FilterFieldTransformConfig.EXCLUDE_FIELDS) + .optional( + FilterFieldTransformConfig.INCLUDE_FIELDS, + Conditions.notEmpty(FilterFieldTransformConfig.INCLUDE_FIELDS)) + .optional( + FilterFieldTransformConfig.EXCLUDE_FIELDS, + Conditions.notEmpty(FilterFieldTransformConfig.EXCLUDE_FIELDS)) .optional(TransformCommonOptions.MULTI_TABLES) .optional(TransformCommonOptions.TABLE_MATCH_REGEX) .build(); diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/jsonpath/JsonPathTransformFactory.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/jsonpath/JsonPathTransformFactory.java index 0220303fd55a..d0411fddb3a1 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/jsonpath/JsonPathTransformFactory.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/jsonpath/JsonPathTransformFactory.java @@ -17,7 +17,11 @@ package org.apache.seatunnel.transform.jsonpath; +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.api.configuration.util.ConditionExtension; +import org.apache.seatunnel.api.configuration.util.Conditions; import org.apache.seatunnel.api.configuration.util.OptionRule; +import org.apache.seatunnel.api.configuration.util.OptionValidationException; import org.apache.seatunnel.api.table.connector.TableTransform; import org.apache.seatunnel.api.table.factory.Factory; import org.apache.seatunnel.api.table.factory.TableTransformFactory; @@ -26,6 +30,9 @@ import com.google.auto.service.AutoService; +import java.util.List; +import java.util.Map; + @AutoService(Factory.class) public class JsonPathTransformFactory implements TableTransformFactory { @Override @@ -36,7 +43,13 @@ public String factoryIdentifier() { @Override public OptionRule optionRule() { return OptionRule.builder() - .optional(JsonPathTransformConfig.COLUMNS) + .required( + JsonPathTransformConfig.COLUMNS, + Conditions.notEmpty(JsonPathTransformConfig.COLUMNS) + .and( + Conditions.extension( + JsonPathTransformConfig.COLUMNS, + new ColumnsValidator()))) .optional(TransformCommonOptions.MULTI_TABLES) .optional(TransformCommonOptions.TABLE_MATCH_REGEX) .optional(TransformCommonOptions.ROW_ERROR_HANDLE_WAY_OPTION) @@ -48,4 +61,44 @@ public TableTransform createTransform(TableTransformFactoryContext context) { return () -> new JsonPathMultiCatalogTransform(context.getCatalogTables(), context.getOptions()); } + + static class ColumnsValidator implements ConditionExtension>> { + @Override + public String description() { + return "each column entry must contain non-empty 'path' and 'dest_field'"; + } + + @Override + public boolean evaluate(ReadonlyConfig config, List> value) + throws OptionValidationException { + if (value == null) { + return false; + } + for (int i = 0; i < value.size(); i++) { + Map entry = value.get(i); + Object path = entry.get("path"); + if (path == null + || (path instanceof String && ((String) path).trim().isEmpty()) + || (path instanceof List && ((List) path).isEmpty())) { + throw new OptionValidationException( + String.format("columns[%d]: 'path' must not be null or empty", i)); + } + Object srcField = entry.get("src_field"); + if (srcField == null + || (srcField instanceof String && ((String) srcField).trim().isEmpty())) { + throw new OptionValidationException( + String.format("columns[%d]: 'src_field' must not be null or empty", i)); + } + Object destField = entry.get("dest_field"); + if (destField == null + || (destField instanceof String && ((String) destField).trim().isEmpty()) + || (destField instanceof List && ((List) destField).isEmpty())) { + throw new OptionValidationException( + String.format( + "columns[%d]: 'dest_field' must not be null or empty", i)); + } + } + return true; + } + } } diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/metadata/MetadataTransformFactory.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/metadata/MetadataTransformFactory.java index b9233cd1d338..479376b8885e 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/metadata/MetadataTransformFactory.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/metadata/MetadataTransformFactory.java @@ -17,6 +17,7 @@ package org.apache.seatunnel.transform.metadata; +import org.apache.seatunnel.api.configuration.util.Conditions; import org.apache.seatunnel.api.configuration.util.OptionRule; import org.apache.seatunnel.api.table.connector.TableTransform; import org.apache.seatunnel.api.table.factory.Factory; @@ -36,7 +37,9 @@ public String factoryIdentifier() { @Override public OptionRule optionRule() { return OptionRule.builder() - .optional(MetadataTransformConfig.METADATA_FIELDS) + .required( + MetadataTransformConfig.METADATA_FIELDS, + Conditions.mapNotEmpty(MetadataTransformConfig.METADATA_FIELDS)) .optional(TransformCommonOptions.MULTI_TABLES) .optional(TransformCommonOptions.TABLE_MATCH_REGEX) .build(); diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/regexextract/RegexExtractTransformFactory.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/regexextract/RegexExtractTransformFactory.java index d6c6fd3a49b8..0502bd27fc7f 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/regexextract/RegexExtractTransformFactory.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/regexextract/RegexExtractTransformFactory.java @@ -17,6 +17,9 @@ package org.apache.seatunnel.transform.regexextract; +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.api.configuration.util.ConditionExtension; +import org.apache.seatunnel.api.configuration.util.Conditions; import org.apache.seatunnel.api.configuration.util.OptionRule; import org.apache.seatunnel.api.table.connector.TableTransform; import org.apache.seatunnel.api.table.factory.Factory; @@ -26,6 +29,8 @@ import com.google.auto.service.AutoService; +import java.util.List; + @AutoService(Factory.class) public class RegexExtractTransformFactory implements TableTransformFactory { @@ -37,13 +42,17 @@ public String factoryIdentifier() { @Override public OptionRule optionRule() { return OptionRule.builder() + .required(RegexExtractTransformConfig.KEY_SOURCE_FIELD) + .required(RegexExtractTransformConfig.KEY_REGEX_PATTERN) .required( - RegexExtractTransformConfig.KEY_SOURCE_FIELD, - RegexExtractTransformConfig.KEY_REGEX_PATTERN, - RegexExtractTransformConfig.KEY_OUTPUT_FIELDS) + RegexExtractTransformConfig.KEY_OUTPUT_FIELDS, + Conditions.notEmpty(RegexExtractTransformConfig.KEY_OUTPUT_FIELDS)) .optional( RegexExtractTransformConfig.KEY_DEFAULT_VALUES, - TransformCommonOptions.MULTI_TABLES) + Conditions.extension( + RegexExtractTransformConfig.KEY_DEFAULT_VALUES, + new DefaultValuesLengthValidator())) + .optional(TransformCommonOptions.MULTI_TABLES) .build(); } @@ -53,4 +62,20 @@ public TableTransform createTransform(TableTransformFactoryContext context) { new RegexExtractMultiCatalogTransform( context.getCatalogTables(), context.getOptions()); } + + static class DefaultValuesLengthValidator implements ConditionExtension> { + @Override + public String description() { + return "'default_values' length must equal 'output_fields' length"; + } + + @Override + public boolean evaluate(ReadonlyConfig config, List value) { + if (value == null) { + return true; + } + List outputFields = config.get(RegexExtractTransformConfig.KEY_OUTPUT_FIELDS); + return outputFields == null || value.size() == outputFields.size(); + } + } } diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/replace/ReplaceTransform.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/replace/ReplaceTransform.java index 0228e1f0ecfa..0ceb74966753 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/replace/ReplaceTransform.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/replace/ReplaceTransform.java @@ -17,7 +17,6 @@ package org.apache.seatunnel.transform.replace; -import org.apache.seatunnel.api.configuration.Option; import org.apache.seatunnel.api.configuration.ReadonlyConfig; import org.apache.seatunnel.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.TableIdentifier; @@ -32,7 +31,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -49,19 +47,9 @@ public class ReplaceTransform extends AbstractCatalogSupportMapTransform { public ReplaceTransform( @NonNull ReadonlyConfig config, @NonNull CatalogTable inputCatalogTable) { super(inputCatalogTable); - validateConflictingReplaceFieldKeys(config); - this.replaceFields.addAll( - getRequiredOption(config, ReplaceTransformConfig.KEY_REPLACE_FIELDS)); - - if (replaceFields.isEmpty()) { - throw TransformCommonError.validationFailed( - String.format( - "Option '%s' must not be empty.", - ReplaceTransformConfig.KEY_REPLACE_FIELDS.key())); - } - - this.pattern = getRequiredOption(config, ReplaceTransformConfig.KEY_PATTERN); - this.replacement = getRequiredOption(config, ReplaceTransformConfig.KEY_REPLACEMENT); + this.replaceFields.addAll(config.get(ReplaceTransformConfig.KEY_REPLACE_FIELDS)); + this.pattern = config.get(ReplaceTransformConfig.KEY_PATTERN); + this.replacement = config.get(ReplaceTransformConfig.KEY_REPLACEMENT); this.isRegex = config.get(ReplaceTransformConfig.KEY_IS_REGEX); this.replaceFirst = config.get(ReplaceTransformConfig.KEY_REPLACE_FIRST); this.regexPattern = initializeRegexPattern(); @@ -73,22 +61,6 @@ public String getPluginName() { return PLUGIN_NAME; } - private void validateConflictingReplaceFieldKeys(ReadonlyConfig config) { - Map sourceMap = config.getSourceMap(); - if (sourceMap.containsKey("replace_field") && sourceMap.containsKey("replace_fields")) { - throw TransformCommonError.validationFailed( - "Options 'replace_field' and 'replace_fields' cannot be configured together."); - } - } - - private T getRequiredOption(ReadonlyConfig config, Option option) { - return config.getOptional(option) - .orElseThrow( - () -> - TransformCommonError.validationFailed( - String.format("Option '%s' is required.", option.key()))); - } - private void initializeFieldIndexes() { SeaTunnelRowType physicalRowType = inputCatalogTable.getTableSchema().toPhysicalRowDataType(); diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/replace/ReplaceTransformFactory.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/replace/ReplaceTransformFactory.java index 4fc22cd11064..c7c7e1ff6430 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/replace/ReplaceTransformFactory.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/replace/ReplaceTransformFactory.java @@ -17,6 +17,7 @@ package org.apache.seatunnel.transform.replace; +import org.apache.seatunnel.api.configuration.util.Conditions; import org.apache.seatunnel.api.configuration.util.OptionRule; import org.apache.seatunnel.api.table.connector.TableTransform; import org.apache.seatunnel.api.table.factory.Factory; @@ -36,10 +37,11 @@ public String factoryIdentifier() { @Override public OptionRule optionRule() { return OptionRule.builder() - .optional( + .required( ReplaceTransformConfig.KEY_REPLACE_FIELDS, - ReplaceTransformConfig.KEY_PATTERN, - ReplaceTransformConfig.KEY_REPLACEMENT) + Conditions.notEmpty(ReplaceTransformConfig.KEY_REPLACE_FIELDS)) + .required(ReplaceTransformConfig.KEY_PATTERN) + .required(ReplaceTransformConfig.KEY_REPLACEMENT) .optional(ReplaceTransformConfig.KEY_IS_REGEX) .optional(ReplaceTransformConfig.KEY_REPLACE_FIRST) .optional(TransformCommonOptions.MULTI_TABLES) diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/rowkind/RowKindExtractorTransformFactory.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/rowkind/RowKindExtractorTransformFactory.java index 8694b72b194f..99011726fc00 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/rowkind/RowKindExtractorTransformFactory.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/rowkind/RowKindExtractorTransformFactory.java @@ -37,6 +37,7 @@ public String factoryIdentifier() { public OptionRule optionRule() { return OptionRule.builder() .optional(RowKindExtractorTransformConfig.CUSTOM_FIELD_NAME) + .optional(RowKindExtractorTransformConfig.TRANSFORM_TYPE) .optional(TransformCommonOptions.MULTI_TABLES) .optional(TransformCommonOptions.TABLE_MATCH_REGEX) .build(); diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/split/SplitTransformFactory.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/split/SplitTransformFactory.java index 517d3ca5ceae..d5385a4f59ea 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/split/SplitTransformFactory.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/split/SplitTransformFactory.java @@ -17,6 +17,7 @@ package org.apache.seatunnel.transform.split; +import org.apache.seatunnel.api.configuration.util.Conditions; import org.apache.seatunnel.api.configuration.util.OptionRule; import org.apache.seatunnel.api.table.connector.TableTransform; import org.apache.seatunnel.api.table.factory.Factory; @@ -36,10 +37,11 @@ public String factoryIdentifier() { @Override public OptionRule optionRule() { return OptionRule.builder() - .optional( - SplitTransformConfig.KEY_SEPARATOR, - SplitTransformConfig.KEY_SPLIT_FIELD, - SplitTransformConfig.KEY_OUTPUT_FIELDS) + .required(SplitTransformConfig.KEY_SEPARATOR) + .required(SplitTransformConfig.KEY_SPLIT_FIELD) + .required( + SplitTransformConfig.KEY_OUTPUT_FIELDS, + Conditions.notEmpty(SplitTransformConfig.KEY_OUTPUT_FIELDS)) .optional(TransformCommonOptions.MULTI_TABLES) .optional(TransformCommonOptions.TABLE_MATCH_REGEX) .build(); diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/SQLTransformFactory.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/SQLTransformFactory.java index f1310cfbc0ad..4a3b6db9c566 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/SQLTransformFactory.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/SQLTransformFactory.java @@ -38,7 +38,7 @@ public String factoryIdentifier() { @Override public OptionRule optionRule() { return OptionRule.builder() - .optional(KEY_QUERY) + .required(KEY_QUERY) .optional(TransformCommonOptions.MULTI_TABLES) .optional(TransformCommonOptions.TABLE_MATCH_REGEX) .build(); diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableFilterConfig.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableFilterConfig.java index 7498f4636365..7ee7203aba97 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableFilterConfig.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableFilterConfig.java @@ -18,7 +18,6 @@ package org.apache.seatunnel.transform.table; import org.apache.seatunnel.shade.com.fasterxml.jackson.annotation.JsonAlias; -import org.apache.seatunnel.shade.com.google.common.base.Preconditions; import org.apache.seatunnel.api.configuration.Option; import org.apache.seatunnel.api.configuration.Options; @@ -107,13 +106,6 @@ public static TableFilterConfig of(ReadonlyConfig config) { filterConfig.setSchemaPattern(config.get(SCHEMA_PATTERN)); filterConfig.setTablePattern(config.get(TABLE_PATTERN)); filterConfig.setPatternMode(config.get(PATTERN_MODE)); - - Preconditions.checkArgument( - filterConfig.getDatabasePattern() != null - || filterConfig.getSchemaPattern() != null - || filterConfig.getTablePattern() != null - || filterConfig.getPatternMode() != null, - "At least one of database_pattern, schema_pattern, table_pattern or pattern_mode must be specified."); return filterConfig; } diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableFilterTransformFactory.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableFilterTransformFactory.java index 987168d9cd90..701d9f56976e 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableFilterTransformFactory.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableFilterTransformFactory.java @@ -17,6 +17,9 @@ package org.apache.seatunnel.transform.table; +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.api.configuration.util.ConditionExtension; +import org.apache.seatunnel.api.configuration.util.Conditions; import org.apache.seatunnel.api.configuration.util.OptionRule; import org.apache.seatunnel.api.table.connector.TableTransform; import org.apache.seatunnel.api.table.factory.Factory; @@ -26,6 +29,9 @@ import com.google.auto.service.AutoService; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + @AutoService(Factory.class) public class TableFilterTransformFactory implements TableTransformFactory { @Override @@ -38,8 +44,15 @@ public OptionRule optionRule() { return OptionRule.builder() .optional( TableFilterConfig.DATABASE_PATTERN, + Conditions.extension( + TableFilterConfig.DATABASE_PATTERN, new RegexValidator())) + .optional( TableFilterConfig.SCHEMA_PATTERN, - TableFilterConfig.TABLE_PATTERN) + Conditions.extension( + TableFilterConfig.SCHEMA_PATTERN, new RegexValidator())) + .optional( + TableFilterConfig.TABLE_PATTERN, + Conditions.extension(TableFilterConfig.TABLE_PATTERN, new RegexValidator())) .optional(TableFilterConfig.PATTERN_MODE) .optional(TransformCommonOptions.MULTI_TABLES) .optional(TransformCommonOptions.TABLE_MATCH_REGEX) @@ -52,4 +65,24 @@ public TableTransform createTransform(TableTransformFactoryContext context) { new TableFilterMultiCatalogTransform( context.getCatalogTables(), context.getOptions()); } + + static class RegexValidator implements ConditionExtension { + @Override + public String description() { + return "must be a valid regular expression"; + } + + @Override + public boolean evaluate(ReadonlyConfig config, String value) { + if (value == null || value.isEmpty()) { + return true; + } + try { + Pattern.compile(value); + return true; + } catch (PatternSyntaxException e) { + return false; + } + } + } } diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableMergeTransformFactory.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableMergeTransformFactory.java index 3db9cb31250f..6bf876a23574 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableMergeTransformFactory.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableMergeTransformFactory.java @@ -17,6 +17,7 @@ package org.apache.seatunnel.transform.table; +import org.apache.seatunnel.api.configuration.util.Conditions; import org.apache.seatunnel.api.configuration.util.OptionRule; import org.apache.seatunnel.api.table.connector.TableTransform; import org.apache.seatunnel.api.table.factory.Factory; @@ -36,7 +37,7 @@ public String factoryIdentifier() { @Override public OptionRule optionRule() { return OptionRule.builder() - .required(TableMergeConfig.TABLE) + .required(TableMergeConfig.TABLE, Conditions.notBlank(TableMergeConfig.TABLE)) .optional(TableMergeConfig.DATABASE, TableMergeConfig.SCHEMA) .optional(TransformCommonOptions.MULTI_TABLES) .optional(TransformCommonOptions.TABLE_MATCH_REGEX) diff --git a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/validator/DataValidatorTransformFactory.java b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/validator/DataValidatorTransformFactory.java index f3d3760d46ae..b391d8339a17 100644 --- a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/validator/DataValidatorTransformFactory.java +++ b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/validator/DataValidatorTransformFactory.java @@ -17,7 +17,11 @@ package org.apache.seatunnel.transform.validator; +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.api.configuration.util.ConditionExtension; +import org.apache.seatunnel.api.configuration.util.Conditions; import org.apache.seatunnel.api.configuration.util.OptionRule; +import org.apache.seatunnel.api.configuration.util.OptionValidationException; import org.apache.seatunnel.api.table.connector.TableTransform; import org.apache.seatunnel.api.table.factory.Factory; import org.apache.seatunnel.api.table.factory.TableTransformFactory; @@ -26,6 +30,9 @@ import com.google.auto.service.AutoService; +import java.util.List; +import java.util.Map; + import static org.apache.seatunnel.transform.validator.DataValidatorTransformConfig.FIELD_RULES; /** Factory for creating DataValidator Transform instances. */ @@ -40,7 +47,10 @@ public String factoryIdentifier() { @Override public OptionRule optionRule() { return OptionRule.builder() - .required(FIELD_RULES) + .required( + FIELD_RULES, + Conditions.notEmpty(FIELD_RULES) + .and(Conditions.extension(FIELD_RULES, new FieldRulesValidator()))) .optional(TransformCommonOptions.MULTI_TABLES) .optional(TransformCommonOptions.TABLE_MATCH_REGEX) .optional(TransformCommonOptions.ROW_ERROR_HANDLE_WAY_OPTION) @@ -53,4 +63,39 @@ public TableTransform createTransform(TableTransformFactoryContext context) { return () -> new DataValidatorTransform(context.getOptions(), context.getCatalogTables().get(0)); } + + static class FieldRulesValidator implements ConditionExtension>> { + @Override + public String description() { + return "each field_rules entry must contain 'field_name' and either 'rule_type' or 'rules'"; + } + + @Override + public boolean evaluate(ReadonlyConfig config, List> value) + throws OptionValidationException { + if (value == null) { + return false; + } + for (int i = 0; i < value.size(); i++) { + Map entry = value.get(i); + Object fieldName = entry.get("field_name"); + if (fieldName == null + || (fieldName instanceof String && ((String) fieldName).trim().isEmpty())) { + throw new OptionValidationException( + String.format( + "field_rules[%d]: 'field_name' must not be null or empty", i)); + } + boolean hasRuleType = entry.containsKey("rule_type"); + Object rulesObj = entry.get("rules"); + boolean hasRules = rulesObj instanceof List && !((List) rulesObj).isEmpty(); + if (!hasRuleType && !hasRules) { + throw new OptionValidationException( + String.format( + "field_rules[%d]: must contain 'rule_type' or non-empty 'rules'", + i)); + } + } + return true; + } + } } diff --git a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/adaptsink/DefineSinkTypeTransformFactoryTest.java b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/adaptsink/DefineSinkTypeTransformFactoryTest.java new file mode 100644 index 000000000000..a3974a35f15f --- /dev/null +++ b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/adaptsink/DefineSinkTypeTransformFactoryTest.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.seatunnel.transform.adaptsink; + +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +class DefineSinkTypeTransformFactoryTest { + + private final OptionRule rule = new DefineSinkTypeTransformFactory().optionRule(); + + private void validate(Map config) { + ConfigValidator.of(ReadonlyConfig.fromMap(config)).validate(rule); + } + + private Map columnEntry(String column, String type) { + Map entry = new HashMap<>(); + entry.put("column", column); + entry.put("type", type); + return entry; + } + + @Test + void testValidConfig() { + Map cfg = new HashMap<>(); + cfg.put( + "columns", + Arrays.asList(columnEntry("id", "bigint"), columnEntry("name", "string"))); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } + + @Test + void testMissingColumnsFails() { + Map cfg = new HashMap<>(); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testEmptyColumnsFails() { + Map cfg = new HashMap<>(); + cfg.put("columns", Collections.emptyList()); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testColumnWithNullNameFails() { + Map cfg = new HashMap<>(); + cfg.put("columns", Arrays.asList(columnEntry(null, "bigint"))); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testColumnWithNullTypeFails() { + Map cfg = new HashMap<>(); + cfg.put("columns", Arrays.asList(columnEntry("id", null))); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testColumnWithEmptyNameFails() { + Map cfg = new HashMap<>(); + cfg.put("columns", Arrays.asList(columnEntry("", "bigint"))); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testDuplicateColumnNameFails() { + Map cfg = new HashMap<>(); + cfg.put("columns", Arrays.asList(columnEntry("id", "bigint"), columnEntry("id", "string"))); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } +} diff --git a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/copy/CopyFieldTransformFactoryTest.java b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/copy/CopyFieldTransformFactoryTest.java new file mode 100644 index 000000000000..39937cca5d96 --- /dev/null +++ b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/copy/CopyFieldTransformFactoryTest.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.seatunnel.transform.copy; + +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +class CopyFieldTransformFactoryTest { + + private final OptionRule rule = new CopyFieldTransformFactory().optionRule(); + + private void validate(Map config) { + ConfigValidator.of(ReadonlyConfig.fromMap(config)).validate(rule); + } + + @Test + void testValidFieldsMapConfig() { + Map cfg = new HashMap<>(); + Map fields = new HashMap<>(); + fields.put("new_col", "old_col"); + cfg.put("fields", fields); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } + + @Test + void testValidSrcDestConfig() { + Map cfg = new HashMap<>(); + cfg.put("src_field", "old_col"); + cfg.put("dest_field", "new_col"); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } + + @Test + void testEmptyFieldsMapFails() { + Map cfg = new HashMap<>(); + cfg.put("fields", Collections.emptyMap()); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testNeitherFieldsNorSrcFieldProvidedFails() { + Map cfg = new HashMap<>(); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testSrcFieldWithoutDestFieldFails() { + Map cfg = new HashMap<>(); + cfg.put("src_field", "old_col"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testDestFieldWithoutSrcFieldFails() { + Map cfg = new HashMap<>(); + cfg.put("dest_field", "new_col"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } +} diff --git a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/dynamiccompile/DynamicCompileTransformFactoryTest.java b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/dynamiccompile/DynamicCompileTransformFactoryTest.java new file mode 100644 index 000000000000..704c80aadd55 --- /dev/null +++ b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/dynamiccompile/DynamicCompileTransformFactoryTest.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.seatunnel.transform.dynamiccompile; + +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +class DynamicCompileTransformFactoryTest { + + private final OptionRule rule = new DynamicCompileTransformFactory().optionRule(); + + private void validate(Map config) { + ConfigValidator.of(ReadonlyConfig.fromMap(config)).validate(rule); + } + + @Test + void testValidSourceCodeConfig() { + Map cfg = new HashMap<>(); + cfg.put("compile_language", "JAVA"); + cfg.put("compile_pattern", "SOURCE_CODE"); + cfg.put("source_code", "public class Transform {}"); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } + + @Test + void testValidAbsolutePathConfig() { + Map cfg = new HashMap<>(); + cfg.put("compile_language", "JAVA"); + cfg.put("compile_pattern", "ABSOLUTE_PATH"); + cfg.put("absolute_path", "/tmp/Transform.java"); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } + + @Test + void testMissingCompileLanguageFails() { + Map cfg = new HashMap<>(); + cfg.put("compile_pattern", "SOURCE_CODE"); + cfg.put("source_code", "public class Transform {}"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testSourceCodeBlankFails() { + Map cfg = new HashMap<>(); + cfg.put("compile_language", "JAVA"); + cfg.put("compile_pattern", "SOURCE_CODE"); + cfg.put("source_code", " "); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testAbsolutePathBlankFails() { + Map cfg = new HashMap<>(); + cfg.put("compile_language", "JAVA"); + cfg.put("compile_pattern", "ABSOLUTE_PATH"); + cfg.put("absolute_path", " "); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testSourceCodeMissingKeyFails() { + Map cfg = new HashMap<>(); + cfg.put("compile_language", "JAVA"); + cfg.put("compile_pattern", "SOURCE_CODE"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testAbsolutePathMissingKeyFails() { + Map cfg = new HashMap<>(); + cfg.put("compile_language", "JAVA"); + cfg.put("compile_pattern", "ABSOLUTE_PATH"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testDefaultPatternMissingSourceCodeFails() { + Map cfg = new HashMap<>(); + cfg.put("compile_language", "JAVA"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } +} diff --git a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformFactoryTest.java b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformFactoryTest.java new file mode 100644 index 000000000000..0d34a6a3ddda --- /dev/null +++ b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformFactoryTest.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.seatunnel.transform.encrypt; + +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +class FieldEncryptTransformFactoryTest { + + private final OptionRule rule = new FieldEncryptTransformFactory().optionRule(); + + private void validate(Map config) { + ConfigValidator.of(ReadonlyConfig.fromMap(config)).validate(rule); + } + + private Map validConfig() { + Map cfg = new HashMap<>(); + cfg.put("fields", Arrays.asList("password", "ssn")); + cfg.put("key", "1234567890abcdef"); + return cfg; + } + + @Test + void testValidConfig() { + Assertions.assertDoesNotThrow(() -> validate(validConfig())); + } + + @Test + void testMissingFieldsFails() { + Map cfg = validConfig(); + cfg.remove("fields"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testEmptyFieldsFails() { + Map cfg = validConfig(); + cfg.put("fields", Collections.emptyList()); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testMissingKeyFails() { + Map cfg = validConfig(); + cfg.remove("key"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testBlankKeyFails() { + Map cfg = validConfig(); + cfg.put("key", " "); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testMaxFieldLengthZeroFails() { + Map cfg = validConfig(); + cfg.put("max_field_length", 0); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testMaxFieldLengthNegativeFails() { + Map cfg = validConfig(); + cfg.put("max_field_length", -1); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testMaxFieldLengthPositiveValid() { + Map cfg = validConfig(); + cfg.put("max_field_length", 1024); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } +} diff --git a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/fieldmapper/FieldMapperTransformFactoryTest.java b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/fieldmapper/FieldMapperTransformFactoryTest.java new file mode 100644 index 000000000000..5ed2b0e2c517 --- /dev/null +++ b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/fieldmapper/FieldMapperTransformFactoryTest.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.seatunnel.transform.fieldmapper; + +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +class FieldMapperTransformFactoryTest { + + private final OptionRule rule = new FieldMapperTransformFactory().optionRule(); + + private void validate(Map config) { + ConfigValidator.of(ReadonlyConfig.fromMap(config)).validate(rule); + } + + @Test + void testValidConfig() { + Map cfg = new HashMap<>(); + Map fieldMapper = new HashMap<>(); + fieldMapper.put("old_name", "new_name"); + cfg.put("field_mapper", fieldMapper); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } + + @Test + void testMissingFieldMapperFails() { + Map cfg = new HashMap<>(); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testEmptyFieldMapperFails() { + Map cfg = new HashMap<>(); + cfg.put("field_mapper", Collections.emptyMap()); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } +} diff --git a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/filter/FilterFieldTransformFactoryTest.java b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/filter/FilterFieldTransformFactoryTest.java new file mode 100644 index 000000000000..489ba5f8c9f0 --- /dev/null +++ b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/filter/FilterFieldTransformFactoryTest.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.seatunnel.transform.filter; + +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +class FilterFieldTransformFactoryTest { + + private final OptionRule rule = new FilterFieldTransformFactory().optionRule(); + + private void validate(Map config) { + ConfigValidator.of(ReadonlyConfig.fromMap(config)).validate(rule); + } + + @Test + void testIncludeFieldsValid() { + Map cfg = new HashMap<>(); + cfg.put("include_fields", Arrays.asList("id", "name")); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } + + @Test + void testExcludeFieldsValid() { + Map cfg = new HashMap<>(); + cfg.put("exclude_fields", Arrays.asList("password", "secret")); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } + + @Test + void testNeitherFieldsFails() { + Map cfg = new HashMap<>(); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testBothFieldsFails() { + Map cfg = new HashMap<>(); + cfg.put("include_fields", Arrays.asList("id")); + cfg.put("exclude_fields", Arrays.asList("password")); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testIncludeFieldsEmptyFails() { + Map cfg = new HashMap<>(); + cfg.put("include_fields", Collections.emptyList()); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testExcludeFieldsEmptyFails() { + Map cfg = new HashMap<>(); + cfg.put("exclude_fields", Collections.emptyList()); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } +} diff --git a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/filter/FilterFieldTransformTest.java b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/filter/FilterFieldTransformTest.java index e1ca9b88276b..b33a83f7b905 100644 --- a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/filter/FilterFieldTransformTest.java +++ b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/filter/FilterFieldTransformTest.java @@ -18,6 +18,8 @@ package org.apache.seatunnel.transform.filter; 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.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.PhysicalColumn; @@ -99,13 +101,15 @@ static void setUp() { @Test void testConfig() { - // test both not set + OptionRule rule = new FilterFieldTransformFactory().optionRule(); + + // test both not set — validated via Factory optionRule OptionValidationException noneSetEx = Assertions.assertThrows( OptionValidationException.class, () -> - new FilterFieldTransform( - ReadonlyConfig.fromMap(new HashMap<>()), catalogTable)); + ConfigValidator.of(ReadonlyConfig.fromMap(new HashMap<>())) + .validate(rule)); Assertions.assertTrue( noneSetEx.getMessage().contains("'include_fields'"), "Should mention include_fields: " + noneSetEx.getMessage()); @@ -121,23 +125,23 @@ void testConfig() { Assertions.assertThrows( OptionValidationException.class, () -> - new FilterFieldTransform( - ReadonlyConfig.fromMap( - new HashMap() { - { - put( - FilterFieldTransformConfig - .INCLUDE_FIELDS - .key(), - filterKeys); - put( - FilterFieldTransformConfig - .EXCLUDE_FIELDS - .key(), - filterKeys); - } - }), - catalogTable)); + ConfigValidator.of( + ReadonlyConfig.fromMap( + new HashMap() { + { + put( + FilterFieldTransformConfig + .INCLUDE_FIELDS + .key(), + filterKeys); + put( + FilterFieldTransformConfig + .EXCLUDE_FIELDS + .key(), + filterKeys); + } + })) + .validate(rule)); Assertions.assertTrue( bothSetEx.getMessage().contains("mutually exclusive"), "Should mention mutually exclusive: " + bothSetEx.getMessage()); diff --git a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/jsonpath/JsonPathTransformFactoryTest.java b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/jsonpath/JsonPathTransformFactoryTest.java new file mode 100644 index 000000000000..8e1b4edfa2ec --- /dev/null +++ b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/jsonpath/JsonPathTransformFactoryTest.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.seatunnel.transform.jsonpath; + +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +class JsonPathTransformFactoryTest { + + private final OptionRule rule = new JsonPathTransformFactory().optionRule(); + + private void validate(Map config) { + ConfigValidator.of(ReadonlyConfig.fromMap(config)).validate(rule); + } + + private Map columnEntry(String path, String srcField, String destField) { + Map entry = new HashMap<>(); + entry.put("path", path); + entry.put("src_field", srcField); + entry.put("dest_field", destField); + return entry; + } + + @Test + void testValidConfig() { + Map cfg = new HashMap<>(); + cfg.put("columns", Arrays.asList(columnEntry("$.data.name", "raw", "name"))); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } + + @Test + void testMissingColumnsFails() { + Map cfg = new HashMap<>(); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testEmptyColumnsFails() { + Map cfg = new HashMap<>(); + cfg.put("columns", Collections.emptyList()); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testColumnWithNullPathFails() { + Map cfg = new HashMap<>(); + Map entry = new HashMap<>(); + entry.put("src_field", "raw"); + entry.put("dest_field", "name"); + cfg.put("columns", Arrays.asList(entry)); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testColumnWithEmptyDestFieldFails() { + Map cfg = new HashMap<>(); + Map entry = new HashMap<>(); + entry.put("path", "$.data.name"); + entry.put("src_field", "raw"); + entry.put("dest_field", ""); + cfg.put("columns", Arrays.asList(entry)); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testColumnWithMissingSrcFieldFails() { + Map cfg = new HashMap<>(); + Map entry = new HashMap<>(); + entry.put("path", "$.data.name"); + entry.put("dest_field", "name"); + cfg.put("columns", Arrays.asList(entry)); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testColumnWithEmptySrcFieldFails() { + Map cfg = new HashMap<>(); + Map entry = new HashMap<>(); + entry.put("path", "$.data.name"); + entry.put("src_field", ""); + entry.put("dest_field", "name"); + cfg.put("columns", Arrays.asList(entry)); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } +} diff --git a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/metadata/MetadataTransformFactoryTest.java b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/metadata/MetadataTransformFactoryTest.java new file mode 100644 index 000000000000..f02f7bcf490e --- /dev/null +++ b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/metadata/MetadataTransformFactoryTest.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.seatunnel.transform.metadata; + +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +class MetadataTransformFactoryTest { + + private final OptionRule rule = new MetadataTransformFactory().optionRule(); + + private void validate(Map config) { + ConfigValidator.of(ReadonlyConfig.fromMap(config)).validate(rule); + } + + @Test + void testValidConfig() { + Map cfg = new HashMap<>(); + Map metadataFields = new HashMap<>(); + metadataFields.put("database", "db_field"); + cfg.put("metadata_fields", metadataFields); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } + + @Test + void testMissingMetadataFieldsFails() { + Map cfg = new HashMap<>(); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testEmptyMetadataFieldsFails() { + Map cfg = new HashMap<>(); + cfg.put("metadata_fields", Collections.emptyMap()); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } +} diff --git a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/regexextract/RegexExtractTransformFactoryTest.java b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/regexextract/RegexExtractTransformFactoryTest.java new file mode 100644 index 000000000000..28731c4076ea --- /dev/null +++ b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/regexextract/RegexExtractTransformFactoryTest.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.seatunnel.transform.regexextract; + +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +class RegexExtractTransformFactoryTest { + + private final OptionRule rule = new RegexExtractTransformFactory().optionRule(); + + private void validate(Map config) { + ConfigValidator.of(ReadonlyConfig.fromMap(config)).validate(rule); + } + + private Map validConfig() { + Map cfg = new HashMap<>(); + cfg.put("source_field", "log_line"); + cfg.put("regex_pattern", "(\\d+)-(\\w+)"); + cfg.put("output_fields", Arrays.asList("id", "name")); + return cfg; + } + + @Test + void testValidConfig() { + Assertions.assertDoesNotThrow(() -> validate(validConfig())); + } + + @Test + void testValidConfigWithMatchingDefaults() { + Map cfg = validConfig(); + cfg.put("default_values", Arrays.asList("0", "unknown")); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } + + @Test + void testMissingSourceFieldFails() { + Map cfg = validConfig(); + cfg.remove("source_field"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testMissingRegexPatternFails() { + Map cfg = validConfig(); + cfg.remove("regex_pattern"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testMissingOutputFieldsFails() { + Map cfg = validConfig(); + cfg.remove("output_fields"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testDefaultValuesLengthMismatchFails() { + Map cfg = validConfig(); + cfg.put("default_values", Arrays.asList("0", "unknown", "extra")); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testEmptyOutputFieldsFails() { + Map cfg = new HashMap<>(); + cfg.put("source_field", "log_line"); + cfg.put("regex_pattern", "(\\d+)"); + cfg.put("output_fields", Collections.emptyList()); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } +} diff --git a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/replace/ReplaceTransformFactoryTest.java b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/replace/ReplaceTransformFactoryTest.java new file mode 100644 index 000000000000..bb93a6edbc85 --- /dev/null +++ b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/replace/ReplaceTransformFactoryTest.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.seatunnel.transform.replace; + +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +class ReplaceTransformFactoryTest { + + private final OptionRule rule = new ReplaceTransformFactory().optionRule(); + + private void validate(Map config) { + ConfigValidator.of(ReadonlyConfig.fromMap(config)).validate(rule); + } + + private Map validConfig() { + Map cfg = new HashMap<>(); + cfg.put("replace_fields", Arrays.asList("name")); + cfg.put("pattern", "old"); + cfg.put("replacement", "new"); + return cfg; + } + + @Test + void testValidConfig() { + Assertions.assertDoesNotThrow(() -> validate(validConfig())); + } + + @Test + void testMissingReplaceFieldsFails() { + Map cfg = validConfig(); + cfg.remove("replace_fields"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testEmptyReplaceFieldsFails() { + Map cfg = validConfig(); + cfg.put("replace_fields", Collections.emptyList()); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testMissingPatternFails() { + Map cfg = validConfig(); + cfg.remove("pattern"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testMissingReplacementFails() { + Map cfg = validConfig(); + cfg.remove("replacement"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testFallbackKeyValid() { + Map cfg = new HashMap<>(); + cfg.put("replace_field", "name"); + cfg.put("pattern", "old"); + cfg.put("replacement", "new"); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } +} diff --git a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/replace/ReplaceTransformTest.java b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/replace/ReplaceTransformTest.java index c716627c955b..a823c77c9f96 100644 --- a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/replace/ReplaceTransformTest.java +++ b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/replace/ReplaceTransformTest.java @@ -18,6 +18,9 @@ package org.apache.seatunnel.transform.replace; 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.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.PhysicalColumn; import org.apache.seatunnel.api.table.catalog.TableIdentifier; @@ -114,22 +117,17 @@ void testMultipleFieldReplaceWithList() { } @Test - void testRejectConflictingReplaceFieldKeys() { + void testFallbackKeyUsedWhenPrimaryAbsent() { Map configMap = new HashMap<>(); configMap.put("replace_field", "name"); - configMap.put( - ReplaceTransformConfig.KEY_REPLACE_FIELDS.key(), Arrays.asList("name", "title")); configMap.put(ReplaceTransformConfig.KEY_PATTERN.key(), "before"); configMap.put(ReplaceTransformConfig.KEY_REPLACEMENT.key(), "after"); - TransformException exception = - Assertions.assertThrows( - TransformException.class, - () -> - new ReplaceTransform( - ReadonlyConfig.fromMap(configMap), catalogTable)); - - Assertions.assertTrue(exception.getMessage().contains("cannot be configured together")); + ReplaceTransform transform = + new ReplaceTransform(ReadonlyConfig.fromMap(configMap), catalogTable); + SeaTunnelRow input = new SeaTunnelRow(new Object[] {1, "before name", "title"}); + SeaTunnelRow output = transform.transformRow(input); + Assertions.assertEquals("after name", output.getField(1)); } @Test @@ -180,19 +178,20 @@ void testInvalidRegexPattern() { @Test void testEmptyReplaceFieldsValidation() { + OptionRule rule = new ReplaceTransformFactory().optionRule(); Map configMap = new HashMap<>(); configMap.put(ReplaceTransformConfig.KEY_REPLACE_FIELDS.key(), new ArrayList()); configMap.put(ReplaceTransformConfig.KEY_PATTERN.key(), "before"); configMap.put(ReplaceTransformConfig.KEY_REPLACEMENT.key(), "after"); - TransformException exception = + OptionValidationException exception = Assertions.assertThrows( - TransformException.class, - () -> - new ReplaceTransform( - ReadonlyConfig.fromMap(configMap), catalogTable)); + OptionValidationException.class, + () -> ConfigValidator.of(ReadonlyConfig.fromMap(configMap)).validate(rule)); - Assertions.assertTrue(exception.getMessage().contains("must not be empty")); + Assertions.assertTrue( + exception.getMessage().contains("replace_fields"), + "Should mention replace_fields: " + exception.getMessage()); } @Test diff --git a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/split/SplitTransformFactoryTest.java b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/split/SplitTransformFactoryTest.java new file mode 100644 index 000000000000..a7f53e00f5f6 --- /dev/null +++ b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/split/SplitTransformFactoryTest.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.seatunnel.transform.split; + +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +class SplitTransformFactoryTest { + + private final OptionRule rule = new SplitTransformFactory().optionRule(); + + private void validate(Map config) { + ConfigValidator.of(ReadonlyConfig.fromMap(config)).validate(rule); + } + + private Map validConfig() { + Map cfg = new HashMap<>(); + cfg.put("separator", " "); + cfg.put("split_field", "name"); + cfg.put("output_fields", Arrays.asList("first_name", "last_name")); + return cfg; + } + + @Test + void testValidConfig() { + Assertions.assertDoesNotThrow(() -> validate(validConfig())); + } + + @Test + void testMissingSeparatorFails() { + Map cfg = validConfig(); + cfg.remove("separator"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testMissingSplitFieldFails() { + Map cfg = validConfig(); + cfg.remove("split_field"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testMissingOutputFieldsFails() { + Map cfg = validConfig(); + cfg.remove("output_fields"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testEmptyOutputFieldsFails() { + Map cfg = validConfig(); + cfg.put("output_fields", Collections.emptyList()); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } +} diff --git a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLTransformFactoryTest.java b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLTransformFactoryTest.java index 5e79df60c137..5add6ab546ae 100644 --- a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLTransformFactoryTest.java +++ b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLTransformFactoryTest.java @@ -18,7 +18,9 @@ package org.apache.seatunnel.transform.sql; 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.api.table.catalog.CatalogTable; import org.apache.seatunnel.api.table.catalog.CatalogTableUtil; import org.apache.seatunnel.api.table.connector.TableTransform; @@ -32,7 +34,9 @@ import org.junit.jupiter.api.Test; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class SQLTransformFactoryTest { @@ -73,4 +77,22 @@ public void testCreateTransformReturnsMultiCatalogTransform() { Assertions.assertNotNull(inner); Assertions.assertTrue(inner instanceof SQLMultiCatalogFlatMapTransform); } + + @Test + public void testValidConfigWithQuery() { + OptionRule rule = new SQLTransformFactory().optionRule(); + Map cfg = new HashMap<>(); + cfg.put("query", "SELECT id, name FROM table1"); + Assertions.assertDoesNotThrow( + () -> ConfigValidator.of(ReadonlyConfig.fromMap(cfg)).validate(rule)); + } + + @Test + public void testMissingQueryFails() { + OptionRule rule = new SQLTransformFactory().optionRule(); + Map cfg = new HashMap<>(); + Assertions.assertThrows( + OptionValidationException.class, + () -> ConfigValidator.of(ReadonlyConfig.fromMap(cfg)).validate(rule)); + } } diff --git a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/table/TableFilterTransformFactoryTest.java b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/table/TableFilterTransformFactoryTest.java new file mode 100644 index 000000000000..3f0e665e99c7 --- /dev/null +++ b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/table/TableFilterTransformFactoryTest.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.seatunnel.transform.table; + +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +class TableFilterTransformFactoryTest { + + private final OptionRule rule = new TableFilterTransformFactory().optionRule(); + + private void validate(Map config) { + ConfigValidator.of(ReadonlyConfig.fromMap(config)).validate(rule); + } + + @Test + void testValidWithTablePattern() { + Map cfg = new HashMap<>(); + cfg.put("table_pattern", "user_.*"); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } + + @Test + void testValidWithDatabasePattern() { + Map cfg = new HashMap<>(); + cfg.put("database_pattern", "prod_.*"); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } + + @Test + void testValidWithSchemaPattern() { + Map cfg = new HashMap<>(); + cfg.put("schema_pattern", "public"); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } + + @Test + void testInvalidDatabasePatternFails() { + Map cfg = new HashMap<>(); + cfg.put("database_pattern", "[unclosed"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testInvalidSchemaPatternFails() { + Map cfg = new HashMap<>(); + cfg.put("schema_pattern", "(missing_paren"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testInvalidTablePatternFails() { + Map cfg = new HashMap<>(); + cfg.put("table_pattern", "*invalid"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testPatternModeWithTablePatternValid() { + Map cfg = new HashMap<>(); + cfg.put("pattern_mode", "EXCLUDE"); + cfg.put("table_pattern", "tmp_.*"); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } +} diff --git a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/validator/DataValidatorTransformFactoryTest.java b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/validator/DataValidatorTransformFactoryTest.java new file mode 100644 index 000000000000..d2f99ec30f7f --- /dev/null +++ b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/validator/DataValidatorTransformFactoryTest.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.seatunnel.transform.validator; + +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +class DataValidatorTransformFactoryTest { + + private final OptionRule rule = new DataValidatorTransformFactory().optionRule(); + + private void validate(Map config) { + ConfigValidator.of(ReadonlyConfig.fromMap(config)).validate(rule); + } + + @Test + void testValidConfig() { + Map cfg = new HashMap<>(); + Map fieldRule = new HashMap<>(); + fieldRule.put("field_name", "age"); + fieldRule.put("rule_type", "NOT_NULL"); + cfg.put("field_rules", Arrays.asList(fieldRule)); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } + + @Test + void testMissingFieldRulesFails() { + Map cfg = new HashMap<>(); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testEmptyFieldRulesFails() { + Map cfg = new HashMap<>(); + cfg.put("field_rules", Collections.emptyList()); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testMissingFieldNameFails() { + Map cfg = new HashMap<>(); + Map fieldRule = new HashMap<>(); + fieldRule.put("rule_type", "NOT_NULL"); + cfg.put("field_rules", Arrays.asList(fieldRule)); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testMissingRuleTypeAndRulesFails() { + Map cfg = new HashMap<>(); + Map fieldRule = new HashMap<>(); + fieldRule.put("field_name", "age"); + cfg.put("field_rules", Arrays.asList(fieldRule)); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } + + @Test + void testNestedRulesValid() { + Map cfg = new HashMap<>(); + Map fieldRule = new HashMap<>(); + fieldRule.put("field_name", "email"); + Map nestedRule = new HashMap<>(); + nestedRule.put("rule_type", "NOT_NULL"); + fieldRule.put("rules", Arrays.asList(nestedRule)); + cfg.put("field_rules", Arrays.asList(fieldRule)); + Assertions.assertDoesNotThrow(() -> validate(cfg)); + } + + @Test + void testEmptyRulesListFails() { + Map cfg = new HashMap<>(); + Map fieldRule = new HashMap<>(); + fieldRule.put("field_name", "email"); + fieldRule.put("rules", Collections.emptyList()); + cfg.put("field_rules", Arrays.asList(fieldRule)); + Assertions.assertThrows(OptionValidationException.class, () -> validate(cfg)); + } +}