diff --git a/key/src/main/java/net/kyori/adventure/key/Key.java b/key/src/main/java/net/kyori/adventure/key/Key.java index 107b1023b..9c242e6c8 100644 --- a/key/src/main/java/net/kyori/adventure/key/Key.java +++ b/key/src/main/java/net/kyori/adventure/key/Key.java @@ -29,6 +29,7 @@ import net.kyori.examination.ExaminableProperty; import org.intellij.lang.annotations.Pattern; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * An identifying object used to fetch and/or store unique objects. @@ -62,6 +63,12 @@ public interface Key extends Comparable, Examinable, Namespaced, Keyed { * @since 4.0.0 */ String MINECRAFT_NAMESPACE = "minecraft"; + /** + * The default namespace and value separator. + * + * @since 4.12.0 + */ + char DEFAULT_SEPARATOR = ':'; /** * Creates a key. @@ -78,7 +85,7 @@ public interface Key extends Comparable, Examinable, Namespaced, Keyed { * @since 4.0.0 */ static @NotNull Key key(final @NotNull @Pattern("(" + KeyImpl.NAMESPACE_PATTERN + ":)?" + KeyImpl.VALUE_PATTERN) String string) { - return key(string, ':'); + return key(string, DEFAULT_SEPARATOR); } /** @@ -142,6 +149,77 @@ public interface Key extends Comparable, Examinable, Namespaced, Keyed { return KeyImpl.COMPARATOR; } + /** + * Checks if {@code string} can be parsed into a {@link Key}. + * + * @param string the input string + * @return {@code true} if {@code string} can be parsed into a {@link Key}, {@code false} otherwise + * @since 4.12.0 + */ + static boolean parseable(final @Nullable String string) { + if (string == null) { + return false; + } + final int index = string.indexOf(DEFAULT_SEPARATOR); + final String namespace = index >= 1 ? string.substring(0, index) : MINECRAFT_NAMESPACE; + final String value = index >= 0 ? string.substring(index + 1) : string; + return parseableNamespace(namespace) && parseableValue(value); + } + + /** + * Checks if {@code value} is a valid namespace. + * + * @param namespace the string to check + * @return {@code true} if {@code value} is a valid namespace, {@code false} otherwise + * @since 4.12.0 + */ + static boolean parseableNamespace(final @NotNull String namespace) { + for (int i = 0, length = namespace.length(); i < length; i++) { + if (!allowedInNamespace(namespace.charAt(i))) { + return false; + } + } + return true; + } + + /** + * Checks if {@code value} is a valid value. + * + * @param value the string to check + * @return {@code true} if {@code value} is a valid value, {@code false} otherwise + * @since 4.12.0 + */ + static boolean parseableValue(final @NotNull String value) { + for (int i = 0, length = value.length(); i < length; i++) { + if (!allowedInValue(value.charAt(i))) { + return false; + } + } + return true; + } + + /** + * Checks if {@code value} is a valid character in a namespace. + * + * @param character the character to check + * @return {@code true} if {@code value} is a valid character in a namespace, {@code false} otherwise + * @since 4.12.0 + */ + static boolean allowedInNamespace(final char character) { + return KeyImpl.allowedInNamespace(character); + } + + /** + * Checks if {@code value} is a valid character in a value. + * + * @param character the character to check + * @return {@code true} if {@code value} is a valid character in a value, {@code false} otherwise + * @since 4.12.0 + */ + static boolean allowedInValue(final char character) { + return KeyImpl.allowedInValue(character); + } + /** * Gets the namespace. * diff --git a/key/src/main/java/net/kyori/adventure/key/KeyImpl.java b/key/src/main/java/net/kyori/adventure/key/KeyImpl.java index fc42f912b..d23ccd560 100644 --- a/key/src/main/java/net/kyori/adventure/key/KeyImpl.java +++ b/key/src/main/java/net/kyori/adventure/key/KeyImpl.java @@ -28,7 +28,6 @@ import java.util.stream.Stream; import net.kyori.examination.ExaminableProperty; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.VisibleForTesting; import static java.util.Objects.requireNonNull; @@ -42,38 +41,18 @@ final class KeyImpl implements Key { private final String value; KeyImpl(final @NotNull String namespace, final @NotNull String value) { - if (!namespaceValid(namespace)) throw new InvalidKeyException(namespace, value, String.format("Non [a-z0-9_.-] character in namespace of Key[%s]", asString(namespace, value))); - if (!valueValid(value)) throw new InvalidKeyException(namespace, value, String.format("Non [a-z0-9/._-] character in value of Key[%s]", asString(namespace, value))); + if (!Key.parseableNamespace(namespace)) throw new InvalidKeyException(namespace, value, String.format("Non [a-z0-9_.-] character in namespace of Key[%s]", asString(namespace, value))); + if (!Key.parseableValue(value)) throw new InvalidKeyException(namespace, value, String.format("Non [a-z0-9/._-] character in value of Key[%s]", asString(namespace, value))); this.namespace = requireNonNull(namespace, "namespace"); this.value = requireNonNull(value, "value"); } - @VisibleForTesting - static boolean namespaceValid(final @NotNull String namespace) { - for (int i = 0, length = namespace.length(); i < length; i++) { - if (!validNamespaceChar(namespace.charAt(i))) { - return false; - } - } - return true; + static boolean allowedInNamespace(final char character) { + return character == '_' || character == '-' || (character >= 'a' && character <= 'z') || (character >= '0' && character <= '9') || character == '.'; } - @VisibleForTesting - static boolean valueValid(final @NotNull String value) { - for (int i = 0, length = value.length(); i < length; i++) { - if (!validValueChar(value.charAt(i))) { - return false; - } - } - return true; - } - - private static boolean validNamespaceChar(final int value) { - return value == '_' || value == '-' || (value >= 'a' && value <= 'z') || (value >= '0' && value <= '9') || value == '.'; - } - - private static boolean validValueChar(final int value) { - return value == '_' || value == '-' || (value >= 'a' && value <= 'z') || (value >= '0' && value <= '9') || value == '/' || value == '.'; + static boolean allowedInValue(final char character) { + return character == '_' || character == '-' || (character >= 'a' && character <= 'z') || (character >= '0' && character <= '9') || character == '.' || character == '/'; } @Override diff --git a/key/src/test/java/net/kyori/adventure/key/KeyTest.java b/key/src/test/java/net/kyori/adventure/key/KeyTest.java index 9885d2b0d..446fd9a80 100644 --- a/key/src/test/java/net/kyori/adventure/key/KeyTest.java +++ b/key/src/test/java/net/kyori/adventure/key/KeyTest.java @@ -90,15 +90,21 @@ void testCompare() { } @Test - void testNamespaceValid() { - assertTrue(KeyImpl.namespaceValid(Key.MINECRAFT_NAMESPACE)); - assertTrue(KeyImpl.namespaceValid("realms")); - assertFalse(KeyImpl.namespaceValid("some/path")); + void testParseable() { + assertTrue(Key.parseable("minecraft:empty")); + assertFalse(Key.parseable("minecraft:Empty")); } @Test - void testValueValid() { - assertTrue(KeyImpl.valueValid("empty")); - assertTrue(KeyImpl.valueValid("some/path")); + void testParseableNamespace() { + assertTrue(Key.parseableNamespace(Key.MINECRAFT_NAMESPACE)); + assertTrue(Key.parseableNamespace("realms")); + assertFalse(Key.parseableNamespace("some/path")); + } + + @Test + void testParseableValue() { + assertTrue(Key.parseableValue("empty")); + assertTrue(Key.parseableValue("some/path")); } }