Skip to content

Commit

Permalink
Merge pull request #81 from LucaBernstein/comment-value
Browse files Browse the repository at this point in the history
Rewrite quoted value parser
  • Loading branch information
LucaBernstein authored Dec 26, 2021
2 parents fd3cc5f + 2666dd9 commit 1696bde
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 16 deletions.
2 changes: 2 additions & 0 deletions bot/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import (
"testing"

"github.com/DATA-DOG/go-sqlmock"
"github.com/LucaBernstein/beancount-bot-tg/db/crud"
tb "gopkg.in/tucnak/telebot.v2"
)

func TestConfigCurrency(t *testing.T) {
// Test dependencies
crud.TEST_MODE = true
chat := &tb.Chat{ID: 12345}
db, mock, err := sqlmock.New()
if err != nil {
Expand Down
40 changes: 39 additions & 1 deletion bot/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

dbWrapper "github.com/LucaBernstein/beancount-bot-tg/db"
"github.com/LucaBernstein/beancount-bot-tg/db/crud"
"github.com/LucaBernstein/beancount-bot-tg/helpers"
"github.com/go-co-op/gocron"
tb "gopkg.in/tucnak/telebot.v2"
)
Expand Down Expand Up @@ -91,6 +92,7 @@ const (
CMD_HELP = "help"
CMD_CANCEL = "cancel"
CMD_SIMPLE = "simple"
CMD_COMMENT = "comment"
CMD_LIST = "list"
CMD_ARCHIVE_ALL = "archiveAll"
CMD_DELETE_ALL = "deleteAll"
Expand All @@ -107,6 +109,7 @@ func (bc *BotController) commandMappings() []*CMD {
{Command: CMD_START, Handler: bc.commandStart, Help: "Give introduction into this bot"},
{Command: CMD_CANCEL, Handler: bc.commandCancel, Help: "Cancel any running commands or transactions"},
{Command: CMD_SIMPLE, Handler: bc.commandCreateSimpleTx, Help: "Record a simple transaction, defaults to today", Optional: "YYYY-MM-DD"},
{Command: CMD_COMMENT, Handler: bc.commandAddComment, Help: "Add arbitrary text to transaction list"},
{Command: CMD_LIST, Handler: bc.commandList, Help: "List your recorded transactions", Optional: "archived"},
{Command: CMD_SUGGEST, Handler: bc.commandSuggestions, Help: "List, add or remove suggestions"},
{Command: CMD_CONFIG, Handler: bc.commandConfig, Help: "Bot configurations"},
Expand Down Expand Up @@ -184,7 +187,7 @@ func (bc *BotController) commandCancel(m *tb.Message) {
func (bc *BotController) commandCreateSimpleTx(m *tb.Message) {
tx := bc.State.Get(m)
if tx != nil {
_, err := bc.Bot.Send(m.Sender, "You are already in a transaction. Please finish it or /cancel it before starting a new one.", clearKeyboard())
_, err := bc.Bot.Send(m.Sender, "You are already in a transaction. Please finish it or /cancel it before starting a new one.")
if err != nil {
bc.Logf(ERROR, m, "Sending bot message failed: %s", err.Error())
}
Expand Down Expand Up @@ -215,6 +218,41 @@ func (bc *BotController) commandCreateSimpleTx(m *tb.Message) {
}
}

func (bc *BotController) commandAddComment(m *tb.Message) {
if bc.State.Get(m) != nil {
bc.Logf(INFO, m, "commandAddComment while in another transaction")
_, err := bc.Bot.Send(m.Sender, "You are already in a transaction. Please finish it or /cancel it before starting a new one.")
if err != nil {
bc.Logf(ERROR, m, "Sending bot message failed: %s", err.Error())
}
return
}
remainingCommand := strings.TrimPrefix(strings.TrimLeft(m.Text, ""), "/"+CMD_COMMENT)
remainingSplits := helpers.SplitQuotedCommand(remainingCommand)
if len(remainingSplits) != 1 {
bc.Logf(INFO, m, "commandAddComment: extracting comment value failed for '%s'", remainingCommand)
_, err := bc.Bot.Send(m.Sender, fmt.Sprintf("Please use this command like this:\n/%s \"<comment to add>\"", CMD_COMMENT))
if err != nil {
bc.Logf(ERROR, m, "Sending bot message failed: %s", err.Error())
}
return
}
comment := remainingSplits[0]
err := bc.Repo.RecordTransaction(m.Chat.ID, comment)
if err != nil {
bc.Logf(ERROR, m, "Something went wrong while recording the comment: "+err.Error())
_, err := bc.Bot.Send(m.Sender, "Something went wrong while recording your comment: "+err.Error(), clearKeyboard())
if err != nil {
bc.Logf(ERROR, m, "Sending bot message failed: %s", err.Error())
}
return
}
_, err = bc.Bot.Send(m.Sender, "Successfully added the comment to your transaction /list", clearKeyboard())
if err != nil {
bc.Logf(ERROR, m, "Sending bot message failed: %s", err.Error())
}
}

func (bc *BotController) commandList(m *tb.Message) {
bc.Logf(TRACE, m, "Listing transactions")
command := strings.Split(m.Text, " ")
Expand Down
28 changes: 28 additions & 0 deletions bot/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func (b *MockBot) reset() {
// GitHub-Issue #16: Panic if plain message without state arrives
func TestTextHandlingWithoutPriorState(t *testing.T) {
// create test dependencies
crud.TEST_MODE = true
chat := &tb.Chat{ID: 12345}
db, mock, err := sqlmock.New()
if err != nil {
Expand Down Expand Up @@ -168,6 +169,33 @@ func TestTransactionListMaxLength(t *testing.T) {
}
}

func TestWritingComment(t *testing.T) {
// create test dependencies
crud.TEST_MODE = true
chat := &tb.Chat{ID: 12345}
db, mock, err := sqlmock.New()
if err != nil {
log.Fatal(err)
}
mock.
ExpectExec(`INSERT INTO "bot::transaction"`).
WithArgs(chat.ID, "; This is a comment").
WillReturnResult(sqlmock.NewResult(1, 1))

bc := NewBotController(db)
bot := &MockBot{}
bc.AddBotAndStart(bot)

bc.commandAddComment(&tb.Message{Chat: chat, Text: "/comment \"; This is a comment\""})
if !strings.Contains(fmt.Sprintf("%v", bot.LastSentWhat), "added the comment") {
t.Errorf("Adding comment should have worked. Got message: %s", bot.LastSentWhat)
}

if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}

func stringArr(i []interface{}) []string {
arr := []string{}
for _, e := range i {
Expand Down
1 change: 1 addition & 0 deletions db/crud/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func logToDb(r *Repo, chat string, level helpers.Level, message string) {
_, err := r.db.Exec(`INSERT INTO "app::log" ("level", "message", "chat") VALUES ($1, $2, $3)`, values...)
if err != nil {
helpers.LogLocalf(helpers.ERROR, nil, "Error inserting log statement into db: %s", err.Error())
log.Fatal("Logging to database was not possible.")
}
} else {
log.Printf("DB LOGGER IS IN TEST MODE")
Expand Down
55 changes: 40 additions & 15 deletions helpers/subcommands.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,22 +60,10 @@ func (sh *SubcommandHandler) Handle(m *tb.Message) error {
return nil
}

// More than just subcommand is left.
// Split by \" and interpret place odd as unquoted and even as quoted
// e.g. 'a b "c d" e f'
remainingCommand := strings.TrimSpace(strings.TrimPrefix(commandRemainder, subcommand))
parameters = []string{}
params := strings.Split(remainingCommand, "\"")
for i, e := range params {
e = strings.TrimSpace(e)
if e == "" {
continue
}
if (i+1)%2 == 0 { // Even: Quoted
parameters = append(parameters, e)
} else { // Odd: Unquoted
parameters = append(parameters, strings.Split(e, " ")...)
}
parameters = SplitQuotedCommand(remainingCommand)
if ArraysEqual(parameters, []string{}) {
return fmt.Errorf("this remainingCommand could not be split: '%s'", remainingCommand)
}
fn(m, parameters...)
return nil
Expand All @@ -99,3 +87,40 @@ func ExtractTypeValue(params ...string) (*TV, error) {
}
return e, nil
}

func SplitQuotedCommand(s string) (res []string) {
isEscaped := false
isQuoted := false
split := ""
for _, c := range s {
if isEscaped {
isEscaped = false
split += string(c)
continue
}
if c == '\\' {
isEscaped = true
continue
}
if c == '"' {
isQuoted = !isQuoted
continue
}
if c == ' ' && !isQuoted {
if split == "" {
continue
}
res = append(res, split)
split = ""
continue
}
split += string(c)
}
if split != "" {
res = append(res, split)
}
if isEscaped || isQuoted {
return []string{}
}
return
}
16 changes: 16 additions & 0 deletions helpers/subcommands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,19 @@ func TestSubcommandHelper(t *testing.T) {
state.testCase(t, sh, "base subcommand \"single arg\" multi arg", false, []string{"single arg", "multi", "arg"})
state.testCase(t, sh, "base notregistered subcommand", true, nil)
}

func TestSplitQuotedCommand(t *testing.T) {
helpers.TestExpectArrEq(t, helpers.SplitQuotedCommand(`something`), []string{"something"}, "")
helpers.TestExpectArrEq(t, helpers.SplitQuotedCommand(`/command`), []string{"/command"}, "")
helpers.TestExpectArrEq(t, helpers.SplitQuotedCommand(` /command `), []string{"/command"}, "")
helpers.TestExpectArrEq(t, helpers.SplitQuotedCommand(` /command anotherone `), []string{"/command", "anotherone"}, "")
helpers.TestExpectArrEq(t, helpers.SplitQuotedCommand(`/command "anotherone"`), []string{"/command", "anotherone"}, "")
helpers.TestExpectArrEq(t, helpers.SplitQuotedCommand(`/command "is quoted" non-quoted and sep`), []string{"/command", "is quoted", "non-quoted", "and", "sep"}, "")
helpers.TestExpectArrEq(t, helpers.SplitQuotedCommand(`/command hello"world"`), []string{"/command", "helloworld"}, "")
helpers.TestExpectArrEq(t, helpers.SplitQuotedCommand(`/command hello "world"`), []string{"/command", "hello", "world"}, "")
helpers.TestExpectArrEq(t, helpers.SplitQuotedCommand(`/command hello" world"`), []string{"/command", "hello world"}, "")
helpers.TestExpectArrEq(t, helpers.SplitQuotedCommand(`/command "wrongly quoted`), []string{}, "quotes opened but not closed")
helpers.TestExpectArrEq(t, helpers.SplitQuotedCommand(`/command \"correctly quoted`), []string{"/command", "\"correctly", "quoted"}, "quotes opened but not closed")
helpers.TestExpectArrEq(t, helpers.SplitQuotedCommand(`/command hello\ world`), []string{"/command", "hello world"}, "")
helpers.TestExpectArrEq(t, helpers.SplitQuotedCommand(`"onlyquoted"`), []string{"onlyquoted"}, "")
}
10 changes: 10 additions & 0 deletions helpers/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,13 @@ func TestExpect(t *testing.T, e1 interface{}, e2 interface{}, msg string) {
t.Errorf(errorMsg, msg, e1, e2)
}
}

func TestExpectArrEq(t *testing.T, e1, e2 []string, msg string) {
if !ArraysEqual(e1, e2) {
errorMsg := "Expected '%v' to be '%v'"
if msg != "" {
errorMsg = "%s -> " + errorMsg
}
t.Errorf(errorMsg, msg, e1, e2)
}
}

0 comments on commit 1696bde

Please sign in to comment.