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

feat: requirements #2

Merged
merged 1 commit into from
Jan 8, 2024
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 .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 11
java-version: 17
- name: Build
run: ./gradlew build
- name : Test Summary
Expand Down
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
# cloud-processors

> [!IMPORTANT]
> Cloud Requirements requires Java 11+.

Command pre- & post-processors for [Cloud v2](https://github.com/incendo/cloud).

## postprocessors

- [cloud-processors-confirmation](./cloud-processors-confirmation)
- [cloud-processors-cooldown](./cloud-processors-cooldown)
- [cloud-processors-requirements](./cloud-processors-requirements)
117 changes: 117 additions & 0 deletions cloud-processors-requirements/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# cloud-processor-requirements

Command requirement system for [Cloud v2](https://github.com/incendo/cloud).

The requirements are evaluated before
the command is executed to determine whether the command sender should be able to execute the command. The requirements
are defined on a per-command basis.

## Installation

Cloud Requirements is not yet available on Maven Central.

## Usage

You create requirements by implementing the `Requirement` interface. It is recommended to create an intermediary
requirement interface that extends `Requirement` that can contain shared logic. This also reduces verbosity introduced
by the generic types. Example:
```java
public interface YourRequirementInterface implements Requirement<YourSenderType, YourRequirementInterface> {

// Example method
@NonNull String errorMessage();
}
```
you can then create a requirement:
```java
public final class YourRequirement implements YourRequirementInterface {

@Override
public @NonNull String errorMessage() {
return "not cool enough";
}

@Override
public boolean evaluateRequirement(final @NonNull CommandContext<YourSenderType> context) {
return false; // You should probably put some logic here :)
}
}
```

You then need to create a `CloudKey<Requirements<YourSenderType, YourRequirementInterface>` which is used to store
the requirements in the command meta and for the processor to access the stored requirements:
```java
public static final CloudKey<Requirements<YourSenderType, YourRequirementInterface>> REQUIREMENT_KEY = CloudKey.of(
"requirements",
new TypeToken<CloudKey<Requirements<YourSenderType, YourRequirementInterface>>>() {}
);
```

You then need to create an instance of the postprocessor and register it to your command manager:
```java
final RequirementPostprocessor<YourSenderType, YourRequirementInterface> postprocessor = RequirementPostprocessor.of(
REQUIREMENTS_KEY,
new YourFailureHandler()
);
commandManager.registerPostprocessor(postprocessor);
```
the failure handler gets invoked when the command sender fails to meet a requirement:
```java
public final class YourFailureHandler implements RequirementFailureHandler<YourSenderType, YourRequirementInterface> {

@Override
public void handleFailure(
final @NonNull CommandContext<YourSenderType> context,
final YourRequirementInterface requirement
) {
context.sender().sendMessage("Requirement failed: " + requirement.errorMessage());
}
}
```

You then need to register the requirements to your command. This step depends on whether you use
[builders](#builders) or [annotations](#annotations).

### Builders

You have two different options when it comes to registering requirements to commands using the command builders.
You may store the requirements directly:
```java
commandBuilder.meta(REQUIREMENT_KEY, Requirements.of(requirement, requirement1, ...));
```

or by using the `RequirementApplicable` system:
```java
// Store this somewhere:
RequirementApplicable.RequirementApplicableFactory<YourSenderType,
YourRequirementInterface> factory = RequirementApplicable.factory(REQUIREMENT_KEY);

// Then register the requirements:
commandBuilder.apply(factory.create(requirement, requirement1, ...));
```

### Annotations

When using `cloud-annotations` you may use the `RequirementBindings` system to register bindings between
annotations and requirements:
```java
// Create some annotation:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface YourAnnotation {
// ...
}

// Then register a binding for it:
RequirementBindings.create(this.annotationParser, REQUIREMENT_KEY).register(
YourAnnotation.class,
annotation -> new YourRequirement()
);

// Then annotate a method with it:
@YourAnnotation
@CommandMethod("command")
public void commandMethod() {
// ...
}
```
9 changes: 9 additions & 0 deletions cloud-processors-requirements/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
plugins {
id("cloud-processors.base-conventions")
id("cloud-processors.publishing-conventions")
}

dependencies {
implementation(libs.cloud.core)
implementation(libs.cloud.annotations)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// MIT License
//
// Copyright (c) 2024 Incendo
//
// 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 org.incendo.cloud.processors.requirements;

import cloud.commandframework.context.CommandContext;
import java.util.List;
import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.NonNull;

/**
* A requirement for a command to be executed.
*
* @param <C> command sender type
* @param <R> requirement type, used for inheritance
* @since 1.0.0
*/
@API(status = API.Status.STABLE, since = "1.0.0")
public interface Requirement<C, R extends Requirement<C, R>> {

/**
* Returns whether the given {@code context} meets the requirement.
*
* @param commandContext command context to evaluate
* @return {@code true} if the context meets the requirement, {@code false} if not
*/
boolean evaluateRequirement(@NonNull CommandContext<C> commandContext);

/**
* Returns the parents of the requirement.
*
* <p>The parents will always be evaluated before {@code this} requirement.</p>
*
* @return the parents
*/
default @NonNull List<@NonNull R> parents() {
return List.of();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//
// MIT License
//
// Copyright (c) 2024 Incendo
//
// 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 org.incendo.cloud.processors.requirements;

import cloud.commandframework.Command;
import cloud.commandframework.keys.CloudKey;
import java.util.List;
import java.util.Objects;
import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.NonNull;

/**
* Utility for adding {@link Requirements} to a {@link Command.Builder}.
*
* <p>The requirements can be applied to the command builder by
* using {@link Command.Builder#apply(Command.Builder.Applicable)}.</p>
*
* @param <C> command sender type
* @param <R> requirement type
* @since 1.0.0
*/
@API(status = API.Status.STABLE, since = "1.0.0")
public final class RequirementApplicable<C, R extends Requirement<C, R>> implements Command.Builder.Applicable<C> {

/**
* Returns a new factory that creates {@link RequirementApplicable} instances.
*
* @param <C> command sender type
* @param <R> requirement type
* @param requirementKey key used to store the requirements in the command meta, should be the same as the key supplied to
* {@link RequirementPostprocessor}
* @return the factory
*/
public static <C, R extends Requirement<C, R>> @NonNull RequirementApplicableFactory<C, R> factory(
final @NonNull CloudKey<Requirements<C, R>> requirementKey
) {
return new RequirementApplicableFactory<>(requirementKey);
}

private final CloudKey<Requirements<C, R>> requirementKey;
private final Requirements<C, R> requirements;

private RequirementApplicable(
final @NonNull CloudKey<Requirements<C, R>> requirementKey,
final @NonNull Requirements<C, R> requirements
) {
this.requirementKey = Objects.requireNonNull(requirementKey, "requirementKey");
this.requirements = Objects.requireNonNull(requirements, "requirements");
}

@Override
public Command.@NonNull Builder<C> applyToCommandBuilder(final Command.@NonNull Builder<C> builder) {
return builder.meta(this.requirementKey, this.requirements);
}


/**
* Factory that produces {@link RequirementApplicable} instances.
*
* @param <C> command sender type
* @param <R> requirement type
* @since 1.0.0
*/
@API(status = API.Status.STABLE, since = "1.0.0")
public static final class RequirementApplicableFactory<C, R extends Requirement<C, R>> {

private final CloudKey<Requirements<C, R>> requirementKey;

private RequirementApplicableFactory(final @NonNull CloudKey<Requirements<C, R>> requirementKey) {
this.requirementKey = Objects.requireNonNull(requirementKey, "requirementKey");
}

/**
* Creates a new {@link RequirementApplicable} using the given {@code requirements}.
*
* @param requirements requirements to apply to the command builder
* @return the {@link RequirementApplicable} instance
*/
public @NonNull RequirementApplicable<C, R> create(final @NonNull Requirements<C, R> requirements) {
return new RequirementApplicable<>(this.requirementKey, requirements);
}

/**
* Creates a new {@link RequirementApplicable} using the given {@code requirements}.
*
* @param requirements requirements to apply to the command builder
* @return the {@link RequirementApplicable} instance
*/
public @NonNull RequirementApplicable<C, R> create(final @NonNull List<@NonNull R> requirements) {
return new RequirementApplicable<>(this.requirementKey, Requirements.of(requirements));
}

/**
* Creates a new {@link RequirementApplicable} using the given {@code requirements}.
*
* @param requirements requirements to apply to the command builder
* @return the {@link RequirementApplicable} instance
*/
@SafeVarargs
@SuppressWarnings("varargs")
public final @NonNull RequirementApplicable<C, R> create(final @NonNull R @NonNull... requirements) {
return new RequirementApplicable<>(this.requirementKey, Requirements.of(requirements));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// MIT License
//
// Copyright (c) 2024 Incendo
//
// 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 org.incendo.cloud.processors.requirements;

import cloud.commandframework.context.CommandContext;
import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.NonNull;

/**
* Handler that gets invoked when a {@link Requirement requirement} is not met.
*
* @param <C> command sender type
* @param <R> requirement type
* @since 1.0.0
*/
@FunctionalInterface
@API(status = API.Status.STABLE, since = "1.0.0")
public interface RequirementFailureHandler<C, R extends Requirement<C, R>> {

/**
* Returns a requirement failure handler that does nothing.
*
* @param <C> command sender type
* @param <R> requirement type
* @return the handler
*/
static <C, R extends Requirement<C, R>> @NonNull RequirementFailureHandler<C, R> noOp() {
return (requirement, context) -> {};
}

/**
* Handles the case where the given {@code context} does not meet the given {@code requirement}.
*
* @param context the context
* @param requirement the unmet requirement
*/
void handleFailure(@NonNull CommandContext<C> context, @NonNull R requirement);
}
Loading
Loading