diff --git a/src/main/java/net/kyori/option/Option.java b/src/main/java/net/kyori/option/Option.java index a6d46be..6e4c52e 100644 --- a/src/main/java/net/kyori/option/Option.java +++ b/src/main/java/net/kyori/option/Option.java @@ -23,6 +23,7 @@ */ package net.kyori.option; +import net.kyori.option.value.ValueType; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -37,7 +38,6 @@ */ @ApiStatus.NonExtendable public interface Option { - /** * Create an option with a boolean value type. * @@ -87,8 +87,20 @@ static > Option enumOption(final String id, final Class * * @return the value type * @since 1.0.0 + * @deprecated for removal since 1.1.0, use {@link #valueType()} instead + */ + @Deprecated + default @NotNull Class type() { + return this.valueType().type(); + } + + /** + * Get information about the option's value type. + * + * @return the value type + * @since 1.0.0 */ - @NotNull Class type(); + @NotNull ValueType valueType(); /** * Get a default value for the option, if any is present. diff --git a/src/main/java/net/kyori/option/OptionImpl.java b/src/main/java/net/kyori/option/OptionImpl.java index d18b61f..0e61017 100644 --- a/src/main/java/net/kyori/option/OptionImpl.java +++ b/src/main/java/net/kyori/option/OptionImpl.java @@ -24,16 +24,17 @@ package net.kyori.option; import java.util.Objects; +import net.kyori.option.value.ValueType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; final class OptionImpl implements Option { private final String id; - private final Class type; + private final ValueType type; private final @Nullable V defaultValue; // excluded from equality comparisons, it does not form part of the option identity - OptionImpl(final @NotNull String id, final @NotNull Class type, final @Nullable V defaultValue) { + OptionImpl(final @NotNull String id, final @NotNull ValueType type, final @Nullable V defaultValue) { this.id = id; this.type = type; this.defaultValue = defaultValue; @@ -45,7 +46,7 @@ final class OptionImpl implements Option { } @Override - public @NotNull Class type() { + public @NotNull ValueType valueType() { return this.type; } diff --git a/src/main/java/net/kyori/option/OptionSchemaImpl.java b/src/main/java/net/kyori/option/OptionSchemaImpl.java index 2a4cf5c..9f82f8b 100644 --- a/src/main/java/net/kyori/option/OptionSchemaImpl.java +++ b/src/main/java/net/kyori/option/OptionSchemaImpl.java @@ -29,6 +29,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import net.kyori.option.value.ValueType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -83,7 +84,7 @@ static final class Instances { } final class MutableImpl implements OptionSchema.Mutable { - Option register(final String id, final Class type, final @Nullable T defaultValue) { + Option register(final String id, final ValueType type, final @Nullable T defaultValue) { final Option ret = new OptionImpl<>( requireNonNull(id, "id"), requireNonNull(type, "type"), @@ -99,27 +100,27 @@ Option register(final String id, final Class type, final @Nullable T d @Override public @NotNull Option stringOption(final @NotNull String id, final @Nullable String defaultValue) { - return this.register(id, String.class, defaultValue); + return this.register(id, ValueType.stringType(), defaultValue); } @Override public @NotNull Option booleanOption(final @NotNull String id, final boolean defaultValue) { - return this.register(id, Boolean.class, defaultValue); + return this.register(id, ValueType.booleanType(), defaultValue); } @Override public @NotNull Option intOption(final @NotNull String id, final int defaultValue) { - return this.register(id, Integer.class, defaultValue); + return this.register(id, ValueType.integerType(), defaultValue); } @Override public @NotNull Option doubleOption(final @NotNull String id, final double defaultValue) { - return this.register(id, Double.class, defaultValue); + return this.register(id, ValueType.doubleType(), defaultValue); } @Override public @NotNull > Option enumOption(final @NotNull String id, final @NotNull Class enumClazz, final @Nullable E defaultValue) { - return this.register(id, enumClazz, defaultValue); + return this.register(id, ValueType.enumType(enumClazz), defaultValue); } @Override diff --git a/src/main/java/net/kyori/option/OptionState.java b/src/main/java/net/kyori/option/OptionState.java index 2c711b6..e0d4859 100644 --- a/src/main/java/net/kyori/option/OptionState.java +++ b/src/main/java/net/kyori/option/OptionState.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.function.Consumer; +import net.kyori.option.value.ValueSource; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -152,6 +153,15 @@ interface Builder { */ @NotNull Builder values(final @NotNull OptionState existing); + /** + * Set a value for all options within the {@link #schema()} where a value is provided by the {@code source}. + * + * @param source a source to populate values + * @return this builder + * @since 1.1.0 + */ + @NotNull Builder values(final @NotNull ValueSource source); + /** * Create a completed option state. * diff --git a/src/main/java/net/kyori/option/OptionStateImpl.java b/src/main/java/net/kyori/option/OptionStateImpl.java index 25e79c5..3258a31 100644 --- a/src/main/java/net/kyori/option/OptionStateImpl.java +++ b/src/main/java/net/kyori/option/OptionStateImpl.java @@ -30,6 +30,7 @@ import java.util.SortedMap; import java.util.TreeMap; import java.util.function.Consumer; +import net.kyori.option.value.ValueSource; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -56,7 +57,7 @@ public boolean has(final @NotNull Option option) { @Override public V value(final @NotNull Option option) { - final V value = option.type().cast(this.values.get(requireNonNull(option, "flag"))); + final V value = option.valueType().type().cast(this.values.get(requireNonNull(option, "flag"))); return value == null ? option.defaultValue() : value; } @@ -209,6 +210,18 @@ private void putAll(final Map, Object> values) { } return this; } + + @Override + public @NotNull Builder values(final @NotNull ValueSource source) { + for (final Option opt : this.schema.knownOptions()) { + final Object value = source.value(opt); + if (value != null) { + this.values.put(opt, value); + } + } + + return this; + } } static final class VersionedBuilderImpl implements OptionState.VersionedBuilder { diff --git a/src/main/java/net/kyori/option/value/ValueSource.java b/src/main/java/net/kyori/option/value/ValueSource.java new file mode 100644 index 0000000..050f24c --- /dev/null +++ b/src/main/java/net/kyori/option/value/ValueSource.java @@ -0,0 +1,98 @@ +/* + * This file is part of option, licensed under the MIT License. + * + * Copyright (c) 2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.option.value; + +import net.kyori.option.Option; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A source for external option values. + * + * @since 1.1.0 + */ +@FunctionalInterface +public interface ValueSource { + /** + * A value source that will extract option values from environment variables. + * + *

Any of the characters {@code :}, {@code /}, and {@code -} will be replaced with {@code _}.

+ * + * @return an environment variable-backed value source with no prefix + * @since 1.1.0 + */ + static @NotNull ValueSource environmentVariable() { + return environmentVariable(""); + } + + /** + * A value source that will extract option values from environment variables. + * + *

Any of the characters {@code :}, {@code /}, and {@code -} will be replaced with {@code _}.

+ * + * @param prefix the prefix for environment lookup, which will be prepended to keys followed by a {@code _}, + * or the empty string for no prefix + * @return an environment variable-backed value source + * @since 1.1.0 + */ + static @NotNull ValueSource environmentVariable(final @NotNull String prefix) { + return new ValueSources.EnvironmentVariable(prefix); + } + + /** + * A value source that will extract option values from system properties. + * + *

Any of the characters {@code :} and {@code /} will be replaced with {@code .}.

+ * + * @return a system property-backed value source with no prefix + * @since 1.1.0 + */ + static @NotNull ValueSource systemProperty() { + return systemProperty(""); + } + + /** + * A value source that will extract option values from system properties. + * + *

Any of the characters {@code :} and {@code /} will be replaced with {@code .}.

+ * + * @param prefix the prefix for property lookup, which will be prepended to properties followed by a {@code .}, + * or the empty string for no prefix + * @return a system property-backed value source + * @since 1.1.0 + */ + static @NotNull ValueSource systemProperty(final @NotNull String prefix) { + return new ValueSources.SystemProperty(prefix); + } + + /** + * Provide a value for the specified option, if any is set. + * + * @param option the option + * @return a value, if set + * @param the value type + * @since 1.1.0 + */ + @Nullable T value(final @NotNull Option option); +} diff --git a/src/main/java/net/kyori/option/value/ValueSources.java b/src/main/java/net/kyori/option/value/ValueSources.java new file mode 100644 index 0000000..2643bbf --- /dev/null +++ b/src/main/java/net/kyori/option/value/ValueSources.java @@ -0,0 +1,97 @@ +/* + * This file is part of option, licensed under the MIT License. + * + * Copyright (c) 2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.option.value; + +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import net.kyori.option.Option; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class ValueSources { + static final ValueSource ENVIRONMENT = new EnvironmentVariable(""); + static final ValueSource SYSTEM_PROPERTIES = new SystemProperty(""); + + private ValueSources() { + } + + static final class EnvironmentVariable implements ValueSource { + private static final Pattern ENVIRONMENT_SUBST_PATTERN = Pattern.compile("[:\\-/]"); + private static final String ENVIRONMENT_VAR_SEPARATOR = "_"; + + private final String prefix; + + EnvironmentVariable(final String prefix) { + this.prefix = prefix.isEmpty() ? "" : prefix.toUpperCase(Locale.ROOT) + ENVIRONMENT_VAR_SEPARATOR; + } + + @Override + public @Nullable T value(final @NotNull Option option) { + final StringBuffer buf = new StringBuffer(option.id().length() + this.prefix.length()); + buf.append(this.prefix); + final Matcher match = ENVIRONMENT_SUBST_PATTERN.matcher(option.id()); + while (match.find()) { + match.appendReplacement(buf, ENVIRONMENT_VAR_SEPARATOR); + } + match.appendTail(buf); + + final String value = System.getenv(buf.toString().toUpperCase(Locale.ROOT)); + if (value == null) { + return null; + } + + return option.valueType().parse(value); + } + } + + static final class SystemProperty implements ValueSource { + private static final Pattern SYSTEM_PROP_SUBST_PATTERN = Pattern.compile("[:/]"); + private static final String SYSTEM_PROPERTY_SEPARATOR = "."; + + private final String prefix; + + SystemProperty(final String prefix) { + this.prefix = prefix.isEmpty() ? "" : prefix + SYSTEM_PROPERTY_SEPARATOR; + } + + @Override + public @Nullable T value(final @NotNull Option option) { + final StringBuffer buf = new StringBuffer(option.id().length() + this.prefix.length()); + buf.append(this.prefix); + final Matcher match = SYSTEM_PROP_SUBST_PATTERN.matcher(option.id()); + while (match.find()) { + match.appendReplacement(buf, SYSTEM_PROPERTY_SEPARATOR); + } + match.appendTail(buf); + + final String value = System.getProperty(buf.toString()); + if (value == null) { + return null; + } + + return option.valueType().parse(value); + } + } +} diff --git a/src/main/java/net/kyori/option/value/ValueType.java b/src/main/java/net/kyori/option/value/ValueType.java new file mode 100644 index 0000000..ad1cf68 --- /dev/null +++ b/src/main/java/net/kyori/option/value/ValueType.java @@ -0,0 +1,106 @@ +/* + * This file is part of option, licensed under the MIT License. + * + * Copyright (c) 2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.option.value; + +import org.jetbrains.annotations.NotNull; + +import static java.util.Objects.requireNonNull; + +/** + * A possible type for an option value. + * + * @param the actual type + * @since 1.1.0 + */ +public interface ValueType { + /** + * Provide a value type for string values. + * + * @return a value type for string values + * @since 1.1.0 + */ + static @NotNull ValueType stringType() { + return ValueTypeImpl.Types.STRING; + } + + /** + * Provide a value type for boolean values. + * + * @return a value type for boolean values + * @since 1.1.0 + */ + static @NotNull ValueType booleanType() { + return ValueTypeImpl.Types.BOOLEAN; + } + + /** + * Provide a value type for integer values. + * + * @return a value type for integer values + * @since 1.1.0 + */ + static @NotNull ValueType integerType() { + return ValueTypeImpl.Types.INT; + } + + /** + * Provide a value type for double values. + * + * @return a value type for double values + * @since 1.1.0 + */ + static @NotNull ValueType doubleType() { + return ValueTypeImpl.Types.DOUBLE; + } + + /** + * Provide a value type for enum values. + * + * @param enumClazz the enum class + * @param the enum type + * @return a value type for enum values + * @since 1.1.0 + */ + static > @NotNull ValueType enumType(final @NotNull Class enumClazz) { + return new ValueTypeImpl.EnumType<>(requireNonNull(enumClazz, "enumClazz")); + } + + /** + * The type representing the value. + * + * @return the object type of the value + * @since 1.1.0 + */ + @NotNull Class type(); + + /** + * Extract a value of this type from a plain string. + * + * @param plainValue a plain-text value + * @return the value coerced to the correct type + * @throws IllegalArgumentException if the value is not of an appropriate type + * @since 1.1.0 + */ + @NotNull T parse(final @NotNull String plainValue) throws IllegalArgumentException; +} diff --git a/src/main/java/net/kyori/option/value/ValueTypeImpl.java b/src/main/java/net/kyori/option/value/ValueTypeImpl.java new file mode 100644 index 0000000..b82799f --- /dev/null +++ b/src/main/java/net/kyori/option/value/ValueTypeImpl.java @@ -0,0 +1,111 @@ +/* + * This file is part of option, licensed under the MIT License. + * + * Copyright (c) 2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.option.value; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +abstract class ValueTypeImpl implements ValueType { + private final Class type; + + ValueTypeImpl(final @NotNull Class type) { + this.type = type; + } + + @Override + public @NotNull Class type() { + return this.type; + } + + static IllegalArgumentException doNotKnowHowToTurn(final String input, final Class expected, final @Nullable String message) { + throw new IllegalArgumentException("Do not know how to turn value '" + input + "' into a " + expected.getName() + (message == null ? "" : ": " + message)); + } + + static final class Types { + private Types() { + } + + static ValueType STRING = new ValueTypeImpl(String.class) { + @Override + public @NotNull String parse(final @NotNull String plainValue) throws IllegalArgumentException { + return plainValue; + } + }; + static ValueType BOOLEAN = new ValueTypeImpl(Boolean.class) { + @Override + public @NotNull Boolean parse(final @NotNull String plainValue) throws IllegalArgumentException { + if (plainValue.equalsIgnoreCase("true")) { + return Boolean.TRUE; + } else if (plainValue.equalsIgnoreCase("false")) { + return Boolean.FALSE; + } else { + throw doNotKnowHowToTurn(plainValue, Boolean.class, null); + } + } + }; + static ValueType INT = new ValueTypeImpl(Integer.class) { + @Override + public @NotNull Integer parse(final @NotNull String plainValue) throws IllegalArgumentException { + try { + return Integer.decode(plainValue); + } catch (final NumberFormatException ex) { + throw doNotKnowHowToTurn(plainValue, Integer.class, ex.getMessage()); + } + } + }; + static ValueType DOUBLE = new ValueTypeImpl(Double.class) { + @Override + public @NotNull Double parse(final @NotNull String plainValue) throws IllegalArgumentException { + try { + return Double.parseDouble(plainValue); + } catch (final NumberFormatException ex) { + throw doNotKnowHowToTurn(plainValue, Double.class, ex.getMessage()); + } + } + }; + } + + static final class EnumType> extends ValueTypeImpl { + private final Map values = new HashMap<>(); + + EnumType(final @NotNull Class type) { + super(type); + for (final E entry : type.getEnumConstants()) { + this.values.put(entry.name().toLowerCase(Locale.ROOT), entry); + } + } + + @Override + public @NotNull E parse(final @NotNull String plainValue) throws IllegalArgumentException { + final E result = this.values.get(plainValue.toLowerCase(Locale.ROOT)); + if (result == null) { + throw doNotKnowHowToTurn(plainValue, this.type(), null); + } + return result; + } + } +} diff --git a/src/main/java/net/kyori/option/value/package-info.java b/src/main/java/net/kyori/option/value/package-info.java new file mode 100644 index 0000000..c3d11d8 --- /dev/null +++ b/src/main/java/net/kyori/option/value/package-info.java @@ -0,0 +1,6 @@ +/** + * Tools related to option value handling. + * + * @since 1.1.0 + */ +package net.kyori.option.value; diff --git a/src/main/java9/module-info.java b/src/main/java9/module-info.java index 3ef63f4..73543f0 100644 --- a/src/main/java9/module-info.java +++ b/src/main/java9/module-info.java @@ -5,4 +5,5 @@ requires static transitive org.jetbrains.annotations; exports net.kyori.option; + exports net.kyori.option.value; } diff --git a/src/test/java/net/kyori/option/value/ValueSourceTest.java b/src/test/java/net/kyori/option/value/ValueSourceTest.java new file mode 100644 index 0000000..9fc032f --- /dev/null +++ b/src/test/java/net/kyori/option/value/ValueSourceTest.java @@ -0,0 +1,80 @@ +/* + * This file is part of option, licensed under the MIT License. + * + * Copyright (c) 2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.option.value; + +import net.kyori.option.Option; +import net.kyori.option.OptionSchema; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class ValueSourceTest { + static OptionSchema.Mutable SCHEMA = OptionSchema.emptySchema(); + + static Option A = SCHEMA.stringOption("a", null); + static Option A_B = SCHEMA.stringOption("a/b", null); + static Option C = SCHEMA.stringOption("c", null); + static Option C_D = SCHEMA.stringOption("c/d", null); + + @Test + void testReadSystemProperties() { + final ValueSource systemProperties = ValueSource.systemProperty("prefix"); + assertNull(systemProperties.value(A)); + assertNull(systemProperties.value(A_B)); + + System.setProperty("a", "test"); + System.setProperty("a.b", "test2"); + + assertNull(systemProperties.value(A)); + assertNull(systemProperties.value(A_B)); + + System.setProperty("prefix.a", "test"); + System.setProperty("prefix.a.b", "test2"); + + assertEquals("test", systemProperties.value(A)); + assertEquals("test2", systemProperties.value(A_B)); + } + + @Test + void testReadSystemPropertiesNoPrefix() { + final ValueSource systemProperties = ValueSource.systemProperty(); + assertNull(systemProperties.value(C)); + assertNull(systemProperties.value(C_D)); + + System.setProperty("c", "test"); + System.setProperty("c.d", "test2"); + + assertEquals("test", systemProperties.value(C)); + assertEquals("test2", systemProperties.value(C_D)); + + System.setProperty("some.c", "test3"); + System.setProperty("some.c.d", "test4"); + + assertEquals("test", systemProperties.value(C)); + assertEquals("test2", systemProperties.value(C_D)); + } + + // we can't set environment variables :( +}