diff options
Diffstat (limited to 'common/src/main/java')
15 files changed, 592 insertions, 0 deletions
diff --git a/common/src/main/java/dev/isxander/yacl3/api/YetAnotherConfigLib.java b/common/src/main/java/dev/isxander/yacl3/api/YetAnotherConfigLib.java index 15ce5bc..5213e90 100644 --- a/common/src/main/java/dev/isxander/yacl3/api/YetAnotherConfigLib.java +++ b/common/src/main/java/dev/isxander/yacl3/api/YetAnotherConfigLib.java @@ -2,6 +2,7 @@ package dev.isxander.yacl3.api; import com.google.common.collect.ImmutableList; import dev.isxander.yacl3.config.ConfigInstance; +import dev.isxander.yacl3.config.v2.api.ConfigClassHandler; import dev.isxander.yacl3.gui.YACLScreen; import dev.isxander.yacl3.impl.YetAnotherConfigLibImpl; import net.minecraft.client.gui.screens.Screen; @@ -51,10 +52,15 @@ public interface YetAnotherConfigLib { return new YetAnotherConfigLibImpl.BuilderImpl(); } + static <T> YetAnotherConfigLib create(ConfigClassHandler<T> configHandler, ConfigBackedBuilder<T> builder) { + return builder.build(configHandler.defaults(), configHandler.instance(), createBuilder().save(configHandler.serializer()::serialize)).build(); + } + /** * Creates an instance using a {@link ConfigInstance} which autofills the save() builder method. * This also takes an easy functional interface that provides defaults and config to help build YACL bindings. */ + @Deprecated static <T> YetAnotherConfigLib create(ConfigInstance<T> configInstance, ConfigBackedBuilder<T> builder) { return builder.build(configInstance.getDefaults(), configInstance.getConfig(), createBuilder().save(configInstance::save)).build(); } diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java new file mode 100644 index 0000000..22e471f --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java @@ -0,0 +1,34 @@ +package dev.isxander.yacl3.config.v2.api; + +import dev.isxander.yacl3.api.YetAnotherConfigLib; +import dev.isxander.yacl3.config.v2.impl.ConfigClassHandlerImpl; + +import java.util.function.Function; + +public interface ConfigClassHandler<T> { + T instance(); + + T defaults(); + + Class<T> configClass(); + + ConfigField<?>[] fields(); + + YetAnotherConfigLib generateGui(); + + boolean supportsAutoGen(); + + ConfigSerializer<T> serializer(); + + static <T> Builder<T> createBuilder(Class<T> configClass) { + return new ConfigClassHandlerImpl.BuilderImpl<>(configClass); + } + + interface Builder<T> { + Builder<T> serializer(Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory); + + Builder<T> autoGen(boolean autoGen); + + ConfigClassHandler<T> build(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigEntry.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigEntry.java new file mode 100644 index 0000000..8b95c3f --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigEntry.java @@ -0,0 +1,18 @@ +package dev.isxander.yacl3.config.v2.api; + +import dev.isxander.yacl3.config.v2.impl.DefaultOptionFactory; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ConfigEntry { + Class<? extends OptionFactory<?>> factory() default DefaultOptionFactory.class; + + String serialName() default ""; + + String comment() default ""; +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigField.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigField.java new file mode 100644 index 0000000..26a309f --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigField.java @@ -0,0 +1,17 @@ +package dev.isxander.yacl3.config.v2.api; + +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +public interface ConfigField<T> { + String serialName(); + + Optional<String> comment(); + + FieldAccess<T> access(); + + @Nullable OptionFactory<T> factory(); + + boolean supportsFactory(); +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigSerializer.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigSerializer.java new file mode 100644 index 0000000..999221d --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigSerializer.java @@ -0,0 +1,13 @@ +package dev.isxander.yacl3.config.v2.api; + +public abstract class ConfigSerializer<T> { + protected final ConfigClassHandler<T> config; + + public ConfigSerializer(ConfigClassHandler<T> config) { + this.config = config; + } + + public abstract void serialize(); + + public abstract void deserialize(); +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/FieldAccess.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/FieldAccess.java new file mode 100644 index 0000000..aed9801 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/FieldAccess.java @@ -0,0 +1,14 @@ +package dev.isxander.yacl3.config.v2.api; + +import java.lang.reflect.Type; + +public interface FieldAccess<T> { + T get(); + + void set(T value); + + String name(); + + Type type(); + +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/GsonConfigSerializerBuilder.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/GsonConfigSerializerBuilder.java new file mode 100644 index 0000000..8d8d6c7 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/GsonConfigSerializerBuilder.java @@ -0,0 +1,68 @@ +package dev.isxander.yacl3.config.v2.api; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import dev.isxander.yacl3.config.ConfigEntry; +import dev.isxander.yacl3.config.v2.impl.serializer.GsonConfigSerializer; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; + +import java.awt.*; +import java.nio.file.Path; +import java.util.function.UnaryOperator; + +public interface GsonConfigSerializerBuilder<T> { + static <T> GsonConfigSerializerBuilder<T> create(ConfigClassHandler<T> config) { + return new GsonConfigSerializer.Builder<>(config); + } + + /** + * Sets the file path to save and load the config from. + */ + GsonConfigSerializerBuilder<T> setPath(Path path); + + /** + * Sets the GSON instance to use. Overrides all YACL defaults such as: + * <ul> + * <li>lower_camel_case field naming policy</li> + * <li>null serialization</li> + * <li>{@link Component}, {@link Style} and {@link Color} type adapters</li> + * </ul> + * Still respects the exclusion strategy to only serialize {@link ConfigEntry} + * but these can be added to with setExclusionStrategies. + * + * @param gsonBuilder gson builder to use + */ + GsonConfigSerializerBuilder<T> overrideGsonBuilder(GsonBuilder gsonBuilder); + + /** + * Sets the GSON instance to use. Overrides all YACL defaults such as: + * <ul> + * <li>lower_camel_case field naming policy</li> + * <li>null serialization</li> + * <li>{@link Component}, {@link Style} and {@link Color} type adapters</li> + * </ul> + * Still respects the exclusion strategy to only serialize {@link ConfigEntry} + * but these can be added to with setExclusionStrategies. + * + * @param gson gson instance to be converted to a builder + */ + GsonConfigSerializerBuilder<T> overrideGsonBuilder(Gson gson); + + /** + * Appends extra configuration to a GSON builder. + * This is the intended way to add functionality to the GSON instance. + * <p> + * By default, YACL sets the GSON with the following options: + * <ul> + * <li>lower_camel_case field naming policy</li> + * <li>null serialization</li> + * <li>{@link Component}, {@link Style} and {@link Color} type adapters</li> + * </ul> + * + * @param gsonBuilder the function to apply to the builder + */ + GsonConfigSerializerBuilder<T> appendGsonBuilder(UnaryOperator<GsonBuilder> gsonBuilder); + + ConfigSerializer<T> build(); +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/OptionFactory.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/OptionFactory.java new file mode 100644 index 0000000..aabfcf0 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/OptionFactory.java @@ -0,0 +1,9 @@ +package dev.isxander.yacl3.config.v2.api; + +import dev.isxander.yacl3.api.Option; + +public interface OptionFactory<T> { + Option<T> create(ConfigField<T> field); + + Class<T> type(); +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigClassHandlerImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigClassHandlerImpl.java new file mode 100644 index 0000000..62aa9b6 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigClassHandlerImpl.java @@ -0,0 +1,101 @@ +package dev.isxander.yacl3.config.v2.impl; + +import dev.isxander.yacl3.api.YetAnotherConfigLib; +import dev.isxander.yacl3.config.v2.api.*; +import dev.isxander.yacl3.platform.YACLPlatform; +import org.apache.commons.lang3.Validate; + +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.function.Function; + +public class ConfigClassHandlerImpl<T> implements ConfigClassHandler<T> { + private final Class<T> configClass; + private final boolean supportsAutoGen; + private final ConfigSerializer<T> serializer; + private final ConfigField<?>[] fields; + + private final T instance, defaults; + + public ConfigClassHandlerImpl(Class<T> configClass, Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory, boolean autoGen) { + this.configClass = configClass; + this.supportsAutoGen = YACLPlatform.getEnvironment().isClient() && autoGen; + + try { + Constructor<T> constructor = configClass.getDeclaredConstructor(); + this.instance = constructor.newInstance(); + this.defaults = constructor.newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to create instance of config class '%s' with no-args constructor.".formatted(configClass.getName()), e); + } + + this.fields = Arrays.stream(configClass.getDeclaredFields()) + .filter(field -> field.isAnnotationPresent(ConfigEntry.class)) + .map(field -> new ConfigFieldImpl<>(this.supportsAutoGen(), field.getAnnotation(ConfigEntry.class), new ReflectionFieldAccess<>(field, instance))) + .toArray(ConfigField[]::new); + this.serializer = serializerFactory.apply(this); + } + + @Override + public T instance() { + return this.instance; + } + + @Override + public T defaults() { + return this.defaults; + } + + @Override + public Class<T> configClass() { + return this.configClass; + } + + @Override + public ConfigField<?>[] fields() { + return this.fields; + } + + @Override + public boolean supportsAutoGen() { + return this.supportsAutoGen; + } + + @Override + public YetAnotherConfigLib generateGui() { + Validate.isTrue(supportsAutoGen(), "Auto GUI generation is not supported for this config class. You either need to enable it in the builder or you are attempting to create a GUI in a dedicated server environment."); + + throw new IllegalStateException(); + } + + @Override + public ConfigSerializer<T> serializer() { + return this.serializer; + } + + public static class BuilderImpl<T> implements Builder<T> { + private final Class<T> configClass; + private Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory; + private boolean autoGen; + + public BuilderImpl(Class<T> configClass) { + this.configClass = configClass; + } + + @Override + public Builder<T> serializer(Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory) { + this.serializerFactory = serializerFactory; + return this; + } + + @Override + public Builder<T> autoGen(boolean autoGen) { + throw new IllegalArgumentException(); + } + + @Override + public ConfigClassHandler<T> build() { + return new ConfigClassHandlerImpl<>(configClass, serializerFactory, autoGen); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigFieldImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigFieldImpl.java new file mode 100644 index 0000000..68bf4b8 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigFieldImpl.java @@ -0,0 +1,74 @@ +package dev.isxander.yacl3.config.v2.impl; + +import dev.isxander.yacl3.config.v2.api.ConfigEntry; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.FieldAccess; +import dev.isxander.yacl3.config.v2.api.OptionFactory; +import org.apache.commons.lang3.NotImplementedException; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Constructor; +import java.util.Optional; + +public class ConfigFieldImpl<T> implements ConfigField<T> { + private final @Nullable OptionFactory<T> factory; + private final String serialName; + private final Optional<String> comment; + private final FieldAccess<T> field; + private final boolean autoGen; + + public ConfigFieldImpl(boolean auto, ConfigEntry entry, FieldAccess<T> field) { + this.serialName = "".equals(entry.serialName()) ? field.name() : entry.serialName(); + this.comment = "".equals(entry.comment()) ? Optional.empty() : Optional.of(entry.comment()); + this.factory = auto ? makeFactory(entry.factory(), this.serialName) : null; + this.autoGen = auto; + this.field = field; + } + + @Override + public String serialName() { + return this.serialName; + } + + @Override + public Optional<String> comment() { + return this.comment; + } + + @Override + public FieldAccess<T> access() { + return field; + } + + @Override + public @Nullable OptionFactory<T> factory() { + return factory; + } + + @Override + public boolean supportsFactory() { + return this.autoGen; + } + + private OptionFactory<T> makeFactory(Class<? extends OptionFactory<?>> clazz, String name) { + if (clazz.equals(DefaultOptionFactory.class)) { + throw new NotImplementedException("Field '%s' does not have an option factory, but auto-gen is enabled.".formatted(this.serialName())); + } + + Constructor<?> constructor; + + try { + constructor = clazz.getConstructor(String.class); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Failed to find (String) constructor for option factory %s.".formatted(clazz.getName()), e); + } + + try { + return (OptionFactory<T>) constructor.newInstance(name); + } catch (ClassCastException e) { + throw new IllegalStateException("Failed to cast option factory %s to OptionFactory<%s>.".formatted(clazz.getName(), field.type().getTypeName()), e); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Failed to create new option factory (class is '%s')".formatted(clazz.getName()), e); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/DefaultOptionFactory.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/DefaultOptionFactory.java new file mode 100644 index 0000000..e32de00 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/DefaultOptionFactory.java @@ -0,0 +1,18 @@ +package dev.isxander.yacl3.config.v2.impl; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.OptionFactory; +import org.apache.commons.lang3.NotImplementedException; + +public class DefaultOptionFactory implements OptionFactory<Object> { + @Override + public Option<Object> create(ConfigField<Object> field) { + throw new NotImplementedException(); + } + + @Override + public Class<Object> type() { + throw new NotImplementedException(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ReflectionFieldAccess.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ReflectionFieldAccess.java new file mode 100644 index 0000000..114137f --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ReflectionFieldAccess.java @@ -0,0 +1,36 @@ +package dev.isxander.yacl3.config.v2.impl; + +import dev.isxander.yacl3.config.v2.api.FieldAccess; + +import java.lang.reflect.Field; +import java.lang.reflect.Type; + +public record ReflectionFieldAccess<T>(Field field, Object instance) implements FieldAccess<T> { + @Override + public T get() { + try { + return (T) field.get(instance); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @Override + public void set(T value) { + try { + field.set(instance, value); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @Override + public String name() { + return field.getName(); + } + + @Override + public Type type() { + return field.getGenericType(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java new file mode 100644 index 0000000..8bbc079 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java @@ -0,0 +1,152 @@ +package dev.isxander.yacl3.config.v2.impl.serializer; + +import com.google.gson.*; +import dev.isxander.yacl3.config.v2.api.ConfigClassHandler; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.ConfigSerializer; +import dev.isxander.yacl3.config.v2.api.GsonConfigSerializerBuilder; +import dev.isxander.yacl3.impl.utils.YACLConstants; +import dev.isxander.yacl3.platform.YACLPlatform; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; + +import java.awt.*; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +public class GsonConfigSerializer<T> extends ConfigSerializer<T> { + private final Gson gson; + private final Path path; + + private GsonConfigSerializer(ConfigClassHandler<T> config, Path path, Gson gson) { + super(config); + this.gson = gson; + this.path = path; + } + + @Override + public void serialize() { + JsonObject root = new JsonObject(); + + for (ConfigField<?> field : config.fields()) { + if (YACLPlatform.isDevelopmentEnv() && field.comment().isPresent()) { + YACLConstants.LOGGER.error("Config field '{}' has a comment, but comments are not supported by Gson. Please remove the comment or switch to a different serializer. This log will not be shown in production.", field.serialName()); + } + + try { + root.add(field.serialName(), gson.toJsonTree(field.access().get())); + } catch (Exception e) { + YACLConstants.LOGGER.error("Failed to serialize config field '{}'.", field.serialName(), e); + } + } + + YACLConstants.LOGGER.info("Serializing {} to '{}'", config.configClass(), path); + try { + Files.writeString(path, gson.toJson(root), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + } catch (Exception e) { + YACLConstants.LOGGER.error("Failed to serialize config class '{}'.", config.configClass().getSimpleName(), e); + } + } + + @Override + public void deserialize() { + if (!Files.exists(path)) { + YACLConstants.LOGGER.info("Config file '{}' does not exist. Creating it with default values.", path); + serialize(); + return; + } + + YACLConstants.LOGGER.info("Deserializing {} from '{}'", config.configClass().getSimpleName(), path); + + String json; + try { + json = Files.readString(path); + } catch (Exception e) { + throw new IllegalStateException("Failed to read '%s' for deserialization.".formatted(path), e); + } + + Map<String, JsonElement> root = gson.fromJson(json, JsonObject.class).asMap(); + List<String> unconsumedKeys = new ArrayList<>(root.keySet()); + + for (ConfigField<?> field : config.fields()) { + if (root.containsKey(field.serialName())) { + try { + field.access().set(gson.fromJson(root.get(field.serialName()), field.access().type())); + } catch (Exception e) { + YACLConstants.LOGGER.error("Failed to deserialize config field '{}'.", field.serialName(), e); + } + } else { + YACLConstants.LOGGER.warn("Config field '{}' was not found in the config file. Skipping.", field.serialName()); + } + + unconsumedKeys.remove(field.serialName()); + } + + if (!unconsumedKeys.isEmpty()) { + YACLConstants.LOGGER.warn("The following keys were not consumed by the config class: {}", String.join(", ", unconsumedKeys)); + } + } + + public static class ColorTypeAdapter implements JsonSerializer<Color>, JsonDeserializer<Color> { + @Override + public Color deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { + return new Color(jsonElement.getAsInt(), true); + } + + @Override + public JsonElement serialize(Color color, Type type, JsonSerializationContext jsonSerializationContext) { + return new JsonPrimitive(color.getRGB()); + } + } + + public static class Builder<T> implements GsonConfigSerializerBuilder<T> { + private final ConfigClassHandler<T> config; + private Path path; + private UnaryOperator<GsonBuilder> gsonBuilder = builder -> builder + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .serializeNulls() + .registerTypeHierarchyAdapter(Component.class, new Component.Serializer()) + .registerTypeHierarchyAdapter(Style.class, new Style.Serializer()) + .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter()) + .setPrettyPrinting(); + + public Builder(ConfigClassHandler<T> config) { + this.config = config; + } + + @Override + public Builder<T> setPath(Path path) { + this.path = path; + return this; + } + + @Override + public Builder<T> overrideGsonBuilder(GsonBuilder gsonBuilder) { + this.gsonBuilder = builder -> gsonBuilder; + return this; + } + + @Override + public Builder<T> overrideGsonBuilder(Gson gson) { + return this.overrideGsonBuilder(gson.newBuilder()); + } + + @Override + public Builder<T> appendGsonBuilder(UnaryOperator<GsonBuilder> gsonBuilder) { + UnaryOperator<GsonBuilder> prev = this.gsonBuilder; + this.gsonBuilder = builder -> gsonBuilder.apply(prev.apply(builder)); + return this; + } + + @Override + public GsonConfigSerializer<T> build() { + return new GsonConfigSerializer<>(config, path, gsonBuilder.apply(new GsonBuilder()).create()); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl3/platform/Env.java b/common/src/main/java/dev/isxander/yacl3/platform/Env.java new file mode 100644 index 0000000..276d294 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/platform/Env.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl3.platform; + +public enum Env { + CLIENT, + SERVER; + + public boolean isClient() { + return this == Env.CLIENT; + } +} diff --git a/common/src/main/java/dev/isxander/yacl3/platform/YACLPlatform.java b/common/src/main/java/dev/isxander/yacl3/platform/YACLPlatform.java new file mode 100644 index 0000000..590723e --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/platform/YACLPlatform.java @@ -0,0 +1,22 @@ +package dev.isxander.yacl3.platform; + +import dev.architectury.injectables.annotations.ExpectPlatform; + +import java.nio.file.Path; + +public final class YACLPlatform { + @ExpectPlatform + public static Env getEnvironment() { + throw new AssertionError(); + } + + @ExpectPlatform + public static Path getConfigDir() { + throw new AssertionError(); + } + + @ExpectPlatform + public static boolean isDevelopmentEnv() { + throw new AssertionError(); + } +} |