Skip to content

Commit

Permalink
feat: requirements
Browse files Browse the repository at this point in the history
moved from cloud-requirements
  • Loading branch information
Citymonstret committed Jan 8, 2024
1 parent dbb9746 commit e8eea15
Show file tree
Hide file tree
Showing 23 changed files with 1,330 additions and 7 deletions.
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

0 comments on commit e8eea15

Please sign in to comment.