Skip to content

Commit

Permalink
Merge pull request #1002 from KyoriPowered/feat/feature-flags
Browse files Browse the repository at this point in the history
feat(api): A feature flag system to handle version compatibility
  • Loading branch information
zml2008 authored Dec 18, 2023
2 parents 1915e23 + 59438cb commit 6fd5262
Show file tree
Hide file tree
Showing 20 changed files with 446 additions and 76 deletions.
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ truth = "1.1.5"
# shared
examination-api = { module = "net.kyori:examination-api", version.ref = "examination" }
examination-string = { module = "net.kyori:examination-string", version.ref = "examination" }
option = { module = "net.kyori:option", version = "1.0.0" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
guava-testlib = { module = "com.google.guava:guava-testlib", version.ref = "guava" }
jetbrainsAnnotations = "org.jetbrains:annotations:24.1.0"
Expand Down
1 change: 1 addition & 0 deletions nbt/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
}

dependencies {
api(libs.option)
api(libs.examination.api)
api(libs.examination.string)
compileOnlyApi(libs.jetbrainsAnnotations)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.TranslationArgument;
import net.kyori.adventure.text.serializer.json.JSONOptions;
import net.kyori.option.OptionState;
import org.jetbrains.annotations.Nullable;

import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.EXTRA;
Expand All @@ -76,13 +78,15 @@ final class ComponentSerializerImpl extends TypeAdapter<Component> {
static final Type COMPONENT_LIST_TYPE = new TypeToken<List<Component>>() {}.getType();
static final Type TRANSLATABLE_ARGUMENT_LIST_TYPE = new TypeToken<List<TranslationArgument>>() {}.getType();

static TypeAdapter<Component> create(final Gson gson) {
return new ComponentSerializerImpl(gson).nullSafe();
static TypeAdapter<Component> create(final OptionState features, final Gson gson) {
return new ComponentSerializerImpl(features.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT), gson).nullSafe();
}

private final boolean emitCompactTextComponent;
private final Gson gson;

private ComponentSerializerImpl(final Gson gson) {
private ComponentSerializerImpl(final boolean emitCompactTextComponent, final Gson gson) {
this.emitCompactTextComponent = emitCompactTextComponent;
this.gson = gson;
}

Expand Down Expand Up @@ -232,6 +236,16 @@ private static <C extends NBTComponent<C, B>, B extends NBTComponentBuilder<C, B

@Override
public void write(final JsonWriter out, final Component value) throws IOException {
if (
value instanceof TextComponent
&& value.children().isEmpty()
&& !value.hasStyling()
&& this.emitCompactTextComponent
) {
out.value(((TextComponent) value).content());
return;
}

out.beginObject();

if (value.hasStyling()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@
import net.kyori.adventure.builder.AbstractBuilder;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import net.kyori.adventure.text.serializer.json.JSONOptions;
import net.kyori.adventure.util.Buildable;
import net.kyori.adventure.util.PlatformAPI;
import net.kyori.option.OptionState;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -122,14 +124,22 @@ static Builder builder() {
* @since 4.0.0
*/
interface Builder extends AbstractBuilder<GsonComponentSerializer>, Buildable.Builder<GsonComponentSerializer>, JSONComponentSerializer.Builder {
@Override
@NotNull Builder options(final @NotNull OptionState flags);

@Override
@NotNull Builder editOptions(final @NotNull Consumer<OptionState.Builder> optionEditor);

/**
* Sets that the serializer should downsample hex colors to named colors.
*
* @return this builder
* @since 4.0.0
*/
@Override
@NotNull Builder downsampleColors();
default @NotNull Builder downsampleColors() {
return this.editOptions(features -> features.value(JSONOptions.EMIT_RGB, false));
}

/**
* Sets a serializer that will be used to interpret legacy hover event {@code value} payloads.
Expand All @@ -154,8 +164,11 @@ interface Builder extends AbstractBuilder<GsonComponentSerializer>, Buildable.Bu
*
* @since 4.0.0
*/
@Deprecated
@Override
@NotNull Builder emitLegacyHoverEvent();
default @NotNull Builder emitLegacyHoverEvent() {
return this.editOptions(b -> b.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.BOTH));
}

/**
* Builds the serializer.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONOptions;
import net.kyori.adventure.util.Services;
import net.kyori.option.OptionState;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import static java.util.Objects.requireNonNull;

final class GsonComponentSerializerImpl implements GsonComponentSerializer {
private static final Optional<Provider> SERVICE = Services.service(Provider.class);
static final Consumer<Builder> BUILDER = SERVICE
Expand All @@ -46,24 +50,22 @@ final class GsonComponentSerializerImpl implements GsonComponentSerializer {
static final class Instances {
static final GsonComponentSerializer INSTANCE = SERVICE
.map(Provider::gson)
.orElseGet(() -> new GsonComponentSerializerImpl(false, null, false));
.orElseGet(() -> new GsonComponentSerializerImpl(JSONOptions.byDataVersion(), null));
static final GsonComponentSerializer LEGACY_INSTANCE = SERVICE
.map(Provider::gsonLegacy)
.orElseGet(() -> new GsonComponentSerializerImpl(true, null, true));
.orElseGet(() -> new GsonComponentSerializerImpl(JSONOptions.byDataVersion().at(2525 /* just before 1.16 */), null));
}

private final Gson serializer;
private final UnaryOperator<GsonBuilder> populator;
private final boolean downsampleColor;
private final net.kyori.adventure.text.serializer.json.@Nullable LegacyHoverEventSerializer legacyHoverSerializer;
private final boolean emitLegacyHover;
private final OptionState flags;

GsonComponentSerializerImpl(final boolean downsampleColor, final net.kyori.adventure.text.serializer.json.@Nullable LegacyHoverEventSerializer legacyHoverSerializer, final boolean emitLegacyHover) {
this.downsampleColor = downsampleColor;
GsonComponentSerializerImpl(final OptionState flags, final net.kyori.adventure.text.serializer.json.@Nullable LegacyHoverEventSerializer legacyHoverSerializer) {
this.flags = flags;
this.legacyHoverSerializer = legacyHoverSerializer;
this.emitLegacyHover = emitLegacyHover;
this.populator = builder -> {
builder.registerTypeAdapterFactory(new SerializerFactory(downsampleColor, legacyHoverSerializer, emitLegacyHover));
builder.registerTypeAdapterFactory(new SerializerFactory(flags, legacyHoverSerializer));
return builder;
};
this.serializer = this.populator.apply(
Expand Down Expand Up @@ -120,46 +122,43 @@ static final class Instances {
}

static final class BuilderImpl implements Builder {
private boolean downsampleColor = false;
private OptionState flags = JSONOptions.byDataVersion(); // latest
private net.kyori.adventure.text.serializer.json.@Nullable LegacyHoverEventSerializer legacyHoverSerializer;
private boolean emitLegacyHover = false;

BuilderImpl() {
BUILDER.accept(this); // let service provider touch the builder before anybody else touches it
}

BuilderImpl(final GsonComponentSerializerImpl serializer) {
this();
this.downsampleColor = serializer.downsampleColor;
this.emitLegacyHover = serializer.emitLegacyHover;
this.flags = serializer.flags;
this.legacyHoverSerializer = serializer.legacyHoverSerializer;
}

@Override
public @NotNull Builder downsampleColors() {
this.downsampleColor = true;
public @NotNull Builder options(final @NotNull OptionState flags) {
this.flags = requireNonNull(flags, "flags");
return this;
}

@Override
public @NotNull Builder legacyHoverEventSerializer(final net.kyori.adventure.text.serializer.json.@Nullable LegacyHoverEventSerializer serializer) {
this.legacyHoverSerializer = serializer;
public @NotNull Builder editOptions(final @NotNull Consumer<OptionState.Builder> optionEditor) {
final OptionState.Builder builder = OptionState.optionState()
.values(this.flags);
requireNonNull(optionEditor, "flagEditor").accept(builder);
this.flags = builder.build();
return this;
}

@Override
public @NotNull Builder emitLegacyHoverEvent() {
this.emitLegacyHover = true;
public @NotNull Builder legacyHoverEventSerializer(final net.kyori.adventure.text.serializer.json.@Nullable LegacyHoverEventSerializer serializer) {
this.legacyHoverSerializer = serializer;
return this;
}

@Override
public @NotNull GsonComponentSerializer build() {
if (this.legacyHoverSerializer == null) {
return this.downsampleColor ? Instances.LEGACY_INSTANCE : Instances.INSTANCE;
} else {
return new GsonComponentSerializerImpl(this.downsampleColor, this.legacyHoverSerializer, this.emitLegacyHover);
}
return new GsonComponentSerializerImpl(this.flags, this.legacyHoverSerializer);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.serializer.json.JSONOptions;
import net.kyori.option.OptionState;
import org.jetbrains.annotations.Nullable;

final class SerializerFactory implements TypeAdapterFactory {
Expand All @@ -55,26 +57,24 @@ final class SerializerFactory implements TypeAdapterFactory {
static final Class<UUID> UUID_TYPE = UUID.class;
static final Class<TranslationArgument> TRANSLATION_ARGUMENT_TYPE = TranslationArgument.class;

private final boolean downsampleColors;
private final OptionState features;
private final net.kyori.adventure.text.serializer.json.LegacyHoverEventSerializer legacyHoverSerializer;
private final boolean emitLegacyHover;

SerializerFactory(final boolean downsampleColors, final net.kyori.adventure.text.serializer.json.@Nullable LegacyHoverEventSerializer legacyHoverSerializer, final boolean emitLegacyHover) {
this.downsampleColors = downsampleColors;
SerializerFactory(final OptionState features, final net.kyori.adventure.text.serializer.json.@Nullable LegacyHoverEventSerializer legacyHoverSerializer) {
this.features = features;
this.legacyHoverSerializer = legacyHoverSerializer;
this.emitLegacyHover = emitLegacyHover;
}

@Override
@SuppressWarnings("unchecked")
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type) {
final Class<? super T> rawType = type.getRawType();
if (COMPONENT_TYPE.isAssignableFrom(rawType)) {
return (TypeAdapter<T>) ComponentSerializerImpl.create(gson);
return (TypeAdapter<T>) ComponentSerializerImpl.create(this.features, gson);
} else if (KEY_TYPE.isAssignableFrom(rawType)) {
return (TypeAdapter<T>) KeySerializer.INSTANCE;
} else if (STYLE_TYPE.isAssignableFrom(rawType)) {
return (TypeAdapter<T>) StyleSerializer.create(this.legacyHoverSerializer, this.emitLegacyHover, gson);
return (TypeAdapter<T>) StyleSerializer.create(this.legacyHoverSerializer, this.features, gson);
} else if (CLICK_ACTION_TYPE.isAssignableFrom(rawType)) {
return (TypeAdapter<T>) ClickEventActionSerializer.INSTANCE;
} else if (HOVER_ACTION_TYPE.isAssignableFrom(rawType)) {
Expand All @@ -86,13 +86,13 @@ public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type) {
} else if (COLOR_WRAPPER_TYPE.isAssignableFrom(rawType)) {
return (TypeAdapter<T>) TextColorWrapper.Serializer.INSTANCE;
} else if (COLOR_TYPE.isAssignableFrom(rawType)) {
return (TypeAdapter<T>) (this.downsampleColors ? TextColorSerializer.DOWNSAMPLE_COLOR : TextColorSerializer.INSTANCE);
return (TypeAdapter<T>) (this.features.value(JSONOptions.EMIT_RGB) ? TextColorSerializer.INSTANCE : TextColorSerializer.DOWNSAMPLE_COLOR);
} else if (TEXT_DECORATION_TYPE.isAssignableFrom(rawType)) {
return (TypeAdapter<T>) TextDecorationSerializer.INSTANCE;
} else if (BLOCK_NBT_POS_TYPE.isAssignableFrom(rawType)) {
return (TypeAdapter<T>) BlockNBTComponentPosSerializer.INSTANCE;
} else if (UUID_TYPE.isAssignableFrom(rawType)) {
return (TypeAdapter<T>) UUIDSerializer.INSTANCE;
return (TypeAdapter<T>) UUIDSerializer.uuidSerializer(this.features);
} else if (TRANSLATION_ARGUMENT_TYPE.isAssignableFrom(rawType)) {
return (TypeAdapter<T>) TranslationArgumentSerializer.create(gson);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public void write(final JsonWriter out, final HoverEvent.ShowEntity value) throw
this.gson.toJson(value.type(), SerializerFactory.KEY_TYPE, out);

out.name(SHOW_ENTITY_ID);
out.value(value.id().toString());
this.gson.toJson(value.id(), SerializerFactory.UUID_TYPE, out);

final @Nullable Component name = value.name();
if (name != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.serializer.json.JSONOptions;
import net.kyori.adventure.util.Codec;
import net.kyori.option.OptionState;
import org.jetbrains.annotations.Nullable;

import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.CLICK_EVENT;
Expand Down Expand Up @@ -80,17 +82,34 @@ final class StyleSerializer extends TypeAdapter<Style> {
}
}

static TypeAdapter<Style> create(final net.kyori.adventure.text.serializer.json.@Nullable LegacyHoverEventSerializer legacyHover, final boolean emitLegacyHover, final Gson gson) {
return new StyleSerializer(legacyHover, emitLegacyHover, gson).nullSafe();
static TypeAdapter<Style> create(final net.kyori.adventure.text.serializer.json.@Nullable LegacyHoverEventSerializer legacyHover, final OptionState features, final Gson gson) {
final JSONOptions.HoverEventValueMode hoverMode = features.value(JSONOptions.EMIT_HOVER_EVENT_TYPE);
return new StyleSerializer(
legacyHover,
hoverMode == JSONOptions.HoverEventValueMode.LEGACY_ONLY || hoverMode == JSONOptions.HoverEventValueMode.BOTH,
hoverMode == JSONOptions.HoverEventValueMode.MODERN_ONLY || hoverMode == JSONOptions.HoverEventValueMode.BOTH,
features.value(JSONOptions.VALIDATE_STRICT_EVENTS),
gson
).nullSafe();
}

private final net.kyori.adventure.text.serializer.json.LegacyHoverEventSerializer legacyHover;
private final boolean emitLegacyHover;
private final boolean emitModernHover;
private final boolean strictEventValues;
private final Gson gson;

private StyleSerializer(final net.kyori.adventure.text.serializer.json.@Nullable LegacyHoverEventSerializer legacyHover, final boolean emitLegacyHover, final Gson gson) {
private StyleSerializer(
final net.kyori.adventure.text.serializer.json.@Nullable LegacyHoverEventSerializer legacyHover,
final boolean emitLegacyHover,
final boolean emitModernHover,
final boolean strictEventValues,
final Gson gson
) {
this.legacyHover = legacyHover;
this.emitLegacyHover = emitLegacyHover;
this.emitModernHover = emitModernHover;
this.strictEventValues = strictEventValues;
this.gson = gson;
}

Expand Down Expand Up @@ -123,6 +142,9 @@ public Style read(final JsonReader in) throws IOException {
if (clickEventField.equals(CLICK_EVENT_ACTION)) {
action = this.gson.fromJson(in, SerializerFactory.CLICK_ACTION_TYPE);
} else if (clickEventField.equals(CLICK_EVENT_VALUE)) {
if (in.peek() == JsonToken.NULL && this.strictEventValues) {
throw ComponentSerializerImpl.notSureHowToDeserialize(CLICK_EVENT_VALUE);
}
value = in.peek() == JsonToken.NULL ? null : in.nextString();
} else {
in.skipValue();
Expand All @@ -148,6 +170,9 @@ public Style read(final JsonReader in) throws IOException {
if (hoverEventObject.has(HOVER_EVENT_CONTENTS)) {
final @Nullable JsonElement rawValue = hoverEventObject.get(HOVER_EVENT_CONTENTS);
if (GsonHacks.isNullOrEmpty(rawValue)) {
if (this.strictEventValues) {
throw ComponentSerializerImpl.notSureHowToDeserialize(rawValue);
}
value = null;
} else if (SerializerFactory.COMPONENT_TYPE.isAssignableFrom(actionType)) {
value = this.gson.fromJson(rawValue, SerializerFactory.COMPONENT_TYPE);
Expand All @@ -161,6 +186,9 @@ public Style read(final JsonReader in) throws IOException {
} else if (hoverEventObject.has(HOVER_EVENT_VALUE)) {
final JsonElement element = hoverEventObject.get(HOVER_EVENT_VALUE);
if (GsonHacks.isNullOrEmpty(element)) {
if (this.strictEventValues) {
throw ComponentSerializerImpl.notSureHowToDeserialize(element);
}
value = null;
} else if (SerializerFactory.COMPONENT_TYPE.isAssignableFrom(actionType)) {
final Component rawValue = this.gson.fromJson(element, SerializerFactory.COMPONENT_TYPE);
Expand All @@ -171,6 +199,9 @@ public Style read(final JsonReader in) throws IOException {
value = null;
}
} else {
if (this.strictEventValues) {
throw ComponentSerializerImpl.notSureHowToDeserialize(hoverEventObject);
}
value = null;
}

Expand Down Expand Up @@ -253,13 +284,13 @@ public void write(final JsonWriter out, final Style value) throws IOException {
}

final @Nullable HoverEvent<?> hoverEvent = value.hoverEvent();
if (hoverEvent != null && (hoverEvent.action() != HoverEvent.Action.SHOW_ACHIEVEMENT || this.emitLegacyHover)) {
if (hoverEvent != null && ((this.emitModernHover && hoverEvent.action() != HoverEvent.Action.SHOW_ACHIEVEMENT) || this.emitLegacyHover)) {
out.name(HOVER_EVENT);
out.beginObject();
out.name(HOVER_EVENT_ACTION);
final HoverEvent.Action<?> action = hoverEvent.action();
this.gson.toJson(action, SerializerFactory.HOVER_ACTION_TYPE, out);
if (action != HoverEvent.Action.SHOW_ACHIEVEMENT) { // legacy action has no modern contents value
if (this.emitModernHover && action != HoverEvent.Action.SHOW_ACHIEVEMENT) { // legacy action has no modern contents value
out.name(HOVER_EVENT_CONTENTS);
if (action == HoverEvent.Action.SHOW_ITEM) {
this.gson.toJson(hoverEvent.value(), SerializerFactory.SHOW_ITEM_TYPE, out);
Expand Down
Loading

0 comments on commit 6fd5262

Please sign in to comment.