Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: json component serializer #856

Merged
merged 14 commits into from
Jun 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .checkstyle/suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<suppress files="api[\\/]src[\\/]main[\\/]java[\\/]net[\\/]kyori[\\/]adventure[\\/]Adventure.java" checks="SummaryJavadoc"/>

<!-- no javadoc on test and internal classes -->
<suppress files="src[\\/](test|jmh)[\\/]java[\\/].*" checks="(FilteringWriteTag|JavadocPackage|MissingJavadoc.*)"/>
<suppress files="src[\\/](test(?:Fixtures)?|jmh)[\\/]java[\\/].*" checks="(FilteringWriteTag|JavadocPackage|MissingJavadoc.*)"/>
<suppress files="api[\\/]src[\\/]main[\\/]java[\\/]net[\\/]kyori[\\/]adventure[\\/]internal[\\/].*" checks="(FilteringWriteTag|JavadocPackage|MissingJavadoc.*)"/>
<suppress files="minimessage[\\/]src[\\/]main[\\/]java[\\/]net[\\/]kyori[\\/]adventure[\\/]text[\\/]minimessage[\\/]parser[\\/].*" checks="(FilteringWriteTag|JavadocPackage|MissingJavadoc.*)"/>

Expand Down
52 changes: 52 additions & 0 deletions api/src/main/java/net/kyori/adventure/util/Services.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,56 @@ private Services() {
}
return Optional.empty();
}

/**
* A fallback service.
*
* <p>When used in tandem with {@link #serviceWithFallback(Class)}, classes that implement this interface
* will be ignored in favour of classes that do not implement this interface.</p>
*
* @since 4.14.0
*/
public interface Fallback {
}

/**
* Locates a service.
*
* <p>If multiple services of this type exist, the first non-fallback service will be returned.</p>
*
* @param type the service type
* @param <P> the service type
* @return a service, or {@link Optional#empty()}
* @see Fallback
* @since 4.14.0
*/
public static <P> @NotNull Optional<P> serviceWithFallback(final @NotNull Class<P> type) {
final ServiceLoader<P> loader = Services0.loader(type);
final Iterator<P> it = loader.iterator();
P firstFallback = null;

while (it.hasNext()) {
final P instance;

try {
instance = it.next();
} catch (final Throwable t) {
if (SERVICE_LOAD_FAILURES_ARE_FATAL) {
throw new IllegalStateException("Encountered an exception loading service " + type, t);
} else {
continue;
}
}

if (instance instanceof Fallback) {
if (firstFallback == null) {
firstFallback = instance;
}
} else {
return Optional.of(instance);
}
}

return Optional.ofNullable(firstFallback);
}
}
3 changes: 2 additions & 1 deletion bom/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ dependencies {
"text-minimessage",
"text-serializer-ansi",
"text-serializer-gson",
"text-serializer-gson-legacy-impl",
"text-serializer-json",
"text-serializer-json-legacy-impl",
"text-serializer-legacy",
"text-serializer-plain"
).forEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import com.diffplug.gradle.spotless.FormatExtension
import me.champeau.jmh.JMHPlugin
import me.champeau.jmh.JmhParameters
import net.ltgt.gradle.errorprone.errorprone
import org.gradle.api.artifacts.type.ArtifactTypeDefinition

plugins {
id("adventure.base-conventions")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import me.champeau.jmh.JMHPlugin
import me.champeau.jmh.JmhBytecodeGeneratorTask

plugins {
id("adventure.common-conventions")
}

val sharedTests by configurations.registering {
isVisible = false
isCanBeResolved = false
isCanBeConsumed = false
}

val sharedTestDirs by configurations.registering {
isVisible = false
isCanBeConsumed = false
extendsFrom(sharedTests.get())
isTransitive = false // we want the directory on its own

attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_API)) // needs to be API to get the unpacked test-fixtures variant
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.CLASSES))
}
}

val sharedBenchmarks by configurations.registering {
isVisible = false
isTransitive = false

attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.CLASSES))
}
}

configurations.testRuntimeOnly {
extendsFrom(sharedTests.get())
}

tasks.test {
testClassesDirs += sharedTestDirs.get()
}

dependencies {
val textSerializerJson = project(":adventure-text-serializer-json")
api(textSerializerJson)
sharedTests.name(testFixtures(textSerializerJson.copy()))
sharedBenchmarks.name(textSerializerJson.copy().capabilities {
requireCapability("${project.group}:adventure-text-serializer-json-benchmarks:${project.version}")
})
annotationProcessor(project(":adventure-annotation-processors"))
}

// Configure benchmarks to read from json project
plugins.withId("me.champeau.jmh") {
configurations.named(JMHPlugin.getJHM_RUNTIME_CLASSPATH_CONFIGURATION()) {
extendsFrom(sharedBenchmarks.get())
}
tasks.named("jmhRunBytecodeGenerator", JmhBytecodeGeneratorTask::class) {
classesDirsToProcess.from(sharedBenchmarks.get())
}
}
2 changes: 2 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ sequenceOf(
"text-minimessage",
"text-serializer-gson",
"text-serializer-gson-legacy-impl",
"text-serializer-json",
"text-serializer-json-legacy-impl",
"text-serializer-legacy",
"text-serializer-plain",
"text-serializer-ansi",
Expand Down
3 changes: 1 addition & 2 deletions text-serializer-gson-legacy-impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ plugins {
}

dependencies {
api(projects.adventureApi)
api(projects.adventureTextSerializerGson)
api(projects.adventureNbt)
api(projects.adventureTextSerializerJsonLegacyImpl)
}

applyJarMetadata("net.kyori.adventure.text.serializer.gson.legacyimpl")
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,27 @@

import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.serializer.gson.LegacyHoverEventSerializer;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

/**
* A legacy {@link HoverEvent} serializer.
*
* @since 4.3.0
* @deprecated for removal since 4.14, use {@link net.kyori.adventure.text.serializer.json.legacyimpl.NBTLegacyHoverEventSerializer the text-serializer-json version} instead.
*/
public interface NBTLegacyHoverEventSerializer extends LegacyHoverEventSerializer {
@Deprecated
@ApiStatus.ScheduledForRemoval(inVersion = "5.0.0")
public interface NBTLegacyHoverEventSerializer extends LegacyHoverEventSerializer, net.kyori.adventure.text.serializer.json.legacyimpl.NBTLegacyHoverEventSerializer {
/**
* Gets the legacy {@link HoverEvent} serializer.
*
* @return a legacy {@link HoverEvent} serializer
* @since 4.3.0
* @deprecated for removal since 4.14, use {@link net.kyori.adventure.text.serializer.json.legacyimpl.NBTLegacyHoverEventSerializer the text-serializer-json version} instead.
*/
@Deprecated
@ApiStatus.ScheduledForRemoval(inVersion = "5.0.0")
static @NotNull LegacyHoverEventSerializer get() {
return NBTLegacyHoverEventSerializerImpl.INSTANCE;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,85 +24,39 @@
package net.kyori.adventure.text.serializer.gson.legacyimpl;

import java.io.IOException;
import java.util.UUID;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.TagStringIO;
import net.kyori.adventure.nbt.api.BinaryTagHolder;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.serializer.gson.LegacyHoverEventSerializer;
import net.kyori.adventure.text.serializer.json.legacyimpl.NBTLegacyHoverEventSerializer;
import net.kyori.adventure.util.Codec;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@Deprecated
final class NBTLegacyHoverEventSerializerImpl implements LegacyHoverEventSerializer {
static final NBTLegacyHoverEventSerializerImpl INSTANCE = new NBTLegacyHoverEventSerializerImpl();
private static final TagStringIO SNBT_IO = TagStringIO.get();
private static final Codec<CompoundBinaryTag, String, IOException, IOException> SNBT_CODEC = Codec.codec(SNBT_IO::asCompound, SNBT_IO::asString);

static final String ITEM_TYPE = "id";
static final String ITEM_COUNT = "Count";
static final String ITEM_TAG = "tag";

static final String ENTITY_NAME = "name";
static final String ENTITY_TYPE = "type";
static final String ENTITY_ID = "id";
static final net.kyori.adventure.text.serializer.json.LegacyHoverEventSerializer NEW_INSTANCE = NBTLegacyHoverEventSerializer.get();

private NBTLegacyHoverEventSerializerImpl() {
}

@Override
public HoverEvent.@NotNull ShowItem deserializeShowItem(final @NotNull Component input) throws IOException {
assertTextComponent(input);
final CompoundBinaryTag contents = SNBT_CODEC.decode(((TextComponent) input).content());
final CompoundBinaryTag tag = contents.getCompound(ITEM_TAG);
return HoverEvent.ShowItem.showItem(
Key.key(contents.getString(ITEM_TYPE)),
contents.getByte(ITEM_COUNT, (byte) 1),
tag == CompoundBinaryTag.empty() ? null : BinaryTagHolder.encode(tag, SNBT_CODEC)
);
return NEW_INSTANCE.deserializeShowItem(input);
}

@Override
public HoverEvent.@NotNull ShowEntity deserializeShowEntity(final @NotNull Component input, final Codec.Decoder<Component, String, ? extends RuntimeException> componentCodec) throws IOException {
assertTextComponent(input);
final CompoundBinaryTag contents = SNBT_CODEC.decode(((TextComponent) input).content());
return HoverEvent.ShowEntity.showEntity(
Key.key(contents.getString(ENTITY_TYPE)),
UUID.fromString(contents.getString(ENTITY_ID)),
componentCodec.decode(contents.getString(ENTITY_NAME))
);
}

private static void assertTextComponent(final Component component) {
if (!(component instanceof TextComponent) || !component.children().isEmpty()) {
throw new IllegalArgumentException("Legacy events must be single Component instances");
}
return NEW_INSTANCE.deserializeShowEntity(input, componentCodec);
}

@Override
public @NotNull Component serializeShowItem(final HoverEvent.@NotNull ShowItem input) throws IOException {
final CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder()
.putString(ITEM_TYPE, input.item().asString())
.putByte(ITEM_COUNT, (byte) input.count());
final @Nullable BinaryTagHolder nbt = input.nbt();
if (nbt != null) {
builder.put(ITEM_TAG, nbt.get(SNBT_CODEC));
}
return Component.text(SNBT_CODEC.encode(builder.build()));
return NEW_INSTANCE.serializeShowItem(input);
}

@Override
public @NotNull Component serializeShowEntity(final HoverEvent.@NotNull ShowEntity input, final Codec.Encoder<Component, String, ? extends RuntimeException> componentCodec) throws IOException {
final CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder()
.putString(ENTITY_ID, input.id().toString())
.putString(ENTITY_TYPE, input.type().asString());
final @Nullable Component name = input.name();
if (name != null) {
builder.putString(ENTITY_NAME, componentCodec.encode(name));
}
return Component.text(SNBT_CODEC.encode(builder.build()));
return NEW_INSTANCE.serializeShowEntity(input, componentCodec);
}
}
5 changes: 1 addition & 4 deletions text-serializer-gson/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
plugins {
id("adventure.common-conventions")
id("adventure.json-impl-conventions")
alias(libs.plugins.jmh)
}

dependencies {
api(projects.adventureApi)
api(libs.gson)
testImplementation(projects.adventureNbt)
annotationProcessor(projects.adventureAnnotationProcessors)
}

applyJarMetadata("net.kyori.adventure.text.serializer.gson")
Original file line number Diff line number Diff line change
Expand Up @@ -53,25 +53,25 @@
import net.kyori.adventure.text.TranslatableComponent;
import org.jetbrains.annotations.Nullable;

final class ComponentSerializerImpl extends TypeAdapter<Component> {
static final String TEXT = "text";
static final String TRANSLATE = "translate";
static final String TRANSLATE_FALLBACK = "fallback";
static final String TRANSLATE_WITH = "with";
static final String SCORE = "score";
static final String SCORE_NAME = "name";
static final String SCORE_OBJECTIVE = "objective";
static final String SCORE_VALUE = "value";
static final String SELECTOR = "selector";
static final String KEYBIND = "keybind";
static final String EXTRA = "extra";
static final String NBT = "nbt";
static final String NBT_INTERPRET = "interpret";
static final String NBT_BLOCK = "block";
static final String NBT_ENTITY = "entity";
static final String NBT_STORAGE = "storage";
static final String SEPARATOR = "separator";
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.EXTRA;
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.KEYBIND;
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.NBT;
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.NBT_BLOCK;
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.NBT_ENTITY;
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.NBT_INTERPRET;
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.NBT_STORAGE;
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.SCORE;
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.SCORE_NAME;
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.SCORE_OBJECTIVE;
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.SCORE_VALUE;
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.SELECTOR;
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.SEPARATOR;
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.TEXT;
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.TRANSLATE;
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.TRANSLATE_FALLBACK;
import static net.kyori.adventure.text.serializer.json.JSONComponentConstants.TRANSLATE_WITH;

final class ComponentSerializerImpl extends TypeAdapter<Component> {
static final Type COMPONENT_LIST_TYPE = new TypeToken<List<Component>>() {}.getType();

static TypeAdapter<Component> create(final Gson gson) {
Expand Down
Loading