diff --git a/slack.go b/slack.go index 782dc6e..3c9541e 100644 --- a/slack.go +++ b/slack.go @@ -12,6 +12,7 @@ import ( "github.com/slack-go/slack" "github.com/slack-go/slack/slackevents" + "github.com/zaiminc/gocat/slackcmd" ) // SlackListener is a http.Handler that can handle slack events. @@ -179,6 +180,11 @@ func (s *SlackListener) handleMessageEvent(ev *slackevents.AppMentionEvent) erro } return nil } + if cmd, _ := slackcmd.Parse(ev.Text); cmd != nil { + log.Printf("[INFO] %s command is Called", cmd.Name()) + // TODO run the command and post the result to slack + return nil + } return nil } diff --git a/slackcmd/lock.go b/slackcmd/lock.go new file mode 100644 index 0000000..1f736f5 --- /dev/null +++ b/slackcmd/lock.go @@ -0,0 +1,11 @@ +package slackcmd + +type Lock struct { + Project string + Env string + Reason string +} + +func (l *Lock) Name() string { + return "Lock" +} diff --git a/slackcmd/parse.go b/slackcmd/parse.go new file mode 100644 index 0000000..410ce47 --- /dev/null +++ b/slackcmd/parse.go @@ -0,0 +1,57 @@ +package slackcmd + +import ( + "fmt" + "regexp" + "strings" +) + +var lockUnlockPattern = regexp.MustCompile(`(unlock|lock) ([0-9a-zA-Z-]+) (staging|production|sandbox|stg|pro|prd)\s*(.*)`) + +func Parse(text string) (Command, error) { + match := findLockUnlock(text) + if match == nil { + return nil, fmt.Errorf("invalid command %q: valid pattern is 'lock|unlock [for ]", text) + } + + var ( + command = match[0][1] + project = match[0][2] + env = match[0][3] + reason = match[0][4] + ) + + switch command { + case "unlock": + if reason != "" { + return nil, fmt.Errorf("invalid command %q: unlock command does not accept reason", text) + } + + return &Unlock{ + Project: project, + Env: env, + }, nil + case "lock": + if reason == "" { + return nil, fmt.Errorf("invalid command %q: lock command requires reason", text) + } + + if !strings.HasPrefix(reason, "for ") { + return nil, fmt.Errorf("invalid command %q: reason must start with 'for'", text) + } + + reason = strings.TrimPrefix(reason, "for ") + + return &Lock{ + Project: project, + Env: env, + Reason: reason, + }, nil + default: + panic("unreachable") + } +} + +func findLockUnlock(text string) [][]string { + return lockUnlockPattern.FindAllStringSubmatch(text, -1) +} diff --git a/slackcmd/parse_test.go b/slackcmd/parse_test.go new file mode 100644 index 0000000..9b5f93d --- /dev/null +++ b/slackcmd/parse_test.go @@ -0,0 +1,92 @@ +package slackcmd + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + validProjects = []string{"myproject1", "myproject-2"} + invalidProjects = []string{"myproject#3", "myproject_4"} + validEnvs = []string{"staging", "production", "sandbox", "stg", "pro", "prd"} + invalidENvs = []string{"stg1", "pro1", "prd1", "prod", "test"} +) + +func TestParse(t *testing.T) { + type test struct { + name string + text string + want Command + err error + } + + var tests = []test{} + + for i, p := range validProjects { + for j, e := range validEnvs { + tests = append(tests, test{ + name: fmt.Sprintf("lock with valid project %d and env %d", i, j), + text: fmt.Sprintf("lock %s %s for deployment of revision a", p, e), + want: &Lock{Project: p, Env: e, Reason: "deployment of revision a"}, + }) + } + } + + for i, p := range invalidProjects { + for j, e := range invalidENvs { + tests = append(tests, test{ + name: fmt.Sprintf("lock with invalid project %d and env %d", i, j), + text: fmt.Sprintf("lock %s %s for deployment of revision a", p, e), + err: fmt.Errorf("invalid command %q: valid pattern is 'lock|unlock [for ]", fmt.Sprintf("lock %s %s for deployment of revision a", p, e)), + }) + } + } + + for i, p := range validProjects { + for j, e := range validEnvs { + tests = append(tests, test{ + name: fmt.Sprintf("unlock with valid project %d and env %d", i, j), + text: fmt.Sprintf("unlock %s %s", p, e), + want: &Unlock{Project: p, Env: e}, + }) + } + } + + for i, p := range invalidProjects { + for j, e := range invalidENvs { + tests = append(tests, test{ + name: fmt.Sprintf("unlock with invalid project %d and env %d", i, j), + text: fmt.Sprintf("unlock %s %s", p, e), + err: fmt.Errorf("invalid command %q: valid pattern is 'lock|unlock [for ]", fmt.Sprintf("unlock %s %s", p, e)), + }) + } + } + + tests = append(tests, test{ + name: "unlock has redundant reason", + text: "unlock myproject1 production for deployment of revision a", + err: fmt.Errorf("invalid command %q: unlock command does not accept reason", "unlock myproject1 production for deployment of revision a"), + }) + + tests = append(tests, test{ + name: "lock missing reason", + text: "lock myproject1 production", + err: fmt.Errorf("invalid command %q: lock command requires reason", "lock myproject1 production"), + }) + + tests = append(tests, test{ + name: "unknown command", + text: "unknown myproject1 production for deployment of revision a", + err: fmt.Errorf("invalid command %q: valid pattern is 'lock|unlock [for ]", "unknown myproject1 production for deployment of revision a"), + }) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Parse(tt.text) + assert.Equal(t, tt.want, got, "result") + assert.Equal(t, tt.err, err, "error") + }) + } +} diff --git a/slackcmd/slackcmd.go b/slackcmd/slackcmd.go new file mode 100644 index 0000000..9843f17 --- /dev/null +++ b/slackcmd/slackcmd.go @@ -0,0 +1,5 @@ +package slackcmd + +type Command interface { + Name() string +} diff --git a/slackcmd/unlock.go b/slackcmd/unlock.go new file mode 100644 index 0000000..cd62f7b --- /dev/null +++ b/slackcmd/unlock.go @@ -0,0 +1,10 @@ +package slackcmd + +type Unlock struct { + Project string + Env string +} + +func (u *Unlock) Name() string { + return "Unlock" +}