CommandLib more or less finished

This commit is contained in:
Miroslav Vasilev 2025-06-18 17:04:06 +03:00
parent 1b0564b39a
commit 82082b128f
14 changed files with 203 additions and 323 deletions

View file

@ -1,86 +0,0 @@
package commandlib
type ArgType byte
const (
StringArg = iota
IntArg
FloatArg
)
func (a ArgType) String() string {
switch a {
case StringArg:
return "word"
case IntArg:
return "number"
case FloatArg:
return "decimal number"
default:
return "unknown"
}
}
type arg struct {
name string
help string
optional bool
argType ArgType
validators []ArgValidator
}
func CreateArg(
name string,
help string,
optional bool,
argType ArgType,
validators ...ArgValidator,
) (res *arg, err error) {
res = &arg{
name: name,
help: help,
argType: argType,
validators: validators,
}
return
}
func CreateStringArg(name string, help string, validators ...ArgValidator) (res *arg) {
res = &arg{
name: name,
help: help,
argType: StringArg,
validators: append([]ArgValidator{StringArgTypeValidator}, validators...),
}
return
}
func (arg *arg) ArgType() ArgType {
return arg.argType
}
func (arg *arg) Name() string {
return arg.name
}
func (arg *arg) IsOptional() bool {
return arg.optional
}
func (arg *arg) Validate(value any) (valid bool, feedback []error) {
feedback = []error{}
for _, validate := range arg.validators {
err := validate(value)
if err != nil {
valid = false
feedback = append(feedback, err)
}
}
return
}

View file

@ -1,49 +0,0 @@
package commandlib
import "fmt"
type ArgValidator = func(value any) (err error)
type argValidationError struct {
message string
}
func CreateArgValidationError(template string, args ...any) *argValidationError {
return &argValidationError{
message: fmt.Sprintf(template, args...),
}
}
func (err *argValidationError) Error() string {
return err.message
}
func StringArgTypeValidator(value any) (err error) {
_, valid := value.(string)
if !valid {
err = CreateArgValidationError("Invalid argument type, expected %v", StringArg)
}
return
}
func IntArgTypeValidator(value any) (err error) {
_, valid := value.(int32)
if !valid {
err = CreateArgValidationError("Invalid type, expected %v", IntArg)
}
return
}
func FloatArgTypeValidator(value any) (err error) {
_, valid := value.(float32)
if !valid {
err = CreateArgValidationError("Invalid type, expected %v", FloatArg)
}
return
}

View file

@ -1,15 +0,0 @@
package commandlib
type argValue struct {
value any
}
func CreateArgValue(val any) *argValue {
return &argValue{
value: val,
}
}
func (aVal *argValue) Value() any {
return aVal.value
}

View file

@ -1,71 +1,70 @@
package commandlib
type ArgumentBase interface {
Name() string
ArgType() ArgType
IsOptional() bool
Validate(value any) (valid bool, feedback []error)
type Command struct {
commandDefinition CommandDefinition
params []Parameter
}
type ArgumentValue interface {
Value() any
func CreateCommand(cmdDef CommandDefinition, parameters []Parameter) Command {
return Command{
commandDefinition: cmdDef,
params: parameters,
}
}
type command struct {
name string
altname string
args []ArgumentBase
work func(argValues []ArgumentValue) (err error)
func (cmd Command) Execute() (err error) {
return cmd.commandDefinition.work(cmd.params...)
}
func CreateCommand(
name string,
altname string,
work func(argValues []ArgumentValue) (err error),
arguments ...ArgumentBase,
) (cmd *command, err error) {
var onlyAcceptingOptionals = false
type commandContextError struct {
err string
}
for _, v := range arguments {
if !v.IsOptional() && onlyAcceptingOptionals {
// Optional arguments can only be placed after non-optional ones
err = CreateCommandLibError(name, "Cannot define non-optional arguments after optional ones.")
cmd = nil
func createCommandContextError(err string) *commandContextError {
return &commandContextError{
err: err,
}
}
return
}
func (cce *commandContextError) Error() string {
return cce.err
}
if v.IsOptional() {
onlyAcceptingOptionals = true
}
type CommandContext struct {
commandString string
tokens []Token
command Command
}
func CreateCommandContext(commandRegistry *CommandRegistry, commandString string) (ctx *CommandContext, err error) {
tokenizer := CreateTokenizer()
tokens, tokenizerError := tokenizer.Tokenize(commandString)
if tokenizerError != nil {
err = tokenizerError
return
}
cmd = new(command)
commandDef := commandRegistry.Match(tokens)
cmd.name = name
cmd.altname = altname
cmd.work = work
if commandDef == nil {
err = createCommandContextError("Unknown command")
return
}
params := commandDef.ParseParameters(tokens)
ctx = &CommandContext{
commandString: commandString,
tokens: tokens,
command: CreateCommand(*commandDef, params),
}
return
}
func (cmd *command) Name() string {
return cmd.name
}
func (cmd *command) Execute(argValues []ArgumentValue) (err error) {
for i, v := range cmd.args {
if i > len(argValues)-1 {
if !v.IsOptional() {
return CreateCommandLibError(cmd.name, "Not enough arguments, found %d, expected more", len(argValues))
} else {
break // There are no more arg values to process, and the remaining arguments are all optional anyway
}
}
}
return cmd.work(argValues)
func (ctx *CommandContext) ExecuteCommand() (err error) {
return ctx.command.Execute()
}

View file

@ -1,30 +0,0 @@
package commandlib
type commandContext struct {
commandString string
tokens []Token
command Command
}
func CreateCommandContext(commandString string) (ctx *commandContext, err error) {
tokenizer := CreateTokenizer()
tokens, tokenizerError := tokenizer.Tokenize(commandString)
if tokenizerError != nil {
err = tokenizerError
return
}
ctx = &commandContext{
commandString: commandString,
tokens: tokens,
}
return
}
func (ctx *commandContext) Execute() (err error) {
ctx.command.Execute()
}

View file

@ -1,4 +0,0 @@
package commandlib
type CommandDefinition struct {
}

View file

@ -1,10 +0,0 @@
package commandlib
type Command interface {
Name() string
DoWork(argValues []ArgumentValue) (err error)
}
type commandRegistry struct {
commands []Command
}

View file

@ -2,18 +2,18 @@ package commandlib
import "fmt"
type commandLibError struct {
type commandError struct {
cmdName string
message string
}
func CreateCommandLibError(cmdName string, msg string, msgArgs ...any) *commandLibError {
return &commandLibError{
func createCommandError(cmdName string, msg string, msgArgs ...any) *commandError {
return &commandError{
cmdName: cmdName,
message: fmt.Sprintf(msg, msgArgs...),
}
}
func (cmdErr *commandLibError) Error() string {
func (cmdErr *commandError) Error() string {
return "Error with command '" + cmdErr.cmdName + "': " + cmdErr.message
}

View file

@ -1,10 +0,0 @@
package commandlib
type Parameter interface {
Value() any
}
type Command interface {
Name() string
Parameters() []Parameter
}

View file

@ -45,7 +45,7 @@ look ::= "look" [ "around" | direction | "at" identifier ] ;
move ::= "move" direction | "go" direction ;
// [Player Name], [Item Name], [Place Name] or just Played Name, Item Name, Place Name
// [Player Name], [Item Name], [Place Name] or just Player Name, Item Name, Place Name
// brackets may be useful in situations where there are multiple identifiers
identifier ::= "[" name "]" | name ;
@ -57,12 +57,10 @@ number ::= digit ( digit )* ; // 123, 12, 97401, etc.
word ::= letter+;
chatMessage ::= ( letter | punctuation | digit | space )+ ;
chatMessage ::= ( letter | digit | space )+ ;
direction ::= "east" | "west" | "north" | "up" | "down" ;
punctuation ::= "," | "." | "!" | "?" | "'" | "/" | '"' | ":" | ";" | "-" | "(" | ")" | "[" | "]" ;
letter ::= "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j"
| "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t"
| "u" | "v" | "w" | "x" | "y" | "z" ;

View file

@ -0,0 +1,25 @@
package commandlib
import "strconv"
type Parameter struct {
value string
}
func CreateParameter(value string) Parameter {
return Parameter{
value: value,
}
}
func (p Parameter) AsString() (res string, err error) {
return p.value, nil
}
func (p Parameter) AsInteger() (res int, err error) {
return strconv.Atoi(p.value)
}
func (p Parameter) AsDecimal() (res float64, err error) {
return strconv.ParseFloat(p.value, 32)
}

View file

@ -0,0 +1,71 @@
package commandlib
import "log"
type TokenMatcher func(tokens []Token) bool
type ParameterParser func(tokens []Token) []Parameter
type CommandWork func(parameters ...Parameter) (err error)
type CommandDefinition struct {
name string
tokenMatcher TokenMatcher
parameterParser ParameterParser
work CommandWork
}
func CreateCommandDefinition(
name string,
tokenMatcher TokenMatcher,
parameterParser ParameterParser,
work CommandWork,
) CommandDefinition {
return CommandDefinition{
name: name,
tokenMatcher: tokenMatcher,
parameterParser: parameterParser,
work: work,
}
}
func (def CommandDefinition) Name() string {
return def.name
}
func (def CommandDefinition) Match(tokens []Token) bool {
return def.tokenMatcher(tokens)
}
func (def CommandDefinition) ParseParameters(tokens []Token) []Parameter {
return def.parameterParser(tokens)
}
func (def CommandDefinition) ExecuteFunc() CommandWork {
return def.work
}
type CommandRegistry struct {
commandDefinitions []CommandDefinition
}
func CreateCommandRegistry(commandDefinitions ...CommandDefinition) *CommandRegistry {
return &CommandRegistry{
commandDefinitions: commandDefinitions,
}
}
func (comReg *CommandRegistry) Register(newCommandDefinitions ...CommandDefinition) {
comReg.commandDefinitions = append(comReg.commandDefinitions, newCommandDefinitions...)
}
func (comReg *CommandRegistry) Match(tokens []Token) (comDef *CommandDefinition) {
for _, v := range comReg.commandDefinitions {
if v.Match(tokens) {
log.Println("Found match", v.Name())
return &v
}
}
return
}

View file

@ -20,7 +20,6 @@ const (
TokenDirection
TokenCommand
TokenSayCommand
TokenSelf
TokenWhitespace
@ -46,8 +45,6 @@ func (tt TokenType) String() string {
return "Direction"
case TokenCommand:
return "Command"
case TokenSayCommand:
return "SayCommand"
case TokenSelf:
return "Self"
case TokenWhitespace:
@ -92,12 +89,15 @@ type tokenPattern struct {
pattern string
}
type tokenizer struct {
// Used to tokenize a string input.
// This is the starting point for parsing a command string.
// Create with [CreateTokenizer]
type Tokenizer struct {
tokenPatterns []tokenPattern
}
func CreateTokenizer() *tokenizer {
return &tokenizer{
func CreateTokenizer() *Tokenizer {
return &Tokenizer{
tokenPatterns: []tokenPattern{
{tokenType: TokenDecimal, pattern: `\b\d+\.\d+\b`},
{tokenType: TokenNumber, pattern: `\b\d+\b`},
@ -111,15 +111,16 @@ func CreateTokenizer() *tokenizer {
}
}
func (t *tokenizer) Tokenize(commandMsg string) (tokens []Token, err error) {
// Tokenize a command string
func (t *Tokenizer) Tokenize(commandString string) (tokens []Token, err error) {
tokens = []Token{}
pos := 0
inputLen := len(commandMsg)
inputLen := len(commandString)
// Continue iterating until we reach the end of the input
for pos < inputLen {
matched := false
remaining := commandMsg[pos:]
remaining := commandString[pos:]
// Iterate through each token type and test its pattern
for _, pattern := range t.tokenPatterns {
@ -133,7 +134,7 @@ func (t *tokenizer) Tokenize(commandMsg string) (tokens []Token, err error) {
return
}
// If the loc isn't nil, that means we've found a match
// If the location of the match isn't nil, that means we've found a match
if loc := re.FindStringIndex(remaining); loc != nil {
lexeme := remaining[loc[0]:loc[1]]
@ -145,9 +146,9 @@ func (t *tokenizer) Tokenize(commandMsg string) (tokens []Token, err error) {
}
}
// Unknown tokens are still added, except carriage return (\r) and newline (\n)
// Unknown tokens are still added
if !matched {
tokens = append(tokens, CreateToken(TokenUnknown, commandMsg[pos:pos+1], pos))
tokens = append(tokens, CreateToken(TokenUnknown, commandString[pos:pos+1], pos))
pos++
}
}

View file

@ -5,9 +5,6 @@ import (
"fmt"
"log"
"net"
"strings"
commandlib "code.haedhutner.dev/mvv/LastMUD/CommandLib"
)
type Command interface {
@ -15,18 +12,6 @@ type Command interface {
}
func main() {
// testcmd, err := commandlib.CreateCommand(
// "test",
// "t",
// func(argValues []commandlib.ArgumentValue) (err error) {
// err = nil
// return
// },
// commandlib.CreateStringArg("test", "test message"),
// )
tokenizer := commandlib.CreateTokenizer()
ln, err := net.Listen("tcp", ":8000")
if err != nil {
@ -41,47 +26,52 @@ func main() {
log.Fatal(err)
}
// cmdRegistry := commandlib.CreateCommandRegistry(
// commandlib.CreateCommandDefinition(
// "exit",
// func(tokens []commandlib.Token) bool {
// return tokens[0].Lexeme() == "exit"
// },
// func(tokens []commandlib.Token) []commandlib.Parameter {
// return nil
// },
// func(parameters ...commandlib.Parameter) (err error) {
// err = conn.Close()
// return
// },
// ),
// )
for {
message, err := bufio.NewReader(conn).ReadString('\n')
message, _ := bufio.NewReader(conn).ReadString('\n')
response := ""
if err != nil {
log.Fatal(err)
}
// if err != nil {
// if err == io.EOF {
// fmt.Println("Client disconnected")
// break
// }
// log.Println("Read error:", err)
// continue
// }
conn.Write([]byte(message + "\n"))
tokens, err := tokenizer.Tokenize(message)
// cmdContext, err := commandlib.CreateCommandContext(cmdRegistry, message)
if err != nil {
response = err.Error()
} else {
lines := make([]string, len(tokens))
for i, tok := range tokens {
lines[i] = tok.String()
}
response = strings.Join(lines, "\n")
}
// if strings.HasPrefix(message, testcmd.Name()) {
// tokens := commandlib.Tokenize(message)
// args := []commandlib.ArgumentValue{}
// for _, v := range tokens[1:] {
// args = append(args, commandlib.CreateArgValue(v))
// }
// err := testcmd.DoWork(args)
// if err != nil {
// fmt.Print(err.Error())
// }
// if err != nil {
// log.Println(err)
// response = err.Error()
// } else {
// fmt.Print("Message Received: ", string(message))
// // err = cmdContext.ExecuteCommand()
// response = strings.ToUpper(message)
// // if err != nil {
// // log.Println(err)
// // response = err.Error()
// // }
// }
conn.Write([]byte(response + "\n> "))