Compare commits
5 Commits
master
...
plutocomma
Author | SHA1 | Date |
---|---|---|
493msi | 9804e9f3f5 | |
493msi | c3db492a6d | |
493msi | 5952a26fee | |
493msi | aa70f1bbe2 | |
493msi | c3bd358bed |
|
@ -17,6 +17,7 @@ can now only be modified only through public setters
|
||||||
* `[PlutoCore]` Refactored `InputBus` and added several convenience methods
|
* `[PlutoCore]` Refactored `InputBus` and added several convenience methods
|
||||||
* `[PlutoCore]` Refactored input callbacks
|
* `[PlutoCore]` Refactored input callbacks
|
||||||
* `[PlutoStatic]` Slight cleanup in the `Display` and `DisplayBuilder` classes
|
* `[PlutoStatic]` Slight cleanup in the `Display` and `DisplayBuilder` classes
|
||||||
|
* `[PlutoCommandParser]` **Initial release**
|
||||||
|
|
||||||
## 20.2.0.0-alpha.1
|
## 20.2.0.0-alpha.1
|
||||||
* `[PlutoLib#cz.tefek.pluto.io.logger]` Refactored the Logger subsystem
|
* `[PlutoLib#cz.tefek.pluto.io.logger]` Refactored the Logger subsystem
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
# plutoengine:plutocommandparser
|
||||||
|
|
||||||
|
PlutoEngine's command parser.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
PlutoCommandParser is an attempt to streamline my previous attempts at command parsers.
|
||||||
|
Its main goal is to provide a modular and flexible tokenizer, parser and evaluator
|
||||||
|
for a simple user-friendly CLI-like language called `PlutoCmd`.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
* High syntax error tolerance
|
||||||
|
* Provide implementations for basic Java types, such as primitives and Strings
|
||||||
|
* Allow extensibility while providing a strong foundation
|
||||||
|
* Complete user control over localization, no hardcoded Strings
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
PlutoCmd is *not* a replacement for standard scripting languages and will most likely
|
||||||
|
never be a Turing-complete language.
|
||||||
|
|
||||||
|
|
||||||
|
## Implementation style
|
||||||
|
|
||||||
|
### Command implementation
|
||||||
|
|
||||||
|
Each command has its own *final* class, abstractions over CommandBase are howered allowed.
|
||||||
|
Note the usage of the `ConstantExpression` annotation over some interface methods. These
|
||||||
|
annotations **must** be respected - methods must be stateless and deterministic.
|
||||||
|
|
||||||
|
## PlutoCmd language specification
|
||||||
|
|
||||||
|
### General syntax
|
||||||
|
|
||||||
|
```
|
||||||
|
[prefix]command [arg1] [arg2] ... [argN]
|
||||||
|
```
|
||||||
|
|
||||||
|
`prefix` is an optional identifier String to distinguish PlutoCmd commands from other commands.
|
||||||
|
|
||||||
|
`command` is an alias for a command, handled by one of the command's handlers
|
||||||
|
|
||||||
|
`argX` is an argument of the invoked command-function, optionally quoted to preserve whitespace.
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
apply plugin: 'java-library'
|
||||||
|
|
||||||
|
description = ""
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api project(":plutolib")
|
||||||
|
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter:5.6.2")
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package cz.tefek.pluto.command;
|
||||||
|
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
|
||||||
|
import cz.tefek.pluto.command.invoke.InvokeHandler;
|
||||||
|
|
||||||
|
public abstract class CommandBase implements ICommand
|
||||||
|
{
|
||||||
|
private final Class<? extends CommandBase> clazz;
|
||||||
|
|
||||||
|
public final Class<?> commandClass()
|
||||||
|
{
|
||||||
|
return this.clazz;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected CommandBase()
|
||||||
|
{
|
||||||
|
this.clazz = this.getClass();
|
||||||
|
|
||||||
|
if (!Modifier.isFinal(this.clazz.getModifiers()))
|
||||||
|
{
|
||||||
|
// Class must be final
|
||||||
|
// Throwing an exception here is okay, since this is the developer's fault
|
||||||
|
//
|
||||||
|
// You can still create abstract wrappers for CommandBase, however implemented commands
|
||||||
|
// must be final.
|
||||||
|
throw new RuntimeException("Command classes MUST be final. Offender: " + this.clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
var methods = this.clazz.getMethods();
|
||||||
|
|
||||||
|
for (var method : methods)
|
||||||
|
{
|
||||||
|
// Silently skip methods without annotations
|
||||||
|
if (!method.isAnnotationPresent(InvokeHandler.class))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var modifiers = method.getModifiers();
|
||||||
|
|
||||||
|
if (Modifier.isStatic(modifiers))
|
||||||
|
{
|
||||||
|
// Method must be non-static
|
||||||
|
// Throwing an exception here is okay, since this is the developer's fault
|
||||||
|
throw new RuntimeException("Invoke handlers MUST NOT be static. Offender: " + method);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Modifier.isPublic(modifiers))
|
||||||
|
{
|
||||||
|
// Method must be public
|
||||||
|
// Throwing an exception here is okay, since this is the developer's fault
|
||||||
|
throw new RuntimeException("Invoke handlers MUST be public. Offender: " + method);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final int hashCode()
|
||||||
|
{
|
||||||
|
return this.name().hashCode();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package cz.tefek.pluto.command;
|
||||||
|
|
||||||
|
import cz.tefek.pluto.annotation.ConstantExpression;
|
||||||
|
|
||||||
|
public interface ICommand
|
||||||
|
{
|
||||||
|
@ConstantExpression
|
||||||
|
String name();
|
||||||
|
|
||||||
|
@ConstantExpression
|
||||||
|
String[] aliases();
|
||||||
|
|
||||||
|
@ConstantExpression
|
||||||
|
String description();
|
||||||
|
|
||||||
|
@ConstantExpression
|
||||||
|
Class<?> commandClass();
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
package cz.tefek.pluto.command.context;
|
||||||
|
|
||||||
|
import cz.tefek.pluto.command.CommandBase;
|
||||||
|
|
||||||
|
public class CommandContextBuilder
|
||||||
|
{
|
||||||
|
private final CommandContext ctx;
|
||||||
|
|
||||||
|
public CommandContextBuilder()
|
||||||
|
{
|
||||||
|
this.ctx = new CommandContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandContextBuilder prefix(String prefix)
|
||||||
|
{
|
||||||
|
this.ctx.usedPrefix = prefix;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandContextBuilder alias(String alias)
|
||||||
|
{
|
||||||
|
this.ctx.usedAlias = alias;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandContextBuilder command(CommandBase command)
|
||||||
|
{
|
||||||
|
this.ctx.command = command;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandContext resolved()
|
||||||
|
{
|
||||||
|
this.ctx.resolved = true;
|
||||||
|
|
||||||
|
return this.ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandContext unresolved(EnumCommandParseFailure cause)
|
||||||
|
{
|
||||||
|
this.ctx.resolved = false;
|
||||||
|
|
||||||
|
return this.ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CommandContext
|
||||||
|
{
|
||||||
|
private boolean resolved;
|
||||||
|
private EnumCommandParseFailure failureCause;
|
||||||
|
|
||||||
|
private String usedPrefix;
|
||||||
|
private String usedAlias;
|
||||||
|
private CommandBase command;
|
||||||
|
|
||||||
|
private CommandContext()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsedPrefix()
|
||||||
|
{
|
||||||
|
return this.usedPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsedAlias()
|
||||||
|
{
|
||||||
|
return this.usedAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandBase getCommand()
|
||||||
|
{
|
||||||
|
return this.command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isResolved()
|
||||||
|
{
|
||||||
|
return this.resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EnumCommandParseFailure getFailureCause()
|
||||||
|
{
|
||||||
|
return this.failureCause;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum EnumCommandParseFailure
|
||||||
|
{
|
||||||
|
UNRESOLVED_PREFIX,
|
||||||
|
UNRESOLVED_COMMAND_NAME,
|
||||||
|
UNRESOLVED_PARAMETERS,
|
||||||
|
UNRESOLVED_UNEXPECTED_STATE
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package cz.tefek.pluto.command.invoke;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Denotes a handler for an invocation of a command.
|
||||||
|
*
|
||||||
|
* <p><em>
|
||||||
|
* Method must be public and non-static.
|
||||||
|
* </em></p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* While classes implementing the Command API must be final, it is not required for the handler methods
|
||||||
|
* as that would be redundant.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author 493msi
|
||||||
|
*
|
||||||
|
* @since 20.2.0.0-alpha.2
|
||||||
|
* */
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
public @interface InvokeHandler
|
||||||
|
{
|
||||||
|
}
|
|
@ -0,0 +1,255 @@
|
||||||
|
package cz.tefek.pluto.command.parser;
|
||||||
|
|
||||||
|
import java.util.PrimitiveIterator.OfInt;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import cz.tefek.pluto.command.CommandBase;
|
||||||
|
import cz.tefek.pluto.command.context.CommandContextBuilder;
|
||||||
|
import cz.tefek.pluto.command.context.CommandContextBuilder.CommandContext;
|
||||||
|
import cz.tefek.pluto.command.context.CommandContextBuilder.EnumCommandParseFailure;
|
||||||
|
import cz.tefek.pluto.command.registry.CommandRegistry;
|
||||||
|
|
||||||
|
public class CommandParser
|
||||||
|
{
|
||||||
|
private final String text;
|
||||||
|
private Set<OfInt> prefixes;
|
||||||
|
private EnumParserState state;
|
||||||
|
|
||||||
|
private StringBuilder prefixBuilder;
|
||||||
|
private StringBuilder commandNameBuilder;
|
||||||
|
|
||||||
|
private CommandBase command;
|
||||||
|
|
||||||
|
private StringBuilder parameterBuilder;
|
||||||
|
|
||||||
|
private CommandContextBuilder ctx;
|
||||||
|
|
||||||
|
private static final int CP_QUOTE = '"';
|
||||||
|
|
||||||
|
public CommandParser(String text)
|
||||||
|
{
|
||||||
|
this.text = text;
|
||||||
|
this.state = EnumParserState.BEGIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean readCodepoint(int cp)
|
||||||
|
{
|
||||||
|
switch (this.state)
|
||||||
|
{
|
||||||
|
case READING_PREFIX:
|
||||||
|
this.prefixBuilder.appendCodePoint(cp);
|
||||||
|
|
||||||
|
this.prefixes.removeIf(ii -> ii.nextInt() != cp);
|
||||||
|
|
||||||
|
if (this.prefixes.isEmpty())
|
||||||
|
{
|
||||||
|
this.state = EnumParserState.END_NO_PREFIX;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasEmptyPrefix())
|
||||||
|
{
|
||||||
|
this.ctx.prefix(this.prefixBuilder.toString());
|
||||||
|
this.state = EnumParserState.READING_COMMAND;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case READING_COMMAND:
|
||||||
|
this.commandNameBuilder.appendCodePoint(cp);
|
||||||
|
|
||||||
|
if (Character.isWhitespace(cp))
|
||||||
|
{
|
||||||
|
if (!this.resolveCommand())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = EnumParserState.READ_WHITESPACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case READ_WHITESPACE:
|
||||||
|
if (Character.isWhitespace(cp))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cp == CP_QUOTE)
|
||||||
|
{
|
||||||
|
this.state = EnumParserState.READING_PARAMETER_QUOTED;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.parameterBuilder.appendCodePoint(cp);
|
||||||
|
this.state = EnumParserState.READING_PARAMETER;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case READING_PARAMETER_QUOTED:
|
||||||
|
if (cp == CP_QUOTE)
|
||||||
|
{
|
||||||
|
this.state = EnumParserState.READING_PARAMETER_CANDIDATE_UNQUOTE;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.parameterBuilder.appendCodePoint(cp);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case READING_PARAMETER:
|
||||||
|
if (Character.isWhitespace(cp))
|
||||||
|
{
|
||||||
|
this.emitParameter();
|
||||||
|
this.state = EnumParserState.READ_WHITESPACE;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.parameterBuilder.appendCodePoint(cp);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case READING_PARAMETER_CANDIDATE_UNQUOTE:
|
||||||
|
if (Character.isWhitespace(cp))
|
||||||
|
{
|
||||||
|
this.emitParameter();
|
||||||
|
this.state = EnumParserState.READ_WHITESPACE;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.parameterBuilder.appendCodePoint(cp);
|
||||||
|
this.state = EnumParserState.READING_PARAMETER_QUOTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case END:
|
||||||
|
default:
|
||||||
|
this.state = EnumParserState.UNEXPECTED_STATE_FALLBACK;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean resolveCommand()
|
||||||
|
{
|
||||||
|
var alias = this.commandNameBuilder.toString();
|
||||||
|
|
||||||
|
this.ctx.alias(alias);
|
||||||
|
|
||||||
|
this.command = CommandRegistry.getByAlias(alias);
|
||||||
|
|
||||||
|
if (this.command == null)
|
||||||
|
{
|
||||||
|
this.state = EnumParserState.END_NO_COMMAND;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx.command(this.command);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void emitParameter()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasEmptyPrefix()
|
||||||
|
{
|
||||||
|
return this.prefixes.stream().anyMatch(Predicate.not(OfInt::hasNext));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse using this parser and supplied prefixes. This function also
|
||||||
|
* resolves the command context and the parameters. Yeah it does a lot of
|
||||||
|
* stuff.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public CommandContext parse(Set<String> prefixes)
|
||||||
|
{
|
||||||
|
if (this.state != EnumParserState.BEGIN)
|
||||||
|
{
|
||||||
|
throw new IllegalStateException("Cannot run a parser that is not in the BEGIN state.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prefixBuilder = new StringBuilder();
|
||||||
|
this.ctx = new CommandContextBuilder();
|
||||||
|
|
||||||
|
this.prefixes = prefixes.stream().map(String::codePoints).map(IntStream::iterator).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
if (prefixes.isEmpty() || this.hasEmptyPrefix())
|
||||||
|
{
|
||||||
|
this.state = EnumParserState.READING_COMMAND;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.state = EnumParserState.READING_PREFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cps = this.text.codePoints();
|
||||||
|
|
||||||
|
for (var cpIt = cps.iterator(); cpIt.hasNext(); )
|
||||||
|
if (!this.readCodepoint(cpIt.next()))
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Update the state for EOF
|
||||||
|
switch (this.state)
|
||||||
|
{
|
||||||
|
case READING_PARAMETER_QUOTED:
|
||||||
|
case READ_WHITESPACE:
|
||||||
|
case READING_PARAMETER:
|
||||||
|
case READING_PARAMETER_CANDIDATE_UNQUOTE:
|
||||||
|
this.state = EnumParserState.END;
|
||||||
|
this.emitParameter();
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case READING_COMMAND:
|
||||||
|
if (this.resolveCommand())
|
||||||
|
{
|
||||||
|
this.state = EnumParserState.END;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the end state
|
||||||
|
switch (this.state)
|
||||||
|
{
|
||||||
|
case READING_PREFIX:
|
||||||
|
case END_NO_PREFIX:
|
||||||
|
return this.ctx.unresolved(EnumCommandParseFailure.UNRESOLVED_PREFIX);
|
||||||
|
|
||||||
|
case END_NO_COMMAND:
|
||||||
|
return this.ctx.unresolved(EnumCommandParseFailure.UNRESOLVED_COMMAND_NAME);
|
||||||
|
|
||||||
|
case END:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return this.ctx.unresolved(EnumCommandParseFailure.UNRESOLVED_UNEXPECTED_STATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point we are 100% sure the command was resolved and can validate the parameters
|
||||||
|
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* TODO: Validate parameters here
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
return this.ctx.resolved();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package cz.tefek.pluto.command.parser;
|
||||||
|
|
||||||
|
public enum EnumParserState
|
||||||
|
{
|
||||||
|
BEGIN,
|
||||||
|
READING_PREFIX,
|
||||||
|
READING_COMMAND,
|
||||||
|
READING_PARAMETER,
|
||||||
|
READING_PARAMETER_QUOTED,
|
||||||
|
READING_PARAMETER_CANDIDATE_UNQUOTE,
|
||||||
|
READ_WHITESPACE,
|
||||||
|
END_NO_PREFIX,
|
||||||
|
END_NO_COMMAND,
|
||||||
|
END_EMISSION_FAILURE,
|
||||||
|
END,
|
||||||
|
UNEXPECTED_STATE_FALLBACK;
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package cz.tefek.pluto.command.platform;
|
||||||
|
|
||||||
|
import cz.tefek.pluto.command.context.CommandContextBuilder.EnumCommandParseFailure;
|
||||||
|
|
||||||
|
public abstract class CommandPlatform
|
||||||
|
{
|
||||||
|
public abstract String getID();
|
||||||
|
|
||||||
|
public abstract String getName();
|
||||||
|
|
||||||
|
public boolean shouldWarnOn(EnumCommandParseFailure failure)
|
||||||
|
{
|
||||||
|
switch (failure)
|
||||||
|
{
|
||||||
|
case UNRESOLVED_PREFIX:
|
||||||
|
case UNRESOLVED_COMMAND_NAME:
|
||||||
|
case UNRESOLVED_UNEXPECTED_STATE:
|
||||||
|
return false;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract int getMessageLimit();
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package cz.tefek.pluto.command.registry;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import cz.tefek.pluto.command.CommandBase;
|
||||||
|
import cz.tefek.pluto.io.logger.Logger;
|
||||||
|
import cz.tefek.pluto.io.logger.SmartSeverity;
|
||||||
|
|
||||||
|
public final class CommandRegistry
|
||||||
|
{
|
||||||
|
private static CommandRegistry instance;
|
||||||
|
|
||||||
|
private final Set<CommandBase> commands;
|
||||||
|
private final Map<String, CommandBase> aliasTable;
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
instance = new CommandRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
private CommandRegistry()
|
||||||
|
{
|
||||||
|
this.aliasTable = new HashMap<>();
|
||||||
|
this.commands = new HashSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerAlias(String alias, CommandBase command)
|
||||||
|
{
|
||||||
|
if (this.aliasTable.containsKey(alias))
|
||||||
|
{
|
||||||
|
Logger.logf(SmartSeverity.ERROR, "Alias '%s' for command '%s' is already used, skipping.%n", alias, command.name());
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.aliasTable.put(alias, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerCommand(CommandBase command)
|
||||||
|
{
|
||||||
|
if (!instance.commands.add(command))
|
||||||
|
{
|
||||||
|
Logger.logf(SmartSeverity.ERROR, "Command '%s' is already registered, skipping.%n", command.name());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.registerAlias(command.name(), command);
|
||||||
|
|
||||||
|
Arrays.stream(command.aliases()).forEach(alias -> instance.registerAlias(alias, command));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CommandBase getByAlias(String alias)
|
||||||
|
{
|
||||||
|
return instance.aliasTable.get(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<CommandBase> getCommands()
|
||||||
|
{
|
||||||
|
return Collections.unmodifiableSet(instance.commands);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clear()
|
||||||
|
{
|
||||||
|
instance = new CommandRegistry();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package cz.tefek.pluto.command.resolver;
|
||||||
|
|
||||||
|
public abstract class AbstractResolveException extends RuntimeException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private static final long serialVersionUID = -8934505669078637864L;
|
||||||
|
|
||||||
|
public AbstractResolveException(Exception exception)
|
||||||
|
{
|
||||||
|
super(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AbstractResolveException(String what)
|
||||||
|
{
|
||||||
|
super(what);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AbstractResolveException()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package cz.tefek.pluto.command.resolver;
|
||||||
|
|
||||||
|
public abstract class AbstractResolver
|
||||||
|
{
|
||||||
|
public abstract Class<?> getOutputType();
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package cz.tefek.pluto.command.resolver;
|
||||||
|
|
||||||
|
public enum EnumResolveFailure
|
||||||
|
{
|
||||||
|
INT_PARSE,
|
||||||
|
LONG_PARSE,
|
||||||
|
FLOAT_PARSE,
|
||||||
|
DOUBLE_PARSE,
|
||||||
|
FRAC_INVALID_PERCENTAGE,
|
||||||
|
FRAC_PERCENTAGE_OUT_OF_RANGE,
|
||||||
|
FRAC_VALUE_HIGHER_THAN_BASE,
|
||||||
|
OTHER;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package cz.tefek.pluto.command.resolver;
|
||||||
|
|
||||||
|
public class ResolveException extends AbstractResolveException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private static final long serialVersionUID = -3098373878754649161L;
|
||||||
|
|
||||||
|
private EnumResolveFailure failure;
|
||||||
|
|
||||||
|
public ResolveException(EnumResolveFailure failure)
|
||||||
|
{
|
||||||
|
this.failure = failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EnumResolveFailure getResolveFailure()
|
||||||
|
{
|
||||||
|
return this.failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package cz.tefek.pluto.command.resolver.generic;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import cz.tefek.pluto.command.resolver.AbstractResolver;
|
||||||
|
|
||||||
|
public abstract class GenericResolver<R> extends AbstractResolver
|
||||||
|
{
|
||||||
|
protected Function<String, R> func;
|
||||||
|
|
||||||
|
public GenericResolver(Function<String, R> func)
|
||||||
|
{
|
||||||
|
this.func = func;
|
||||||
|
}
|
||||||
|
|
||||||
|
public R apply(String param)
|
||||||
|
{
|
||||||
|
return this.func.apply(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract Class<? super R> getOutputType();
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package cz.tefek.pluto.command.resolver.generic;
|
||||||
|
|
||||||
|
public class StringResolver extends GenericResolver<String>
|
||||||
|
{
|
||||||
|
public StringResolver()
|
||||||
|
{
|
||||||
|
super(String::valueOf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<String> getOutputType()
|
||||||
|
{
|
||||||
|
return String.class;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package cz.tefek.pluto.command.resolver.primitive;
|
||||||
|
|
||||||
|
import cz.tefek.pluto.command.resolver.EnumResolveFailure;
|
||||||
|
import cz.tefek.pluto.command.resolver.ResolveException;
|
||||||
|
|
||||||
|
public class BasicDoubleResolver extends DoubleResolver
|
||||||
|
{
|
||||||
|
public BasicDoubleResolver()
|
||||||
|
{
|
||||||
|
super(str ->
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Double.parseDouble(str);
|
||||||
|
}
|
||||||
|
catch (NumberFormatException e)
|
||||||
|
{
|
||||||
|
throw new ResolveException(EnumResolveFailure.DOUBLE_PARSE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package cz.tefek.pluto.command.resolver.primitive;
|
||||||
|
|
||||||
|
import cz.tefek.pluto.command.resolver.EnumResolveFailure;
|
||||||
|
import cz.tefek.pluto.command.resolver.ResolveException;
|
||||||
|
|
||||||
|
public class BasicIntResolver extends IntResolver
|
||||||
|
{
|
||||||
|
public BasicIntResolver()
|
||||||
|
{
|
||||||
|
super(str ->
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Integer.parseInt(str);
|
||||||
|
}
|
||||||
|
catch (NumberFormatException e)
|
||||||
|
{
|
||||||
|
throw new ResolveException(EnumResolveFailure.INT_PARSE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package cz.tefek.pluto.command.resolver.primitive;
|
||||||
|
|
||||||
|
import cz.tefek.pluto.command.resolver.EnumResolveFailure;
|
||||||
|
import cz.tefek.pluto.command.resolver.ResolveException;
|
||||||
|
|
||||||
|
public class BasicLongResolver extends LongResolver
|
||||||
|
{
|
||||||
|
public BasicLongResolver()
|
||||||
|
{
|
||||||
|
super(str ->
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Long.parseLong(str);
|
||||||
|
}
|
||||||
|
catch (NumberFormatException e)
|
||||||
|
{
|
||||||
|
throw new ResolveException(EnumResolveFailure.LONG_PARSE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package cz.tefek.pluto.command.resolver.primitive;
|
||||||
|
|
||||||
|
import java.util.function.ToDoubleFunction;
|
||||||
|
|
||||||
|
import cz.tefek.pluto.command.resolver.AbstractResolver;
|
||||||
|
|
||||||
|
public class DoubleResolver extends AbstractResolver
|
||||||
|
{
|
||||||
|
protected ToDoubleFunction<String> func;
|
||||||
|
|
||||||
|
public DoubleResolver(ToDoubleFunction<String> func)
|
||||||
|
{
|
||||||
|
this.func = func;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double apply(String param)
|
||||||
|
{
|
||||||
|
return this.func.applyAsDouble(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> getOutputType()
|
||||||
|
{
|
||||||
|
return double.class;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package cz.tefek.pluto.command.resolver.primitive;
|
||||||
|
|
||||||
|
import cz.tefek.pluto.command.resolver.EnumResolveFailure;
|
||||||
|
import cz.tefek.pluto.command.resolver.ResolveException;
|
||||||
|
|
||||||
|
public class IntFractionResolver extends IntResolver
|
||||||
|
{
|
||||||
|
public IntFractionResolver(int base)
|
||||||
|
{
|
||||||
|
super(str -> parseAmount(str, base));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int parseAmount(String amountString, int base) throws ResolveException
|
||||||
|
{
|
||||||
|
if (amountString.equalsIgnoreCase("all") || amountString.equalsIgnoreCase("everything"))
|
||||||
|
{
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amountString.equalsIgnoreCase("half"))
|
||||||
|
{
|
||||||
|
return base / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amountString.endsWith("%"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
float percentage = Float.parseFloat(amountString.substring(0, amountString.length() - 1));
|
||||||
|
|
||||||
|
if (percentage < 0 || percentage > 100)
|
||||||
|
{
|
||||||
|
throw new ResolveException(EnumResolveFailure.FRAC_PERCENTAGE_OUT_OF_RANGE);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Math.round(percentage / 100.0f * base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (NumberFormatException e1)
|
||||||
|
{
|
||||||
|
throw new ResolveException(EnumResolveFailure.FRAC_INVALID_PERCENTAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int amount = Integer.parseInt(amountString);
|
||||||
|
|
||||||
|
if (amount > base)
|
||||||
|
{
|
||||||
|
throw new ResolveException(EnumResolveFailure.FRAC_VALUE_HIGHER_THAN_BASE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
catch (NumberFormatException e)
|
||||||
|
{
|
||||||
|
throw new ResolveException(EnumResolveFailure.INT_PARSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package cz.tefek.pluto.command.resolver.primitive;
|
||||||
|
|
||||||
|
import java.util.function.ToIntFunction;
|
||||||
|
|
||||||
|
import cz.tefek.pluto.command.resolver.AbstractResolver;
|
||||||
|
|
||||||
|
public class IntResolver extends AbstractResolver
|
||||||
|
{
|
||||||
|
protected ToIntFunction<String> func;
|
||||||
|
|
||||||
|
public IntResolver(ToIntFunction<String> func)
|
||||||
|
{
|
||||||
|
this.func = func;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int apply(String param)
|
||||||
|
{
|
||||||
|
return this.func.applyAsInt(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final Class<?> getOutputType()
|
||||||
|
{
|
||||||
|
return int.class;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package cz.tefek.pluto.command.resolver.primitive;
|
||||||
|
|
||||||
|
import java.util.function.ToLongFunction;
|
||||||
|
|
||||||
|
import cz.tefek.pluto.command.resolver.AbstractResolver;
|
||||||
|
|
||||||
|
public class LongResolver extends AbstractResolver
|
||||||
|
{
|
||||||
|
protected ToLongFunction<String> func;
|
||||||
|
|
||||||
|
public LongResolver(ToLongFunction<String> func)
|
||||||
|
{
|
||||||
|
this.func = func;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long apply(String param)
|
||||||
|
{
|
||||||
|
return this.func.applyAsLong(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final Class<?> getOutputType()
|
||||||
|
{
|
||||||
|
return long.class;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package cz.tefek.pluto.command.resolver;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import cz.tefek.pluto.command.resolver.primitive.BasicIntResolver;
|
||||||
|
|
||||||
|
class BasicIntResolverTest
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
void parse()
|
||||||
|
{
|
||||||
|
var resolver = new BasicIntResolver();
|
||||||
|
|
||||||
|
Assertions.assertEquals(5, resolver.apply("5"));
|
||||||
|
|
||||||
|
Assertions.assertEquals(-50, resolver.apply("-50"));
|
||||||
|
|
||||||
|
Assertions.assertEquals(155, resolver.apply("155"));
|
||||||
|
|
||||||
|
Assertions.assertEquals(0, resolver.apply("0"));
|
||||||
|
|
||||||
|
Assertions.assertEquals(-0, resolver.apply("-0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exceptions()
|
||||||
|
{
|
||||||
|
var resolver = new BasicIntResolver();
|
||||||
|
|
||||||
|
// No foreign characters
|
||||||
|
Assertions.assertThrows(NumberFormatException.class, () -> resolver.apply("abc"));
|
||||||
|
|
||||||
|
// No floats
|
||||||
|
Assertions.assertThrows(NumberFormatException.class, () -> resolver.apply("12.5"));
|
||||||
|
|
||||||
|
// No empty strings
|
||||||
|
Assertions.assertThrows(NumberFormatException.class, () -> resolver.apply(""));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package cz.tefek.pluto.command.resolver.command;
|
||||||
|
|
||||||
|
import cz.tefek.pluto.command.CommandBase;
|
||||||
|
import cz.tefek.pluto.command.invoke.InvokeHandler;
|
||||||
|
|
||||||
|
public final class TestCommand extends CommandBase
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public String name()
|
||||||
|
{
|
||||||
|
return "test";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] aliases()
|
||||||
|
{
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String description()
|
||||||
|
{
|
||||||
|
return "The test command - prints Hello World to stdout.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@InvokeHandler
|
||||||
|
public void invoke()
|
||||||
|
{
|
||||||
|
System.out.println("Hello World!");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package cz.tefek.pluto.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Denotes that the target field or method should be a constant expression - it has no state and always yields
|
||||||
|
* the same deterministic result for given input. Generally, annotated methods should be thread-safe, however
|
||||||
|
* this is not required.
|
||||||
|
*
|
||||||
|
* @author 493msi
|
||||||
|
*
|
||||||
|
* @since 20.2.0.0-alpha.2
|
||||||
|
* */
|
||||||
|
@Retention(RetentionPolicy.CLASS)
|
||||||
|
@Target({ ElementType.METHOD, ElementType.FIELD })
|
||||||
|
public @interface ConstantExpression
|
||||||
|
{
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
include 'plutolib',
|
include 'plutolib',
|
||||||
|
'plutocommandparser',
|
||||||
'plutostatic',
|
'plutostatic',
|
||||||
'plutotexturing',
|
'plutotexturing',
|
||||||
'plutomesher',
|
'plutomesher',
|
||||||
|
|
Loading…
Reference in New Issue