diff --git a/src/main/java/net/kyori/option/Option.java b/src/main/java/net/kyori/option/Option.java index 6519fcc..a6d46be 100644 --- a/src/main/java/net/kyori/option/Option.java +++ b/src/main/java/net/kyori/option/Option.java @@ -1,7 +1,7 @@ /* * This file is part of option, licensed under the MIT License. * - * Copyright (c) 2023 KyoriPowered + * Copyright (c) 2023-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 @@ -47,9 +47,11 @@ public interface Option { * @param defaultValue the default value * @return the flag instance * @since 1.0.0 + * @deprecated For removal since 1.1.0, create new options within an {@link OptionSchema} instead */ + @Deprecated static Option booleanOption(final String id, final boolean defaultValue) { - return OptionImpl.option(id, Boolean.class, defaultValue); + return OptionSchema.globalSchema().booleanOption(id, defaultValue); } /** @@ -63,9 +65,11 @@ static Option booleanOption(final String id, final boolean defaultValue * @param the enum type * @return the flag instance * @since 1.0.0 + * @deprecated For removal since 1.1.0, create new options within an {@link OptionSchema} instead */ + @Deprecated static > Option enumOption(final String id, final Class enumClazz, final E defaultValue) { - return OptionImpl.option(id, enumClazz, defaultValue); + return OptionSchema.globalSchema().enumOption(id, enumClazz, defaultValue); } /** diff --git a/src/main/java/net/kyori/option/OptionImpl.java b/src/main/java/net/kyori/option/OptionImpl.java index 2b3f493..d18b61f 100644 --- a/src/main/java/net/kyori/option/OptionImpl.java +++ b/src/main/java/net/kyori/option/OptionImpl.java @@ -1,7 +1,7 @@ /* * This file is part of option, licensed under the MIT License. * - * Copyright (c) 2023 KyoriPowered + * Copyright (c) 2023-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 @@ -24,15 +24,10 @@ package net.kyori.option; import java.util.Objects; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import static java.util.Objects.requireNonNull; - final class OptionImpl implements Option { - private static final Set KNOWN_KEYS = ConcurrentHashMap.newKeySet(); private final String id; private final Class type; @@ -44,18 +39,6 @@ final class OptionImpl implements Option { this.defaultValue = defaultValue; } - static Option option(final String id, final Class type, final @Nullable T defaultValue) { - if (!KNOWN_KEYS.add(id)) { - throw new IllegalStateException("Key " + id + " has already been used. Option keys must be unique."); - } - - return new OptionImpl<>( - requireNonNull(id, "id"), - requireNonNull(type, "type"), - defaultValue - ); - } - @Override public @NotNull String id() { return this.id; diff --git a/src/main/java/net/kyori/option/OptionSchema.java b/src/main/java/net/kyori/option/OptionSchema.java new file mode 100644 index 0000000..a2fc186 --- /dev/null +++ b/src/main/java/net/kyori/option/OptionSchema.java @@ -0,0 +1,203 @@ +/* + * 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; + +import java.util.Set; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static java.util.Objects.requireNonNull; + +/** + * Represents a 'universe' of known options. + * + * @since 1.1.0 + */ +@ApiStatus.NonExtendable +public interface OptionSchema { + /** + * Retrieve the globally-shared option schema. + * + *

This mostly exists for backwards compatibility, and should not be used in new software.

+ * + * @return the global schema + * @since 1.1.0 + */ + static OptionSchema.@NotNull Mutable globalSchema() { + return OptionSchemaImpl.Instances.GLOBAL; + } + + /** + * Return a mutable schema that's a child of the specified schema. + * + *

This schema will inherit all options defined in the parent schema at the time the child schema is created.

+ * + * @param schema the parent schema + * @return the mutable child schema + * @since 1.1.0 + */ + static OptionSchema.@NotNull Mutable childSchema(final @NotNull OptionSchema schema) { + final OptionSchemaImpl impl; + if (schema instanceof OptionSchemaImpl.MutableImpl) { + impl = (OptionSchemaImpl) ((Mutable) schema).frozenView(); + } else { + impl = (OptionSchemaImpl) schema; + } + + return new OptionSchemaImpl(requireNonNull(impl, "impl")).new MutableImpl(); + } + + /** + * Create an empty schema inheriting from nothing but the contents + * of the global schema at invocation time. + * + * @return a mutable schema + * @since 1.1.0 + */ + static OptionSchema.@NotNull Mutable emptySchema() { + return new OptionSchemaImpl(null).new MutableImpl(); + } + + /** + * Return all known options contained within this schema, and recursively through its parents. + * + * @return known options + * @since 1.1.0 + */ + @NotNull Set> knownOptions(); + + /** + * Return whether the provided option is known within this scheam. + * + * @param option the option + * @return whether the option is known + * @since 1.1.0 + */ + boolean has(final @NotNull Option option); + + /** + * Create a builder for an unversioned option state containing only options within this schema. + * + * @return the builder + * @since 1.1.0 + */ + OptionState.@NotNull Builder stateBuilder(); + + /** + * Create a builder for a versioned option state containing only values for options within this schema. + * + * @return the builder + * @since 1.1.0 + */ + OptionState.@NotNull VersionedBuilder versionedStateBuilder(); + + /** + * Create an empty option state within this schema. + * + * @return the empty state + * @since 1.1.0 + */ + OptionState emptyState(); + + /** + * A mutable view of an option schema that allows registering new options into the schema. + * + * @since 1.1.0 + */ + @ApiStatus.NonExtendable + interface Mutable extends OptionSchema { + /** + * Create an option with a string value type. + * + *

Flag keys must not be reused within a schema tree.

+ * + * @param id the flag id + * @param defaultValue the default value + * @return the flag instance + * @since 1.1.0 + */ + @NotNull Option stringOption(final @NotNull String id, final @Nullable String defaultValue); + + /** + * Create an option with a boolean value type. + * + *

Flag keys must not be reused within a schema tree.

+ * + * @param id the flag id + * @param defaultValue the default value + * @return the flag instance + * @since 1.1.0 + */ + @NotNull Option booleanOption(final @NotNull String id, final boolean defaultValue); + + /** + * Create an option with an integer value type. + * + *

Flag keys must not be reused within a schema tree.

+ * + * @param id the flag id + * @param defaultValue the default value + * @return the flag instance + * @since 1.1.0 + */ + @NotNull Option intOption(final @NotNull String id, final int defaultValue); + + /** + * Create an option with a double value type. + * + *

Flag keys must not be reused within a schema tree.

+ * + * @param id the flag id + * @param defaultValue the default value + * @return the flag instance + * @since 1.1.0 + */ + @NotNull Option doubleOption(final @NotNull String id, final double defaultValue); + + /** + * Create an option with an enum value type. + * + *

Flag keys must not be reused within a schema tree.

+ * + * @param id the flag id + * @param enumClazz the value type + * @param defaultValue the default value + * @param the enum type + * @return the flag instance + * @since 1.1.0 + */ + > @NotNull Option enumOption(final @NotNull String id, final @NotNull Class enumClazz, final @Nullable E defaultValue); + + /** + * Return a view of this schema which does not allow consumers to register new options. + * + *

This allows exposing known options within a schema without the risk of having values polluted.

+ * + * @return the frozen view of this schema + * @since 1.1.0 + */ + @NotNull OptionSchema frozenView(); + } +} diff --git a/src/main/java/net/kyori/option/OptionSchemaImpl.java b/src/main/java/net/kyori/option/OptionSchemaImpl.java new file mode 100644 index 0000000..2a4cf5c --- /dev/null +++ b/src/main/java/net/kyori/option/OptionSchemaImpl.java @@ -0,0 +1,162 @@ +/* + * 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; + +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static java.util.Objects.requireNonNull; + +final class OptionSchemaImpl implements OptionSchema { + final OptionState emptyState; + final ConcurrentMap> options = new ConcurrentHashMap<>(); + + OptionSchemaImpl(final @Nullable OptionSchemaImpl parent) { + if (parent != null) { + this.options.putAll(parent.options); + } + this.emptyState = new OptionStateImpl(this, new IdentityHashMap<>()); + } + + @Override + public @NotNull Set> knownOptions() { + return Collections.unmodifiableSet(new HashSet<>(this.options.values())); + } + + @Override + public boolean has(final @NotNull Option option) { + final Option own = this.options.get(option.id()); + return own != null && own.equals(option); + } + + @Override + public OptionState.@NotNull Builder stateBuilder() { + return new OptionStateImpl.BuilderImpl(this); + } + + @Override + public OptionState.@NotNull VersionedBuilder versionedStateBuilder() { + return new OptionStateImpl.VersionedBuilderImpl(this); + } + + @Override + public OptionState emptyState() { + return this.emptyState; + } + + @Override + public String toString() { + return "OptionSchemaImpl{" + + "options=" + this.options + + '}'; + } + + static final class Instances { + static OptionSchemaImpl.MutableImpl GLOBAL = new OptionSchemaImpl(null).new MutableImpl(); + } + + final class MutableImpl implements OptionSchema.Mutable { + Option register(final String id, final Class type, final @Nullable T defaultValue) { + final Option ret = new OptionImpl<>( + requireNonNull(id, "id"), + requireNonNull(type, "type"), + defaultValue + ); + + if (OptionSchemaImpl.this.options.putIfAbsent(id, ret) != null) { + throw new IllegalStateException("Key " + id + " has already been used. Option keys must be unique within a schema."); + } + + return ret; + } + + @Override + public @NotNull Option stringOption(final @NotNull String id, final @Nullable String defaultValue) { + return this.register(id, String.class, defaultValue); + } + + @Override + public @NotNull Option booleanOption(final @NotNull String id, final boolean defaultValue) { + return this.register(id, Boolean.class, defaultValue); + } + + @Override + public @NotNull Option intOption(final @NotNull String id, final int defaultValue) { + return this.register(id, Integer.class, defaultValue); + } + + @Override + public @NotNull Option doubleOption(final @NotNull String id, final double defaultValue) { + return this.register(id, Double.class, defaultValue); + } + + @Override + public @NotNull > Option enumOption(final @NotNull String id, final @NotNull Class enumClazz, final @Nullable E defaultValue) { + return this.register(id, enumClazz, defaultValue); + } + + @Override + public @NotNull OptionSchema frozenView() { + return OptionSchemaImpl.this; + } + + // base scheam methods + + @Override + public @NotNull Set> knownOptions() { + return OptionSchemaImpl.this.knownOptions(); + } + + @Override + public boolean has(final @NotNull Option option) { + return OptionSchemaImpl.this.has(option); + } + + @Override + public OptionState.@NotNull Builder stateBuilder() { + return OptionSchemaImpl.this.stateBuilder(); + } + + @Override + public OptionState.@NotNull VersionedBuilder versionedStateBuilder() { + return OptionSchemaImpl.this.versionedStateBuilder(); + } + + @Override + public OptionState emptyState() { + return OptionSchemaImpl.this.emptyState(); + } + + @Override + public String toString() { + return "MutableImpl{schema=" + OptionSchemaImpl.this + "}"; + } + } +} diff --git a/src/main/java/net/kyori/option/OptionState.java b/src/main/java/net/kyori/option/OptionState.java index 6fc2ba1..2c711b6 100644 --- a/src/main/java/net/kyori/option/OptionState.java +++ b/src/main/java/net/kyori/option/OptionState.java @@ -1,7 +1,7 @@ /* * This file is part of option, licensed under the MIT License. * - * Copyright (c) 2023 KyoriPowered + * Copyright (c) 2023-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 @@ -40,9 +40,11 @@ public interface OptionState { * * @return the empty option state * @since 1.0.0 + * @deprecated for removal since 1.1.0, access via {@link OptionSchema#emptyState()} instead */ + @Deprecated static OptionState emptyOptionState() { - return OptionStateImpl.EMPTY; + return OptionSchema.globalSchema().emptyState(); } /** @@ -50,9 +52,11 @@ static OptionState emptyOptionState() { * * @return the builder * @since 1.0.0 + * @deprecated for removal since 1.1.0, create states via {@link OptionSchema#stateBuilder()} instead */ + @Deprecated static @NotNull Builder optionState() { - return new OptionStateImpl.BuilderImpl(); + return OptionSchema.globalSchema().stateBuilder(); } /** @@ -60,11 +64,20 @@ static OptionState emptyOptionState() { * * @return the builder * @since 1.0.0 + * @deprecated for removal since 1.1.0, create states via {@link OptionSchema#versionedStateBuilder()} instead */ + @Deprecated static @NotNull VersionedBuilder versionedOptionState() { - return new OptionStateImpl.VersionedBuilderImpl(); + return OptionSchema.globalSchema().versionedStateBuilder(); } + /** + * Get the option schema defining options that can be set within this state. + * + * @return the option schema + * @since 1.1.0 + */ + @NotNull OptionSchema schema(); /** * Get whether this state contains a certain option at all. diff --git a/src/main/java/net/kyori/option/OptionStateImpl.java b/src/main/java/net/kyori/option/OptionStateImpl.java index c32d6d0..25e79c5 100644 --- a/src/main/java/net/kyori/option/OptionStateImpl.java +++ b/src/main/java/net/kyori/option/OptionStateImpl.java @@ -1,7 +1,7 @@ /* * This file is part of option, licensed under the MIT License. * - * Copyright (c) 2023 KyoriPowered + * Copyright (c) 2023-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 @@ -36,13 +36,19 @@ import static java.util.Objects.requireNonNull; final class OptionStateImpl implements OptionState { - static final OptionState EMPTY = new OptionStateImpl(new IdentityHashMap<>()); + private final OptionSchema schema; private final IdentityHashMap, Object> values; - OptionStateImpl(final IdentityHashMap, Object> values) { + OptionStateImpl(final OptionSchema schema, final IdentityHashMap, Object> values) { + this.schema = schema; this.values = new IdentityHashMap<>(values); } + @Override + public @NotNull OptionSchema schema() { + return this.schema; + } + @Override public boolean has(final @NotNull Option option) { return this.values.containsKey(requireNonNull(option, "flag")); @@ -75,16 +81,23 @@ public String toString() { } static final class VersionedImpl implements Versioned { + private final OptionSchema schema; private final SortedMap sets; private final int targetVersion; private final OptionState filtered; - VersionedImpl(final SortedMap sets, final int targetVersion, final OptionState filtered) { + VersionedImpl(final OptionSchema schema, final SortedMap sets, final int targetVersion, final OptionState filtered) { + this.schema = schema; this.sets = sets; this.targetVersion = targetVersion; this.filtered = filtered; } + @Override + public @NotNull OptionSchema schema() { + return this.schema; + } + @Override public boolean has(final @NotNull Option option) { return this.filtered.has(option); @@ -102,12 +115,12 @@ public V value(final @NotNull Option option) { @Override public @NotNull Versioned at(final int version) { - return new VersionedImpl(this.sets, version, flattened(this.sets, version)); + return new VersionedImpl(this.schema, this.sets, version, flattened(this.schema, this.sets, version)); } - public static OptionState flattened(final SortedMap versions, final int targetVersion) { + public static OptionState flattened(final OptionSchema schema, final SortedMap versions, final int targetVersion) { final Map applicable = versions.headMap(targetVersion + 1); - final OptionState.Builder builder = OptionState.optionState(); + final OptionState.Builder builder = schema.stateBuilder(); for (final OptionState child : applicable.values()) { builder.values(child); } @@ -121,6 +134,7 @@ public boolean equals(final @Nullable Object other) { if (other == null || getClass() != other.getClass()) return false; final VersionedImpl that = (VersionedImpl) other; return this.targetVersion == that.targetVersion + && Objects.equals(this.schema, that.schema) && Objects.equals(this.sets, that.sets) && Objects.equals(this.filtered, that.filtered); } @@ -128,6 +142,7 @@ public boolean equals(final @Nullable Object other) { @Override public int hashCode() { return Objects.hash( + this.schema, this.sets, this.targetVersion, this.filtered @@ -137,7 +152,8 @@ public int hashCode() { @Override public String toString() { return this.getClass().getSimpleName() + "{" + - "sets=" + this.sets + + "schema=" + this.schema + + ", sets=" + this.sets + ", targetVersion=" + this.targetVersion + ", filtered=" + this.filtered + '}'; @@ -145,17 +161,26 @@ public String toString() { } static final class BuilderImpl implements OptionState.Builder { + private final OptionSchema schema; private final IdentityHashMap, Object> values = new IdentityHashMap<>(); + BuilderImpl(final OptionSchema schema) { + this.schema = schema; + } + @Override public @NotNull OptionState build() { - if (this.values.isEmpty()) return EMPTY; + if (this.values.isEmpty()) return this.schema.emptyState(); - return new OptionStateImpl(this.values); + return new OptionStateImpl(this.schema, this.values); } @Override public @NotNull Builder value(final @NotNull Option option, final @NotNull V value) { + if (!this.schema.has(option)) { + throw new IllegalStateException("Option '" + option.id() + "' was not present in active schema"); + } + this.values.put( requireNonNull(option, "flag"), requireNonNull(value, "value") @@ -163,12 +188,22 @@ static final class BuilderImpl implements OptionState.Builder { return this; } + private void putAll(final Map, Object> values) { + for (final Map.Entry, Object> entry : values.entrySet()) { + if (!this.schema.has(entry.getKey())) { + throw new IllegalStateException("Option '" + entry.getKey().id() + "' was not present in active schema"); + } + + this.values.put(entry.getKey(), entry.getValue()); + } + } + @Override public @NotNull Builder values(final @NotNull OptionState existing) { if (existing instanceof OptionStateImpl) { - this.values.putAll(((OptionStateImpl) existing).values); + this.putAll(((OptionStateImpl) existing).values); } else if (existing instanceof VersionedImpl) { - this.values.putAll(((OptionStateImpl) ((VersionedImpl) existing).filtered).values); + this.putAll(((OptionStateImpl) ((VersionedImpl) existing).filtered).values); } else { throw new IllegalArgumentException("existing set " + existing + " is of an unknown implementation type"); } @@ -177,12 +212,17 @@ static final class BuilderImpl implements OptionState.Builder { } static final class VersionedBuilderImpl implements OptionState.VersionedBuilder { + private final OptionSchema schema; private final Map builders = new TreeMap<>(); + VersionedBuilderImpl(final @NotNull OptionSchema schema) { + this.schema = schema; + } + @Override public OptionState.@NotNull Versioned build() { if (this.builders.isEmpty()) { - return new VersionedImpl(Collections.emptySortedMap(), 0, OptionState.emptyOptionState()); + return new VersionedImpl(this.schema, Collections.emptySortedMap(), 0, this.schema.emptyState()); } final SortedMap built = new TreeMap<>(); @@ -190,13 +230,13 @@ static final class VersionedBuilderImpl implements OptionState.VersionedBuilder built.put(entry.getKey(), entry.getValue().build()); } // generate 'flattened' latest element - return new VersionedImpl(built, built.lastKey(), VersionedImpl.flattened(built, built.lastKey())); + return new VersionedImpl(this.schema, built, built.lastKey(), VersionedImpl.flattened(this.schema, built, built.lastKey())); } @Override public @NotNull VersionedBuilder version(final int version, final @NotNull Consumer versionBuilder) { requireNonNull(versionBuilder, "versionBuilder") - .accept(this.builders.computeIfAbsent(version, $ -> new OptionStateImpl.BuilderImpl())); + .accept(this.builders.computeIfAbsent(version, $ -> new OptionStateImpl.BuilderImpl(this.schema))); return this; } } diff --git a/src/test/java/net/kyori/option/OptionConfigTest.java b/src/test/java/net/kyori/option/OptionConfigTest.java index c43d088..7ffec86 100644 --- a/src/test/java/net/kyori/option/OptionConfigTest.java +++ b/src/test/java/net/kyori/option/OptionConfigTest.java @@ -1,7 +1,7 @@ /* * This file is part of option, licensed under the MIT License. * - * Copyright (c) 2017-2023 KyoriPowered + * Copyright (c) 2017-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 @@ -36,25 +36,27 @@ enum TestEnum { ONE, TWO, THREE } - private static final Option ONE = Option.booleanOption(key("one"), true); - private static final Option TWO = Option.booleanOption(key("two"), false); - private static final Option ENUM_FLAG = Option.enumOption(key("enum_flag"), TestEnum.class, TestEnum.ONE); + private static final OptionSchema.Mutable UNSAFE_SCHEMA = OptionSchema.emptySchema(); + private static final OptionSchema SCHEMA = UNSAFE_SCHEMA.frozenView(); + private static final Option ONE = UNSAFE_SCHEMA.booleanOption(key("one"), true); + private static final Option TWO = UNSAFE_SCHEMA.booleanOption(key("two"), false); + private static final Option ENUM_FLAG = UNSAFE_SCHEMA.enumOption(key("enum_flag"), TestEnum.class, TestEnum.ONE); @Test void testEmpty() { - assertFalse(OptionState.emptyOptionState().has(ONE)); - assertFalse(OptionState.emptyOptionState().has(TWO)); - assertFalse(OptionState.emptyOptionState().has(ENUM_FLAG)); + assertFalse(SCHEMA.emptyState().has(ONE)); + assertFalse(SCHEMA.emptyState().has(TWO)); + assertFalse(SCHEMA.emptyState().has(ENUM_FLAG)); } @Test void testEmptyEqualToBuilder() { - assertEquals(OptionState.emptyOptionState(), OptionState.optionState().build()); + assertEquals(SCHEMA.emptyState(), SCHEMA.stateBuilder().build()); } @Test void testFixedValue() { - final OptionState set = OptionState.optionState() + final OptionState set = SCHEMA.stateBuilder() .value(ONE, false) .build(); @@ -65,7 +67,7 @@ void testFixedValue() { @Test void testDefaultValues() { - final OptionState set = OptionState.optionState() + final OptionState set = SCHEMA.stateBuilder() .build(); assertFalse(set.has(ONE)); @@ -76,7 +78,7 @@ void testDefaultValues() { @Test void testMixedTypes() { - final OptionState set = OptionState.optionState() + final OptionState set = SCHEMA.stateBuilder() .value(ONE, false) .value(ENUM_FLAG, TestEnum.THREE) .build(); @@ -88,12 +90,12 @@ void testMixedTypes() { @Test void testBuilderFromExisting() { - final OptionState existing = OptionState.optionState() + final OptionState existing = SCHEMA.stateBuilder() .value(ONE, false) .value(ENUM_FLAG, TestEnum.THREE) .build(); - final OptionState updated = OptionState.optionState() + final OptionState updated = SCHEMA.stateBuilder() .values(existing) .build(); @@ -102,7 +104,7 @@ void testBuilderFromExisting() { @Test void testVersionedBaseLevel() { - final OptionState.Versioned versioned = OptionState.versionedOptionState() + final OptionState.Versioned versioned = SCHEMA.versionedStateBuilder() .version(0, b -> b .value(TWO, true) .value(ENUM_FLAG, TestEnum.THREE)) @@ -118,7 +120,7 @@ void testVersionedBaseLevel() { @Test void testVersionLower() { - final OptionState.Versioned versioned = OptionState.versionedOptionState() + final OptionState.Versioned versioned = SCHEMA.versionedStateBuilder() .version(0, b -> b .value(TWO, true) .value(ENUM_FLAG, TestEnum.THREE)) @@ -136,7 +138,7 @@ void testVersionLower() { @Test void testVersionHigher() { - final OptionState.Versioned versioned = OptionState.versionedOptionState() + final OptionState.Versioned versioned = SCHEMA.versionedStateBuilder() .version(0, b -> b .value(TWO, true) .value(ENUM_FLAG, TestEnum.THREE)) @@ -155,7 +157,7 @@ void testVersionHigher() { @Test void testVersionBetweenSteps() { - final OptionState.Versioned versioned = OptionState.versionedOptionState() + final OptionState.Versioned versioned = SCHEMA.versionedStateBuilder() .version(0, b -> b .value(TWO, true) .value(ENUM_FLAG, TestEnum.THREE)) diff --git a/src/test/java/net/kyori/option/OptionTest.java b/src/test/java/net/kyori/option/OptionSchemaTest.java similarity index 63% rename from src/test/java/net/kyori/option/OptionTest.java rename to src/test/java/net/kyori/option/OptionSchemaTest.java index af8066d..101e9e5 100644 --- a/src/test/java/net/kyori/option/OptionTest.java +++ b/src/test/java/net/kyori/option/OptionSchemaTest.java @@ -1,7 +1,7 @@ /* * This file is part of option, licensed under the MIT License. * - * Copyright (c) 2023 KyoriPowered + * Copyright (c) 2023-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 @@ -28,12 +28,30 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; -public class OptionTest { +public class OptionSchemaTest { @Test void testMultipleOfKeysFails() { final String id = "test_flag"; - assertDoesNotThrow(() -> Option.booleanOption(id, false)); - assertThrows(IllegalStateException.class, () -> Option.booleanOption(id, false)); + final OptionSchema.Mutable schema = OptionSchema.emptySchema(); + assertDoesNotThrow(() -> schema.booleanOption(id, false)); + assertThrows(IllegalStateException.class, () -> schema.booleanOption(id, false)); + } + + @Test + void testSameKeyInSeparateSchemas() { + final String id = "test_flag"; + + assertDoesNotThrow(() -> OptionSchema.emptySchema().booleanOption(id, false)); + assertDoesNotThrow(() -> OptionSchema.emptySchema().booleanOption(id, false)); + } + + @Test + void testInheritedDoesThrow() { + final String id = "test_flag"; + + final OptionSchema.Mutable schema = OptionSchema.emptySchema(); + assertDoesNotThrow(() -> schema.booleanOption(id, false)); + assertThrows(IllegalStateException.class, () -> OptionSchema.childSchema(schema).booleanOption(id, false)); } }