Skip to content

Commit

Permalink
[#1228] review: fixed javadoc of EnumValueValidator + header definiti…
Browse files Browse the repository at this point in the history
…on; added unit test for EnumValueValidator; adjusted message and description of thrown exception if enum value is not known;

Signed-off-by: Thomas Jaeckle <thomas.jaeckle@bosch.io>
  • Loading branch information
thjaeckle committed Nov 30, 2021
1 parent 0c49924 commit 99b51c1
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ public enum DittoHeaderDefinition implements HeaderDefinition {
CHANNEL("channel", String.class, true, true,
HeaderValueValidators.getDittoChannelValidator()),

/**
* Header definition for "live" {@link #CHANNEL} commands defining the {@link LiveChannelTimeoutStrategy} to apply
* when a live command timed out.
* <p>
* Key: {@code "on-live-channel-timeout"}, Java type: {@code String}.
* </p>
*
* @since 2.3.0
*/
ON_LIVE_CHANNEL_TIMEOUT("on-live-channel-timeout", LiveChannelTimeoutStrategy.class, String.class, true, false,
HeaderValueValidators.getEnumValidator(LiveChannelTimeoutStrategy.values())),

/**
* Header definition for origin value that is set to the id of the originating session.
* <p>
Expand Down Expand Up @@ -239,18 +251,6 @@ public enum DittoHeaderDefinition implements HeaderDefinition {
TIMEOUT("timeout", DittoDuration.class, String.class, true, true,
HeaderValueValidators.getTimeoutValueValidator()),

/**
* Header definition for when a thing query command with smart channel selection should wait for a live response
* before falling back to the twin response.
* <p>
* Key: {@code "twin-fallback-after"}, Java type: {@code String}.
* </p>
*
* @since 2.3.0
*/
ON_LIVE_CHANNEL_TIMEOUT("on-live-channel-timeout", LiveChannelTimeoutStrategy.class, String.class, true, false,
HeaderValueValidators.getEnumValidator(LiveChannelTimeoutStrategy.values())),

/**
* Header definition for the entity ID related to the command/event/response/error.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@
*/
package org.eclipse.ditto.base.model.headers;

import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotEmpty;
import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull;

import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
Expand All @@ -25,7 +30,7 @@
import org.eclipse.ditto.base.model.exceptions.DittoHeaderInvalidException;

/**
* This validator checks if a normalized CharSequence denote an enum value.
* This validator checks if a normalized CharSequence denotes a known enum value.
*
* @since 2.3.0
*/
Expand All @@ -34,28 +39,36 @@ final class EnumValueValidator extends AbstractHeaderValueValidator {

private final Set<String> enumValueSet;
private final String errorDescription;
private final Class<?> enumDeclaringType;

private EnumValueValidator(final Enum<?>[] enumValues) {
private EnumValueValidator(final List<Enum<?>> enumValues) {
super(String.class::equals);
enumValueSet = groupByNormalizedName(enumValues);
enumDeclaringType = enumValues.get(0).getDeclaringClass();
enumValueSet = Collections.unmodifiableSet(new LinkedHashSet<>(groupByNormalizedName(enumValues)));
errorDescription = formatErrorDescription(enumValueSet);
}

/**
* Returns an instance of {@code DittoChannelValueValidator}.
* Returns an instance of {@code EnumValueValidator}.
*
* @return the instance.
* @param enumValues the known and allowed enum values this EnumValueValidator validates for.
* @return the enum validator instance.
* @throws IllegalArgumentException if {@code enumValues} is empty.
* @throws NullPointerException if {@code enumValues} is {@code null}.
*/
static EnumValueValidator getInstance(final Enum<?>[] enumValues) {
return new EnumValueValidator(enumValues);
checkNotNull(enumValues, "enumValues");
final List<Enum<?>> enums = Arrays.asList(enumValues);
checkNotEmpty(enums, "enumValues");
return new EnumValueValidator(enums);
}

@Override
protected void validateValue(final HeaderDefinition definition, final CharSequence value) {
final String normalizedValue = normalize(value);
if (!enumValueSet.contains(normalizedValue)) {
throw DittoHeaderInvalidException.newInvalidTypeBuilder(definition, value,
DittoHeaderDefinition.ON_LIVE_CHANNEL_TIMEOUT.getKey())
"enum value of type '" + enumDeclaringType.getSimpleName() + "'")
.description(errorDescription)
.build();
}
Expand All @@ -65,17 +78,15 @@ private static String normalize(final CharSequence charSequence) {
return charSequence.toString().trim().toLowerCase(Locale.ENGLISH);
}

private static Set<String> groupByNormalizedName(final Enum<?>[] enumValues) {
final Set<String> set = Arrays.stream(enumValues)
private static List<String> groupByNormalizedName(final Collection<Enum<?>> enumValues) {
return enumValues.stream()
.map(Enum::toString)
.map(EnumValueValidator::normalize)
.collect(Collectors.toSet());
return Collections.unmodifiableSet(set);
.collect(Collectors.toList());
}

private static String formatErrorDescription(final Collection<String> normalizedNames) {
final String valuesString = normalizedNames.stream()
.collect(Collectors.joining(">, <", "<", ">"));
return MessageFormat.format("The value must either be one of: {0}.", valuesString);
final String valuesString = String.join("|", normalizedNames);
return MessageFormat.format("The value must be one of: <{0}>.", valuesString);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,15 @@
*/
public enum LiveChannelTimeoutStrategy {

/**
* Strategy which lets the request fail with the timeout error.
*/
FAIL("fail"),

/**
* Strategy which - instead of letting the timed out live request fail - will fall back to the value delivered by
* the twin instead.
*/
USE_TWIN("use-twin");

private final String headerValue;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2021 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.ditto.base.model.headers;

import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf;
import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable;

import org.eclipse.ditto.base.model.exceptions.DittoHeaderInvalidException;
import org.junit.Test;

/**
* Unit tests for {@link EnumValueValidator}.
*/
public final class EnumValueValidatorTest {

private static final EnumValueValidator underTest = EnumValueValidator.getInstance(FancyTestEnum.values());

private static final DittoHeaderDefinition KNOWN_HEADER_DEFINITION =
DittoHeaderDefinition.ON_LIVE_CHANNEL_TIMEOUT;

@Test
public void assertImmutability() {
assertInstancesOf(EnumValueValidator.class, areImmutable());
}

@Test
public void tryToAcceptNullDefinition() {
assertThatNullPointerException()
.isThrownBy(() -> underTest.accept(null, FancyTestEnum.BAZ_BAR.s))
.withMessage("The definition must not be null!")
.withNoCause();
}

@Test
public void tryToInitializeWithNullEnumValues() {
assertThatNullPointerException()
.isThrownBy(() -> EnumValueValidator.getInstance(null))
.withMessage("The enumValues must not be null!")
.withNoCause();
}

@Test
public void tryToInitializeWithEmptyEnumValues() {
assertThatIllegalArgumentException()
.isThrownBy(() -> EnumValueValidator.getInstance(new Enum<?>[] {}))
.withMessage("The enumValues must not be empty!")
.withNoCause();
}

@Test
public void ensureValidEnumValuesDoNotThrowException() {
assertThatNoException()
.isThrownBy(() ->
underTest.validateValue(KNOWN_HEADER_DEFINITION, "foo")
);
assertThatNoException()
.isThrownBy(() ->
underTest.validateValue(KNOWN_HEADER_DEFINITION, FancyTestEnum.BAZ_BAR.s)
);
}

@Test
public void invalidEnumValuesThrowException() {
assertThatThrownBy(() ->
underTest.validateValue(KNOWN_HEADER_DEFINITION, "what")
)
.isInstanceOf(DittoHeaderInvalidException.class)
.hasMessage("The value 'what' of the header 'on-live-channel-timeout' is not a valid enum value of " +
"type 'FancyTestEnum'.")
.matches(ex -> ((DittoHeaderInvalidException) ex).getDescription()
.filter(desc -> desc.equals("The value must be one of: <foo|bar|baz-bar>."))
.isPresent(), "Contains the expected description");
}


enum FancyTestEnum {
FOO("foo"),
BAR("bar"),
BAZ_BAR("baz-bar");

private final String s;

FancyTestEnum(final String s) {
this.s = s;
}

@Override
public String toString() {
return s;
}
}
}

0 comments on commit 99b51c1

Please sign in to comment.