diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index d3b8628445721..452512d6e5e5e 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -175,6 +175,8 @@ static TransportVersion def(int id) { public static final TransportVersion COHERE_BIT_EMBEDDING_TYPE_SUPPORT_ADDED_BACKPORT_8_X = def(8_840_0_01); public static final TransportVersion ELASTICSEARCH_9_0 = def(9_000_0_00); public static final TransportVersion COHERE_BIT_EMBEDDING_TYPE_SUPPORT_ADDED = def(9_001_0_00); + public static final TransportVersion COHERE_EMBEDDING_TYPES_SUPPORT_ADDED = def(9_003_0_00); + /* * STOP! READ THIS FIRST! No, really, * ____ _____ ___ ____ _ ____ _____ _ ____ _____ _ _ ___ ____ _____ ___ ____ ____ _____ _ diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequest.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequest.java index bd59cdbded9fa..b2d171a4f44a4 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequest.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequest.java @@ -21,6 +21,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.util.EnumSet; import java.util.List; import java.util.Objects; @@ -30,7 +31,7 @@ public class CohereEmbeddingsRequest extends CohereRequest { private final List input; private final CohereEmbeddingsTaskSettings taskSettings; private final String model; - private final CohereEmbeddingType embeddingType; + private final EnumSet embeddingTypes; private final String inferenceEntityId; public CohereEmbeddingsRequest(List input, CohereEmbeddingsModel embeddingsModel) { @@ -40,7 +41,7 @@ public CohereEmbeddingsRequest(List input, CohereEmbeddingsModel embeddi this.input = Objects.requireNonNull(input); taskSettings = embeddingsModel.getTaskSettings(); model = embeddingsModel.getServiceSettings().getCommonSettings().modelId(); - embeddingType = embeddingsModel.getServiceSettings().getEmbeddingType(); + embeddingTypes = embeddingsModel.getServiceSettings().getEmbeddingTypes(); inferenceEntityId = embeddingsModel.getInferenceEntityId(); } @@ -49,7 +50,7 @@ public HttpRequest createHttpRequest() { HttpPost httpPost = new HttpPost(account.uri()); ByteArrayEntity byteEntity = new ByteArrayEntity( - Strings.toString(new CohereEmbeddingsRequestEntity(input, taskSettings, model, embeddingType)).getBytes(StandardCharsets.UTF_8) + Strings.toString(new CohereEmbeddingsRequestEntity(input, taskSettings, model, embeddingTypes)).getBytes(StandardCharsets.UTF_8) ); httpPost.setEntity(byteEntity); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequestEntity.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequestEntity.java index a972cbbac959d..b0149f5d4be3a 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequestEntity.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequestEntity.java @@ -17,6 +17,7 @@ import org.elasticsearch.xpack.inference.services.cohere.embeddings.CohereEmbeddingsTaskSettings; import java.io.IOException; +import java.util.EnumSet; import java.util.List; import java.util.Objects; @@ -26,7 +27,7 @@ public record CohereEmbeddingsRequestEntity( List input, CohereEmbeddingsTaskSettings taskSettings, @Nullable String model, - @Nullable CohereEmbeddingType embeddingType + @Nullable EnumSet embeddingTypes ) implements ToXContentObject { private static final String SEARCH_DOCUMENT = "search_document"; @@ -54,8 +55,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(INPUT_TYPE_FIELD, convertToString(taskSettings.getInputType())); } - if (embeddingType != null) { - builder.field(EMBEDDING_TYPES_FIELD, List.of(embeddingType.toRequestString())); + if (embeddingTypes != null) { + builder.field(EMBEDDING_TYPES_FIELD, embeddingTypes.stream().map(CohereEmbeddingType::toRequestString).toList()); } if (taskSettings.getTruncation() != null) { diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/ServiceUtils.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/ServiceUtils.java index 1ddae3cc8df95..78ddab9e7474d 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/ServiceUtils.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/ServiceUtils.java @@ -29,6 +29,7 @@ import java.net.URI; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; @@ -73,7 +74,7 @@ public static T removeAsType(Map sourceMap, String key, Clas /** * Remove the object from the map and cast to the expected type. - * If the object cannot be cast to type and error is added to the + * If the object cannot be cast to type an error is added to the * {@code validationException} parameter * * @param sourceMap Map containing fields @@ -98,6 +99,45 @@ public static T removeAsType(Map sourceMap, String key, Clas } } + /** + * Remove a list of objects from the map and cast each entry to the expected type. + * If the object cannot be cast to a List or any of the entries cannot be cast + * to the type an error is added to the {@code validationException} parameter + * + * @param sourceMap Map containing fields + * @param key The key of the object to remove + * @param type The expected type of each list item in the removed object + * @param validationException If the value is not of type {@code type} + * @return {@code null} if not present else the object cast to type List of type T + * @param The expected type + */ + @SuppressWarnings("unchecked") + public static List removeAsListOfType( + Map sourceMap, + String key, + Class type, + ValidationException validationException + ) { + Object o = sourceMap.remove(key); + if (o == null) { + return null; + } + + if (List.class.isAssignableFrom(o.getClass())) { + // check each list entry + for (Object listItem : (List) o) { + if (type.isAssignableFrom(listItem.getClass()) == false) { + validationException.addValidationError(invalidTypeErrorMsg(key, listItem, type.getSimpleName())); + } + } + + return (List) o; + } else { + validationException.addValidationError(invalidTypeErrorMsg(key, o, List.class.getSimpleName())); + return null; + } + } + /** * Remove the object from the map and cast to first assignable type in the expected types list. * If the object cannot be cast to one of the types an error is added to the @@ -254,6 +294,10 @@ public static String mustBeNonEmptyString(String settingName, String scope) { return Strings.format("[%s] Invalid value empty string. [%s] must be a non-empty string", scope, settingName); } + public static String mustBeNonEmptyList(String settingName, String scope) { + return Strings.format("[%s] Invalid value empty list. [%s] must be a non-empty list", scope, settingName); + } + public static String invalidTimeValueMsg(String timeValueStr, String settingName, String scope, String exceptionMsg) { return Strings.format( "[%s] Invalid time value [%s]. [%s] must be a valid time value string: %s", @@ -401,6 +445,31 @@ public static String extractOptionalString( return optionalField; } + public static List extractOptionalStringArray( + Map map, + String settingName, + String scope, + ValidationException validationException + ) { + int initialValidationErrorCount = validationException.validationErrors().size(); + List optionalField = ServiceUtils.removeAsListOfType(map, settingName, String.class, validationException); + + if (validationException.validationErrors().size() > initialValidationErrorCount) { + // new validation error occurred + return null; + } + + if (optionalField != null && optionalField.isEmpty()) { + validationException.addValidationError(ServiceUtils.mustBeNonEmptyList(settingName, scope)); + } + + if (validationException.validationErrors().size() > initialValidationErrorCount) { + return null; + } + + return optionalField; + } + public static Integer extractRequiredPositiveInteger( Map map, String settingName, @@ -611,6 +680,37 @@ public static > E extractOptionalEnum( return null; } + public static > EnumSet extractOptionalEnumSet( + Map map, + String settingName, + String scope, + EnumConstructor constructor, + EnumSet validValues, + ValidationException validationException + ) { + var enumStringArray = extractOptionalStringArray(map, settingName, scope, validationException); + if (enumStringArray == null) { + return null; + } + + var createdEnums = new ArrayList(); + for (String enumString : enumStringArray) { + try { + var createdEnum = constructor.apply(enumString); + validateEnumValue(createdEnum, validValues); + createdEnums.add(createdEnum); + } catch (IllegalArgumentException e) { + var validValuesAsStrings = validValues.stream() + .map(value -> value.toString().toLowerCase(Locale.ROOT)) + .toArray(String[]::new); + validationException.addValidationError(invalidValue(settingName, scope, enumString, validValuesAsStrings)); + return null; + } + } + + return EnumSet.copyOf(createdEnums); + } + public static Boolean extractOptionalBoolean(Map map, String settingName, ValidationException validationException) { return ServiceUtils.removeAsType(map, settingName, Boolean.class, validationException); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereService.java index 6c2d3bb96d74d..17cfc9b10b6bb 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereService.java @@ -325,7 +325,7 @@ public Model updateModelWithEmbeddingDetails(Model model, int embeddingSize) { serviceSettings.getCommonSettings().modelId(), serviceSettings.getCommonSettings().rateLimitSettings() ), - serviceSettings.getEmbeddingType() + serviceSettings.getEmbeddingTypes() ); return new CohereEmbeddingsModel(embeddingsModel, updatedServiceSettings); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsServiceSettings.java index b25b9fc8fd351..734705048b94e 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsServiceSettings.java @@ -24,22 +24,26 @@ import java.io.IOException; import java.util.EnumSet; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import static org.elasticsearch.xpack.inference.services.ServiceUtils.extractOptionalEnum; +import static org.elasticsearch.xpack.inference.services.ServiceUtils.extractOptionalEnumSet; public class CohereEmbeddingsServiceSettings extends FilteredXContentObject implements ServiceSettings { public static final String NAME = "cohere_embeddings_service_settings"; static final String EMBEDDING_TYPE = "embedding_type"; + static final String EMBEDDING_TYPES = "embedding_types"; + public static CohereEmbeddingsServiceSettings fromMap(Map map, ConfigurationParseContext context) { ValidationException validationException = new ValidationException(); var commonServiceSettings = CohereServiceSettings.fromMap(map, context); - CohereEmbeddingType embeddingTypes = parseEmbeddingType(map, context, validationException); + EnumSet embeddingTypes = parseEmbeddingTypes(map, context, validationException); if (validationException.validationErrors().isEmpty() == false) { throw validationException; @@ -48,33 +52,52 @@ public static CohereEmbeddingsServiceSettings fromMap(Map map, C return new CohereEmbeddingsServiceSettings(commonServiceSettings, embeddingTypes); } - static CohereEmbeddingType parseEmbeddingType( + static EnumSet parseEmbeddingTypes( Map map, ConfigurationParseContext context, ValidationException validationException ) { return switch (context) { - case REQUEST -> Objects.requireNonNullElse( - extractOptionalEnum( + case REQUEST -> { + var embeddingType = extractOptionalEnum( map, EMBEDDING_TYPE, ModelConfigurations.SERVICE_SETTINGS, CohereEmbeddingType::fromString, EnumSet.allOf(CohereEmbeddingType.class), validationException - ), - CohereEmbeddingType.FLOAT - ); + ); + + if (embeddingType == null) { + yield Objects.requireNonNullElse( + extractOptionalEnumSet( + map, + EMBEDDING_TYPES, + ModelConfigurations.SERVICE_SETTINGS, + CohereEmbeddingType::fromString, + EnumSet.allOf(CohereEmbeddingType.class), + validationException + ), + EnumSet.of(CohereEmbeddingType.FLOAT) + ); + } else { + yield EnumSet.of(embeddingType); + } + } case PERSISTENT -> { - var embeddingType = ServiceUtils.extractOptionalString( + var persistedEmbeddingType = ServiceUtils.extractOptionalString( map, EMBEDDING_TYPE, ModelConfigurations.SERVICE_SETTINGS, validationException ); - yield fromCohereOrDenseVectorEnumValues(embeddingType, validationException); + var embeddingType = fromCohereOrDenseVectorEnumValues(persistedEmbeddingType, validationException); + if (embeddingType == null) { + yield EnumSet.of(CohereEmbeddingType.FLOAT); + } else { + yield EnumSet.of(embeddingType); + } } - }; } @@ -108,16 +131,22 @@ static CohereEmbeddingType fromCohereOrDenseVectorEnumValues(String enumString, } private final CohereServiceSettings commonSettings; - private final CohereEmbeddingType embeddingType; + private final EnumSet embeddingTypes; - public CohereEmbeddingsServiceSettings(CohereServiceSettings commonSettings, CohereEmbeddingType embeddingType) { + public CohereEmbeddingsServiceSettings(CohereServiceSettings commonSettings, EnumSet embeddingTypes) { this.commonSettings = commonSettings; - this.embeddingType = Objects.requireNonNull(embeddingType); + this.embeddingTypes = Objects.requireNonNull(embeddingTypes); } public CohereEmbeddingsServiceSettings(StreamInput in) throws IOException { - commonSettings = new CohereServiceSettings(in); - embeddingType = Objects.requireNonNullElse(in.readOptionalEnum(CohereEmbeddingType.class), CohereEmbeddingType.FLOAT); + this.commonSettings = new CohereServiceSettings(in); + + if (in.getTransportVersion().onOrAfter(TransportVersions.COHERE_EMBEDDING_TYPES_SUPPORT_ADDED)) { + this.embeddingTypes = in.readEnumSet(CohereEmbeddingType.class); + } else { + var embeddingType = Objects.requireNonNullElse(in.readOptionalEnum(CohereEmbeddingType.class), CohereEmbeddingType.FLOAT); + this.embeddingTypes = EnumSet.of(embeddingType); + } } public CohereServiceSettings getCommonSettings() { @@ -140,12 +169,23 @@ public String modelId() { } public CohereEmbeddingType getEmbeddingType() { - return embeddingType; + return getFirstEmbeddingType(); } + public EnumSet getEmbeddingTypes() { + return embeddingTypes; + } + + // What to return when we have multiple embedding types? + // For now, default to float @Override public DenseVectorFieldMapper.ElementType elementType() { - return embeddingType == null ? DenseVectorFieldMapper.ElementType.FLOAT : embeddingType.toElementType(); + // ugly + return embeddingTypes.size() > 1 ? DenseVectorFieldMapper.ElementType.FLOAT : getFirstEmbeddingType().toElementType(); + } + + public List elementTypes() { + return embeddingTypes.stream().map(CohereEmbeddingType::toElementType).toList(); } @Override @@ -158,7 +198,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.startObject(); commonSettings.toXContentFragment(builder, params); - builder.field(EMBEDDING_TYPE, elementType()); + builder.field(EMBEDDING_TYPES, elementTypes()); builder.endObject(); return builder; @@ -167,7 +207,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws @Override protected XContentBuilder toXContentFragmentOfExposedFields(XContentBuilder builder, Params params) throws IOException { commonSettings.toXContentFragmentOfExposedFields(builder, params); - builder.field(EMBEDDING_TYPE, elementType()); + builder.field(EMBEDDING_TYPES, elementTypes()); return builder; } @@ -180,7 +220,16 @@ public TransportVersion getMinimalSupportedVersion() { @Override public void writeTo(StreamOutput out) throws IOException { commonSettings.writeTo(out); - out.writeOptionalEnum(CohereEmbeddingType.translateToVersion(embeddingType, out.getTransportVersion())); + + if (out.getTransportVersion().onOrAfter(TransportVersions.COHERE_EMBEDDING_TYPES_SUPPORT_ADDED)) { + out.writeEnumSet( + EnumSet.copyOf( + embeddingTypes.stream().map(t -> CohereEmbeddingType.translateToVersion(t, out.getTransportVersion())).toList() + ) + ); + } else { + out.writeEnumSet(EnumSet.of(CohereEmbeddingType.translateToVersion(getFirstEmbeddingType(), out.getTransportVersion()))); + } } @Override @@ -188,11 +237,15 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CohereEmbeddingsServiceSettings that = (CohereEmbeddingsServiceSettings) o; - return Objects.equals(commonSettings, that.commonSettings) && Objects.equals(embeddingType, that.embeddingType); + return Objects.equals(commonSettings, that.commonSettings) && Objects.equals(embeddingTypes, that.embeddingTypes); } @Override public int hashCode() { - return Objects.hash(commonSettings, embeddingType); + return Objects.hash(commonSettings, embeddingTypes); + } + + private CohereEmbeddingType getFirstEmbeddingType() { + return embeddingTypes.toArray(CohereEmbeddingType[]::new)[0]; } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/ServiceUtilsTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/ServiceUtilsTests.java index e3df0f0b5a2e1..a263caa93fa20 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/ServiceUtilsTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/ServiceUtilsTests.java @@ -25,6 +25,7 @@ import org.elasticsearch.xpack.inference.results.InferenceTextEmbeddingByteResultsTests; import org.elasticsearch.xpack.inference.results.TextEmbeddingResultsTests; +import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; import java.util.List; @@ -33,6 +34,7 @@ import static org.elasticsearch.xpack.inference.services.ServiceUtils.convertToUri; import static org.elasticsearch.xpack.inference.services.ServiceUtils.createUri; import static org.elasticsearch.xpack.inference.services.ServiceUtils.extractOptionalEnum; +import static org.elasticsearch.xpack.inference.services.ServiceUtils.extractOptionalEnumSet; import static org.elasticsearch.xpack.inference.services.ServiceUtils.extractOptionalPositiveInteger; import static org.elasticsearch.xpack.inference.services.ServiceUtils.extractOptionalPositiveLong; import static org.elasticsearch.xpack.inference.services.ServiceUtils.extractOptionalString; @@ -728,6 +730,118 @@ public void testExtractOptionalEnum_ReturnsClassification_WhenValueIsAcceptable( assertTrue(map.isEmpty()); } + public void testExtractOptionalEnumSet_ReturnsNull_WhenFieldDoesNotExist() { + var validation = new ValidationException(); + Map map = modifiableMap(Map.of("key", "value")); + var createdEnum = extractOptionalEnumSet(map, "abc", "scope", InputType::fromString, EnumSet.allOf(InputType.class), validation); + + assertNull(createdEnum); + assertTrue(validation.validationErrors().isEmpty()); + assertThat(map.size(), is(1)); + } + + public void testExtractOptionalEnumSet_ReturnsNullAndAddsException_WhenAnInvalidValueExists() { + var validation = new ValidationException(); + Map map = modifiableMap(Map.of("key", List.of(InputType.INGEST.toString(), "invalid_value"))); + var createdEnum = extractOptionalEnumSet( + map, + "key", + "scope", + InputType::fromString, + EnumSet.of(InputType.INGEST, InputType.SEARCH), + validation + ); + + assertNull(createdEnum); + assertFalse(validation.validationErrors().isEmpty()); + assertTrue(map.isEmpty()); + assertThat( + validation.validationErrors().get(0), + is("[scope] Invalid value [invalid_value] received. [key] must be one of [ingest, search]") + ); + } + + public void testExtractOptionalEnumSet_ReturnsNullAndAddsException_WhenValueIsNotList() { + var validation = new ValidationException(); + Map map = modifiableMap(Map.of("key", InputType.INGEST.toString())); + var createdEnum = extractOptionalEnumSet( + map, + "key", + "scope", + InputType::fromString, + EnumSet.of(InputType.INGEST, InputType.SEARCH), + validation + ); + + assertNull(createdEnum); + assertFalse(validation.validationErrors().isEmpty()); + assertTrue(map.isEmpty()); + assertThat( + validation.validationErrors().get(0), + is("field [key] is not of the expected type. The value [ingest] cannot be converted to a [List]") + ); + } + + public void testExtractOptionalEnumSet_ReturnsNullAndAddsException_WhenOneListValueIsNotString() { + var validation = new ValidationException(); + Map map = modifiableMap(Map.of("key", List.of(3, InputType.INGEST.toString()))); + var createdEnum = extractOptionalEnumSet( + map, + "key", + "scope", + InputType::fromString, + EnumSet.of(InputType.INGEST, InputType.SEARCH), + validation + ); + + assertNull(createdEnum); + assertFalse(validation.validationErrors().isEmpty()); + assertTrue(map.isEmpty()); + assertThat( + validation.validationErrors().get(0), + is("field [key] is not of the expected type. The value [3] cannot be converted to a [String]") + ); + } + + public void testExtractOptionalEnumSet_ReturnsNullAndAddsException_WhenListIsEmpty() { + var validation = new ValidationException(); + Map map = modifiableMap(Map.of("key", new ArrayList<>())); + var createdEnum = extractOptionalEnumSet( + map, + "key", + "scope", + InputType::fromString, + EnumSet.of(InputType.INGEST, InputType.SEARCH), + validation + ); + + assertNull(createdEnum); + assertFalse(validation.validationErrors().isEmpty()); + assertTrue(map.isEmpty()); + assertThat(validation.validationErrors().get(0), is("[scope] Invalid value empty list. [key] must be a non-empty list")); + } + + public void testExtractOptionalEnumSet_ReturnsNullAndAddsException_WhenValueIsNotPartOfTheAcceptableValues() { + var validation = new ValidationException(); + Map map = modifiableMap(Map.of("key", List.of(InputType.INGEST.toString(), InputType.UNSPECIFIED.toString()))); + var createdEnum = extractOptionalEnumSet(map, "key", "scope", InputType::fromString, EnumSet.of(InputType.INGEST), validation); + + assertNull(createdEnum); + assertFalse(validation.validationErrors().isEmpty()); + assertTrue(map.isEmpty()); + assertThat(validation.validationErrors().get(0), is("[scope] Invalid value [unspecified] received. [key] must be one of [ingest]")); + } + + public void testExtractOptionalEnumSet_ReturnsEnumSet_WhenValuesAreAcceptable() { + var validation = new ValidationException(); + Map map = modifiableMap(Map.of("key", List.of(InputType.INGEST.toString(), InputType.SEARCH.toString()))); + var createdEnumSet = extractOptionalEnumSet(map, "key", "scope", InputType::fromString, EnumSet.allOf(InputType.class), validation); + + assertThat(createdEnumSet, is(EnumSet.of(InputType.INGEST, InputType.SEARCH))); + assertTrue(validation.validationErrors().isEmpty()); + assertTrue(map.isEmpty()); + } + public void testExtractOptionalTimeValue_ReturnsNull_WhenKeyDoesNotExist() { var validation = new ValidationException(); Map map = modifiableMap(Map.of("key", 1));