Readded PlutoCommandParser

This commit is contained in:
493msi 2020-09-20 03:46:35 +02:00
parent b476087fcd
commit c3bd358bed
21 changed files with 826 additions and 0 deletions

View File

@ -0,0 +1,9 @@
apply plugin: 'java-library'
description = ""
dependencies {
api project(":plutolib")
testImplementation("org.junit.jupiter:junit-jupiter:5.6.2")
}

View File

@ -0,0 +1,18 @@
package cz.tefek.pluto.command;
public abstract class CommandBase
{
public abstract String name();
public abstract String[] aliases();
public abstract String description();
public abstract Class<?> commandClass();
@Override
public final int hashCode()
{
return this.name().hashCode();
}
}

View File

@ -0,0 +1,96 @@
package cz.tefek.pluto.command.context;
import cz.tefek.pluto.command.CommandBase;
public class CommandContextBuilder
{
private 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;
}
}

View File

@ -0,0 +1,252 @@
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 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:
this.state = EnumParserState.UNEXPECTED_STATE_FALLBACK;
return false;
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;
}
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;
}
this.text.codePoints().takeWhile(this::readCodepoint);
// 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();
}
}

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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()
{
}
}

View File

@ -0,0 +1,6 @@
package cz.tefek.pluto.command.resolver;
public abstract class AbstractResolver
{
public abstract Class<?> getOutputType();
}

View File

@ -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;
}

View File

@ -0,0 +1,21 @@
package cz.tefek.pluto.command.resolver;
import java.util.function.Function;
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<R> getOutputType();
}

View File

@ -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;
}
}

View File

@ -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);
}
});
}
}

View File

@ -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);
}
});
}
}

View File

@ -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);
}
});
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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(""));
}
}

View File

@ -1,4 +1,5 @@
include 'plutolib',
'plutocommandparser',
'plutostatic',
'plutotexturing',
'plutomesher',