-
Notifications
You must be signed in to change notification settings - Fork 208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ExampleValues annotation for schema generation along with date examples #5088
Merged
dlvenable
merged 2 commits into
opensearch-project:main
from
dlvenable:5077-examples-on-properties
Oct 23, 2024
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
52 changes: 52 additions & 0 deletions
52
...prepper-api/src/main/java/org/opensearch/dataprepper/model/annotations/ExampleValues.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package org.opensearch.dataprepper.model.annotations; | ||
|
||
import java.lang.annotation.Documented; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.Target; | ||
|
||
import static java.lang.annotation.ElementType.FIELD; | ||
import static java.lang.annotation.RetentionPolicy.RUNTIME; | ||
|
||
/** | ||
* Use this annotation to provide example values for plugin configuration. | ||
* | ||
* @since 2.11 | ||
*/ | ||
@Documented | ||
@Retention(RUNTIME) | ||
@Target({FIELD}) | ||
public @interface ExampleValues { | ||
/** | ||
* One or more examples. | ||
* @return the examples. | ||
* @since 2.11 | ||
*/ | ||
Example[] value(); | ||
|
||
/** | ||
* A single example. | ||
* | ||
* @since 2.11 | ||
*/ | ||
@interface Example { | ||
/** | ||
* The example value | ||
* @return The example value | ||
* | ||
* @since 2.11 | ||
*/ | ||
String value(); | ||
|
||
/** | ||
* A description of the example value. | ||
* | ||
* @since 2.11 | ||
*/ | ||
String description() default ""; | ||
} | ||
} |
50 changes: 50 additions & 0 deletions
50
.../main/java/org/opensearch/dataprepper/schemas/ExampleValuesInstanceAttributeOverride.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package org.opensearch.dataprepper.schemas; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.fasterxml.jackson.databind.node.ArrayNode; | ||
import com.fasterxml.jackson.databind.node.ObjectNode; | ||
import com.github.victools.jsonschema.generator.FieldScope; | ||
import com.github.victools.jsonschema.generator.InstanceAttributeOverrideV2; | ||
import com.github.victools.jsonschema.generator.SchemaGenerationContext; | ||
import org.opensearch.dataprepper.model.annotations.ExampleValues; | ||
|
||
import java.util.Arrays; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.stream.Collectors; | ||
|
||
class ExampleValuesInstanceAttributeOverride implements InstanceAttributeOverrideV2<FieldScope> { | ||
@Override | ||
public void overrideInstanceAttributes(final ObjectNode fieldSchema, final FieldScope fieldScope, final SchemaGenerationContext context) { | ||
final ExampleValues exampleValuesAnnotation = fieldScope.getAnnotationConsideringFieldAndGetterIfSupported(ExampleValues.class); | ||
if(exampleValuesAnnotation != null && exampleValuesAnnotation.value().length > 0) { | ||
final ObjectMapper objectMapper = context.getGeneratorConfig().getObjectMapper(); | ||
|
||
addExampleSchema(fieldSchema, objectMapper, exampleValuesAnnotation); | ||
} | ||
} | ||
|
||
private void addExampleSchema(final ObjectNode fieldSchema, final ObjectMapper objectMapper, final ExampleValues exampleValuesAnnotation) { | ||
final List<Map<String, String>> exampleValues = Arrays.stream(exampleValuesAnnotation.value()) | ||
.map(ExampleValuesInstanceAttributeOverride::createExampleMap).collect(Collectors.toList()); | ||
final ArrayNode exampleNode = objectMapper.convertValue(exampleValues, ArrayNode.class); | ||
|
||
fieldSchema.putArray("examples") | ||
.addAll(exampleNode); | ||
} | ||
|
||
private static Map<String, String> createExampleMap(final ExampleValues.Example example) { | ||
final HashMap<String, String> exampleMap = new HashMap<>(); | ||
exampleMap.put("example", example.value()); | ||
if(example.description() != null && !example.description().isEmpty()) { | ||
exampleMap.put("description", example.description()); | ||
} | ||
return exampleMap; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
187 changes: 187 additions & 0 deletions
187
...t/java/org/opensearch/dataprepper/schemas/ExampleValuesInstanceAttributeOverrideTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package org.opensearch.dataprepper.schemas; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.fasterxml.jackson.databind.node.JsonNodeType; | ||
import com.fasterxml.jackson.databind.node.ObjectNode; | ||
import com.github.victools.jsonschema.generator.FieldScope; | ||
import com.github.victools.jsonschema.generator.SchemaGenerationContext; | ||
import com.github.victools.jsonschema.generator.SchemaGeneratorConfig; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Nested; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.junit.jupiter.params.ParameterizedTest; | ||
import org.junit.jupiter.params.provider.ValueSource; | ||
import org.mockito.Mock; | ||
import org.mockito.junit.jupiter.MockitoExtension; | ||
import org.opensearch.dataprepper.model.annotations.ExampleValues; | ||
|
||
import java.util.Map; | ||
import java.util.UUID; | ||
|
||
import static org.hamcrest.CoreMatchers.equalTo; | ||
import static org.hamcrest.CoreMatchers.notNullValue; | ||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.mockito.Mockito.mock; | ||
import static org.mockito.Mockito.spy; | ||
import static org.mockito.Mockito.verifyNoInteractions; | ||
import static org.mockito.Mockito.when; | ||
|
||
@ExtendWith(MockitoExtension.class) | ||
class ExampleValuesInstanceAttributeOverrideTest { | ||
|
||
@Mock | ||
private ObjectNode fieldSchema; | ||
|
||
@Mock | ||
private FieldScope fieldScope; | ||
|
||
@Mock | ||
private SchemaGenerationContext context; | ||
private ObjectMapper objectMapper; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
objectMapper = new ObjectMapper(); | ||
fieldSchema = spy(objectMapper.convertValue(Map.of(UUID.randomUUID().toString(), UUID.randomUUID().toString()), ObjectNode.class)); | ||
} | ||
|
||
private ExampleValuesInstanceAttributeOverride createObjectUnderTest() { | ||
return new ExampleValuesInstanceAttributeOverride(); | ||
} | ||
|
||
@Test | ||
void overrideInstanceAttributes_does_not_modify_fieldSchema_if_no_ExampleValues_annotation() { | ||
createObjectUnderTest().overrideInstanceAttributes(fieldSchema, fieldScope, context); | ||
|
||
verifyNoInteractions(fieldSchema); | ||
} | ||
|
||
@Nested | ||
class WithExampleValuesAnnotation { | ||
@Mock | ||
private ExampleValues exampleValuesAnnotation; | ||
|
||
@Mock | ||
private SchemaGeneratorConfig schemaGeneratorConfig; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
when(fieldScope.getAnnotationConsideringFieldAndGetterIfSupported(ExampleValues.class)) | ||
.thenReturn(exampleValuesAnnotation); | ||
} | ||
|
||
@Test | ||
void overrideInstanceAttributes_does_not_modify_fieldSchema_if_no_ExampleValues_annotation_is_empty() { | ||
when(exampleValuesAnnotation.value()).thenReturn(new ExampleValues.Example[]{}); | ||
|
||
createObjectUnderTest().overrideInstanceAttributes(fieldSchema, fieldScope, context); | ||
|
||
verifyNoInteractions(fieldSchema); | ||
} | ||
|
||
@Test | ||
void overrideInstanceAttributes_adds_examples_when_one_ExampleValue() { | ||
when(context.getGeneratorConfig()).thenReturn(schemaGeneratorConfig); | ||
when(schemaGeneratorConfig.getObjectMapper()).thenReturn(objectMapper); | ||
|
||
final ExampleValues.Example example = mock(ExampleValues.Example.class); | ||
final String value = UUID.randomUUID().toString(); | ||
final String description = UUID.randomUUID().toString(); | ||
when(example.value()).thenReturn(value); | ||
when(example.description()).thenReturn(description); | ||
final ExampleValues.Example[] examples = {example}; | ||
when(exampleValuesAnnotation.value()).thenReturn(examples); | ||
|
||
createObjectUnderTest().overrideInstanceAttributes(fieldSchema, fieldScope, context); | ||
|
||
final JsonNode examplesNode = fieldSchema.get("examples"); | ||
assertThat(examplesNode, notNullValue()); | ||
|
||
assertThat(examplesNode.getNodeType(), equalTo(JsonNodeType.ARRAY)); | ||
assertThat(examplesNode.size(), equalTo(1)); | ||
final JsonNode firstExampleNode = examplesNode.get(0); | ||
assertThat(firstExampleNode, notNullValue()); | ||
assertThat(firstExampleNode.getNodeType(), equalTo(JsonNodeType.OBJECT)); | ||
assertThat(firstExampleNode.get("example"), notNullValue()); | ||
assertThat(firstExampleNode.get("example").getNodeType(), equalTo(JsonNodeType.STRING)); | ||
assertThat(firstExampleNode.get("example").textValue(), equalTo(value)); | ||
assertThat(firstExampleNode.get("description"), notNullValue()); | ||
assertThat(firstExampleNode.get("description").getNodeType(), equalTo(JsonNodeType.STRING)); | ||
assertThat(firstExampleNode.get("description").textValue(), equalTo(description)); | ||
} | ||
|
||
@Test | ||
void overrideInstanceAttributes_adds_examples_when_one_ExampleValue_with_no_description() { | ||
when(context.getGeneratorConfig()).thenReturn(schemaGeneratorConfig); | ||
when(schemaGeneratorConfig.getObjectMapper()).thenReturn(objectMapper); | ||
|
||
final ExampleValues.Example example = mock(ExampleValues.Example.class); | ||
final String value = UUID.randomUUID().toString(); | ||
final String description = UUID.randomUUID().toString(); | ||
when(example.value()).thenReturn(value); | ||
final ExampleValues.Example[] examples = {example}; | ||
when(exampleValuesAnnotation.value()).thenReturn(examples); | ||
|
||
createObjectUnderTest().overrideInstanceAttributes(fieldSchema, fieldScope, context); | ||
|
||
final JsonNode examplesNode = fieldSchema.get("examples"); | ||
assertThat(examplesNode, notNullValue()); | ||
|
||
assertThat(examplesNode.getNodeType(), equalTo(JsonNodeType.ARRAY)); | ||
assertThat(examplesNode.size(), equalTo(1)); | ||
final JsonNode firstExampleNode = examplesNode.get(0); | ||
assertThat(firstExampleNode, notNullValue()); | ||
assertThat(firstExampleNode.getNodeType(), equalTo(JsonNodeType.OBJECT)); | ||
assertThat(firstExampleNode.get("example"), notNullValue()); | ||
assertThat(firstExampleNode.get("example").getNodeType(), equalTo(JsonNodeType.STRING)); | ||
assertThat(firstExampleNode.get("example").textValue(), equalTo(value)); | ||
assertThat(firstExampleNode.has("description"), equalTo(false)); | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(ints = {2, 3, 5}) | ||
void overrideInstanceAttributes_adds_examples_when_multiple_ExampleValue(final int numberOfExamples) { | ||
when(context.getGeneratorConfig()).thenReturn(schemaGeneratorConfig); | ||
when(schemaGeneratorConfig.getObjectMapper()).thenReturn(objectMapper); | ||
|
||
final ExampleValues.Example[] examples = new ExampleValues.Example[numberOfExamples]; | ||
for (int i = 0; i < numberOfExamples; i++) { | ||
final ExampleValues.Example example = mock(ExampleValues.Example.class); | ||
final String value = UUID.randomUUID().toString(); | ||
final String description = UUID.randomUUID().toString(); | ||
when(example.value()).thenReturn(value); | ||
when(example.description()).thenReturn(description); | ||
|
||
examples[i] = example; | ||
} | ||
when(exampleValuesAnnotation.value()).thenReturn(examples); | ||
|
||
createObjectUnderTest().overrideInstanceAttributes(fieldSchema, fieldScope, context); | ||
|
||
final JsonNode examplesNode = fieldSchema.get("examples"); | ||
assertThat(examplesNode, notNullValue()); | ||
|
||
assertThat(examplesNode.getNodeType(), equalTo(JsonNodeType.ARRAY)); | ||
assertThat(examplesNode.size(), equalTo(numberOfExamples)); | ||
|
||
for (int i = 0; i < numberOfExamples; i++) { | ||
final JsonNode exampleNode = examplesNode.get(0); | ||
assertThat(exampleNode, notNullValue()); | ||
assertThat(exampleNode.getNodeType(), equalTo(JsonNodeType.OBJECT)); | ||
assertThat(exampleNode.get("example"), notNullValue()); | ||
assertThat(exampleNode.get("example").getNodeType(), equalTo(JsonNodeType.STRING)); | ||
assertThat(exampleNode.get("example").textValue(), notNullValue()); | ||
assertThat(exampleNode.get("description"), notNullValue()); | ||
assertThat(exampleNode.get("description").getNodeType(), equalTo(JsonNodeType.STRING)); | ||
assertThat(exampleNode.get("description").textValue(), notNullValue()); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,12 @@ | ||
package org.opensearch.dataprepper.schemas; | ||
|
||
import com.fasterxml.jackson.annotation.JsonClassDescription; | ||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import com.fasterxml.jackson.annotation.JsonPropertyDescription; | ||
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.JsonNodeType; | ||
import com.fasterxml.jackson.databind.node.ObjectNode; | ||
import com.github.victools.jsonschema.generator.Module; | ||
import com.github.victools.jsonschema.generator.OptionPreset; | ||
|
@@ -14,6 +16,7 @@ | |
import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationOption; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.opensearch.dataprepper.model.annotations.ExampleValues; | ||
import org.opensearch.dataprepper.model.annotations.UsesDataPrepperPlugin; | ||
import org.opensearch.dataprepper.model.configuration.PluginModel; | ||
import org.opensearch.dataprepper.plugin.ClasspathPluginProvider; | ||
|
@@ -23,8 +26,10 @@ | |
import java.util.List; | ||
|
||
import static com.github.victools.jsonschema.module.jackson.JacksonOption.RESPECT_JSONPROPERTY_REQUIRED; | ||
import static org.hamcrest.CoreMatchers.equalTo; | ||
import static org.hamcrest.CoreMatchers.instanceOf; | ||
import static org.hamcrest.CoreMatchers.is; | ||
import static org.hamcrest.CoreMatchers.notNullValue; | ||
import static org.hamcrest.MatcherAssert.assertThat; | ||
|
||
public class JsonSchemaConverterIT { | ||
|
@@ -59,6 +64,38 @@ void testSubTypes() throws JsonProcessingException { | |
anyOfNode.forEach(aggregateActionNode -> assertThat(aggregateActionNode.has(PROPERTIES_KEY), is(true))); | ||
} | ||
|
||
@Test | ||
void test_examples() throws JsonProcessingException { | ||
final ObjectNode jsonSchemaNode = objectUnderTest.convertIntoJsonSchema( | ||
SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON, TestConfig.class); | ||
assertThat(jsonSchemaNode, instanceOf(ObjectNode.class)); | ||
final JsonNode propertiesNode = jsonSchemaNode.at("/" + PROPERTIES_KEY); | ||
assertThat(propertiesNode, instanceOf(ObjectNode.class)); | ||
assertThat(propertiesNode.has("string_value_with_two_examples"), is(true)); | ||
final JsonNode propertyNode = propertiesNode.at("/string_value_with_two_examples"); | ||
assertThat(propertyNode.has("type"), equalTo(true)); | ||
assertThat(propertyNode.get("type").getNodeType(), equalTo(JsonNodeType.STRING)); | ||
|
||
assertThat(propertyNode.has("examples"), equalTo(true)); | ||
assertThat(propertyNode.get("examples").getNodeType(), equalTo(JsonNodeType.ARRAY)); | ||
assertThat(propertyNode.get("examples").size(), equalTo(2)); | ||
assertThat(propertyNode.get("examples").get(0), notNullValue()); | ||
assertThat(propertyNode.get("examples").get(0).getNodeType(), equalTo(JsonNodeType.OBJECT)); | ||
assertThat(propertyNode.get("examples").get(0).has("example"), equalTo(true)); | ||
assertThat(propertyNode.get("examples").get(0).get("example").getNodeType(), equalTo(JsonNodeType.STRING)); | ||
assertThat(propertyNode.get("examples").get(0).get("example").textValue(), equalTo("some example value")); | ||
assertThat(propertyNode.get("examples").get(0).has("description"), equalTo(false)); | ||
|
||
assertThat(propertyNode.get("examples").get(1), notNullValue()); | ||
assertThat(propertyNode.get("examples").get(1).getNodeType(), equalTo(JsonNodeType.OBJECT)); | ||
assertThat(propertyNode.get("examples").get(1).has("example"), equalTo(true)); | ||
assertThat(propertyNode.get("examples").get(1).get("example").getNodeType(), equalTo(JsonNodeType.STRING)); | ||
assertThat(propertyNode.get("examples").get(1).get("example").textValue(), equalTo("second example value")); | ||
assertThat(propertyNode.get("examples").get(1).has("description"), equalTo(true)); | ||
assertThat(propertyNode.get("examples").get(1).get("description").getNodeType(), equalTo(JsonNodeType.STRING)); | ||
assertThat(propertyNode.get("examples").get(1).get("description").textValue(), equalTo("This is the second value.")); | ||
} | ||
|
||
@JsonClassDescription("test config") | ||
static class TestConfig { | ||
@JsonPropertyDescription("The aggregate action description") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe make this description more generic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just for testing. |
||
|
@@ -68,5 +105,16 @@ static class TestConfig { | |
public PluginModel getAction() { | ||
return action; | ||
} | ||
|
||
@JsonProperty("string_value_with_two_examples") | ||
@ExampleValues({ | ||
@ExampleValues.Example("some example value"), | ||
@ExampleValues.Example(value = "second example value", description = "This is the second value.") | ||
}) | ||
private String stringValueWithTwoExamples; | ||
|
||
public String getStringValueWithTwoExamples() { | ||
return stringValueWithTwoExamples; | ||
} | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it have to be an inner interface? As it is inner, at the time of specifying these example values it looks overly verbose
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually quite common for annotations that are coupled with their parent. You can add
ExampleValues
as a static import.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pushed a revision that imports
ExampleValues.Example
.