From 417989aea1afadc36bb9ac6692ac13ae5fb5b3b3 Mon Sep 17 00:00:00 2001 From: lucko Date: Thu, 21 May 2026 18:50:01 +0100 Subject: [PATCH] Add common placeholders module (#4267) --- common/placeholders/build.gradle | 45 ++ .../common/placeholders/Placeholder.java | 117 +++++ .../placeholders/PlaceholderContext.java | 110 +++++ .../placeholders/PlaceholderRegistry.java | 100 ++++ .../placeholders/PlaceholderResolver.java | 106 ++++ .../common/placeholders/Placeholders.java | 461 ++++++++++++++++++ .../placeholders/PlaceholderRegistryTest.java | 88 ++++ .../placeholders/PlaceholderResolverTest.java | 69 +++ .../common/placeholders/PlaceholderTest.java | 56 +++ .../common/placeholders/PlaceholdersTest.java | 105 ++++ .../org.mockito.plugins.MockMaker | 1 + settings.gradle | 1 + 12 files changed, 1259 insertions(+) create mode 100644 common/placeholders/build.gradle create mode 100644 common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/Placeholder.java create mode 100644 common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderContext.java create mode 100644 common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderRegistry.java create mode 100644 common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderResolver.java create mode 100644 common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/Placeholders.java create mode 100644 common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderRegistryTest.java create mode 100644 common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderResolverTest.java create mode 100644 common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderTest.java create mode 100644 common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholdersTest.java create mode 100644 common/placeholders/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/common/placeholders/build.gradle b/common/placeholders/build.gradle new file mode 100644 index 0000000000..1551982384 --- /dev/null +++ b/common/placeholders/build.gradle @@ -0,0 +1,45 @@ +plugins { + id("java-library") + id("jacoco") +} + +test { + useJUnitPlatform() +} + +jacocoTestReport { + dependsOn test +} + +dependencies { + api project(':api') + + compileOnly 'org.checkerframework:checker-qual:3.49.3' + compileOnly 'org.jetbrains:annotations:26.0.2' + + testImplementation 'org.junit.jupiter:junit-jupiter:5.13.0' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + testImplementation 'org.testcontainers:testcontainers-junit-jupiter:2.0.2' + testImplementation 'org.mockito:mockito-core:5.18.0' + testImplementation 'org.mockito:mockito-junit-jupiter:5.18.0' +} + +publishing { + //repositories { + // maven { + // url = 'https://nexus.lucko.me/repository/maven-snapshots/' + // credentials { + // username = luckoNexusUsername + // password = luckoNexusPassword + // } + // } + //} + publications { + mavenJava(MavenPublication) { + from components.java + artifactId = 'common-placeholders' + version = "${project.ext.fullVersion}-SNAPSHOT" + } + } +} \ No newline at end of file diff --git a/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/Placeholder.java b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/Placeholder.java new file mode 100644 index 0000000000..dbb7e6fafa --- /dev/null +++ b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/Placeholder.java @@ -0,0 +1,117 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * 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 me.lucko.luckperms.common.placeholders; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.jetbrains.annotations.NotNull; + +/** + * A placeholder definition. + */ +public interface Placeholder { + + /** + * Get the id of the placeholder. + * + * @return the id + */ + @NonNull String id(); + + /** + * A placeholder function that uses the standard {@link PlaceholderContext} + */ + interface BasicPlaceholderFunction { + /** + * Resolve the value of this placeholder with some given context. + * + * @param ctx the context + * @return the resolved value + */ + @NonNull String resolve(@NonNull PlaceholderContext ctx); + } + + /** + * A placeholder function that uses the extended {@link PlaceholderContext.WithArgument} + */ + interface UsingArgumentPlaceholderFunction { + /** + * Resolve the value of this placeholder with some given context. + * + * @param ctx the context + * @return the resolved value + */ + @NonNull String resolve(PlaceholderContext.@NonNull WithArgument ctx); + } + + /** A basic placeholder */ + interface Basic extends Placeholder, BasicPlaceholderFunction {} + + /** A placeholder that uses an argument */ + interface UsingArgument extends Placeholder, UsingArgumentPlaceholderFunction {} + + /** + * Create a standard placeholder using the given resolver function. + * + * @param id the id + * @param resolver the resolver function + * @return the placeholder + */ + static Basic basic(@NonNull String id, Placeholder.@NonNull BasicPlaceholderFunction resolver) { + return new Basic() { + @Override + public @NonNull String id() { + return id; + } + + @Override + public @NotNull String resolve(@NotNull PlaceholderContext ctx) { + return resolver.resolve(ctx); + } + }; + } + + /** + * Create a dynamic placeholder using the given resolver function. + * + * @param id the id + * @param resolver the resolver function + * @return the placeholder + */ + static UsingArgument usingArgument(@NonNull String id, Placeholder.@NonNull UsingArgumentPlaceholderFunction resolver) { + return new UsingArgument() { + @Override + public @NonNull String id() { + return id; + } + + @Override + public @NotNull String resolve(PlaceholderContext.@NotNull WithArgument ctx) { + return resolver.resolve(ctx); + } + }; + } + +} diff --git a/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderContext.java b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderContext.java new file mode 100644 index 0000000000..d1b2bd75d7 --- /dev/null +++ b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderContext.java @@ -0,0 +1,110 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * 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 me.lucko.luckperms.common.placeholders; + +import net.luckperms.api.LuckPerms; +import net.luckperms.api.cacheddata.CachedDataManager; +import net.luckperms.api.cacheddata.CachedMetaData; +import net.luckperms.api.cacheddata.CachedPermissionData; +import net.luckperms.api.model.user.User; +import net.luckperms.api.query.QueryOptions; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * The context passed to a {@link Placeholder} resolve request. + */ +public class PlaceholderContext { + + /** The LuckPerms API instance */ + private final @NonNull LuckPerms api; + /** The user for the player the placeholder is being resolved for */ + private final @NonNull User user; + /** The query options for the player the placeholder is being resolved for */ + private final @NonNull QueryOptions queryOptions; + + public PlaceholderContext(@NonNull LuckPerms api, @NonNull User user, @NonNull QueryOptions queryOptions) { + this.api = api; + this.user = user; + this.queryOptions = queryOptions; + } + + public @NonNull LuckPerms api() { + return this.api; + } + + public @NonNull User user() { + return this.user; + } + + public @NonNull QueryOptions queryOptions() { + return this.queryOptions; + } + + public @NonNull CachedDataManager userData() { + return this.user.getCachedData(); + } + + public @NonNull CachedPermissionData permissionData() { + return this.user.getCachedData().getPermissionData(this.queryOptions); + } + + public @NonNull CachedMetaData metaData() { + return this.user.getCachedData().getMetaData(this.queryOptions); + } + + /** + * Create a copy of this placeholder context, additionally including an argument. + * + * @param argument the argument + * @return the new context + */ + public WithArgument withArgument(@NonNull String argument) { + return new WithArgument(this.api, this.user, this.queryOptions, argument); + } + + /** + * Extension of {@link PlaceholderContext} with an extra dynamic argument provided by the requester. + */ + public static class WithArgument extends PlaceholderContext { + + /** An additional argument passed to the placeholder resolve request */ + private final @NonNull String argument; + + public WithArgument(@NonNull LuckPerms api, @NonNull User user, @NonNull QueryOptions queryOptions, @NonNull String argument) { + super(api, user, queryOptions); + this.argument = argument; + } + + /** + * Gets the argument. + * + * @return the argument + */ + public @NonNull String argument() { + return this.argument; + } + } +} diff --git a/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderRegistry.java b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderRegistry.java new file mode 100644 index 0000000000..bde5198594 --- /dev/null +++ b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderRegistry.java @@ -0,0 +1,100 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * 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 me.lucko.luckperms.common.placeholders; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * A registry of standard/built-in {@link Placeholder}s. + */ +public class PlaceholderRegistry { + + private static final List PLACEHOLDERS = Collections.unmodifiableList(Arrays.asList( + Placeholders.PREFIX, + Placeholders.SUFFIX, + Placeholders.ALL_META, + Placeholders.META, + Placeholders.PREFIX_ELEMENT, + Placeholders.SUFFIX_ELEMENT, + Placeholders.ALL_CONTEXT, + Placeholders.CONTEXT, + Placeholders.GROUPS, + Placeholders.INHERITED_GROUPS, + Placeholders.PRIMARY_GROUP_NAME, + Placeholders.HAS_PERMISSION, + Placeholders.INHERITS_PERMISSION, + Placeholders.CHECK_PERMISSION, + Placeholders.IN_GROUP, + Placeholders.INHERITS_GROUP, + Placeholders.ON_TRACK, + Placeholders.HAS_GROUPS_ON_TRACK, + Placeholders.HIGHEST_GROUP_BY_WEIGHT, + Placeholders.LOWEST_GROUP_BY_WEIGHT, + Placeholders.HIGHEST_INHERITED_GROUP_BY_WEIGHT, + Placeholders.LOWEST_INHERITED_GROUP_BY_WEIGHT, + Placeholders.HIGHEST_GROUP_WEIGHT, + Placeholders.CURRENT_GROUP_ON_TRACK, + Placeholders.NEXT_GROUP_ON_TRACK, + Placeholders.PREVIOUS_GROUP_ON_TRACK, + Placeholders.FIRST_GROUP_ON_TRACKS, + Placeholders.LAST_GROUP_ON_TRACKS, + Placeholders.EXPIRY_TIME, + Placeholders.INHERITED_EXPIRY_TIME, + Placeholders.GROUP_EXPIRY_TIME, + Placeholders.INHERITED_GROUP_EXPIRY_TIME + )); + + private static final Map PLACEHOLDER_MAP = PLACEHOLDERS.stream() + .collect(Collectors.toMap(Placeholder::id, Function.identity())); + + /** + * Get a list of all placeholders. + * + * @return a list of placeholders + */ + public static @NonNull List getAll() { + return PLACEHOLDERS; + } + + /** + * Lookup a placeholder by id. + * + * @param id the id to lookup + * @return the placeholder, if found + */ + public static @Nullable Placeholder lookup(@NonNull String id) { + return PLACEHOLDER_MAP.get(id); + } + +} diff --git a/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderResolver.java b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderResolver.java new file mode 100644 index 0000000000..5eed8ad0ec --- /dev/null +++ b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderResolver.java @@ -0,0 +1,106 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * 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 me.lucko.luckperms.common.placeholders; + +import java.util.Collection; +import java.util.Locale; + +/** + * Resolves placeholders using a simple string format: + * + *

+ *

    + *
  • placeholder
  • + *
  • placeholder_argument
  • + *
+ *

+ * + *

Note: this resolver does not parse placeholders mid-string, it expects to receive the parsed + * placeholder string as input.

+ */ +public class PlaceholderResolver { + + /** The placeholders used by this resolver */ + private final Collection placeholders; + + /** + * Create a resolver using the built-in placeholders registered in {@link PlaceholderRegistry}. + */ + public PlaceholderResolver() { + this(PlaceholderRegistry.getAll()); + } + + /** + * Create a resolver using a custom list of placeholders. + * + * @param placeholders the placeholders + */ + public PlaceholderResolver(Collection placeholders) { + this.placeholders = placeholders; + } + + /** + * Resolve the placeholder value of a given input string + * + * @param input the input string + * @return the resolved value, or null if no placeholder matched + */ + public String resolve(PlaceholderContext ctx, String input) { + input = input.toLowerCase(Locale.ROOT); + for (Placeholder placeholder : this.placeholders) { + String result = attemptResolve(ctx, input, placeholder); + if (result != null) { + return result; + } + } + return null; + } + + /** + * Attempt to resolve the placeholder value of a given input string for a specific placeholder. + * + * @param ctx the placeholder context + * @param input the input string + * @param placeholder the placeholder to attempt to resolve with + * @return the resolved value if the placeholder matches the input, or null if it does not match + */ + protected String attemptResolve(PlaceholderContext ctx, String input, Placeholder placeholder) { + String id = placeholder.id(); + if (placeholder instanceof Placeholder.Basic) { + if (input.equals(id)) { + return ((Placeholder.Basic) placeholder).resolve(ctx); + } + } else if (placeholder instanceof Placeholder.UsingArgument) { + if (input.startsWith(id + "_") && input.length() > (id.length() + 1)) { + String argument = input.substring(id.length() + 1); + return ((Placeholder.UsingArgument) placeholder).resolve(ctx.withArgument(argument)); + } + } else { + throw new IllegalArgumentException("Unknown placeholder type: " + placeholder.getClass()); + } + return null; + } +} diff --git a/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/Placeholders.java b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/Placeholders.java new file mode 100644 index 0000000000..0979aed198 --- /dev/null +++ b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/Placeholders.java @@ -0,0 +1,461 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * 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 me.lucko.luckperms.common.placeholders; + +import net.luckperms.api.metastacking.DuplicateRemovalFunction; +import net.luckperms.api.metastacking.MetaStackDefinition; +import net.luckperms.api.metastacking.MetaStackElement; +import net.luckperms.api.model.group.Group; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeType; +import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.track.Track; +import org.jetbrains.annotations.VisibleForTesting; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A class containing the standard, built-in placeholders. + */ +public final class Placeholders { + private Placeholders() { + throw new AssertionError(); + } + + // Basic meta placeholders + + /** Outputs the user's prefix. */ + public static final Placeholder.Basic PREFIX = Placeholder.basic("prefix", (ctx) -> stringNullToEmpty(ctx.metaData().getPrefix())); + + /** Outputs the user's suffix. */ + public static final Placeholder.Basic SUFFIX = Placeholder.basic("suffix", (ctx) -> stringNullToEmpty(ctx.metaData().getSuffix())); + + /** Outputs all meta values for a given key, separated by commas. */ + public static final Placeholder.UsingArgument ALL_META = Placeholder.usingArgument("all_meta", (ctx) -> { + List values = ctx.metaData().getMeta().getOrDefault(ctx.argument(), Collections.emptyList()); + return values.isEmpty() ? "" : String.join(", ", values); + }); + + /** Outputs a specific meta value for a given key. */ + public static final Placeholder.UsingArgument META = Placeholder.usingArgument("meta", (ctx) -> stringNullToEmpty(ctx.metaData().getMetaValue(ctx.argument()))); + + // Meta stack element placeholders + /** Outputs the user's prefix from a specific meta stack element. */ + public static final Placeholder.UsingArgument PREFIX_ELEMENT = Placeholder.usingArgument("prefix_element", (ctx) -> { + MetaStackElement stackElement = ctx.api().getMetaStackFactory().fromString(ctx.argument()).orElse(null); + if (stackElement == null) { + throw new IllegalArgumentException("Invalid meta stack element: " + ctx.argument()); + } + + MetaStackDefinition stackDefinition = ctx.api().getMetaStackFactory().createDefinition( + Collections.singletonList(stackElement), DuplicateRemovalFunction.RETAIN_ALL, "", "", ""); + QueryOptions newOptions = ctx.queryOptions().toBuilder() + .option(MetaStackDefinition.PREFIX_STACK_KEY, stackDefinition) + .option(MetaStackDefinition.SUFFIX_STACK_KEY, stackDefinition) + .build(); + + return stringNullToEmpty(ctx.userData().getMetaData(newOptions).getPrefix()); + }); + + /** Outputs the user's suffix from a specific meta stack element. */ + public static final Placeholder.UsingArgument SUFFIX_ELEMENT = Placeholder.usingArgument("suffix_element", (ctx) -> { + MetaStackElement stackElement = ctx.api().getMetaStackFactory().fromString(ctx.argument()).orElse(null); + if (stackElement == null) { + throw new IllegalArgumentException("Invalid meta stack element: " + ctx.argument()); + } + + MetaStackDefinition stackDefinition = ctx.api().getMetaStackFactory().createDefinition( + Collections.singletonList(stackElement), DuplicateRemovalFunction.RETAIN_ALL, "", "", ""); + QueryOptions newOptions = ctx.queryOptions().toBuilder() + .option(MetaStackDefinition.PREFIX_STACK_KEY, stackDefinition) + .option(MetaStackDefinition.SUFFIX_STACK_KEY, stackDefinition) + .build(); + + return Objects.toString(ctx.userData().getMetaData(newOptions).getSuffix(), ""); + }); + + // Context placeholders + + /** Outputs all context key-value pairs, separated by commas. */ + public static final Placeholder.Basic ALL_CONTEXT = Placeholder.basic("all_context", (ctx) -> + ctx.queryOptions().context().toSet().stream() + .map(c -> c.getKey() + "=" + c.getValue()) + .collect(Collectors.joining(", ")) + ); + + /** Outputs all values for a specific context key, separated by commas. */ + public static final Placeholder.UsingArgument CONTEXT = Placeholder.usingArgument("context", (ctx) -> String.join(", ", ctx.queryOptions().context().getValues(ctx.argument()))); + + // Group placeholders + + /** Outputs the user's directly assigned groups, separated by commas. */ + public static final Placeholder.Basic GROUPS = Placeholder.basic("groups", (ctx) -> + ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .map(InheritanceNode::getGroupName) + .map(name -> convertGroupDisplayName(ctx, name)) + .collect(Collectors.joining(", ")) + ); + + /** Outputs all groups the user inherits from, separated by commas. */ + public static final Placeholder.Basic INHERITED_GROUPS = Placeholder.basic("inherited_groups", (ctx) -> + ctx.user().getInheritedGroups(ctx.queryOptions()).stream() + .map(Group::getFriendlyName) + .collect(Collectors.joining(", ")) + ); + + /** Outputs the user's primary group name. */ + public static final Placeholder.Basic PRIMARY_GROUP_NAME = Placeholder.basic("primary_group_name", (ctx) -> + convertGroupDisplayName(ctx, ctx.user().getPrimaryGroup()) + ); + + // Permission check placeholders + /** Checks if the user has a specific permission node directly assigned (outputs "true" or "false"). */ + public static final Placeholder.UsingArgument HAS_PERMISSION = Placeholder.usingArgument("has_permission", (ctx) -> + String.valueOf(ctx.user().getNodes().stream() + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .anyMatch(n -> n.getKey().equals(ctx.argument()))) + ); + + /** Checks if the user inherits a specific permission node (outputs "true" or "false"). */ + public static final Placeholder.UsingArgument INHERITS_PERMISSION = Placeholder.usingArgument("inherits_permission", (ctx) -> + String.valueOf(ctx.user().resolveInheritedNodes(ctx.queryOptions()).stream() + .filter(n -> n.getContexts().isSatisfiedBy(ctx.queryOptions().context())) + .anyMatch(n -> n.getKey().equals(ctx.argument()))) + ); + + /** Checks the result of a permission check (outputs "true" or "false"). */ + public static final Placeholder.UsingArgument CHECK_PERMISSION = Placeholder.usingArgument("check_permission", (ctx) -> + String.valueOf(ctx.permissionData().checkPermission(ctx.argument()).asBoolean()) + ); + + /** Checks if the user is directly in a specific group (outputs "true" or "false"). */ + public static final Placeholder.UsingArgument IN_GROUP = Placeholder.usingArgument("in_group", (ctx) -> + String.valueOf(ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .map(InheritanceNode::getGroupName) + .anyMatch(s -> s.equalsIgnoreCase(ctx.argument()))) + ); + + /** Checks if the user inherits from a specific group (outputs "true" or "false"). */ + public static final Placeholder.UsingArgument INHERITS_GROUP = Placeholder.usingArgument("inherits_group", (ctx) -> + String.valueOf(ctx.user().getInheritedGroups(ctx.queryOptions()).stream() + .anyMatch(g -> g.getName().equalsIgnoreCase(ctx.argument()))) + ); + + // Track placeholders + /** Checks if the user's primary group is on a specific track (outputs "true" or "false"). */ + public static final Placeholder.UsingArgument ON_TRACK = Placeholder.usingArgument("on_track", (ctx) -> + String.valueOf(Optional.ofNullable(ctx.api().getTrackManager().getTrack(ctx.argument())) + .map(t -> t.containsGroup(ctx.user().getPrimaryGroup())) + .orElse(false)) + ); + + /** Checks if the user has any groups on a specific track (outputs "true" or "false"). */ + public static final Placeholder.UsingArgument HAS_GROUPS_ON_TRACK = Placeholder.usingArgument("has_groups_on_track", (ctx) -> + String.valueOf(Optional.ofNullable(ctx.api().getTrackManager().getTrack(ctx.argument())) + .map(t -> ctx.user().getNodes(NodeType.INHERITANCE).stream() + .map(InheritanceNode::getGroupName) + .anyMatch(t::containsGroup) + ) + .orElse(false)) + ); + + // Group weight placeholders + + /** Outputs the name of the user's highest weighted directly assigned group. */ + public static final Placeholder.Basic HIGHEST_GROUP_BY_WEIGHT = Placeholder.basic("highest_group_by_weight", (ctx) -> + ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .map(InheritanceNode::getGroupName) + .map(n -> ctx.api().getGroupManager().getGroup(n)) + .filter(Objects::nonNull) + .max(Comparator.comparingInt(g -> g.getWeight().orElse(0))) + .map(Group::getName) + .map(name -> convertGroupDisplayName(ctx, name)) + .orElse("") + ); + + /** Outputs the name of the user's lowest weighted directly assigned group. */ + public static final Placeholder.Basic LOWEST_GROUP_BY_WEIGHT = Placeholder.basic("lowest_group_by_weight", (ctx) -> + ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .map(InheritanceNode::getGroupName) + .map(n -> ctx.api().getGroupManager().getGroup(n)) + .filter(Objects::nonNull) + .min(Comparator.comparingInt(g -> g.getWeight().orElse(0))) + .map(Group::getName) + .map(name -> convertGroupDisplayName(ctx, name)) + .orElse("") + ); + + /** Outputs the name of the user's highest weighted inherited group. */ + public static final Placeholder.Basic HIGHEST_INHERITED_GROUP_BY_WEIGHT = Placeholder.basic("highest_inherited_group_by_weight", (ctx) -> + ctx.user().getInheritedGroups(ctx.queryOptions()).stream() + .max(Comparator.comparingInt(g -> g.getWeight().orElse(0))) + .map(Group::getName) + .map(name -> convertGroupDisplayName(ctx, name)) + .orElse("") + ); + + /** Outputs the name of the user's lowest weighted inherited group. */ + public static final Placeholder.Basic LOWEST_INHERITED_GROUP_BY_WEIGHT = Placeholder.basic("lowest_inherited_group_by_weight", (ctx) -> + ctx.user().getInheritedGroups(ctx.queryOptions()).stream() + .min(Comparator.comparingInt(g -> g.getWeight().orElse(0))) + .map(Group::getName) + .map(name -> convertGroupDisplayName(ctx, name)) + .orElse("") + ); + + /** Outputs the weight value of the user's highest weighted directly assigned group. */ + public static final Placeholder.Basic HIGHEST_GROUP_WEIGHT = Placeholder.basic("highest_group_weight", (ctx) -> + String.valueOf(ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .map(InheritanceNode::getGroupName) + .map(n -> ctx.api().getGroupManager().getGroup(n)) + .filter(Objects::nonNull) + .map(Group::getWeight) + .filter(OptionalInt::isPresent) + .mapToInt(OptionalInt::getAsInt) + .max() + .orElse(0)) + ); + + // Track position placeholders + + /** Outputs the user's current group on a specific track. */ + public static final Placeholder.UsingArgument CURRENT_GROUP_ON_TRACK = Placeholder.usingArgument("current_group_on_track", (ctx) -> { + Track track = ctx.api().getTrackManager().getTrack(ctx.argument()); + if (track == null) { + return ""; + } + + List groups = ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(n -> track.containsGroup(n.getGroupName())) + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .distinct() + .map(n -> ctx.api().getGroupManager().getGroup(n.getGroupName())) + .collect(Collectors.toList()); + + if (groups.size() != 1) { + return ""; + } + + return groups.get(0).getFriendlyName(); + }); + + /** Outputs the next group on a specific track. */ + public static final Placeholder.UsingArgument NEXT_GROUP_ON_TRACK = Placeholder.usingArgument("next_group_on_track", (ctx) -> { + Track track = ctx.api().getTrackManager().getTrack(ctx.argument()); + if (track == null || track.getGroups().size() <= 1) { + return ""; + } + + List groups = ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(n -> track.containsGroup(n.getGroupName())) + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .distinct() + .map(n -> ctx.api().getGroupManager().getGroup(n.getGroupName())) + .collect(Collectors.toList()); + + if (groups.size() != 1) { + return ""; + } + + return Objects.toString(convertGroupDisplayName(ctx, track.getNext(groups.get(0))), ""); + }); + + /** Outputs the previous group on a specific track. */ + public static final Placeholder.UsingArgument PREVIOUS_GROUP_ON_TRACK = Placeholder.usingArgument("previous_group_on_track", (ctx) -> { + Track track = ctx.api().getTrackManager().getTrack(ctx.argument()); + if (track == null || track.getGroups().size() <= 1) { + return ""; + } + + List groups = ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(n -> track.containsGroup(n.getGroupName())) + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .distinct() + .map(n -> ctx.api().getGroupManager().getGroup(n.getGroupName())) + .collect(Collectors.toList()); + + if (groups.size() != 1) { + return ""; + } + + return Objects.toString(convertGroupDisplayName(ctx, track.getPrevious(groups.get(0))), ""); + }); + + /** Outputs the first group the user has on a comma-separated list of tracks. */ + public static final Placeholder.UsingArgument FIRST_GROUP_ON_TRACKS = Placeholder.usingArgument("first_group_on_tracks", (ctx) -> { + List tracks = Arrays.stream(ctx.argument().split(",")).map(String::trim).collect(Collectors.toList()); + Set groups = ctx.user().getInheritedGroups(ctx.queryOptions()).stream().map(Group::getName).collect(Collectors.toSet()); + + return tracks.stream() + .map(n -> ctx.api().getTrackManager().getTrack(n)) + .filter(Objects::nonNull) + .map(Track::getGroups) + .map(trackGroups -> trackGroups.stream().filter(groups::contains).findFirst()) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .map(name -> convertGroupDisplayName(ctx, name)) + .orElse(""); + }); + + /** Outputs the last group the user has on a comma-separated list of tracks. */ + public static final Placeholder.UsingArgument LAST_GROUP_ON_TRACKS = Placeholder.usingArgument("last_group_on_tracks", (ctx) -> { + List tracks = Arrays.stream(ctx.argument().split(",")).map(String::trim).collect(Collectors.toList()); + Set groups = ctx.user().getInheritedGroups(ctx.queryOptions()).stream().map(Group::getName).collect(Collectors.toSet()); + + return tracks.stream() + .map(n -> ctx.api().getTrackManager().getTrack(n)) + .filter(Objects::nonNull) + .map(Track::getGroups) + .map(list -> { + List copy = new ArrayList<>(list); + Collections.reverse(copy); + return copy; + }) + .map(trackGroups -> trackGroups.stream().filter(groups::contains).findFirst()) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .map(name -> convertGroupDisplayName(ctx, name)) + .orElse(""); + }); + + // Expiry time placeholders + + /** Outputs the expiry time remaining for a specific permission node. */ + public static final Placeholder.UsingArgument EXPIRY_TIME = Placeholder.usingArgument("expiry_time", (ctx) -> + ctx.user().getNodes().stream() + .filter(Node::hasExpiry) + .filter(n -> n.getKey().equals(ctx.argument())) + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .map(Node::getExpiryDuration) + .filter(Objects::nonNull) + .filter(d -> !d.isNegative()) + .findFirst() + .map(Placeholders::formatDuration) + .orElse("") + ); + + /** Outputs the expiry time remaining for a specific inherited permission node. */ + public static final Placeholder.UsingArgument INHERITED_EXPIRY_TIME = Placeholder.usingArgument("inherited_expiry_time", (ctx) -> + ctx.user().resolveInheritedNodes(ctx.queryOptions()).stream() + .filter(Node::hasExpiry) + .filter(n -> n.getKey().equals(ctx.argument())) + .map(Node::getExpiryDuration) + .filter(Objects::nonNull) + .filter(d -> !d.isNegative()) + .findFirst() + .map(Placeholders::formatDuration) + .orElse("") + ); + + /** Outputs the expiry time remaining for a specific group membership. */ + public static final Placeholder.UsingArgument GROUP_EXPIRY_TIME = Placeholder.usingArgument("group_expiry_time", (ctx) -> + ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(Node::hasExpiry) + .filter(n -> n.getGroupName().equals(ctx.argument())) + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .map(Node::getExpiryDuration) + .filter(Objects::nonNull) + .filter(d -> !d.isNegative()) + .findFirst() + .map(Placeholders::formatDuration) + .orElse("") + ); + + /** Outputs the expiry time remaining for a specific inherited group membership. */ + public static final Placeholder.UsingArgument INHERITED_GROUP_EXPIRY_TIME = Placeholder.usingArgument("inherited_group_expiry_time", (ctx) -> + ctx.user().resolveInheritedNodes(ctx.queryOptions()).stream() + .filter(Node::hasExpiry) + .filter(NodeType.INHERITANCE::matches) + .map(NodeType.INHERITANCE::cast) + .filter(n -> n.getGroupName().equals(ctx.argument())) + .map(Node::getExpiryDuration) + .filter(Objects::nonNull) + .filter(d -> !d.isNegative()) + .findFirst() + .map(Placeholders::formatDuration) + .orElse("") + ); + + private static String stringNullToEmpty(String string) { + return string == null ? "" : string; + } + + private static String convertGroupDisplayName(PlaceholderContext ctx, String groupName) { + Group group = ctx.api().getGroupManager().getGroup(groupName); + return group != null ? group.getFriendlyName() : groupName; + } + + // simple version of me.lucko.luckperms.common.util.DurationFormatter + @VisibleForTesting + static String formatDuration(Duration duration) { + if (duration == null || duration.isNegative()) { + return ""; + } + + long seconds = duration.getSeconds(); + StringBuilder builder = new StringBuilder(); + + ChronoUnit[] units = {ChronoUnit.YEARS, ChronoUnit.MONTHS, ChronoUnit.WEEKS, + ChronoUnit.DAYS, ChronoUnit.HOURS, ChronoUnit.MINUTES, ChronoUnit.SECONDS}; + String[] labels = {"y", "mo", "w", "d", "h", "m", "s"}; + + for (int i = 0; i < units.length; i++) { + long unitSeconds = units[i].getDuration().getSeconds(); + long n = seconds / unitSeconds; + if (n > 0) { + seconds -= unitSeconds * n; + if (builder.length() > 0) { + builder.append(" "); + } + builder.append(n).append(labels[i]); + } + if (seconds <= 0) { + break; + } + } + + return builder.length() == 0 ? "0s" : builder.toString(); + } +} diff --git a/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderRegistryTest.java b/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderRegistryTest.java new file mode 100644 index 0000000000..acce7a7324 --- /dev/null +++ b/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderRegistryTest.java @@ -0,0 +1,88 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * 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 me.lucko.luckperms.common.placeholders; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +public class PlaceholderRegistryTest { + + @Test + public void testAllPlaceholdersAreRegistered() { + List returnedByAllMethod = PlaceholderRegistry.getAll(); + Map inClass = Arrays.stream(Placeholders.class.getDeclaredFields()) + .filter(f -> Placeholder.class.isAssignableFrom(f.getType())) + .collect(Collectors.toMap(Field::getName, f -> { + try { + return (Placeholder) f.get(null); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }, (a, b) -> { throw new UnsupportedOperationException(); }, LinkedHashMap::new)); + + assertEquals(new ArrayList<>(inClass.values()), returnedByAllMethod); + inClass.forEach((fieldName, placeholder) -> + assertEquals(fieldName.toLowerCase(Locale.ROOT), placeholder.id(), "Placeholder " + fieldName + " has an id that doesn't match its field name") + ); + } + + @Test + public void testPlaceholdersDontOverlap() { + for (Placeholder placeholder : PlaceholderRegistry.getAll()) { + for (Placeholder other : PlaceholderRegistry.getAll()) { + if (placeholder == other) { + continue; + } + + assertNotEquals(placeholder.id(), other.id(), "Placeholder " + placeholder + " has the same id as " + other); + assertFalse(other instanceof Placeholder.UsingArgument && placeholder.id().startsWith(other.id()), "Placeholder " + placeholder.id() + " has an id that overlaps with " + other.id()); + } + } + } + + @Test + public void testRegistryLookup() { + Placeholder value = PlaceholderRegistry.lookup("prefix_element"); + assertSame(Placeholders.PREFIX_ELEMENT, value); + + assertNull(PlaceholderRegistry.lookup("non_existent")); + } + +} diff --git a/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderResolverTest.java b/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderResolverTest.java new file mode 100644 index 0000000000..cdfe998a4e --- /dev/null +++ b/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderResolverTest.java @@ -0,0 +1,69 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * 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 me.lucko.luckperms.common.placeholders; + +import net.luckperms.api.LuckPerms; +import net.luckperms.api.model.user.User; +import net.luckperms.api.query.QueryOptions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; + +public class PlaceholderResolverTest { + + private final List placeholders = Arrays.asList( + Placeholder.basic("test_simple", ctx -> "hello world"), + Placeholder.usingArgument("test_arg", ctx -> "hello " + ctx.argument()) + ); + private final PlaceholderContext ctx = new PlaceholderContext(mock(LuckPerms.class), mock(User.class), mock(QueryOptions.class)); + private final PlaceholderResolver resolver = new PlaceholderResolver(this.placeholders); + + @Test + public void testNullResolve() { + assertNull(this.resolver.resolve(this.ctx, "non_existent")); + assertNull(this.resolver.resolve(this.ctx, "")); + + assertNull(this.resolver.resolve(this.ctx, "test_simple_hello")); // reject basic with arg provided + assertNull(this.resolver.resolve(this.ctx, "test_arg")); // reject usingArgument without arg provided + assertNull(this.resolver.resolve(this.ctx, "test_arg_")); // reject usingArgument without arg provided + } + + @Test + public void testBasicResolve() { + assertEquals("hello world", this.resolver.resolve(this.ctx, "test_simple")); + } + + @Test + public void testUsingArgumentResolve() { + assertEquals("hello there", this.resolver.resolve(this.ctx, "test_arg_there")); + } + +} diff --git a/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderTest.java b/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderTest.java new file mode 100644 index 0000000000..362e492f8e --- /dev/null +++ b/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderTest.java @@ -0,0 +1,56 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * 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 me.lucko.luckperms.common.placeholders; + +import net.luckperms.api.LuckPerms; +import net.luckperms.api.model.user.User; +import net.luckperms.api.query.QueryOptions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +public class PlaceholderTest { + + private final PlaceholderContext ctx = new PlaceholderContext(mock(LuckPerms.class), mock(User.class), mock(QueryOptions.class)); + + @Test + public void testBasic() { + Placeholder.Basic placeholder = Placeholder.basic("test", ctx -> "hello"); + + assertEquals("test", placeholder.id()); + assertEquals("hello", placeholder.resolve(this.ctx)); + } + + @Test + public void testUsingArgument() { + Placeholder.UsingArgument placeholder = Placeholder.usingArgument("test", ctx -> "hello " + ctx.argument()); + + assertEquals("test", placeholder.id()); + assertEquals("hello world", placeholder.resolve(this.ctx.withArgument("world"))); + } + +} diff --git a/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholdersTest.java b/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholdersTest.java new file mode 100644 index 0000000000..d1ca22a1bd --- /dev/null +++ b/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholdersTest.java @@ -0,0 +1,105 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * 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 me.lucko.luckperms.common.placeholders; + +import net.luckperms.api.LuckPerms; +import net.luckperms.api.cacheddata.CachedDataManager; +import net.luckperms.api.cacheddata.CachedMetaData; +import net.luckperms.api.cacheddata.CachedPermissionData; +import net.luckperms.api.model.user.User; +import net.luckperms.api.query.QueryOptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class PlaceholdersTest { + + @Mock private LuckPerms api; + @Mock private User user; + @Mock private QueryOptions queryOptions; + + @Mock private CachedDataManager cachedDataManager; + @Mock private CachedPermissionData cachedPermissionData; + @Mock private CachedMetaData cachedMetaData; + + private PlaceholderContext ctx; + + @BeforeEach + public void setupMocks() { + lenient().when(this.user.getCachedData()).thenReturn(this.cachedDataManager); + lenient().when(this.cachedDataManager.getPermissionData(this.queryOptions)).thenReturn(this.cachedPermissionData); + lenient().when(this.cachedDataManager.getMetaData(this.queryOptions)).thenReturn(this.cachedMetaData); + + this.ctx = new PlaceholderContext(this.api, this.user, this.queryOptions); + } + + // test some of the basic / simple / most used placeholders - the others are too difficult to test + // using mocks only. + + @Test + public void testPrefix() { + when(this.cachedMetaData.getPrefix()).thenReturn("test prefix"); + assertEquals("test prefix", Placeholders.PREFIX.resolve(this.ctx)); + } + + @Test + public void testSuffix() { + when(this.cachedMetaData.getSuffix()).thenReturn("test suffix"); + assertEquals("test suffix", Placeholders.SUFFIX.resolve(this.ctx)); + } + + @Test + public void testMeta() { + when(this.cachedMetaData.getMetaValue("test_key")).thenReturn("hello"); + assertEquals("hello", Placeholders.META.resolve(this.ctx.withArgument("test_key"))); + } + + @Test + public void testFormatDuration() { + Duration duration = ChronoUnit.YEARS.getDuration().multipliedBy(5) + .plus(ChronoUnit.MONTHS.getDuration().multipliedBy(4)) + .plus(ChronoUnit.WEEKS.getDuration().multipliedBy(3)) + .plusDays(2) + .plusHours(1) + .plusMinutes(6) + .plusSeconds(7); + + assertEquals("5y 4mo 3w 2d 1h 6m 7s", Placeholders.formatDuration(duration)); + assertEquals("1m 10s", Placeholders.formatDuration(Duration.ofMinutes(1).plusSeconds(10))); + assertEquals("0s", Placeholders.formatDuration(Duration.ZERO)); + } + +} diff --git a/common/placeholders/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/common/placeholders/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..ca6ee9cea8 --- /dev/null +++ b/common/placeholders/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index c4e6bb3cac..6c3dd0ec1a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,6 +22,7 @@ include ( 'common', 'common:loader-utils', 'common:minecraft', + 'common:placeholders', 'bukkit', 'bukkit:loader', 'bukkit-legacy',