diff --git a/data-prepper-api/src/main/java/org/opensearch/dataprepper/model/annotations/AlsoRequired.java b/data-prepper-api/src/main/java/org/opensearch/dataprepper/model/annotations/AlsoRequired.java new file mode 100644 index 0000000000..1252f7bb62 --- /dev/null +++ b/data-prepper-api/src/main/java/org/opensearch/dataprepper/model/annotations/AlsoRequired.java @@ -0,0 +1,36 @@ +package org.opensearch.dataprepper.model.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used in schema generation to define the names and corresponding values of other required + * configurations if the configuration represented by the annotated field/method is present. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface AlsoRequired { + /** + * Array of Required annotations, each representing a required property with its allowed values. + */ + Required[] values(); + + /** + * Annotation to represent a required property and its allowed values. + */ + @interface Required { + /** + * Name of the required property. + */ + String name(); + + /** + * Allowed values for the required property. The default value of {} means any non-null value is allowed. + */ + String[] allowedValues() default {}; + } +} diff --git a/data-prepper-plugin-schema/src/main/java/org/opensearch/dataprepper/schemas/JsonSchemaConverter.java b/data-prepper-plugin-schema/src/main/java/org/opensearch/dataprepper/schemas/JsonSchemaConverter.java index c17d0e50ee..9bff7b2aa7 100644 --- a/data-prepper-plugin-schema/src/main/java/org/opensearch/dataprepper/schemas/JsonSchemaConverter.java +++ b/data-prepper-plugin-schema/src/main/java/org/opensearch/dataprepper/schemas/JsonSchemaConverter.java @@ -14,6 +14,7 @@ import com.github.victools.jsonschema.generator.SchemaGeneratorConfigPart; import com.github.victools.jsonschema.generator.SchemaGeneratorGeneralConfigPart; import com.github.victools.jsonschema.generator.SchemaVersion; +import org.opensearch.dataprepper.model.annotations.AlsoRequired; import org.opensearch.dataprepper.model.event.EventKey; import org.opensearch.dataprepper.model.annotations.DataPrepperPlugin; import org.opensearch.dataprepper.model.annotations.UsesDataPrepperPlugin; @@ -22,12 +23,14 @@ import org.slf4j.LoggerFactory; import java.util.Collections; +import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; public class JsonSchemaConverter { private static final Logger LOG = LoggerFactory.getLogger(JsonSchemaConverter.class); + static final String KEY_VALUE_PAIR_DELIMITER = ":"; static final String DEPRECATED_SINCE_KEY = "deprecated"; private final List jsonSchemaGeneratorModules; private final PluginProvider pluginProvider; @@ -47,11 +50,13 @@ public ObjectNode convertIntoJsonSchema( overrideInstanceAttributeWithDeprecated(scopeSchemaGeneratorConfigPart); overrideTargetTypeWithUsesDataPrepperPlugin(scopeSchemaGeneratorConfigPart); resolveDefaultValueFromJsonProperty(scopeSchemaGeneratorConfigPart); + resolveDependentRequiresFields(scopeSchemaGeneratorConfigPart); overrideDataPrepperPluginTypeAttribute(configBuilder.forTypesInGeneral(), schemaVersion, optionPreset); resolveDataPrepperTypes(scopeSchemaGeneratorConfigPart); final SchemaGeneratorConfig config = configBuilder.build(); final SchemaGenerator generator = new SchemaGenerator(config); + return generator.generateSchema(clazz); } @@ -109,6 +114,23 @@ private void resolveDefaultValueFromJsonProperty( }); } + private void resolveDependentRequiresFields( + final SchemaGeneratorConfigPart scopeSchemaGeneratorConfigPart) { + scopeSchemaGeneratorConfigPart.withDependentRequiresResolver(field -> Optional + .ofNullable(field.getAnnotationConsideringFieldAndGetter(AlsoRequired.class)) + .map(alsoRequired -> Arrays.stream(alsoRequired.values()) + .map(required -> { + final String property = required.name(); + final String[] allowedValues = required.allowedValues(); + if (allowedValues.length == 0) { + return property; + } + return property + KEY_VALUE_PAIR_DELIMITER + Arrays.toString(allowedValues); + }) + .collect(Collectors.toList())) + .orElse(null)); + } + private void resolveDataPrepperTypes(final SchemaGeneratorConfigPart scopeSchemaGeneratorConfigPart) { scopeSchemaGeneratorConfigPart.withTargetTypeOverridesResolver(field -> { if(field.getType().getErasedType().equals(EventKey.class)) { diff --git a/data-prepper-plugin-schema/src/test/java/org/opensearch/dataprepper/schemas/JsonSchemaConverterTest.java b/data-prepper-plugin-schema/src/test/java/org/opensearch/dataprepper/schemas/JsonSchemaConverterTest.java index 2b756d4698..7642e93e90 100644 --- a/data-prepper-plugin-schema/src/test/java/org/opensearch/dataprepper/schemas/JsonSchemaConverterTest.java +++ b/data-prepper-plugin-schema/src/test/java/org/opensearch/dataprepper/schemas/JsonSchemaConverterTest.java @@ -4,12 +4,14 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.github.victools.jsonschema.generator.Module; import com.github.victools.jsonschema.generator.OptionPreset; import com.github.victools.jsonschema.generator.SchemaVersion; import org.junit.jupiter.api.Test; +import org.opensearch.dataprepper.model.annotations.AlsoRequired; import org.opensearch.dataprepper.model.event.EventKey; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -64,6 +66,21 @@ void testConvertIntoJsonSchemaWithCustomJacksonModule() throws JsonProcessingExc assertThat(propertiesNode, instanceOf(ObjectNode.class)); assertThat(propertiesNode.has("test_attribute_with_getter"), is(true)); assertThat(propertiesNode.has("custom_test_attribute"), is(true)); + final JsonNode dependentRequiredNode = jsonSchemaNode.at("/dependentRequired"); + assertThat(dependentRequiredNode, instanceOf(ObjectNode.class)); + assertThat(dependentRequiredNode.has("test_mutually_exclusive_attribute_a"), is(true)); + assertThat(dependentRequiredNode.at("/test_mutually_exclusive_attribute_a"), + instanceOf(ArrayNode.class)); + final ArrayNode dependentRequiredProperty1 = (ArrayNode) dependentRequiredNode.at( + "/test_mutually_exclusive_attribute_a"); + assertThat(dependentRequiredProperty1.size(), equalTo(1)); + assertThat(dependentRequiredProperty1.get(0), equalTo( + TextNode.valueOf("test_mutually_exclusive_attribute_b:[null, \"test_value\"]"))); + final ArrayNode dependentRequiredProperty2 = (ArrayNode) dependentRequiredNode.at( + "/test_dependent_required_property_with_default_allowed_values"); + assertThat(dependentRequiredProperty2.size(), equalTo(1)); + assertThat(dependentRequiredProperty2.get(0), equalTo( + TextNode.valueOf("test_mutually_exclusive_attribute_a"))); } @Test @@ -88,6 +105,20 @@ static class TestConfig { @JsonProperty(defaultValue = "default_value") private String testAttributeWithDefaultValue; + @JsonProperty + @AlsoRequired(values = { + @AlsoRequired.Required(name="test_mutually_exclusive_attribute_b", allowedValues = {"null", "\"test_value\""}) + }) + private String testMutuallyExclusiveAttributeA; + + private String testMutuallyExclusiveAttributeB; + + @JsonProperty + @AlsoRequired(values = { + @AlsoRequired.Required(name="test_mutually_exclusive_attribute_a") + }) + private String testDependentRequiredPropertyWithDefaultAllowedValues; + public String getTestAttributeWithGetter() { return testAttributeWithGetter; }