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

text-minimessage: Add a builder for PlaceholderResolvers #665

Merged
merged 2 commits into from
Jan 16, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -33,18 +34,30 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import static java.util.Objects.requireNonNull;

/**
* A resolver for user-defined placeholders.
*
* @since 4.10.0
*/
@FunctionalInterface
public interface PlaceholderResolver {
/**
* Create a new builder for a {@link PlaceholderResolver}.
*
* @return a new builder
* @since 4.10.0
*/
static @NotNull Builder builder() {
return new PlaceholderResolverBuilderImpl();
}

/**
* Constructs a placeholder resolver from a map.
*
* <p>The provided map is used as the backing for the returned placeholder resolver.
* This means that changes to the map will be reflected in the placeholder resolver.</p>
* <p>The returned placeholder resolver will make a copy of the provided map.
* This means that changes to the map will not be reflected in the placeholder resolver.</p>
*
* @param map the map
* @return the placeholder resolver
Expand Down Expand Up @@ -110,7 +123,7 @@ public interface PlaceholderResolver {
* @since 4.10.0
*/
static @NotNull PlaceholderResolver combining(final @NotNull Iterable<? extends PlaceholderResolver> resolvers) {
final List<PlaceholderResolver> copiedResolvers = new ArrayList<>();
final List<PlaceholderResolver> copiedResolvers = resolvers instanceof Collection<?> ? new ArrayList<>(((Collection<?>) resolvers).size()) : new ArrayList<>();

for (final PlaceholderResolver resolver : Objects.requireNonNull(resolvers, "resolvers")) {
copiedResolvers.add(Objects.requireNonNull(resolver, "resolvers cannot contain null elements"));
Expand All @@ -133,7 +146,7 @@ public interface PlaceholderResolver {
* @return the placeholder resolver
* @since 4.10.0
*/
static @NotNull PlaceholderResolver dynamic(final @NotNull Function<String, Replacement<?>> resolver) {
static @NotNull PlaceholderResolver dynamic(final @NotNull Function<String, @Nullable Replacement<?>> resolver) {
return new DynamicPlaceholderResolver(Objects.requireNonNull(resolver, "resolver"));
}

Expand All @@ -159,4 +172,101 @@ public interface PlaceholderResolver {
* @since 4.10.0
*/
@Nullable Replacement<?> resolve(final @NotNull String key);

/**
* A builder to gradually construct placeholder resolvers.
*
* <p>Entries added later will take priority over entries added earlier.</p>
*
* @since 4.10.0
*/
interface Builder {
/**
* Add a single placeholder to this resolver.
*
* @param placeholder the placeholder
* @return this builder
* @since 4.10.0
*/
@NotNull Builder placeholder(final @NotNull Placeholder<?> placeholder);

/**
* Add placeholders to this resolver.
*
* @param placeholders placeholders to add
* @return this builder
* @since 4.10.0
*/
default @NotNull Builder placeholders(final @NotNull Placeholder<?> @NotNull... placeholders) {
return this.placeholders(Arrays.asList(requireNonNull(placeholders, "placeholders")));
}

/**
* Add placeholders to this resolver.
*
* @param placeholders placeholders to add
* @return this builder
* @since 4.10.0
*/
@NotNull Builder placeholders(final @NotNull Iterable<Placeholder<?>> placeholders);

/**
* Add placeholders to this resolver.
*
* <p>A snapshot of the map will be added to this resolver, rather than a live view.</p>
*
* @param replacements placeholders to add
* @return this builder
* @since 4.10.0
*/
@NotNull Builder placeholders(final @NotNull Map<String, Replacement<?>> replacements);

/**
* Add a placeholder resolver to those queried by the result of this builder.
*
* @param resolver the resolver to add
* @return this builder
* @since 4.10.0
*/
@NotNull Builder resolver(final @NotNull PlaceholderResolver resolver);

/**
* Add placeholder resolvers to those queried by the result of this builder.
*
* @param resolvers the resolvers to add
* @return this builder
* @since 4.10.0
*/
@NotNull Builder resolvers(final @NotNull PlaceholderResolver@NotNull... resolvers);

/**
* Add placeholder resolvers to those queried by the result of this builder.
*
* @param resolvers the resolvers to add
* @return this builder
* @since 4.10.0
*/
@NotNull Builder resolvers(final @NotNull Iterable<? extends PlaceholderResolver> resolvers);

/**
* Add a resolver that dynamically queries and caches based on the provided function.
*
* @param dynamic the function to query for replacements
* @return this builder
* @since 4.10.0
*/
default @NotNull Builder dynamic(final @NotNull Function<String, @Nullable Replacement<?>> dynamic) {
return this.resolver(PlaceholderResolver.dynamic(dynamic));
}

/**
* Create a placeholder resolver based on the input.
*
* <p>If no elements are added, this may return an empty resolver.</p>
*
* @return the resolver
* @since 4.10.0
*/
@NotNull PlaceholderResolver build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* This file is part of adventure, licensed under the MIT License.
*
* Copyright (c) 2017-2022 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.adventure.text.minimessage.placeholder;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.NotNull;

import static java.util.Objects.requireNonNull;

class PlaceholderResolverBuilderImpl implements PlaceholderResolver.Builder {
private final Map<String, Replacement<?>> replacements = new HashMap<>();
private final List<PlaceholderResolver> resolvers = new ArrayList<>();

@Override
public @NotNull PlaceholderResolver.Builder placeholder(@NotNull final Placeholder<?> placeholder) {
this.replacements.put(
placeholder.key(), placeholder
);
return this;
}

@Override
public @NotNull PlaceholderResolver.Builder placeholders(@NotNull final Iterable<Placeholder<?>> placeholders) {
for (final Placeholder<?> placeholder : requireNonNull(placeholders, "placeholders")) {
this.replacements.put(placeholder.key(), placeholder);
}
return this;
}

@Override
public @NotNull PlaceholderResolver.Builder placeholders(@NotNull final Map<String, Replacement<?>> replacements) {
for (final Map.Entry<String, Replacement<?>> entry : replacements.entrySet()) {
this.replacements.put(
requireNonNull(entry.getKey(), "replacements[?].key()"),
requireNonNull(entry.getValue(), () -> "replacements[" + entry.getKey() + "]")
);
}
return this;
}

@Override
public @NotNull PlaceholderResolver.Builder resolver(@NotNull final PlaceholderResolver resolver) {
this.popMap();
this.resolvers.add(requireNonNull(resolver, "resolver"));
return this;
}

@Override
public @NotNull PlaceholderResolver.Builder resolvers(@NotNull final PlaceholderResolver @NotNull... resolvers) {
this.popMap();
for (final PlaceholderResolver resolver : requireNonNull(resolvers, "resolvers")) {
this.resolvers.add(requireNonNull(resolver, "resolvers[?]"));
}
return this;
}

@Override
public @NotNull PlaceholderResolver.Builder resolvers(@NotNull final Iterable<? extends PlaceholderResolver> resolvers) {
this.popMap();
for (final PlaceholderResolver resolver : requireNonNull(resolvers, "resolvers")) {
this.resolvers.add(requireNonNull(resolver, "resolvers[?]"));
}
return this;
}

private void popMap() {
if (!this.replacements.isEmpty()) {
this.resolvers.add(new MapPlaceholderResolver(new HashMap<>(this.replacements)));
this.replacements.clear();
}
}

@Override
public @NotNull PlaceholderResolver build() {
this.popMap();
if (this.resolvers.size() == 0) {
return EmptyPlaceholderResolver.INSTANCE;
} else if (this.resolvers.size() == 1) {
return this.resolvers.get(0);
} else {
final List<PlaceholderResolver> resolvers = new ArrayList<>(this.resolvers);
Collections.reverse(resolvers);
return new GroupedPlaceholderResolver(resolvers);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* This file is part of adventure, licensed under the MIT License.
*
* Copyright (c) 2017-2022 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.adventure.text.minimessage;

import java.util.Arrays;
import java.util.List;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.placeholder.Placeholder;
import net.kyori.adventure.text.minimessage.placeholder.PlaceholderResolver;
import net.kyori.adventure.text.minimessage.placeholder.Replacement;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class PlaceholderResolverTest {

@Test
void testEmptyBuilder() {
assertEquals(PlaceholderResolver.empty(), PlaceholderResolver.builder().build());
}

@Test
void testBuilderUnpacksSingleElement() {
final PlaceholderResolver test = key -> Replacement.miniMessage("hello");
assertEquals(test, PlaceholderResolver.builder().resolver(test).build());
}

@Test
void testSingleAndResolversCombine() {
final List<Placeholder<?>> placeholders = Arrays.asList(
Placeholder.component("foo", Component.text("fizz")),
Placeholder.miniMessage("overlapping", "from list")
);
final PlaceholderResolver resolver = key -> {
switch (key) {
case "one": return Replacement.miniMessage("fish");
case "overlapping": return Replacement.miniMessage("from resolver");
default: return null;
}
};

final PlaceholderResolver built = PlaceholderResolver.builder()
.placeholders(placeholders)
.resolver(resolver)
.build();

// from placeholders only
assertEquals(Component.text("fizz"), built.resolve("foo").value());

// from resolver only
assertEquals("fish", built.resolve("one").value());

// shared, resolver takes priority
assertEquals("from resolver", built.resolve("overlapping").value());
}

}