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 package commandlib
type ArgumentBase interface { type Command struct {
Name() string commandDefinition CommandDefinition
ArgType() ArgType params []Parameter
IsOptional() bool
Validate(value any) (valid bool, feedback []error)
} }
type ArgumentValue interface { func CreateCommand(cmdDef CommandDefinition, parameters []Parameter) Command {
Value() any return Command{
commandDefinition: cmdDef,
params: parameters,
}
} }
type command struct { func (cmd Command) Execute() (err error) {
name string return cmd.commandDefinition.work(cmd.params...)
altname string
args []ArgumentBase
work func(argValues []ArgumentValue) (err error)
} }
func CreateCommand( type commandContextError struct {
name string, err string
altname string, }
work func(argValues []ArgumentValue) (err error),
arguments ...ArgumentBase,
) (cmd *command, err error) {
var onlyAcceptingOptionals = false
for _, v := range arguments { func createCommandContextError(err string) *commandContextError {
if !v.IsOptional() && onlyAcceptingOptionals { return &commandContextError{
// Optional arguments can only be placed after non-optional ones err: err,
err = CreateCommandLibError(name, "Cannot define non-optional arguments after optional ones.") }
cmd = nil }
func (cce *commandContextError) Error() string {
return cce.err
}
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
}
commandDef := commandRegistry.Match(tokens)
if commandDef == nil {
err = createCommandContextError("Unknown command")
return
}
params := commandDef.ParseParameters(tokens)
ctx = &CommandContext{
commandString: commandString,
tokens: tokens,
command: CreateCommand(*commandDef, params),
}
return return
} }
if v.IsOptional() { func (ctx *CommandContext) ExecuteCommand() (err error) {
onlyAcceptingOptionals = true return ctx.command.Execute()
}
}
cmd = new(command)
cmd.name = name
cmd.altname = altname
cmd.work = work
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)
} }

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" import "fmt"
type commandLibError struct { type commandError struct {
cmdName string cmdName string
message string message string
} }
func CreateCommandLibError(cmdName string, msg string, msgArgs ...any) *commandLibError { func createCommandError(cmdName string, msg string, msgArgs ...any) *commandError {
return &commandLibError{ return &commandError{
cmdName: cmdName, cmdName: cmdName,
message: fmt.Sprintf(msg, msgArgs...), message: fmt.Sprintf(msg, msgArgs...),
} }
} }
func (cmdErr *commandLibError) Error() string { func (cmdErr *commandError) Error() string {
return "Error with command '" + cmdErr.cmdName + "': " + cmdErr.message 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 ; 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 // brackets may be useful in situations where there are multiple identifiers
identifier ::= "[" name "]" | name ; identifier ::= "[" name "]" | name ;
@ -57,12 +57,10 @@ number ::= digit ( digit )* ; // 123, 12, 97401, etc.
word ::= letter+; word ::= letter+;
chatMessage ::= ( letter | punctuation | digit | space )+ ; chatMessage ::= ( letter | digit | space )+ ;
direction ::= "east" | "west" | "north" | "up" | "down" ; direction ::= "east" | "west" | "north" | "up" | "down" ;
punctuation ::= "," | "." | "!" | "?" | "'" | "/" | '"' | ":" | ";" | "-" | "(" | ")" | "[" | "]" ;
letter ::= "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" letter ::= "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j"
| "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t"
| "u" | "v" | "w" | "x" | "y" | "z" ; | "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 TokenDirection
TokenCommand TokenCommand
TokenSayCommand
TokenSelf TokenSelf
TokenWhitespace TokenWhitespace
@ -46,8 +45,6 @@ func (tt TokenType) String() string {
return "Direction" return "Direction"
case TokenCommand: case TokenCommand:
return "Command" return "Command"
case TokenSayCommand:
return "SayCommand"
case TokenSelf: case TokenSelf:
return "Self" return "Self"
case TokenWhitespace: case TokenWhitespace:
@ -92,12 +89,15 @@ type tokenPattern struct {
pattern string 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 tokenPatterns []tokenPattern
} }
func CreateTokenizer() *tokenizer { func CreateTokenizer() *Tokenizer {
return &tokenizer{ return &Tokenizer{
tokenPatterns: []tokenPattern{ tokenPatterns: []tokenPattern{
{tokenType: TokenDecimal, pattern: `\b\d+\.\d+\b`}, {tokenType: TokenDecimal, pattern: `\b\d+\.\d+\b`},
{tokenType: TokenNumber, pattern: `\b\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{} tokens = []Token{}
pos := 0 pos := 0
inputLen := len(commandMsg) inputLen := len(commandString)
// Continue iterating until we reach the end of the input // Continue iterating until we reach the end of the input
for pos < inputLen { for pos < inputLen {
matched := false matched := false
remaining := commandMsg[pos:] remaining := commandString[pos:]
// Iterate through each token type and test its pattern // Iterate through each token type and test its pattern
for _, pattern := range t.tokenPatterns { for _, pattern := range t.tokenPatterns {
@ -133,7 +134,7 @@ func (t *tokenizer) Tokenize(commandMsg string) (tokens []Token, err error) {
return 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 { if loc := re.FindStringIndex(remaining); loc != nil {
lexeme := remaining[loc[0]:loc[1]] 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 { if !matched {
tokens = append(tokens, CreateToken(TokenUnknown, commandMsg[pos:pos+1], pos)) tokens = append(tokens, CreateToken(TokenUnknown, commandString[pos:pos+1], pos))
pos++ pos++
} }
} }

View file

@ -5,9 +5,6 @@ import (
"fmt" "fmt"
"log" "log"
"net" "net"
"strings"
commandlib "code.haedhutner.dev/mvv/LastMUD/CommandLib"
) )
type Command interface { type Command interface {
@ -15,18 +12,6 @@ type Command interface {
} }
func main() { 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") ln, err := net.Listen("tcp", ":8000")
if err != nil { if err != nil {
@ -41,47 +26,52 @@ func main() {
log.Fatal(err) 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 { for {
message, err := bufio.NewReader(conn).ReadString('\n') message, _ := bufio.NewReader(conn).ReadString('\n')
response := "" response := ""
if err != nil { // if err != nil {
log.Fatal(err)
} // if err == io.EOF {
// fmt.Println("Client disconnected")
// break
// }
// log.Println("Read error:", err)
// continue
// }
conn.Write([]byte(message + "\n")) 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 { // if err != nil {
// fmt.Print(err.Error()) // log.Println(err)
// } // response = err.Error()
// } else { // } 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> ")) conn.Write([]byte(response + "\n> "))