001//
002// MIT License
003//
004// Copyright (c) 2021 Alexander Söderberg & Contributors
005//
006// Permission is hereby granted, free of charge, to any person obtaining a copy
007// of this software and associated documentation files (the "Software"), to deal
008// in the Software without restriction, including without limitation the rights
009// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010// copies of the Software, and to permit persons to whom the Software is
011// furnished to do so, subject to the following conditions:
012//
013// The above copyright notice and this permission notice shall be included in all
014// copies or substantial portions of the Software.
015//
016// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022// SOFTWARE.
023//
024package cloud.commandframework.arguments.standard;
025
026import cloud.commandframework.ArgumentDescription;
027import cloud.commandframework.arguments.CommandArgument;
028import cloud.commandframework.arguments.parser.ArgumentParseResult;
029import cloud.commandframework.arguments.parser.ArgumentParser;
030import cloud.commandframework.captions.CaptionVariable;
031import cloud.commandframework.captions.StandardCaptionKeys;
032import cloud.commandframework.context.CommandContext;
033import cloud.commandframework.exceptions.parsing.NoInputProvidedException;
034import cloud.commandframework.exceptions.parsing.ParserException;
035import org.checkerframework.checker.nullness.qual.NonNull;
036import org.checkerframework.checker.nullness.qual.Nullable;
037
038import java.util.EnumSet;
039import java.util.List;
040import java.util.Queue;
041import java.util.function.BiFunction;
042import java.util.stream.Collectors;
043
044/**
045 * Argument type that recognizes enums
046 *
047 * @param <C> Argument sender
048 * @param <E> Enum type
049 */
050@SuppressWarnings("unused")
051public class EnumArgument<C, E extends Enum<E>> extends CommandArgument<C, E> {
052
053    protected EnumArgument(
054            final @NonNull Class<E> enumClass,
055            final boolean required,
056            final @NonNull String name,
057            final @NonNull String defaultValue,
058            final @Nullable BiFunction<@NonNull CommandContext<C>, @NonNull String,
059                    @NonNull List<@NonNull String>> suggestionsProvider,
060            final @NonNull ArgumentDescription defaultDescription
061    ) {
062        super(required, name, new EnumParser<>(enumClass), defaultValue, enumClass, suggestionsProvider, defaultDescription);
063    }
064
065    /**
066     * Create a new builder
067     *
068     * @param name      Name of the argument
069     * @param enumClass Enum class
070     * @param <C>       Command sender type
071     * @param <E>       Enum type
072     * @return Created builder
073     */
074    public static <C, E extends Enum<E>> EnumArgument.@NonNull Builder<C, E> newBuilder(
075            final @NonNull Class<E> enumClass,
076            final @NonNull String name
077    ) {
078        return new EnumArgument.Builder<>(name, enumClass);
079    }
080
081    /**
082     * Create a new required command argument
083     *
084     * @param enumClass Enum class
085     * @param name      Name of the argument
086     * @param <C>       Command sender type
087     * @param <E>       Enum type
088     * @return Created argument
089     */
090    public static <C, E extends Enum<E>> @NonNull CommandArgument<C, E> of(
091            final @NonNull Class<E> enumClass,
092            final @NonNull String name
093    ) {
094        return EnumArgument.<C, E>newBuilder(enumClass, name).asRequired().build();
095    }
096
097    /**
098     * Create a new optional command argument
099     *
100     * @param enumClass Enum class
101     * @param name      Name of the argument
102     * @param <C>       Command sender type
103     * @param <E>       Enum type
104     * @return Created argument
105     */
106    public static <C, E extends Enum<E>> @NonNull CommandArgument<C, E> optional(
107            final @NonNull Class<E> enumClass,
108            final @NonNull String name
109    ) {
110        return EnumArgument.<C, E>newBuilder(enumClass, name).asOptional().build();
111    }
112
113    /**
114     * Create a new optional command argument with a default value
115     *
116     * @param enumClass    Enum class
117     * @param name         Name of the argument
118     * @param defaultValue Default value
119     * @param <C>          Command sender type
120     * @param <E>          Enum type
121     * @return Created argument
122     */
123    public static <C, E extends Enum<E>> @NonNull CommandArgument<C, E> optional(
124            final @NonNull Class<E> enumClass,
125            final @NonNull String name,
126            final @NonNull E defaultValue
127    ) {
128        return EnumArgument.<C, E>newBuilder(enumClass, name).asOptionalWithDefault(defaultValue.name().toLowerCase()).build();
129    }
130
131
132    public static final class Builder<C, E extends Enum<E>> extends CommandArgument.Builder<C, E> {
133
134        private final Class<E> enumClass;
135
136        private Builder(final @NonNull String name, final @NonNull Class<E> enumClass) {
137            super(enumClass, name);
138            this.enumClass = enumClass;
139        }
140
141        @Override
142        public @NonNull CommandArgument<C, E> build() {
143            return new EnumArgument<>(this.enumClass, this.isRequired(), this.getName(),
144                    this.getDefaultValue(), this.getSuggestionsProvider(), this.getDefaultDescription()
145            );
146        }
147
148    }
149
150
151    public static final class EnumParser<C, E extends Enum<E>> implements ArgumentParser<C, E> {
152
153        private final Class<E> enumClass;
154        private final EnumSet<E> allowedValues;
155
156        /**
157         * Construct a new enum parser
158         *
159         * @param enumClass Enum class
160         */
161        public EnumParser(final @NonNull Class<E> enumClass) {
162            this.enumClass = enumClass;
163            this.allowedValues = EnumSet.allOf(enumClass);
164        }
165
166        @Override
167        public @NonNull ArgumentParseResult<E> parse(
168                final @NonNull CommandContext<C> commandContext,
169                final @NonNull Queue<@NonNull String> inputQueue
170        ) {
171            final String input = inputQueue.peek();
172            if (input == null) {
173                return ArgumentParseResult.failure(new NoInputProvidedException(
174                        EnumParser.class,
175                        commandContext
176                ));
177            }
178
179            for (final E value : this.allowedValues) {
180                if (value.name().equalsIgnoreCase(input)) {
181                    inputQueue.remove();
182                    return ArgumentParseResult.success(value);
183                }
184            }
185
186            return ArgumentParseResult.failure(new EnumParseException(input, this.enumClass, commandContext));
187        }
188
189        @Override
190        public @NonNull List<@NonNull String> suggestions(
191                final @NonNull CommandContext<C> commandContext,
192                final @NonNull String input
193        ) {
194            return EnumSet.allOf(this.enumClass).stream().map(e -> e.name().toLowerCase()).collect(Collectors.toList());
195        }
196
197        @Override
198        public boolean isContextFree() {
199            return true;
200        }
201
202    }
203
204
205    public static final class EnumParseException extends ParserException {
206
207        private static final long serialVersionUID = 3465389578951428862L;
208        private final String input;
209        private final Class<? extends Enum<?>> enumClass;
210
211        /**
212         * Construct a new enum parse exception
213         *
214         * @param input     Input
215         * @param enumClass Enum class
216         * @param context   Command context
217         */
218        public EnumParseException(
219                final @NonNull String input,
220                final @NonNull Class<? extends Enum<?>> enumClass,
221                final @NonNull CommandContext<?> context
222        ) {
223            super(
224                    EnumParser.class,
225                    context,
226                    StandardCaptionKeys.ARGUMENT_PARSE_FAILURE_ENUM,
227                    CaptionVariable.of("input", input),
228                    CaptionVariable.of("acceptableValues", join(enumClass))
229            );
230            this.input = input;
231            this.enumClass = enumClass;
232        }
233
234        @SuppressWarnings({"unchecked", "rawtypes"})
235        private static @NonNull String join(final @NonNull Class<? extends Enum> clazz) {
236            final EnumSet<?> enumSet = EnumSet.allOf(clazz);
237            return enumSet.stream()
238                    .map(e -> e.toString().toLowerCase())
239                    .collect(Collectors.joining(", "));
240        }
241
242        /**
243         * Get the input provided by the sender
244         *
245         * @return Input
246         */
247        public @NonNull String getInput() {
248            return this.input;
249        }
250
251        /**
252         * Get the enum class that was attempted to be parsed
253         *
254         * @return Enum class
255         */
256        public @NonNull Class<? extends Enum<?>> getEnumClass() {
257            return this.enumClass;
258        }
259
260    }
261
262}