diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f51c60c03..e27f857b6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -3,12 +3,16 @@ name: Bug Report about: Create a report to help us improve --- -### What happend +### What happened ### Expected behavior ### Steps to reproduce +#### reproducible code + +#### manifest.yaml + ### Versions - Go: - slack-go/slack: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f82ae690f..97dacfc66 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,31 +7,30 @@ on: pull_request: jobs: - ci: - runs-on: ubuntu-latest - name: lint - steps: - - uses: actions/checkout@v2 - - name: golangci-lint - uses: golangci/golangci-lint-action@v2 - with: - version: v1.32 test: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: matrix: go: - - '1.12' - - '1.13' - - '1.14' - - '1.15' + - '1.16' + - '1.17' + - '1.18' name: test go-${{ matrix.go }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - name: run test run: go test -v -race ./... env: GO111MODULE: on + lint: + runs-on: ubuntu-20.04 + name: lint + steps: + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest diff --git a/README.md b/README.md index 7be890664..39b04ce83 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ -Slack API in Go [![GoDoc](https://godoc.org/github.com/slack-go/slack?status.svg)](https://godoc.org/github.com/slack-go/slack) [![Build Status](https://travis-ci.org/slack-go/slack.svg)](https://travis-ci.org/slack-go/slack) +Slack API in Go [![Go Reference](https://pkg.go.dev/badge/github.com/slack-go/slack.svg)](https://pkg.go.dev/github.com/slack-go/slack) =============== -This is the original Slack library for Go created by Norberto Lopes, transferred to a Github organization. -[![Join the chat at https://gitter.im/go-slack/Lobby](https://badges.gitter.im/go-slack/Lobby.svg)](https://gitter.im/go-slack/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +This is the original Slack library for Go created by Norberto Lopes, transferred to a GitHub organization. + +You can also chat with us on the #slack-go, #slack-go-ja Slack channel on the Gophers Slack. ![logo](logo.png "icon") @@ -14,7 +15,7 @@ a fully managed way. There is currently no major version released. Therefore, minor version releases may include backward incompatible changes. -See [CHANGELOG.md](https://github.com/slack-go/slack/blob/master/CHANGELOG.md) for more information about the changes. +See [CHANGELOG.md](https://github.com/slack-go/slack/blob/master/CHANGELOG.md) or [Releases](https://github.com/slack-go/slack/releases) for more information about the changes. ## Installing @@ -38,7 +39,7 @@ func main() { // If you set debugging, it will log all requests to the console // Useful when encountering issues // slack.New("YOUR_TOKEN_HERE", slack.OptionDebug(true)) - groups, err := api.GetGroups(false) + groups, err := api.GetUserGroups(false) if err != nil { fmt.Printf("%s\n", err) return @@ -69,8 +70,15 @@ func main() { } ``` +## Minimal Socket Mode usage: + +See https://github.com/slack-go/slack/blob/master/examples/socketmode/socketmode.go + + ## Minimal RTM usage: +As mentioned in https://api.slack.com/rtm - for most applications, Socket Mode is a better way to communicate with Slack. + See https://github.com/slack-go/slack/blob/master/examples/websocket/websocket.go diff --git a/apps.go b/apps.go index d9749438d..10d429752 100644 --- a/apps.go +++ b/apps.go @@ -44,15 +44,18 @@ func (api *Client) ListEventAuthorizationsContext(ctx context.Context, eventCont } func (api *Client) UninstallApp(clientID, clientSecret string) error { + return api.UninstallAppContext(context.Background(), clientID, clientSecret) +} + +func (api *Client) UninstallAppContext(ctx context.Context, clientID, clientSecret string) error { values := url.Values{ - "token": {api.token}, "client_id": {clientID}, "client_secret": {clientSecret}, } response := SlackResponse{} - err := api.getMethod(context.Background(), "apps.uninstall", values, &response) + err := api.getMethod(ctx, "apps.uninstall", api.token, values, &response) if err != nil { return err } diff --git a/attachments.go b/attachments.go index b5b79f9fe..f4eb9b932 100644 --- a/attachments.go +++ b/attachments.go @@ -17,7 +17,7 @@ type AttachmentAction struct { Name string `json:"name"` // Required. Text string `json:"text"` // Required. Style string `json:"style,omitempty"` // Optional. Allowed values: "default", "primary", "danger". - Type actionType `json:"type"` // Required. Must be set to "button" or "select". + Type ActionType `json:"type"` // Required. Must be set to "button" or "select". Value string `json:"value,omitempty"` // Optional. DataSource string `json:"data_source,omitempty"` // Optional. MinQueryLength int `json:"min_query_length,omitempty"` // Optional. Default value is 1. @@ -29,7 +29,7 @@ type AttachmentAction struct { } // actionType returns the type of the action -func (a AttachmentAction) actionType() actionType { +func (a AttachmentAction) actionType() ActionType { return a.Type } @@ -80,6 +80,11 @@ type Attachment struct { ImageURL string `json:"image_url,omitempty"` ThumbURL string `json:"thumb_url,omitempty"` + ServiceName string `json:"service_name,omitempty"` + ServiceIcon string `json:"service_icon,omitempty"` + FromURL string `json:"from_url,omitempty"` + OriginalURL string `json:"original_url,omitempty"` + Fields []AttachmentField `json:"fields,omitempty"` Actions []AttachmentAction `json:"actions,omitempty"` MarkdownIn []string `json:"mrkdwn_in,omitempty"` diff --git a/audit.go b/audit.go new file mode 100644 index 000000000..041ffd77a --- /dev/null +++ b/audit.go @@ -0,0 +1,142 @@ +package slack + +import ( + "context" + "net/url" + "strconv" +) + +type AuditLogResponse struct { + Entries []AuditEntry `json:"entries"` + SlackResponse +} + +type AuditEntry struct { + ID string `json:"id"` + DateCreate int `json:"date_create"` + Action string `json:"action"` + Actor struct { + Type string `json:"type"` + User AuditUser `json:"user"` + } `json:"actor"` + Entity struct { + Type string `json:"type"` + // Only one of the below will be completed, based on the value of Type a user, a channel, a file, an app, a workspace, or an enterprise + User AuditUser `json:"user"` + Channel AuditChannel `json:"channel"` + File AuditFile `json:"file"` + App AuditApp `json:"app"` + Workspace AuditWorkspace `json:"workspace"` + Enterprise AuditEnterprise `json:"enterprise"` + } `json:"entity"` + Context struct { + Location struct { + Type string `json:"type"` + ID string `json:"id"` + Name string `json:"name"` + Domain string `json:"domain"` + } `json:"location"` + UA string `json:"ua"` + IPAddress string `json:"ip_address"` + } `json:"context"` +} + +type AuditUser struct { + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Team string `json:"team"` +} + +type AuditChannel struct { + ID string `json:"id"` + Name string `json:"name"` + Privacy string `json:"privacy"` + IsShared bool `json:"is_shared"` + IsOrgShared bool `json:"is_org_shared"` +} + +type AuditFile struct { + ID string `json:"id"` + Name string `json:"name"` + Filetype string `json:"filetype"` + Title string `json:"title"` +} + +type AuditApp struct { + ID string `json:"id"` + Name string `json:"name"` + IsDistributed bool `json:"is_distributed"` + IsDirectoryApproved bool `json:"is_directory_approved"` + IsWorkflowApp bool `json:"is_workflow_app"` + Scopes []string `json:"scopes"` +} + +type AuditWorkspace struct { + ID string `json:"id"` + Name string `json:"name"` + Domain string `json:"domain"` +} + +type AuditEnterprise struct { + ID string `json:"id"` + Name string `json:"name"` + Domain string `json:"domain"` +} + +// AuditLogParameters contains all the parameters necessary (including the optional ones) for a GetAuditLogs() request +type AuditLogParameters struct { + Limit int + Cursor string + Latest int + Oldest int + Action string + Actor string + Entity string +} + +func (api *Client) auditLogsRequest(ctx context.Context, path string, values url.Values) (*AuditLogResponse, error) { + response := &AuditLogResponse{} + err := api.getMethod(ctx, path, api.token, values, response) + if err != nil { + return nil, err + } + return response, response.Err() +} + +// GetAuditLogs retrieves a page of audit entires according to the parameters given +func (api *Client) GetAuditLogs(params AuditLogParameters) (entries []AuditEntry, nextCursor string, err error) { + return api.GetAuditLogsContext(context.Background(), params) +} + +// GetAuditLogsContext retrieves a page of audit entries according to the parameters given with a custom context +func (api *Client) GetAuditLogsContext(ctx context.Context, params AuditLogParameters) (entries []AuditEntry, nextCursor string, err error) { + values := url.Values{} + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Oldest != 0 { + values.Add("oldest", strconv.Itoa(params.Oldest)) + } + if params.Latest != 0 { + values.Add("latest", strconv.Itoa(params.Latest)) + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Action != "" { + values.Add("action", params.Action) + } + if params.Actor != "" { + values.Add("actor", params.Actor) + } + if params.Entity != "" { + values.Add("entity", params.Entity) + } + + response, err := api.auditLogsRequest(ctx, "audit/v1/logs", values) + if err != nil { + return nil, "", err + } + return response.Entries, response.ResponseMetadata.Cursor, response.Err() +} diff --git a/audit_test.go b/audit_test.go new file mode 100644 index 000000000..631b631e2 --- /dev/null +++ b/audit_test.go @@ -0,0 +1,85 @@ +package slack + +import ( + "net/http" + "testing" +) + +func getAuditLogs(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + response := []byte(`{"entries": [ + { + "id": "0123a45b-6c7d-8900-e12f-3456789gh0i1", + "date_create": 1521214343, + "action": "user_login", + "actor": { + "type": "user", + "user": { + "id": "W123AB456", + "name": "Charlie Parker", + "email": "bird@slack.com" + } + }, + "entity": { + "type": "user", + "user": { + "id": "W123AB456", + "name": "Charlie Parker", + "email": "bird@slack.com" + } + }, + "context": { + "location": { + "type": "enterprise", + "id": "E1701NCCA", + "name": "Birdland", + "domain": "birdland" + }, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36", + "ip_address": "1.23.45.678" + } + } + ] + }`) + rw.Write(response) +} + +func TestGetAuditLogs(t *testing.T) { + http.HandleFunc("/audit/v1/logs", getAuditLogs) + + once.Do(startServer) + api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) + + events, nextCursor, err := api.GetAuditLogs(AuditLogParameters{}) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + + if len(events) != 1 { + t.Fatal("Should have been 1 event") + } + + // test the first login + event1 := events[0] + + if event1.Action != "user_login" { + t.Fatal(ErrIncorrectResponse) + } + if event1.Entity.User.Email != "bird@slack.com" { + t.Fatal(ErrIncorrectResponse) + } + if event1.Context.Location.Domain != "birdland" { + t.Fatal(ErrIncorrectResponse) + } + if event1.DateCreate != 1521214343 { + t.Fatal(ErrIncorrectResponse) + } + if event1.Context.IPAddress != "1.23.45.678" { + t.Fatal(ErrIncorrectResponse) + } + + if nextCursor != "" { + t.Fatal(ErrIncorrectResponse) + } +} diff --git a/block.go b/block.go index 9946a2943..240f55279 100644 --- a/block.go +++ b/block.go @@ -9,14 +9,15 @@ package slack type MessageBlockType string const ( - MBTSection MessageBlockType = "section" - MBTDivider MessageBlockType = "divider" - MBTImage MessageBlockType = "image" - MBTAction MessageBlockType = "actions" - MBTContext MessageBlockType = "context" - MBTFile MessageBlockType = "file" - MBTInput MessageBlockType = "input" - MBTHeader MessageBlockType = "header" + MBTSection MessageBlockType = "section" + MBTDivider MessageBlockType = "divider" + MBTImage MessageBlockType = "image" + MBTAction MessageBlockType = "actions" + MBTContext MessageBlockType = "context" + MBTFile MessageBlockType = "file" + MBTInput MessageBlockType = "input" + MBTHeader MessageBlockType = "header" + MBTRichText MessageBlockType = "rich_text" ) // Block defines an interface all block types should implement @@ -35,7 +36,7 @@ type Blocks struct { type BlockAction struct { ActionID string `json:"action_id"` BlockID string `json:"block_id"` - Type actionType `json:"type"` + Type ActionType `json:"type"` Text TextBlockObject `json:"text"` Value string `json:"value"` ActionTs string `json:"action_ts"` @@ -58,7 +59,7 @@ type BlockAction struct { } // actionType returns the type of the action -func (b BlockAction) actionType() actionType { +func (b BlockAction) actionType() ActionType { return b.Type } diff --git a/block_context.go b/block_context.go index c37bf27e9..384fee22b 100644 --- a/block_context.go +++ b/block_context.go @@ -3,7 +3,7 @@ package slack // ContextBlock defines data that is used to display message context, which can // include both images and text. // -// More Information: https://api.slack.com/reference/messaging/blocks#actions +// More Information: https://api.slack.com/reference/messaging/blocks#context type ContextBlock struct { Type MessageBlockType `json:"type"` BlockID string `json:"block_id,omitempty"` diff --git a/block_conv.go b/block_conv.go index f2d744c49..1a2c57e9f 100644 --- a/block_conv.go +++ b/block_conv.go @@ -2,9 +2,8 @@ package slack import ( "encoding/json" + "errors" "fmt" - - "github.com/pkg/errors" ) type sumtype struct { @@ -65,6 +64,8 @@ func (b *Blocks) UnmarshalJSON(data []byte) error { block = &ImageBlock{} case "input": block = &InputBlock{} + case "rich_text": + block = &RichTextBlock{} case "section": block = &SectionBlock{} default: @@ -181,6 +182,8 @@ func (b *BlockElements) UnmarshalJSON(data []byte) error { blockElement = &OverflowBlockElement{} case "datepicker": blockElement = &DatePickerBlockElement{} + case "timepicker": + blockElement = &TimePickerBlockElement{} case "plain_text_input": blockElement = &PlainTextInputBlockElement{} case "checkboxes": diff --git a/block_element.go b/block_element.go index bf0684408..643529ff0 100644 --- a/block_element.go +++ b/block_element.go @@ -167,6 +167,12 @@ func (s *ButtonBlockElement) WithStyle(style Style) *ButtonBlockElement { return s } +// WithConfirm adds a confirmation dialogue to the button object and returns the modified ButtonBlockElement +func (s *ButtonBlockElement) WithConfirm(confirm *ConfirmationBlockObject) *ButtonBlockElement { + s.Confirm = confirm + return s +} + // NewButtonBlockElement returns an instance of a new button element to be used within a block func NewButtonBlockElement(actionID, value string, text *TextBlockObject) *ButtonBlockElement { return &ButtonBlockElement{ @@ -389,13 +395,18 @@ func NewTimePickerBlockElement(actionID string) *TimePickerBlockElement { // // More Information: https://api.slack.com/reference/block-kit/block-elements#input type PlainTextInputBlockElement struct { - Type MessageElementType `json:"type"` - ActionID string `json:"action_id,omitempty"` - Placeholder *TextBlockObject `json:"placeholder,omitempty"` - InitialValue string `json:"initial_value,omitempty"` - Multiline bool `json:"multiline,omitempty"` - MinLength int `json:"min_length,omitempty"` - MaxLength int `json:"max_length,omitempty"` + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + Placeholder *TextBlockObject `json:"placeholder,omitempty"` + InitialValue string `json:"initial_value,omitempty"` + Multiline bool `json:"multiline,omitempty"` + MinLength int `json:"min_length,omitempty"` + MaxLength int `json:"max_length,omitempty"` + DispatchActionConfig *DispatchActionConfig `json:"dispatch_action_config,omitempty"` +} + +type DispatchActionConfig struct { + TriggerActionsOn []string `json:"trigger_actions_on,omitempty"` } // ElementType returns the type of the Element diff --git a/block_object.go b/block_object.go index eae177f49..f70405eba 100644 --- a/block_object.go +++ b/block_object.go @@ -138,6 +138,10 @@ func (s TextBlockObject) MixedElementType() MixedElementType { // Validate checks if TextBlockObject has valid values func (s TextBlockObject) Validate() error { + if s.Type != "plain_text" && s.Type != "mrkdwn" { + return errors.New("type must be either of plain_text or mrkdwn") + } + // https://github.com/slack-go/slack/issues/881 if s.Type == "mrkdwn" && s.Emoji { return errors.New("emoji cannot be true in mrkdown") @@ -183,8 +187,9 @@ func (s ConfirmationBlockObject) validateType() MessageObjectType { } // WithStyle add styling to confirmation object -func (s *ConfirmationBlockObject) WithStyle(style Style) { +func (s *ConfirmationBlockObject) WithStyle(style Style) *ConfirmationBlockObject { s.Style = style + return s } // NewConfirmationBlockObject returns an instance of a new Confirmation Block Object diff --git a/block_object_test.go b/block_object_test.go index 869fc30c3..9889fae41 100644 --- a/block_object_test.go +++ b/block_object_test.go @@ -99,6 +99,24 @@ func TestValidateTextBlockObject(t *testing.T) { }, expected: nil, }, + { + input: TextBlockObject{ + Type: "mrkdwn", + Text: "testText", + Emoji: false, + Verbatim: false, + }, + expected: nil, + }, + { + input: TextBlockObject{ + Type: "invalid", + Text: "testText", + Emoji: false, + Verbatim: false, + }, + expected: errors.New("type must be either of plain_text or mrkdwn"), + }, { input: TextBlockObject{ Type: "mrkdwn", diff --git a/block_rich_text.go b/block_rich_text.go new file mode 100644 index 000000000..281db213a --- /dev/null +++ b/block_rich_text.go @@ -0,0 +1,383 @@ +package slack + +import ( + "encoding/json" +) + +// RichTextBlock defines a new block of type rich_text. +// More Information: https://api.slack.com/changelog/2019-09-what-they-see-is-what-you-get-and-more-and-less +type RichTextBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + Elements []RichTextElement `json:"elements"` +} + +func (b RichTextBlock) BlockType() MessageBlockType { + return b.Type +} + +func (e *RichTextBlock) UnmarshalJSON(b []byte) error { + var raw struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id"` + RawElements []json.RawMessage `json:"elements"` + } + if string(b) == "{}" { + return nil + } + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + elems := make([]RichTextElement, 0, len(raw.RawElements)) + for _, r := range raw.RawElements { + var s struct { + Type RichTextElementType `json:"type"` + } + if err := json.Unmarshal(r, &s); err != nil { + return err + } + var elem RichTextElement + switch s.Type { + case RTESection: + elem = &RichTextSection{} + default: + elems = append(elems, &RichTextUnknown{ + Type: s.Type, + Raw: string(r), + }) + continue + } + if err := json.Unmarshal(r, &elem); err != nil { + return err + } + elems = append(elems, elem) + } + *e = RichTextBlock{ + Type: raw.Type, + BlockID: raw.BlockID, + Elements: elems, + } + return nil +} + +// NewRichTextBlock returns a new instance of RichText Block. +func NewRichTextBlock(blockID string, elements ...RichTextElement) *RichTextBlock { + return &RichTextBlock{ + Type: MBTRichText, + BlockID: blockID, + Elements: elements, + } +} + +type RichTextElementType string + +type RichTextElement interface { + RichTextElementType() RichTextElementType +} + +const ( + RTEList RichTextElementType = "rich_text_list" + RTEPreformatted RichTextElementType = "rich_text_preformatted" + RTEQuote RichTextElementType = "rich_text_quote" + RTESection RichTextElementType = "rich_text_section" + RTEUnknown RichTextElementType = "rich_text_unknown" +) + +type RichTextUnknown struct { + Type RichTextElementType + Raw string +} + +func (u RichTextUnknown) RichTextElementType() RichTextElementType { + return u.Type +} + +type RichTextSection struct { + Type RichTextElementType `json:"type"` + Elements []RichTextSectionElement `json:"elements"` +} + +// ElementType returns the type of the Element +func (s RichTextSection) RichTextElementType() RichTextElementType { + return s.Type +} + +func (e *RichTextSection) UnmarshalJSON(b []byte) error { + var raw struct { + RawElements []json.RawMessage `json:"elements"` + } + if string(b) == "{}" { + return nil + } + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + elems := make([]RichTextSectionElement, 0, len(raw.RawElements)) + for _, r := range raw.RawElements { + var s struct { + Type RichTextSectionElementType `json:"type"` + } + if err := json.Unmarshal(r, &s); err != nil { + return err + } + var elem RichTextSectionElement + switch s.Type { + case RTSEText: + elem = &RichTextSectionTextElement{} + case RTSEChannel: + elem = &RichTextSectionChannelElement{} + case RTSEUser: + elem = &RichTextSectionUserElement{} + case RTSEEmoji: + elem = &RichTextSectionEmojiElement{} + case RTSELink: + elem = &RichTextSectionLinkElement{} + case RTSETeam: + elem = &RichTextSectionTeamElement{} + case RTSEUserGroup: + elem = &RichTextSectionUserGroupElement{} + case RTSEDate: + elem = &RichTextSectionDateElement{} + case RTSEBroadcast: + elem = &RichTextSectionBroadcastElement{} + case RTSEColor: + elem = &RichTextSectionColorElement{} + default: + elems = append(elems, &RichTextSectionUnknownElement{ + Type: s.Type, + Raw: string(r), + }) + continue + } + if err := json.Unmarshal(r, elem); err != nil { + return err + } + elems = append(elems, elem) + } + *e = RichTextSection{ + Type: RTESection, + Elements: elems, + } + return nil +} + +// NewRichTextSectionBlockElement . +func NewRichTextSection(elements ...RichTextSectionElement) *RichTextSection { + return &RichTextSection{ + Type: RTESection, + Elements: elements, + } +} + +type RichTextSectionElementType string + +const ( + RTSEBroadcast RichTextSectionElementType = "broadcast" + RTSEChannel RichTextSectionElementType = "channel" + RTSEColor RichTextSectionElementType = "color" + RTSEDate RichTextSectionElementType = "date" + RTSEEmoji RichTextSectionElementType = "emoji" + RTSELink RichTextSectionElementType = "link" + RTSETeam RichTextSectionElementType = "team" + RTSEText RichTextSectionElementType = "text" + RTSEUser RichTextSectionElementType = "user" + RTSEUserGroup RichTextSectionElementType = "usergroup" + + RTSEUnknown RichTextSectionElementType = "unknown" +) + +type RichTextSectionElement interface { + RichTextSectionElementType() RichTextSectionElementType +} + +type RichTextSectionTextStyle struct { + Bold bool `json:"bold,omitempty"` + Italic bool `json:"italic,omitempty"` + Strike bool `json:"strike,omitempty"` + Code bool `json:"code,omitempty"` +} + +type RichTextSectionTextElement struct { + Type RichTextSectionElementType `json:"type"` + Text string `json:"text"` + Style *RichTextSectionTextStyle `json:"style,omitempty"` +} + +func (r RichTextSectionTextElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionTextElement(text string, style *RichTextSectionTextStyle) *RichTextSectionTextElement { + return &RichTextSectionTextElement{ + Type: RTSEText, + Text: text, + Style: style, + } +} + +type RichTextSectionChannelElement struct { + Type RichTextSectionElementType `json:"type"` + ChannelID string `json:"channel_id"` + Style *RichTextSectionTextStyle `json:"style,omitempty"` +} + +func (r RichTextSectionChannelElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionChannelElement(channelID string, style *RichTextSectionTextStyle) *RichTextSectionChannelElement { + return &RichTextSectionChannelElement{ + Type: RTSEText, + ChannelID: channelID, + Style: style, + } +} + +type RichTextSectionUserElement struct { + Type RichTextSectionElementType `json:"type"` + UserID string `json:"user_id"` + Style *RichTextSectionTextStyle `json:"style,omitempty"` +} + +func (r RichTextSectionUserElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionUserElement(userID string, style *RichTextSectionTextStyle) *RichTextSectionUserElement { + return &RichTextSectionUserElement{ + Type: RTSEUser, + UserID: userID, + Style: style, + } +} + +type RichTextSectionEmojiElement struct { + Type RichTextSectionElementType `json:"type"` + Name string `json:"name"` + SkinTone int `json:"skin_tone"` + Style *RichTextSectionTextStyle `json:"style,omitempty"` +} + +func (r RichTextSectionEmojiElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionEmojiElement(name string, skinTone int, style *RichTextSectionTextStyle) *RichTextSectionEmojiElement { + return &RichTextSectionEmojiElement{ + Type: RTSEEmoji, + Name: name, + SkinTone: skinTone, + Style: style, + } +} + +type RichTextSectionLinkElement struct { + Type RichTextSectionElementType `json:"type"` + URL string `json:"url"` + Text string `json:"text"` + Style *RichTextSectionTextStyle `json:"style,omitempty"` +} + +func (r RichTextSectionLinkElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionLinkElement(url, text string, style *RichTextSectionTextStyle) *RichTextSectionLinkElement { + return &RichTextSectionLinkElement{ + Type: RTSELink, + URL: url, + Text: text, + Style: style, + } +} + +type RichTextSectionTeamElement struct { + Type RichTextSectionElementType `json:"type"` + TeamID string `json:"team_id"` + Style *RichTextSectionTextStyle `json:"style.omitempty"` +} + +func (r RichTextSectionTeamElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionTeamElement(teamID string, style *RichTextSectionTextStyle) *RichTextSectionTeamElement { + return &RichTextSectionTeamElement{ + Type: RTSETeam, + TeamID: teamID, + Style: style, + } +} + +type RichTextSectionUserGroupElement struct { + Type RichTextSectionElementType `json:"type"` + UsergroupID string `json:"usergroup_id"` +} + +func (r RichTextSectionUserGroupElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionUserGroupElement(usergroupID string) *RichTextSectionUserGroupElement { + return &RichTextSectionUserGroupElement{ + Type: RTSEUserGroup, + UsergroupID: usergroupID, + } +} + +type RichTextSectionDateElement struct { + Type RichTextSectionElementType `json:"type"` + Timestamp string `json:"timestamp"` +} + +func (r RichTextSectionDateElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionDateElement(timestamp string) *RichTextSectionDateElement { + return &RichTextSectionDateElement{ + Type: RTSEDate, + Timestamp: timestamp, + } +} + +type RichTextSectionBroadcastElement struct { + Type RichTextSectionElementType `json:"type"` + Range string `json:"range"` +} + +func (r RichTextSectionBroadcastElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionBroadcastElement(rangeStr string) *RichTextSectionBroadcastElement { + return &RichTextSectionBroadcastElement{ + Type: RTSEBroadcast, + Range: rangeStr, + } +} + +type RichTextSectionColorElement struct { + Type RichTextSectionElementType `json:"type"` + Value string `json:"value"` +} + +func (r RichTextSectionColorElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionColorElement(value string) *RichTextSectionColorElement { + return &RichTextSectionColorElement{ + Type: RTSEColor, + Value: value, + } +} + +type RichTextSectionUnknownElement struct { + Type RichTextSectionElementType `json:"type"` + Raw string +} + +func (r RichTextSectionUnknownElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} diff --git a/block_rich_text_test.go b/block_rich_text_test.go new file mode 100644 index 000000000..4c889eba5 --- /dev/null +++ b/block_rich_text_test.go @@ -0,0 +1,118 @@ +package slack + +import ( + "encoding/json" + "testing" + + "github.com/go-test/deep" +) + +const ( + dummyPayload = `{ + "type":"rich_text", + "block_id":"FaYCD", + "elements": [ + { + "type":"rich_text_section", + "elements": [ + { + "type":"channel", + "channel_id":"C012345678" + }, + { + "type":"text", + "text":"dummy_text" + } + ] + } + ] +}` +) + +func TestRichTextBlock_UnmarshalJSON(t *testing.T) { + cases := []struct { + raw []byte + expected RichTextBlock + err error + }{ + { + []byte(`{"elements":[{"type":"rich_text_unknown"},{"type":"rich_text_section"}]}`), + RichTextBlock{ + Elements: []RichTextElement{ + &RichTextUnknown{Type: RTEUnknown, Raw: `{"type":"rich_text_unknown"}`}, + &RichTextSection{Type: RTESection, Elements: []RichTextSectionElement{}}, + }, + }, + nil, + }, + { + []byte(`{"type": "rich_text","block_id":"blk","elements":[]}`), + RichTextBlock{ + Type: MBTRichText, + BlockID: "blk", + Elements: []RichTextElement{}, + }, + nil, + }, + } + for _, tc := range cases { + var actual RichTextBlock + err := json.Unmarshal(tc.raw, &actual) + if err != nil { + if tc.err == nil { + t.Errorf("unexpected error: %s", err) + } + t.Errorf("expected error is %s, but got %s", tc.err, err) + } + if tc.err != nil { + t.Errorf("expected to raise an error %s", tc.err) + } + if diff := deep.Equal(actual, tc.expected); diff != nil { + t.Errorf("actual value does not match expected one\n%s", diff) + } + } +} + +func TestRichTextSection_UnmarshalJSON(t *testing.T) { + cases := []struct { + raw []byte + expected RichTextSection + err error + }{ + { + []byte(`{"elements":[{"type":"unknown","value":10},{"type":"text","text":"hi"}]}`), + RichTextSection{ + Type: RTESection, + Elements: []RichTextSectionElement{ + &RichTextSectionUnknownElement{Type: RTSEUnknown, Raw: `{"type":"unknown","value":10}`}, + &RichTextSectionTextElement{Type: RTSEText, Text: "hi"}, + }, + }, + nil, + }, + { + []byte(`{"type": "rich_text_section","elements":[]}`), + RichTextSection{ + Type: RTESection, + Elements: []RichTextSectionElement{}, + }, + nil, + }, + } + for _, tc := range cases { + var actual RichTextSection + err := json.Unmarshal(tc.raw, &actual) + if err != nil { + if tc.err == nil { + t.Errorf("unexpected error: %s", err) + } + t.Errorf("expected error is %s, but got %s", tc.err, err) + } + if tc.err != nil { + t.Errorf("expected to raise an error %s", tc.err) + } + if diff := deep.Equal(actual, tc.expected); diff != nil { + t.Errorf("actual value does not match expected one\n%s", diff) + } + } +} diff --git a/channels.go b/channels.go index c05521d0f..2fca8b92e 100644 --- a/channels.go +++ b/channels.go @@ -3,8 +3,6 @@ package slack import ( "context" "net/url" - "strconv" - "time" ) type channelResponseFull struct { @@ -19,12 +17,6 @@ type channelResponseFull struct { } // Channel contains information about the channel -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api type Channel struct { GroupConversation IsChannel bool `json:"is_channel"` @@ -42,673 +34,3 @@ func (api *Client) channelRequest(ctx context.Context, path string, values url.V return response, response.Err() } - -// GetChannelsOption option provided when getting channels. -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -type GetChannelsOption func(*ChannelPagination) error - -// GetChannelsOptionExcludeMembers excludes the members collection from each channel. -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func GetChannelsOptionExcludeMembers() GetChannelsOption { - return func(p *ChannelPagination) error { - p.excludeMembers = true - return nil - } -} - -// GetChannelsOptionExcludeArchived excludes archived channels from results. -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func GetChannelsOptionExcludeArchived() GetChannelsOption { - return func(p *ChannelPagination) error { - p.excludeArchived = true - return nil - } -} - -// ArchiveChannel archives the given channel -// see https://api.slack.com/methods/channels.archive -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) ArchiveChannel(channelID string) error { - return api.ArchiveChannelContext(context.Background(), channelID) -} - -// ArchiveChannelContext archives the given channel with a custom context -// see https://api.slack.com/methods/channels.archive -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string) (err error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - } - - _, err = api.channelRequest(ctx, "channels.archive", values) - return err -} - -// UnarchiveChannel unarchives the given channel -// see https://api.slack.com/methods/channels.unarchive -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) UnarchiveChannel(channelID string) error { - return api.UnarchiveChannelContext(context.Background(), channelID) -} - -// UnarchiveChannelContext unarchives the given channel with a custom context -// see https://api.slack.com/methods/channels.unarchive -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string) (err error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - } - - _, err = api.channelRequest(ctx, "channels.unarchive", values) - return err -} - -// CreateChannel creates a channel with the given name and returns a *Channel -// see https://api.slack.com/methods/channels.create -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) CreateChannel(channelName string) (*Channel, error) { - return api.CreateChannelContext(context.Background(), channelName) -} - -// CreateChannelContext creates a channel with the given name and returns a *Channel with a custom context -// see https://api.slack.com/methods/channels.create -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) CreateChannelContext(ctx context.Context, channelName string) (*Channel, error) { - values := url.Values{ - "token": {api.token}, - "name": {channelName}, - } - - response, err := api.channelRequest(ctx, "channels.create", values) - if err != nil { - return nil, err - } - return &response.Channel, nil -} - -// GetChannelHistory retrieves the channel history -// see https://api.slack.com/methods/channels.history -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetChannelHistory(channelID string, params HistoryParameters) (*History, error) { - return api.GetChannelHistoryContext(context.Background(), channelID, params) -} - -// GetChannelHistoryContext retrieves the channel history with a custom context -// see https://api.slack.com/methods/channels.history -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID string, params HistoryParameters) (*History, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - } - if params.Latest != DEFAULT_HISTORY_LATEST { - values.Add("latest", params.Latest) - } - if params.Oldest != DEFAULT_HISTORY_OLDEST { - values.Add("oldest", params.Oldest) - } - if params.Count != DEFAULT_HISTORY_COUNT { - values.Add("count", strconv.Itoa(params.Count)) - } - if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { - if params.Inclusive { - values.Add("inclusive", "1") - } else { - values.Add("inclusive", "0") - } - } - - if params.Unreads != DEFAULT_HISTORY_UNREADS { - if params.Unreads { - values.Add("unreads", "1") - } else { - values.Add("unreads", "0") - } - } - - response, err := api.channelRequest(ctx, "channels.history", values) - if err != nil { - return nil, err - } - return &response.History, nil -} - -// GetChannelInfo retrieves the given channel -// see https://api.slack.com/methods/channels.info -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetChannelInfo(channelID string) (*Channel, error) { - return api.GetChannelInfoContext(context.Background(), channelID) -} - -// GetChannelInfoContext retrieves the given channel with a custom context -// see https://api.slack.com/methods/channels.info -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetChannelInfoContext(ctx context.Context, channelID string) (*Channel, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "include_locale": {strconv.FormatBool(true)}, - } - - response, err := api.channelRequest(ctx, "channels.info", values) - if err != nil { - return nil, err - } - return &response.Channel, nil -} - -// InviteUserToChannel invites a user to a given channel and returns a *Channel -// see https://api.slack.com/methods/channels.invite -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) InviteUserToChannel(channelID, user string) (*Channel, error) { - return api.InviteUserToChannelContext(context.Background(), channelID, user) -} - -// InviteUserToChannelContext invites a user to a given channel and returns a *Channel with a custom context -// see https://api.slack.com/methods/channels.invite -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, user string) (*Channel, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "user": {user}, - } - - response, err := api.channelRequest(ctx, "channels.invite", values) - if err != nil { - return nil, err - } - return &response.Channel, nil -} - -// JoinChannel joins the currently authenticated user to a channel -// see https://api.slack.com/methods/channels.join -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) JoinChannel(channelName string) (*Channel, error) { - return api.JoinChannelContext(context.Background(), channelName) -} - -// JoinChannelContext joins the currently authenticated user to a channel with a custom context -// see https://api.slack.com/methods/channels.join -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) JoinChannelContext(ctx context.Context, channelName string) (*Channel, error) { - values := url.Values{ - "token": {api.token}, - "name": {channelName}, - } - - response, err := api.channelRequest(ctx, "channels.join", values) - if err != nil { - return nil, err - } - return &response.Channel, nil -} - -// LeaveChannel makes the authenticated user leave the given channel -// see https://api.slack.com/methods/channels.leave -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) LeaveChannel(channelID string) (bool, error) { - return api.LeaveChannelContext(context.Background(), channelID) -} - -// LeaveChannelContext makes the authenticated user leave the given channel with a custom context -// see https://api.slack.com/methods/channels.leave -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) LeaveChannelContext(ctx context.Context, channelID string) (bool, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - } - - response, err := api.channelRequest(ctx, "channels.leave", values) - if err != nil { - return false, err - } - - return response.NotInChannel, nil -} - -// KickUserFromChannel kicks a user from a given channel -// see https://api.slack.com/methods/channels.kick -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) KickUserFromChannel(channelID, user string) error { - return api.KickUserFromChannelContext(context.Background(), channelID, user) -} - -// KickUserFromChannelContext kicks a user from a given channel with a custom context -// see https://api.slack.com/methods/channels.kick -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, user string) (err error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "user": {user}, - } - - _, err = api.channelRequest(ctx, "channels.kick", values) - return err -} - -func newChannelPagination(c *Client, options ...GetChannelsOption) (cp ChannelPagination) { - cp = ChannelPagination{ - c: c, - limit: 200, // per slack api documentation. - } - - for _, opt := range options { - opt(&cp) - } - - return cp -} - -// ChannelPagination allows for paginating over the channels -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -type ChannelPagination struct { - Channels []Channel - limit int - excludeArchived bool - excludeMembers bool - previousResp *ResponseMetadata - c *Client -} - -// Done checks if the pagination has completed -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (ChannelPagination) Done(err error) bool { - return err == errPaginationComplete -} - -// Failure checks if pagination failed. -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (t ChannelPagination) Failure(err error) error { - if t.Done(err) { - return nil - } - - return err -} - -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (t ChannelPagination) Next(ctx context.Context) (_ ChannelPagination, err error) { - var ( - resp *channelResponseFull - ) - - if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") { - return t, errPaginationComplete - } - - t.previousResp = t.previousResp.initialize() - - values := url.Values{ - "limit": {strconv.Itoa(t.limit)}, - "exclude_archived": {strconv.FormatBool(t.excludeArchived)}, - "exclude_members": {strconv.FormatBool(t.excludeMembers)}, - "token": {t.c.token}, - "cursor": {t.previousResp.Cursor}, - } - - if resp, err = t.c.channelRequest(ctx, "channels.list", values); err != nil { - return t, err - } - - t.c.Debugf("GetChannelsContext: got %d channels; metadata %v", len(resp.Channels), resp.Metadata) - t.Channels = resp.Channels - t.previousResp = &resp.Metadata - - return t, nil -} - -// GetChannelsPaginated fetches channels in a paginated fashion, see GetChannelsContext for usage. -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetChannelsPaginated(options ...GetChannelsOption) ChannelPagination { - return newChannelPagination(api, options...) -} - -// GetChannels retrieves all the channels -// see https://api.slack.com/methods/channels.list -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetChannels(excludeArchived bool, options ...GetChannelsOption) ([]Channel, error) { - return api.GetChannelsContext(context.Background(), excludeArchived, options...) -} - -// GetChannelsContext retrieves all the channels with a custom context -// see https://api.slack.com/methods/channels.list -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool, options ...GetChannelsOption) (results []Channel, err error) { - if excludeArchived { - options = append(options, GetChannelsOptionExcludeArchived()) - } - - p := api.GetChannelsPaginated(options...) - for err == nil { - p, err = p.Next(ctx) - if err == nil { - results = append(results, p.Channels...) - } else if rateLimitedError, ok := err.(*RateLimitedError); ok { - select { - case <-ctx.Done(): - err = ctx.Err() - case <-time.After(rateLimitedError.RetryAfter): - err = nil - } - } - } - - return results, p.Failure(err) -} - -// SetChannelReadMark sets the read mark of a given channel to a specific point -// Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a -// timer before making the call. In this way, any further updates needed during the timeout will not generate extra calls -// (just one per channel). This is useful for when reading scroll-back history, or following a busy live channel. A -// timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout. -// see https://api.slack.com/methods/channels.mark -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) SetChannelReadMark(channelID, ts string) error { - return api.SetChannelReadMarkContext(context.Background(), channelID, ts) -} - -// SetChannelReadMarkContext sets the read mark of a given channel to a specific point with a custom context -// For more details see SetChannelReadMark documentation -// see https://api.slack.com/methods/channels.mark -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts string) (err error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "ts": {ts}, - } - - _, err = api.channelRequest(ctx, "channels.mark", values) - return err -} - -// RenameChannel renames a given channel -// see https://api.slack.com/methods/channels.rename -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) RenameChannel(channelID, name string) (*Channel, error) { - return api.RenameChannelContext(context.Background(), channelID, name) -} - -// RenameChannelContext renames a given channel with a custom context -// see https://api.slack.com/methods/channels.rename -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) RenameChannelContext(ctx context.Context, channelID, name string) (*Channel, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "name": {name}, - } - - // XXX: the created entry in this call returns a string instead of a number - // so I may have to do some workaround to solve it. - response, err := api.channelRequest(ctx, "channels.rename", values) - if err != nil { - return nil, err - } - return &response.Channel, nil -} - -// SetChannelPurpose sets the channel purpose and returns the purpose that was successfully set -// see https://api.slack.com/methods/channels.setPurpose -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) SetChannelPurpose(channelID, purpose string) (string, error) { - return api.SetChannelPurposeContext(context.Background(), channelID, purpose) -} - -// SetChannelPurposeContext sets the channel purpose and returns the purpose that was successfully set with a custom context -// see https://api.slack.com/methods/channels.setPurpose -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) SetChannelPurposeContext(ctx context.Context, channelID, purpose string) (string, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "purpose": {purpose}, - } - - response, err := api.channelRequest(ctx, "channels.setPurpose", values) - if err != nil { - return "", err - } - return response.Purpose, nil -} - -// SetChannelTopic sets the channel topic and returns the topic that was successfully set -// see https://api.slack.com/methods/channels.setTopic -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) SetChannelTopic(channelID, topic string) (string, error) { - return api.SetChannelTopicContext(context.Background(), channelID, topic) -} - -// SetChannelTopicContext sets the channel topic and returns the topic that was successfully set with a custom context -// see https://api.slack.com/methods/channels.setTopic -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) SetChannelTopicContext(ctx context.Context, channelID, topic string) (string, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "topic": {topic}, - } - - response, err := api.channelRequest(ctx, "channels.setTopic", values) - if err != nil { - return "", err - } - return response.Topic, nil -} - -// GetChannelReplies gets an entire thread (a message plus all the messages in reply to it). -// see https://api.slack.com/methods/channels.replies -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetChannelReplies(channelID, thread_ts string) ([]Message, error) { - return api.GetChannelRepliesContext(context.Background(), channelID, thread_ts) -} - -// GetChannelRepliesContext gets an entire thread (a message plus all the messages in reply to it) with a custom context -// see https://api.slack.com/methods/channels.replies -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetChannelRepliesContext(ctx context.Context, channelID, thread_ts string) ([]Message, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "thread_ts": {thread_ts}, - } - response, err := api.channelRequest(ctx, "channels.replies", values) - if err != nil { - return nil, err - } - return response.History.Messages, nil -} diff --git a/chat.go b/chat.go index f5b80346b..34848d151 100644 --- a/chat.go +++ b/chat.go @@ -86,12 +86,7 @@ func NewPostMessageParameters() PostMessageParameters { // DeleteMessage deletes a message in a channel func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) { - respChannel, respTimestamp, _, err := api.SendMessageContext( - context.Background(), - channel, - MsgOptionDelete(messageTimestamp), - ) - return respChannel, respTimestamp, err + return api.DeleteMessageContext(context.Background(), channel, messageTimestamp) } // DeleteMessageContext deletes a message in a channel with a custom context @@ -108,8 +103,15 @@ func (api *Client) DeleteMessageContext(ctx context.Context, channel, messageTim // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. func (api *Client) ScheduleMessage(channelID, postAt string, options ...MsgOption) (string, string, error) { + return api.ScheduleMessageContext(context.Background(), channelID, postAt, options...) +} + +// ScheduleMessageContext sends a message to a channel with a custom context +// +// For more details, see ScheduleMessage documentation. +func (api *Client) ScheduleMessageContext(ctx context.Context, channelID, postAt string, options ...MsgOption) (string, string, error) { respChannel, respTimestamp, _, err := api.SendMessageContext( - context.Background(), + ctx, channelID, MsgOptionSchedule(postAt), MsgOptionCompose(options...), @@ -121,13 +123,7 @@ func (api *Client) ScheduleMessage(channelID, postAt string, options ...MsgOptio // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. func (api *Client) PostMessage(channelID string, options ...MsgOption) (string, string, error) { - respChannel, respTimestamp, _, err := api.SendMessageContext( - context.Background(), - channelID, - MsgOptionPost(), - MsgOptionCompose(options...), - ) - return respChannel, respTimestamp, err + return api.PostMessageContext(context.Background(), channelID, options...) } // PostMessageContext sends a message to a channel with a custom context @@ -146,12 +142,7 @@ func (api *Client) PostMessageContext(ctx context.Context, channelID string, opt // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. func (api *Client) PostEphemeral(channelID, userID string, options ...MsgOption) (string, error) { - return api.PostEphemeralContext( - context.Background(), - channelID, - userID, - options..., - ) + return api.PostEphemeralContext(context.Background(), channelID, userID, options...) } // PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context @@ -168,12 +159,7 @@ func (api *Client) PostEphemeralContext(ctx context.Context, channelID, userID s // UpdateMessage updates a message in a channel func (api *Client) UpdateMessage(channelID, timestamp string, options ...MsgOption) (string, string, string, error) { - return api.SendMessageContext( - context.Background(), - channelID, - MsgOptionUpdate(timestamp), - MsgOptionCompose(options...), - ) + return api.UpdateMessageContext(context.Background(), channelID, timestamp, options...) } // UpdateMessageContext updates a message in a channel @@ -188,7 +174,12 @@ func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestam // UnfurlMessage unfurls a message in a channel func (api *Client) UnfurlMessage(channelID, timestamp string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) { - return api.SendMessageContext(context.Background(), channelID, MsgOptionUnfurl(timestamp, unfurls), MsgOptionCompose(options...)) + return api.UnfurlMessageContext(context.Background(), channelID, timestamp, unfurls, options...) +} + +// UnfurlMessageContext unfurls a message in a channel with a custom context +func (api *Client) UnfurlMessageContext(ctx context.Context, channelID, timestamp string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) { + return api.SendMessageContext(ctx, channelID, MsgOptionUnfurl(timestamp, unfurls), MsgOptionCompose(options...)) } // UnfurlMessageWithAuthURL sends an unfurl request containing an @@ -220,7 +211,7 @@ func (api *Client) SendMessageContext(ctx context.Context, channelID string, opt response chatResponseFull ) - if req, parser, err = buildSender(api.endpoint, options...).BuildRequest(api.token, channelID); err != nil { + if req, parser, err = buildSender(api.endpoint, options...).BuildRequestContext(ctx, api.token, channelID); err != nil { return "", "", "", err } @@ -301,6 +292,10 @@ type sendConfig struct { } func (t sendConfig) BuildRequest(token, channelID string) (req *http.Request, _ func(*chatResponseFull) responseParser, err error) { + return t.BuildRequestContext(context.Background(), token, channelID) +} + +func (t sendConfig) BuildRequestContext(ctx context.Context, token, channelID string) (req *http.Request, _ func(*chatResponseFull) responseParser, err error) { if t, err = applyMsgOptions(token, channelID, t.apiurl, t.options...); err != nil { return nil, nil, err } @@ -315,9 +310,9 @@ func (t sendConfig) BuildRequest(token, channelID string) (req *http.Request, _ responseType: t.responseType, replaceOriginal: t.replaceOriginal, deleteOriginal: t.deleteOriginal, - }.BuildRequest() + }.BuildRequestContext(ctx) default: - return formSender{endpoint: t.endpoint, values: t.values}.BuildRequest() + return formSender{endpoint: t.endpoint, values: t.values}.BuildRequestContext(ctx) } } @@ -327,7 +322,11 @@ type formSender struct { } func (t formSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) { - req, err := formReq(t.endpoint, t.values) + return t.BuildRequestContext(context.Background()) +} + +func (t formSender) BuildRequestContext(ctx context.Context) (*http.Request, func(*chatResponseFull) responseParser, error) { + req, err := formReq(ctx, t.endpoint, t.values) return req, func(resp *chatResponseFull) responseParser { return newJSONParser(resp) }, err @@ -344,7 +343,11 @@ type responseURLSender struct { } func (t responseURLSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) { - req, err := jsonReq(t.endpoint, Msg{ + return t.BuildRequestContext(context.Background()) +} + +func (t responseURLSender) BuildRequestContext(ctx context.Context) (*http.Request, func(*chatResponseFull) responseParser, error) { + req, err := jsonReq(ctx, t.endpoint, Msg{ Text: t.values.Get("text"), Timestamp: t.values.Get("ts"), Attachments: t.attachments, @@ -744,7 +747,6 @@ func (api *Client) GetPermalink(params *PermalinkParameters) (string, error) { // GetPermalinkContext returns the permalink for a message using a custom context. func (api *Client) GetPermalinkContext(ctx context.Context, params *PermalinkParameters) (string, error) { values := url.Values{ - "token": {api.token}, "channel": {params.Channel}, "message_ts": {params.Ts}, } @@ -754,7 +756,7 @@ func (api *Client) GetPermalinkContext(ctx context.Context, params *PermalinkPar Permalink string `json:"permalink"` SlackResponse }{} - err := api.getMethod(ctx, "chat.getPermalink", values, &response) + err := api.getMethod(ctx, "chat.getPermalink", api.token, values, &response) if err != nil { return "", err } diff --git a/conversation.go b/conversation.go index 6fc4a0839..299362601 100644 --- a/conversation.go +++ b/conversation.go @@ -455,9 +455,10 @@ func (api *Client) GetConversationRepliesContext(ctx context.Context, params *Ge type GetConversationsParameters struct { Cursor string - ExcludeArchived string + ExcludeArchived bool Limit int Types []string + TeamID string } // GetConversations returns the list of channels in a Slack team @@ -479,8 +480,11 @@ func (api *Client) GetConversationsContext(ctx context.Context, params *GetConve if params.Types != nil { values.Add("types", strings.Join(params.Types, ",")) } - if params.ExcludeArchived == "true" { - values.Add("exclude_archived", "true") + if params.ExcludeArchived { + values.Add("exclude_archived", strconv.FormatBool(params.ExcludeArchived)) + } + if params.TeamID != "" { + values.Add("team_id", params.TeamID) } response := struct { diff --git a/dialog_select.go b/dialog_select.go index 385cef683..3d6be989e 100644 --- a/dialog_select.go +++ b/dialog_select.go @@ -54,6 +54,20 @@ func NewStaticSelectDialogInput(name, label string, options []DialogSelectOption } } +// NewExternalSelectDialogInput constructor for a `external` datasource menu input +func NewExternalSelectDialogInput(name, label string, options []DialogSelectOption) *DialogInputSelect { + return &DialogInputSelect{ + DialogInput: DialogInput{ + Type: InputTypeSelect, + Name: name, + Label: label, + Optional: true, + }, + DataSource: DialogDataSourceExternal, + Options: options, + } +} + // NewGroupedSelectDialogInput creates grouped options select input for Dialogs. func NewGroupedSelectDialogInput(name, label string, options []DialogOptionGroup) *DialogInputSelect { return &DialogInputSelect{ diff --git a/examples/buttons/buttons.go b/examples/buttons/buttons.go index 438324fe0..82772faa1 100644 --- a/examples/buttons/buttons.go +++ b/examples/buttons/buttons.go @@ -4,12 +4,25 @@ import ( "encoding/json" "fmt" "net/http" + "os" "github.com/slack-go/slack" ) func main() { - api := slack.New("YOUR_TOKEN_HERE") + var token, channel string + var ok bool + token, ok = os.LookupEnv("SLACK_TOKEN") + if !ok { + fmt.Println("Missing SLACK_TOKEN in environment") + os.Exit(1) + } + channel, ok = os.LookupEnv("SLACK_CHANNEL") + if !ok { + fmt.Println("Missing SLACK_CHANNEL in environment") + os.Exit(1) + } + api := slack.New(token) attachment := slack.Attachment{ Pretext: "pretext", Fallback: "We don't currently support your client", @@ -33,7 +46,7 @@ func main() { } message := slack.MsgOptionAttachments(attachment) - channelID, timestamp, err := api.PostMessage("CHANNEL_ID", slack.MsgOptionText("", false), message) + channelID, timestamp, err := api.PostMessage(channel, slack.MsgOptionText("", false), message) if err != nil { fmt.Printf("Could not send message: %v", err) } @@ -48,5 +61,5 @@ func actionHandler(w http.ResponseWriter, r *http.Request) { if err != nil { fmt.Printf("Could not parse action response JSON: %v", err) } - fmt.Printf("Message button pressed by user %s with value %s", payload.User.Name, payload.Value) + fmt.Printf("Message button pressed by user %s with value %s", payload.User.Name, payload.ActionCallback.AttachmentActions[0].Value) } diff --git a/examples/channels/channels.go b/examples/channels/channels.go deleted file mode 100644 index b41c57495..000000000 --- a/examples/channels/channels.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/slack-go/slack" -) - -func main() { - api := slack.New("YOUR_TOKEN_HERE") - channels, err := api.GetChannels(false) - if err != nil { - fmt.Printf("%s\n", err) - return - } - for _, channel := range channels { - fmt.Println(channel.Name) - // channel is of type conversation & groupConversation - // see all available methods in `conversation.go` - } -} diff --git a/examples/connparams/connparams.go b/examples/connparams/connparams.go index e90905553..2bf2dce1d 100644 --- a/examples/connparams/connparams.go +++ b/examples/connparams/connparams.go @@ -10,8 +10,13 @@ import ( ) func main() { + token, ok := os.LookupEnv("SLACK_TOKEN") + if !ok { + fmt.Println("Missing SLACK_TOKEN in environment") + os.Exit(1) + } api := slack.New( - "YOUR TOKEN HERE", + token, slack.OptionDebug(true), slack.OptionLog(log.New(os.Stdout, "slack-bot: ", log.Lshortfile|log.LstdFlags)), ) diff --git a/examples/groups/groups.go b/examples/groups/groups.go deleted file mode 100644 index ed05a6a48..000000000 --- a/examples/groups/groups.go +++ /dev/null @@ -1,22 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/slack-go/slack" -) - -func main() { - api := slack.New("YOUR_TOKEN_HERE") - // If you set debugging, it will log all requests to the console - // Useful when encountering issues - // api.SetDebug(true) - groups, err := api.GetGroups(false) - if err != nil { - fmt.Printf("%s\n", err) - return - } - for _, group := range groups { - fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name) - } -} diff --git a/examples/ims/ims.go b/examples/ims/ims.go deleted file mode 100644 index 363f06f44..000000000 --- a/examples/ims/ims.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/slack-go/slack" -) - -func main() { - api := slack.New("YOUR_TOKEN_HERE") - - userID := "USER_ID" - - _, _, channelID, err := api.OpenIMChannel(userID) - - if err != nil { - fmt.Printf("%s\n", err) - } - - api.PostMessage(channelID, slack.MsgOptionText("Hello World!", false)) -} diff --git a/examples/modal_users/users.go b/examples/modal_users/users.go index 9706a0b9e..8f1d36f79 100644 --- a/examples/modal_users/users.go +++ b/examples/modal_users/users.go @@ -35,10 +35,16 @@ func main() { inviteeOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, nil, "invitee", memberOptions...) inviteeBlock := slack.NewInputBlock("invitee", inviteeText, inviteeOption) + // Section with users select - this input will not be included in the view_submission's view.state.values, + // but instead be sent as a "block_actions" request additionalInviteeText := slack.NewTextBlockObject(slack.PlainTextType, "Invitee from complete list of users", false, false) - additionalInviteeOption := slack.NewOptionsSelectBlockElement(slack.OptTypeUser, additionalInviteeText, "") + additionalInviteeOption := slack.NewOptionsSelectBlockElement(slack.OptTypeUser, additionalInviteeText, "user") additionalInviteeSection := slack.NewSectionBlock(additionalInviteeText, nil, slack.NewAccessory(additionalInviteeOption)) + // Input with users select - this input will be included in the view_submission's view.state.values + // It can be fetched as for example "payload.View.State.Values["user"]["user"].SelectedUser" + additionalInviteeBlock := slack.NewInputBlock("user", additionalInviteeText, additionalInviteeOption) + checkboxTxt := slack.NewTextBlockObject(slack.PlainTextType, "Checkbox", false, false) checkboxOptions := createOptionBlockObjects([]string{"option 1", "option 2", "option 3"}, false) checkboxOptionsBlock := slack.NewCheckboxGroupsBlockElement("chkbox", checkboxOptions...) @@ -58,6 +64,7 @@ func main() { channelNameBlock, inviteeBlock, additionalInviteeSection, + additionalInviteeBlock, checkboxBlock, summaryBlock, }, diff --git a/examples/pins/pins.go b/examples/pins/pins.go index f820c9940..d13d2d2c8 100644 --- a/examples/pins/pins.go +++ b/examples/pins/pins.go @@ -43,20 +43,21 @@ func main() { postAsUserID = authTest.UserID // Create a temporary channel - channel, err := api.CreateChannel(channelName) + channel, err := api.CreateConversation(channelName, false) if err != nil { // If the channel exists, that means we just need to unarchive it if err.Error() == "name_taken" { err = nil - if channels, err = api.GetChannels(false); err != nil { + params := &slack.GetConversationsParameters{ExcludeArchived: false} + if channels, _, err = api.GetConversations(params); err != nil { fmt.Println("Could not retrieve channels") return } for _, archivedChannel := range channels { if archivedChannel.Name == channelName { if archivedChannel.IsArchived { - err = api.UnarchiveChannel(archivedChannel.ID) + err = api.UnArchiveConversation(archivedChannel.ID) if err != nil { fmt.Printf("Could not unarchive %s: %s\n", archivedChannel.ID, err) return @@ -111,7 +112,7 @@ func main() { return } - if err = api.ArchiveChannel(channelID); err != nil { + if err = api.UnArchiveConversation(channelID); err != nil { fmt.Printf("Error archiving channel: %s\n", err) return } diff --git a/examples/reactions/reactions.go b/examples/reactions/reactions.go index 05ff05759..4e19f508a 100644 --- a/examples/reactions/reactions.go +++ b/examples/reactions/reactions.go @@ -43,12 +43,12 @@ func main() { postToUserID = authTest.UserID // Find the channel. - _, _, chanID, err := api.OpenIMChannel(postToUserID) + channel, _, _, err := api.OpenConversation(&slack.OpenConversationParameters{ChannelID: postToUserID}) if err != nil { fmt.Printf("Error opening IM: %s\n", err) return } - postToChannelID = chanID + postToChannelID = channel.ID fmt.Printf("Posting as %s (%s) in DM with %s (%s), channel %s\n", postAsUserName, postAsUserID, postToUserName, postToUserID, postToChannelID) diff --git a/examples/socketmode/socketmode.go b/examples/socketmode/socketmode.go index 69e34c294..6f1e65b48 100644 --- a/examples/socketmode/socketmode.go +++ b/examples/socketmode/socketmode.go @@ -15,7 +15,8 @@ import ( func main() { appToken := os.Getenv("SLACK_APP_TOKEN") if appToken == "" { - + fmt.Fprintf(os.Stderr, "SLACK_APP_TOKEN must be set.\n") + os.Exit(1) } if !strings.HasPrefix(appToken, "xapp-") { diff --git a/examples/websocket/websocket.go b/examples/websocket/websocket.go index 85b6d163c..96e110e39 100644 --- a/examples/websocket/websocket.go +++ b/examples/websocket/websocket.go @@ -9,8 +9,13 @@ import ( ) func main() { + token, ok := os.LookupEnv("SLACK_TOKEN") + if !ok { + fmt.Println("Missing SLACK_TOKEN in environment") + os.Exit(1) + } api := slack.New( - "YOUR TOKEN HERE", + token, slack.OptionDebug(true), slack.OptionLog(log.New(os.Stdout, "slack-bot: ", log.Lshortfile|log.LstdFlags)), ) diff --git a/examples/workflow_step/README.md b/examples/workflow_step/README.md new file mode 100644 index 000000000..da378b894 --- /dev/null +++ b/examples/workflow_step/README.md @@ -0,0 +1,59 @@ +#WorkflowStep + +Have you ever wanted to run an app from a Slack workflow? This sample app shows you how it works. + +Slack describes some of the basics here: +https://api.slack.com/workflows/steps +https://api.slack.com/tutorials/workflow-builder-steps + + +1. Start the example app localy on port 8080 + + +2. Use ngrok to expose your app to the internet + +```shell + ./ngrok http 8080 +``` +Copy the https forwarding URL and paste it into the app manifest down below (event_subscription request_url and interactivity request_url) + + +3. Create a new Slack App at api.slack.com/apps from an app manifest + +The manifest of a sample Slack App looks like this: +```yaml +display_information: + name: Workflowstep-Example +features: + bot_user: + display_name: Workflowstep-Example + always_online: false + workflow_steps: + - name: Example Step + callback_id: example-step +oauth_config: + scopes: + bot: + - workflow.steps:execute +settings: + event_subscriptions: + request_url: https://*****.ngrok.io/api/v1/example-step + bot_events: + - workflow_step_execute + interactivity: + is_enabled: true + request_url: https://*****.ngrok.io/api/v1/interaction + org_deploy_enabled: false + socket_mode_enabled: false + token_rotation_enabled: false +``` + +("Interactivity" and "Enable Events" should be turned on) + +4. Slack Workflow (**paid plan required!**) + 1. Create a new Workflow at app.slack.com/workflow-builder + 2. give it a name + 3. select "Planned date & time" + 4. add another step and select "Example Step" from App Workflowstep-Example + 5. configure your app and hit save + 6. don't forget to publish your workflow \ No newline at end of file diff --git a/examples/workflow_step/go.mod b/examples/workflow_step/go.mod new file mode 100644 index 000000000..1df243ce5 --- /dev/null +++ b/examples/workflow_step/go.mod @@ -0,0 +1,9 @@ +module workflowstep-example + +go 1.17 + +require ( + github.com/gorilla/websocket v1.4.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/slack-go/slack v0.10.1 // indirect +) diff --git a/examples/workflow_step/go.sum b/examples/workflow_step/go.sum new file mode 100644 index 000000000..e95148d32 --- /dev/null +++ b/examples/workflow_step/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/slack-go/slack v0.10.1 h1:BGbxa0kMsGEvLOEoZmYs8T1wWfoZXwmQFBb6FgYCXUA= +github.com/slack-go/slack v0.10.1/go.mod h1:wWL//kk0ho+FcQXcBTmEafUI5dz4qz5f4mMk8oIkioQ= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/examples/workflow_step/handler.go b/examples/workflow_step/handler.go new file mode 100644 index 000000000..b25a5be42 --- /dev/null +++ b/examples/workflow_step/handler.go @@ -0,0 +1,211 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "time" + + "github.com/slack-go/slack" + "github.com/slack-go/slack/slackevents" +) + +const ( + IDSelectOptionBlock = "select-option-block" + IDExampleSelectInput = "example-select-input" +) + +func handleMyWorkflowStep(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + // see: https://github.com/slack-go/slack/blob/master/examples/eventsapi/events.go + body, err := ioutil.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + eventsAPIEvent, err := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionNoVerifyToken()) + if err != nil { + log.Printf("[ERROR] Failed on parsing event: %s", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // see: https://api.slack.com/apis/connections/events-api#subscriptions + if eventsAPIEvent.Type == slackevents.URLVerification { + var r *slackevents.ChallengeResponse + err := json.Unmarshal([]byte(body), &r) + if err != nil { + log.Printf("[ERROR] Failed to decode json message on event url_verification: %s", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text") + w.Write([]byte(r.Challenge)) + return + } + + // see: https://api.slack.com/apis/connections/events-api#receiving_events + if eventsAPIEvent.Type == slackevents.CallbackEvent { + innerEvent := eventsAPIEvent.InnerEvent + + switch ev := innerEvent.Data.(type) { + + // see: https://api.slack.com/events/workflow_step_execute + case *slackevents.WorkflowStepExecuteEvent: + if ev.CallbackID == MyExampleWorkflowStepCallbackID { + go doHeavyLoad(ev.WorkflowStep) + + w.WriteHeader(http.StatusOK) + return + } + w.WriteHeader(http.StatusBadRequest) + log.Printf("[WARN] unknown callbackID: %s", ev.CallbackID) + return + + default: + w.WriteHeader(http.StatusBadRequest) + log.Printf("[WARN] unknown inner event type: %s", eventsAPIEvent.InnerEvent.Type) + return + } + } + + w.WriteHeader(http.StatusBadRequest) + log.Printf("[WARN] unknown event type: %s", eventsAPIEvent.Type) +} + +func handleInteraction(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + jsonStr, err := url.QueryUnescape(string(body)[8:]) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + var message slack.InteractionCallback + if err := json.Unmarshal([]byte(jsonStr), &message); err != nil { + log.Printf("[ERROR] Failed to decode json message from slack: %s", jsonStr) + w.WriteHeader(http.StatusInternalServerError) + return + } + + switch message.Type { + case slack.InteractionTypeWorkflowStepEdit: + // https://api.slack.com/workflows/steps#handle_config_view + err := replyWithConfigurationView(message, "", "") + if err != nil { + log.Printf("[ERROR] Failed to open configuration modal in slack: %s", err.Error()) + } + + case slack.InteractionTypeViewSubmission: + // https://api.slack.com/workflows/steps#handle_view_submission + + // process user inputs + // this is just for demonstration, so we print it to console only + blockAction := message.View.State.Values + selectedOption := blockAction[IDSelectOptionBlock][IDExampleSelectInput].SelectedOption.Value + log.Println(fmt.Sprintf("user selected: %s", selectedOption)) + + in := &slack.WorkflowStepInputs{ + IDExampleSelectInput: slack.WorkflowStepInputElement{ + Value: selectedOption, + SkipVariableReplacement: false, + }, + } + + err := saveUserSettingsForWorkflowStep(message.WorkflowStep.WorkflowStepEditID, in, nil) + if err != nil { + log.Printf("[ERROR] Failed on doing a POST request to workflows.updateStep: %s", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + } + + default: + log.Printf("[WARN] unknown message type: %s", message.Type) + w.WriteHeader(http.StatusInternalServerError) + } +} + +func replyWithConfigurationView(message slack.InteractionCallback, privateMetaData string, externalID string) error { + headerText := slack.NewTextBlockObject("mrkdwn", "Hello World!\nThis is your workflow step app configuration view", false, false) + headerSection := slack.NewSectionBlock(headerText, nil, nil) + + options := []*slack.OptionBlockObject{} + options = append( + options, + slack.NewOptionBlockObject("one", slack.NewTextBlockObject("plain_text", "One", false, false), nil), + ) + + options = append( + options, + slack.NewOptionBlockObject("two", slack.NewTextBlockObject("plain_text", "Two", false, false), nil), + ) + + options = append( + options, + slack.NewOptionBlockObject("three", slack.NewTextBlockObject("plain_text", "Three", false, false), nil), + ) + + selection := slack.NewOptionsSelectBlockElement( + "static_select", + slack.NewTextBlockObject("plain_text", "your choice", false, false), + IDExampleSelectInput, + options..., + ) + + // preselect option, if workflow step input is defined + initialOption, ok := slack.GetInitialOptionFromWorkflowStepInput(selection, message.WorkflowStep.Inputs, options) + if ok { + selection.InitialOption = initialOption + } + + inputBlock := slack.NewInputBlock( + IDSelectOptionBlock, + slack.NewTextBlockObject("plain_text", "Select an option", false, false), + selection, + ) + + blocks := slack.Blocks{ + BlockSet: []slack.Block{ + headerSection, + inputBlock, + }, + } + + cmr := slack.NewConfigurationModalRequest(blocks, privateMetaData, externalID) + _, err := appCtx.slack.OpenView(message.TriggerID, cmr.ModalViewRequest) + return err +} + +func saveUserSettingsForWorkflowStep(workflowStepEditID string, inputs *slack.WorkflowStepInputs, outputs *[]slack.WorkflowStepOutput) error { + return appCtx.slack.SaveWorkflowStepConfiguration(workflowStepEditID, inputs, outputs) +} + +func doHeavyLoad(workflowStep slackevents.EventWorkflowStep) { + // process user configuration e.g. inputs + log.Printf("Inputs:") + for name, input := range *workflowStep.Inputs { + log.Printf(fmt.Sprintf("%s: %s", name, input.Value)) + } + + // do heavy load + time.Sleep(10 * time.Second) + log.Println("Done") +} diff --git a/examples/workflow_step/main.go b/examples/workflow_step/main.go new file mode 100644 index 000000000..45494d086 --- /dev/null +++ b/examples/workflow_step/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + + "github.com/slack-go/slack" +) + +type ( + appContext struct { + slack *slack.Client + config configuration + } + configuration struct { + botToken string + signingSecret string + } + SecretsVerifierMiddleware struct { + handler http.Handler + } +) + +const ( + APIBaseURL = "/api/v1" + // MyExampleWorkflowStepCallbackID is configured in slack (api.slack.com/apps). + // Select your app or create a new one. Then choose menu "Workflow Steps"... + MyExampleWorkflowStepCallbackID = "example-step" +) + +var appCtx appContext + +func main() { + appCtx.config.botToken = os.Getenv("SLACK_BOT_TOKEN") + appCtx.config.signingSecret = os.Getenv("SLACK_SIGNING_SECRET") + + appCtx.slack = slack.New(appCtx.config.botToken) + + mux := http.NewServeMux() + mux.HandleFunc(fmt.Sprintf("%s/interaction", APIBaseURL), handleInteraction) + mux.HandleFunc(fmt.Sprintf("%s/%s", APIBaseURL, MyExampleWorkflowStepCallbackID), handleMyWorkflowStep) + middleware := NewSecretsVerifierMiddleware(mux) + + log.Printf("starting server on :8080") + log.Fatal(http.ListenAndServe(":8080", middleware)) +} diff --git a/examples/workflow_step/middleware.go b/examples/workflow_step/middleware.go new file mode 100644 index 000000000..0684e50d6 --- /dev/null +++ b/examples/workflow_step/middleware.go @@ -0,0 +1,41 @@ +package main + +import ( + "bytes" + "io/ioutil" + "net/http" + + "github.com/slack-go/slack" +) + +func (v *SecretsVerifierMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + r.Body.Close() + r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + + sv, err := slack.NewSecretsVerifier(r.Header, appCtx.config.signingSecret) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + if _, err := sv.Write(body); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + if err := sv.Ensure(); err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + v.handler.ServeHTTP(w, r) +} + +func NewSecretsVerifierMiddleware(h http.Handler) *SecretsVerifierMiddleware { + return &SecretsVerifierMiddleware{h} +} diff --git a/files.go b/files.go index 713038f06..e7e71c495 100644 --- a/files.go +++ b/files.go @@ -202,7 +202,14 @@ func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count, // GetFile retreives a given file from its private download URL func (api *Client) GetFile(downloadURL string, writer io.Writer) error { - return downloadFile(api.httpclient, api.token, downloadURL, writer, api) + return api.GetFileContext(context.Background(), downloadURL, writer) +} + +// GetFileContext retreives a given file from its private download URL with a custom context +// +// For more details, see GetFile documentation. +func (api *Client) GetFileContext(ctx context.Context, downloadURL string, writer io.Writer) error { + return downloadFile(ctx, api.httpclient, api.token, downloadURL, writer, api) } // GetFiles retrieves all files according to the parameters given @@ -210,40 +217,6 @@ func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) return api.GetFilesContext(context.Background(), params) } -// ListFiles retrieves all files according to the parameters given. Uses cursor based pagination. -func (api *Client) ListFiles(params ListFilesParameters) ([]File, *ListFilesParameters, error) { - return api.ListFilesContext(context.Background(), params) -} - -// ListFilesContext retrieves all files according to the parameters given with a custom context. Uses cursor based pagination. -func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParameters) ([]File, *ListFilesParameters, error) { - values := url.Values{ - "token": {api.token}, - } - - if params.User != DEFAULT_FILES_USER { - values.Add("user", params.User) - } - if params.Channel != DEFAULT_FILES_CHANNEL { - values.Add("channel", params.Channel) - } - if params.Limit != DEFAULT_FILES_COUNT { - values.Add("limit", strconv.Itoa(params.Limit)) - } - if params.Cursor != "" { - values.Add("cursor", params.Cursor) - } - - response, err := api.fileRequest(ctx, "files.list", values) - if err != nil { - return nil, nil, err - } - - params.Cursor = response.Metadata.Cursor - - return response.Files, ¶ms, nil -} - // GetFilesContext retrieves all files according to the parameters given with a custom context func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameters) ([]File, *Paging, error) { values := url.Values{ @@ -281,6 +254,42 @@ func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameter return response.Files, &response.Paging, nil } +// ListFiles retrieves all files according to the parameters given. Uses cursor based pagination. +func (api *Client) ListFiles(params ListFilesParameters) ([]File, *ListFilesParameters, error) { + return api.ListFilesContext(context.Background(), params) +} + +// ListFilesContext retrieves all files according to the parameters given with a custom context. +// +// For more details, see ListFiles documentation. +func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParameters) ([]File, *ListFilesParameters, error) { + values := url.Values{ + "token": {api.token}, + } + + if params.User != DEFAULT_FILES_USER { + values.Add("user", params.User) + } + if params.Channel != DEFAULT_FILES_CHANNEL { + values.Add("channel", params.Channel) + } + if params.Limit != DEFAULT_FILES_COUNT { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + + response, err := api.fileRequest(ctx, "files.list", values) + if err != nil { + return nil, nil, err + } + + params.Cursor = response.Metadata.Cursor + + return response.Files, ¶ms, nil +} + // UploadFile uploads a file func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) { return api.UploadFileContext(context.Background(), params) @@ -295,9 +304,7 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam return nil, err } response := &fileResponseFull{} - values := url.Values{ - "token": {api.token}, - } + values := url.Values{} if params.Filetype != "" { values.Add("filetype", params.Filetype) } @@ -318,14 +325,15 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam } if params.Content != "" { values.Add("content", params.Content) + values.Add("token", api.token) err = api.postMethod(ctx, "files.upload", values, response) } else if params.File != "" { - err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", values, response, api) + err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", api.token, values, response, api) } else if params.Reader != nil { if params.Filename == "" { return nil, fmt.Errorf("files.upload: FileUploadParameters.Filename is mandatory when using FileUploadParameters.Reader") } - err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.Filename, "file", values, params.Reader, response, api) + err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.Filename, "file", api.token, values, params.Reader, response, api) } if err != nil { diff --git a/go.mod b/go.mod index 32fc4b939..5cc8e1a7e 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/slack-go/slack +go 1.16 + require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-test/deep v1.0.4 + github.com/google/go-cmp v0.5.7 github.com/gorilla/websocket v1.4.2 - github.com/pkg/errors v0.8.0 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.2.2 ) - -go 1.13 diff --git a/go.sum b/go.sum index d01bacbf1..194956433 100644 --- a/go.sum +++ b/go.sum @@ -2,11 +2,13 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/groups.go b/groups.go index 4c5793dc3..b77f909db 100644 --- a/groups.go +++ b/groups.go @@ -1,574 +1,7 @@ package slack -import ( - "context" - "net/url" - "strconv" -) - // Group contains all the information for a group -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api type Group struct { GroupConversation IsGroup bool `json:"is_group"` } - -type groupResponseFull struct { - Group Group `json:"group"` - Groups []Group `json:"groups"` - Purpose string `json:"purpose"` - Topic string `json:"topic"` - NotInGroup bool `json:"not_in_group"` - NoOp bool `json:"no_op"` - AlreadyClosed bool `json:"already_closed"` - AlreadyOpen bool `json:"already_open"` - AlreadyInGroup bool `json:"already_in_group"` - Channel Channel `json:"channel"` - History - SlackResponse -} - -func (api *Client) groupRequest(ctx context.Context, path string, values url.Values) (*groupResponseFull, error) { - response := &groupResponseFull{} - err := api.postMethod(ctx, path, values, response) - if err != nil { - return nil, err - } - - return response, response.Err() -} - -// ArchiveGroup archives a private group -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) ArchiveGroup(group string) error { - return api.ArchiveGroupContext(context.Background(), group) -} - -// ArchiveGroupContext archives a private group -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - } - - _, err := api.groupRequest(ctx, "groups.archive", values) - return err -} - -// UnarchiveGroup unarchives a private group -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) UnarchiveGroup(group string) error { - return api.UnarchiveGroupContext(context.Background(), group) -} - -// UnarchiveGroupContext unarchives a private group -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) error { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - } - - _, err := api.groupRequest(ctx, "groups.unarchive", values) - return err -} - -// CreateGroup creates a private group -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) CreateGroup(group string) (*Group, error) { - return api.CreateGroupContext(context.Background(), group) -} - -// CreateGroupContext creates a private group -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) CreateGroupContext(ctx context.Context, group string) (*Group, error) { - values := url.Values{ - "token": {api.token}, - "name": {group}, - } - - response, err := api.groupRequest(ctx, "groups.create", values) - if err != nil { - return nil, err - } - return &response.Group, nil -} - -// CreateChildGroup creates a new private group archiving the old one -// This method takes an existing private group and performs the following steps: -// 1. Renames the existing group (from "example" to "example-archived"). -// 2. Archives the existing group. -// 3. Creates a new group with the name of the existing group. -// 4. Adds all members of the existing group to the new group. -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) CreateChildGroup(group string) (*Group, error) { - return api.CreateChildGroupContext(context.Background(), group) -} - -// CreateChildGroupContext creates a new private group archiving the old one with a custom context -// For more information see CreateChildGroup -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) CreateChildGroupContext(ctx context.Context, group string) (*Group, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - } - - response, err := api.groupRequest(ctx, "groups.createChild", values) - if err != nil { - return nil, err - } - return &response.Group, nil -} - -// GetGroupHistory fetches all the history for a private group -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*History, error) { - return api.GetGroupHistoryContext(context.Background(), group, params) -} - -// GetGroupHistoryContext fetches all the history for a private group with a custom context -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, params HistoryParameters) (*History, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - } - if params.Latest != DEFAULT_HISTORY_LATEST { - values.Add("latest", params.Latest) - } - if params.Oldest != DEFAULT_HISTORY_OLDEST { - values.Add("oldest", params.Oldest) - } - if params.Count != DEFAULT_HISTORY_COUNT { - values.Add("count", strconv.Itoa(params.Count)) - } - if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { - if params.Inclusive { - values.Add("inclusive", "1") - } else { - values.Add("inclusive", "0") - } - } - if params.Unreads != DEFAULT_HISTORY_UNREADS { - if params.Unreads { - values.Add("unreads", "1") - } else { - values.Add("unreads", "0") - } - } - - response, err := api.groupRequest(ctx, "groups.history", values) - if err != nil { - return nil, err - } - return &response.History, nil -} - -// InviteUserToGroup invites a specific user to a private group -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) InviteUserToGroup(group, user string) (*Group, bool, error) { - return api.InviteUserToGroupContext(context.Background(), group, user) -} - -// InviteUserToGroupContext invites a specific user to a private group with a custom context -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) InviteUserToGroupContext(ctx context.Context, group, user string) (*Group, bool, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - "user": {user}, - } - - response, err := api.groupRequest(ctx, "groups.invite", values) - if err != nil { - return nil, false, err - } - return &response.Group, response.AlreadyInGroup, nil -} - -// LeaveGroup makes authenticated user leave the group -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) LeaveGroup(group string) error { - return api.LeaveGroupContext(context.Background(), group) -} - -// LeaveGroupContext makes authenticated user leave the group with a custom context -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) LeaveGroupContext(ctx context.Context, group string) (err error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - } - - _, err = api.groupRequest(ctx, "groups.leave", values) - return err -} - -// KickUserFromGroup kicks a user from a group -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) KickUserFromGroup(group, user string) error { - return api.KickUserFromGroupContext(context.Background(), group, user) -} - -// KickUserFromGroupContext kicks a user from a group with a custom context -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user string) (err error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - "user": {user}, - } - - _, err = api.groupRequest(ctx, "groups.kick", values) - return err -} - -// GetGroups retrieves all groups -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetGroups(excludeArchived bool) ([]Group, error) { - return api.GetGroupsContext(context.Background(), excludeArchived) -} - -// GetGroupsContext retrieves all groups with a custom context -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetGroupsContext(ctx context.Context, excludeArchived bool) ([]Group, error) { - values := url.Values{ - "token": {api.token}, - } - if excludeArchived { - values.Add("exclude_archived", "1") - } - - response, err := api.groupRequest(ctx, "groups.list", values) - if err != nil { - return nil, err - } - return response.Groups, nil -} - -// GetGroupInfo retrieves the given group -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetGroupInfo(group string) (*Group, error) { - return api.GetGroupInfoContext(context.Background(), group) -} - -// GetGroupInfoContext retrieves the given group with a custom context -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetGroupInfoContext(ctx context.Context, group string) (*Group, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - "include_locale": {strconv.FormatBool(true)}, - } - - response, err := api.groupRequest(ctx, "groups.info", values) - if err != nil { - return nil, err - } - return &response.Group, nil -} - -// SetGroupReadMark sets the read mark on a private group -// Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a -// timer before making the call. In this way, any further updates needed during the timeout will not generate extra -// calls (just one per channel). This is useful for when reading scroll-back history, or following a busy live -// channel. A timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout. -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) SetGroupReadMark(group, ts string) error { - return api.SetGroupReadMarkContext(context.Background(), group, ts) -} - -// SetGroupReadMarkContext sets the read mark on a private group with a custom context -// For more details see SetGroupReadMark -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string) (err error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - "ts": {ts}, - } - - _, err = api.groupRequest(ctx, "groups.mark", values) - return err -} - -// OpenGroup opens a private group -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) OpenGroup(group string) (bool, bool, error) { - return api.OpenGroupContext(context.Background(), group) -} - -// OpenGroupContext opens a private group with a custom context -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) OpenGroupContext(ctx context.Context, group string) (bool, bool, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - } - - response, err := api.groupRequest(ctx, "groups.open", values) - if err != nil { - return false, false, err - } - return response.NoOp, response.AlreadyOpen, nil -} - -// RenameGroup renames a group -// XXX: They return a channel, not a group. What is this crap? :( -// Inconsistent api it seems. -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) RenameGroup(group, name string) (*Channel, error) { - return api.RenameGroupContext(context.Background(), group, name) -} - -// RenameGroupContext renames a group with a custom context -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) RenameGroupContext(ctx context.Context, group, name string) (*Channel, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - "name": {name}, - } - - // XXX: the created entry in this call returns a string instead of a number - // so I may have to do some workaround to solve it. - response, err := api.groupRequest(ctx, "groups.rename", values) - if err != nil { - return nil, err - } - return &response.Channel, nil -} - -// SetGroupPurpose sets the group purpose -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) SetGroupPurpose(group, purpose string) (string, error) { - return api.SetGroupPurposeContext(context.Background(), group, purpose) -} - -// SetGroupPurposeContext sets the group purpose with a custom context -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) SetGroupPurposeContext(ctx context.Context, group, purpose string) (string, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - "purpose": {purpose}, - } - - response, err := api.groupRequest(ctx, "groups.setPurpose", values) - if err != nil { - return "", err - } - return response.Purpose, nil -} - -// SetGroupTopic sets the group topic -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) SetGroupTopic(group, topic string) (string, error) { - return api.SetGroupTopicContext(context.Background(), group, topic) -} - -// SetGroupTopicContext sets the group topic with a custom context -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) SetGroupTopicContext(ctx context.Context, group, topic string) (string, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - "topic": {topic}, - } - - response, err := api.groupRequest(ctx, "groups.setTopic", values) - if err != nil { - return "", err - } - return response.Topic, nil -} - -// GetGroupReplies gets an entire thread (a message plus all the messages in reply to it). -// see https://api.slack.com/methods/groups.replies -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetGroupReplies(channelID, thread_ts string) ([]Message, error) { - return api.GetGroupRepliesContext(context.Background(), channelID, thread_ts) -} - -// GetGroupRepliesContext gets an entire thread (a message plus all the messages in reply to it) with a custom context -// see https://api.slack.com/methods/groups.replies -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetGroupRepliesContext(ctx context.Context, channelID, thread_ts string) ([]Message, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "thread_ts": {thread_ts}, - } - response, err := api.groupRequest(ctx, "groups.replies", values) - if err != nil { - return nil, err - } - return response.History.Messages, nil -} diff --git a/im.go b/im.go index a884b2953..7c4bc2572 100644 --- a/im.go +++ b/im.go @@ -1,11 +1,5 @@ package slack -import ( - "context" - "net/url" - "strconv" -) - type imChannel struct { ID string `json:"id"` } @@ -21,200 +15,7 @@ type imResponseFull struct { } // IM contains information related to the Direct Message channel -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api type IM struct { Conversation IsUserDeleted bool `json:"is_user_deleted"` } - -func (api *Client) imRequest(ctx context.Context, path string, values url.Values) (*imResponseFull, error) { - response := &imResponseFull{} - err := api.postMethod(ctx, path, values, response) - if err != nil { - return nil, err - } - - return response, response.Err() -} - -// CloseIMChannel closes the direct message channel -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) CloseIMChannel(channel string) (bool, bool, error) { - return api.CloseIMChannelContext(context.Background(), channel) -} - -// CloseIMChannelContext closes the direct message channel with a custom context -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) CloseIMChannelContext(ctx context.Context, channel string) (bool, bool, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channel}, - } - - response, err := api.imRequest(ctx, "im.close", values) - if err != nil { - return false, false, err - } - return response.NoOp, response.AlreadyClosed, nil -} - -// OpenIMChannel opens a direct message channel to the user provided as argument -// Returns some status and the channel ID -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) OpenIMChannel(user string) (bool, bool, string, error) { - return api.OpenIMChannelContext(context.Background(), user) -} - -// OpenIMChannelContext opens a direct message channel to the user provided as argument with a custom context -// Returns some status and the channel ID -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) OpenIMChannelContext(ctx context.Context, user string) (bool, bool, string, error) { - values := url.Values{ - "token": {api.token}, - "user": {user}, - } - - response, err := api.imRequest(ctx, "im.open", values) - if err != nil { - return false, false, "", err - } - return response.NoOp, response.AlreadyOpen, response.Channel.ID, nil -} - -// MarkIMChannel sets the read mark of a direct message channel to a specific point -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) MarkIMChannel(channel, ts string) (err error) { - return api.MarkIMChannelContext(context.Background(), channel, ts) -} - -// MarkIMChannelContext sets the read mark of a direct message channel to a specific point with a custom context -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string) error { - values := url.Values{ - "token": {api.token}, - "channel": {channel}, - "ts": {ts}, - } - - _, err := api.imRequest(ctx, "im.mark", values) - return err -} - -// GetIMHistory retrieves the direct message channel history -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetIMHistory(channel string, params HistoryParameters) (*History, error) { - return api.GetIMHistoryContext(context.Background(), channel, params) -} - -// GetIMHistoryContext retrieves the direct message channel history with a custom context -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, params HistoryParameters) (*History, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channel}, - } - if params.Latest != DEFAULT_HISTORY_LATEST { - values.Add("latest", params.Latest) - } - if params.Oldest != DEFAULT_HISTORY_OLDEST { - values.Add("oldest", params.Oldest) - } - if params.Count != DEFAULT_HISTORY_COUNT { - values.Add("count", strconv.Itoa(params.Count)) - } - if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { - if params.Inclusive { - values.Add("inclusive", "1") - } else { - values.Add("inclusive", "0") - } - } - if params.Unreads != DEFAULT_HISTORY_UNREADS { - if params.Unreads { - values.Add("unreads", "1") - } else { - values.Add("unreads", "0") - } - } - - response, err := api.imRequest(ctx, "im.history", values) - if err != nil { - return nil, err - } - return &response.History, nil -} - -// GetIMChannels returns the list of direct message channels -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetIMChannels() ([]IM, error) { - return api.GetIMChannelsContext(context.Background()) -} - -// GetIMChannelsContext returns the list of direct message channels with a custom context -// -// Deprecated: channels.*, groups.* im.* and mpim.* methods will be deprecated in the next version. -// In Slack, these API are no longer available for newly Apps created after June 10th, 2020. -// Also, existing applications will not be able to use these APIs after February 24th, 2021. -// -// See also: https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api -func (api *Client) GetIMChannelsContext(ctx context.Context) ([]IM, error) { - values := url.Values{ - "token": {api.token}, - } - - response, err := api.imRequest(ctx, "im.list", values) - if err != nil { - return nil, err - } - return response.IMs, nil -} diff --git a/info.go b/info.go index ec7062407..fde2bc98e 100644 --- a/info.go +++ b/info.go @@ -321,10 +321,13 @@ type UserPrefs struct { } func (api *Client) GetUserPrefs() (*UserPrefsCarrier, error) { - values := url.Values{"token": {api.token}} + return api.GetUserPrefsContext(context.Background()) +} + +func (api *Client) GetUserPrefsContext(ctx context.Context) (*UserPrefsCarrier, error) { response := UserPrefsCarrier{} - err := api.getMethod(context.Background(), "users.prefs.get", values, &response) + err := api.getMethod(ctx, "users.prefs.get", api.token, url.Values{}, &response) if err != nil { return nil, err } diff --git a/interactions.go b/interactions.go index 2515e290f..e362caa86 100644 --- a/interactions.go +++ b/interactions.go @@ -9,11 +9,11 @@ import ( type InteractionType string // ActionType type represents the type of action (attachment, block, etc.) -type actionType string +type ActionType string // action is an interface that should be implemented by all callback action types type action interface { - actionType() actionType + actionType() ActionType } // Types of interactions that can be received. @@ -28,31 +28,34 @@ const ( InteractionTypeViewSubmission = InteractionType("view_submission") InteractionTypeViewClosed = InteractionType("view_closed") InteractionTypeShortcut = InteractionType("shortcut") + InteractionTypeWorkflowStepEdit = InteractionType("workflow_step_edit") ) // InteractionCallback is sent from slack when a user interactions with a button or dialog. type InteractionCallback struct { - Type InteractionType `json:"type"` - Token string `json:"token"` - CallbackID string `json:"callback_id"` - ResponseURL string `json:"response_url"` - TriggerID string `json:"trigger_id"` - ActionTs string `json:"action_ts"` - Team Team `json:"team"` - Channel Channel `json:"channel"` - User User `json:"user"` - OriginalMessage Message `json:"original_message"` - Message Message `json:"message"` - Name string `json:"name"` - Value string `json:"value"` - MessageTs string `json:"message_ts"` - AttachmentID string `json:"attachment_id"` - ActionCallback ActionCallbacks `json:"actions"` - View View `json:"view"` - ActionID string `json:"action_id"` - APIAppID string `json:"api_app_id"` - BlockID string `json:"block_id"` - Container Container `json:"container"` + Type InteractionType `json:"type"` + Token string `json:"token"` + CallbackID string `json:"callback_id"` + ResponseURL string `json:"response_url"` + TriggerID string `json:"trigger_id"` + ActionTs string `json:"action_ts"` + Team Team `json:"team"` + Channel Channel `json:"channel"` + User User `json:"user"` + OriginalMessage Message `json:"original_message"` + Message Message `json:"message"` + Name string `json:"name"` + Value string `json:"value"` + MessageTs string `json:"message_ts"` + AttachmentID string `json:"attachment_id"` + ActionCallback ActionCallbacks `json:"actions"` + View View `json:"view"` + ActionID string `json:"action_id"` + APIAppID string `json:"api_app_id"` + BlockID string `json:"block_id"` + Container Container `json:"container"` + Enterprise Enterprise `json:"enterprise"` + WorkflowStep InteractionWorkflowStep `json:"workflow_step"` DialogSubmissionCallback ViewSubmissionCallback ViewClosedCallback @@ -121,12 +124,26 @@ type Container struct { Type string `json:"type"` ViewID string `json:"view_id"` MessageTs string `json:"message_ts"` + ThreadTs string `json:"thread_ts,omitempty"` AttachmentID json.Number `json:"attachment_id"` ChannelID string `json:"channel_id"` IsEphemeral bool `json:"is_ephemeral"` IsAppUnfurl bool `json:"is_app_unfurl"` } +type Enterprise struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type InteractionWorkflowStep struct { + WorkflowStepEditID string `json:"workflow_step_edit_id,omitempty"` + WorkflowID string `json:"workflow_id"` + StepID string `json:"step_id"` + Inputs *WorkflowStepInputs `json:"inputs,omitempty"` + Outputs *[]WorkflowStepOutput `json:"outputs,omitempty"` +} + // ActionCallback is a convenience struct defined to allow dynamic unmarshalling of // the "actions" value in Slack's JSON response, which varies depending on block type type ActionCallbacks struct { diff --git a/interactions_test.go b/interactions_test.go index c4f98b88d..f662425dd 100644 --- a/interactions_test.go +++ b/interactions_test.go @@ -93,19 +93,35 @@ const ( "type": "plain_text", "text": "meal choice" }, - "blocks": [{ - "type": "input", - "block_id": "multi-line", - "label": { - "type": "plain_text", - "text": "dietary restrictions" + "blocks": [ + { + "type": "input", + "block_id": "multi-line", + "label": { + "type": "plain_text", + "text": "dietary restrictions" + }, + "element": { + "type": "plain_text_input", + "multiline": true, + "action_id": "ml-value" + } }, - "element": { - "type": "plain_text_input", - "multiline": true, - "action_id": "ml-value" + { + "type": "input", + "block_id": "target_channel", + "label": { + "type": "plain_text", + "text": "Select a channel to post the result on" + }, + "element": { + "type": "conversations_select", + "action_id": "target_select", + "default_to_current_conversation": true, + "response_url_enabled": true + } } - }], + ], "state": { "values": { "multi-line": { @@ -113,10 +129,25 @@ const ( "type": "plain_text_input", "value": "No onions" } + }, + "target_channel": { + "target_select": { + "type": "conversations_select", + "value": "C1AB2C3DE" + } } } } - } + }, + "hash": "156663117.cd33ad1f", + "response_urls": [ + { + "block_id": "target_channel", + "action_id": "target_select", + "channel_id": "C1AB2C3DE", + "response_url": "https:\/\/hooks.slack.com\/app\/ABC12312\/1234567890\/A100B100C100d100" + } + ] }` ) @@ -231,6 +262,21 @@ func TestViewSubmissionCallback(t *testing.T) { Multiline: true, }, ), + NewInputBlock( + "target_channel", + NewTextBlockObject( + "plain_text", + "Select a channel to post the result on", + false, + false, + ), + &SelectBlockElement{ + Type: "conversations_select", + ActionID: "target_select", + DefaultToCurrentConversation: true, + ResponseURLEnabled: true, + }, + ), }, }, State: &ViewState{ @@ -241,6 +287,23 @@ func TestViewSubmissionCallback(t *testing.T) { Value: "No onions", }, }, + "target_channel": map[string]BlockAction{ + "target_select": BlockAction{ + Type: "conversations_select", + Value: "C1AB2C3DE", + }, + }, + }, + }, + }, + ViewSubmissionCallback: ViewSubmissionCallback{ + Hash: "156663117.cd33ad1f", + ResponseURLs: []ViewSubmissionCallbackResponseURL{ + { + BlockID: "target_channel", + ActionID: "target_select", + ChannelID: "C1AB2C3DE", + ResponseURL: "https://hooks.slack.com/app/ABC12312/1234567890/A100B100C100d100", }, }, }, @@ -386,8 +449,92 @@ func TestInteractionCallback_InteractionTypeBlockActions_Unmarshal(t *testing.T) assert.Equal(t, cb.State, "") assert.Equal(t, cb.BlockActionState.Values["section_block_id"]["multi_convos"].actionType(), - actionType(MultiOptTypeConversations)) + ActionType(MultiOptTypeConversations)) assert.Equal(t, cb.BlockActionState.Values["section_block_id"]["multi_convos"].SelectedConversations, []string{"G12345"}) } + +func TestInteractionCallback_Container_Marshal_And_Unmarshal(t *testing.T) { + // Contrived - you generally won't see all of the fields set in a single message + raw := []byte( + ` + { + "container": { + "type": "message", + "view_id": "viewID", + "message_ts": "messageTS", + "attachment_id": "123", + "channel_id": "channelID", + "is_ephemeral": false, + "is_app_unfurl": false + } + } + `) + + expected := &InteractionCallback{ + Container: Container{ + Type: "message", + ViewID: "viewID", + MessageTs: "messageTS", + AttachmentID: "123", + ChannelID: "channelID", + IsEphemeral: false, + IsAppUnfurl: false, + }, + RawState: json.RawMessage(`{}`), + } + + actual := new(InteractionCallback) + err := json.Unmarshal(raw, actual) + assert.NoError(t, err) + assert.Equal(t, expected.Container, actual.Container) + + expectedJSON := []byte(`{"type":"message","view_id":"viewID","message_ts":"messageTS","attachment_id":123,"channel_id":"channelID","is_ephemeral":false,"is_app_unfurl":false}`) + actualJSON, err := json.Marshal(actual.Container) + assert.NoError(t, err) + assert.Equal(t, expectedJSON, actualJSON) +} + +func TestInteractionCallback_In_Thread_Container_Marshal_And_Unmarshal(t *testing.T) { + // Contrived - you generally won't see all of the fields set in a single message + raw := []byte( + ` + { + "container": { + "type": "message", + "view_id": "viewID", + "message_ts": "messageTS", + "thread_ts": "threadTS", + "attachment_id": "123", + "channel_id": "channelID", + "is_ephemeral": false, + "is_app_unfurl": false + } + } + `) + + expected := &InteractionCallback{ + Container: Container{ + Type: "message", + ViewID: "viewID", + MessageTs: "messageTS", + ThreadTs: "threadTS", + AttachmentID: "123", + ChannelID: "channelID", + IsEphemeral: false, + IsAppUnfurl: false, + }, + RawState: json.RawMessage(`{}`), + } + + actual := new(InteractionCallback) + err := json.Unmarshal(raw, actual) + assert.NoError(t, err) + assert.Equal(t, expected.Container, actual.Container) + + expectedJSON := []byte(`{"type":"message","view_id":"viewID","message_ts":"messageTS","thread_ts":"threadTS","attachment_id":123,"channel_id":"channelID","is_ephemeral":false,"is_app_unfurl":false}`) + actualJSON, err := json.Marshal(actual.Container) + assert.NoError(t, err) + assert.Equal(t, expectedJSON, actualJSON) +} diff --git a/messageID.go b/messageID.go index a17472b4f..689ee80db 100644 --- a/messageID.go +++ b/messageID.go @@ -1,6 +1,6 @@ package slack -import "sync" +import "sync/atomic" // IDGenerator provides an interface for generating integer ID values. type IDGenerator interface { @@ -11,20 +11,20 @@ type IDGenerator interface { // concurrent use by multiple goroutines. func NewSafeID(startID int) IDGenerator { return &safeID{ - nextID: startID, - mutex: &sync.Mutex{}, + nextID: int64(startID), } } type safeID struct { - nextID int - mutex *sync.Mutex + nextID int64 } +// make sure safeID implements the IDGenerator interface. +var _ IDGenerator = (*safeID)(nil) + +// Next implements IDGenerator.Next. func (s *safeID) Next() int { - s.mutex.Lock() - defer s.mutex.Unlock() - id := s.nextID - s.nextID++ - return id + id := atomic.AddInt64(&s.nextID, 1) + + return int(id) } diff --git a/messageID_test.go b/messageID_test.go new file mode 100644 index 000000000..143fd636e --- /dev/null +++ b/messageID_test.go @@ -0,0 +1,46 @@ +package slack + +import ( + "testing" +) + +func TestNewSafeID(t *testing.T) { + idgen := NewSafeID(1) + id1 := idgen.Next() + id2 := idgen.Next() + if id1 == id2 { + t.Fatalf("id1 and id2 are same: id1: %d, id2: %d", id1, id2) + } + + idgen = NewSafeID(100) + id100 := idgen.Next() + id101 := idgen.Next() + if id2 == id100 { + t.Fatalf("except id2 and id100 not same: id2: %d, id101: %d", id2, id100) + } + if id100 == id101 { + t.Fatalf("id1 and id2 are same: id100: %d, id101: %d", id100, id101) + } +} + +var id int + +func BenchmarkNewSafeID(b *testing.B) { + b.ReportAllocs() + + idgen := NewSafeID(1) + for i := 0; i < b.N; i++ { + id = idgen.Next() + } +} + +func BenchmarkNewSafeIDParallel(b *testing.B) { + b.ReportAllocs() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + idgen := NewSafeID(1) + id = idgen.Next() + } + }) +} diff --git a/messages.go b/messages.go index 999df5d94..2cc31d5bf 100644 --- a/messages.go +++ b/messages.go @@ -19,6 +19,38 @@ type Message struct { PreviousMessage *Msg `json:"previous_message,omitempty"` } +// Msg SubTypes (https://api.slack.com/events/message) +const ( + MsgSubTypeBotMessage = "bot_message" // [Events API, RTM] A message was posted by an integration + MsgSubTypeMeMessage = "me_message" // [Events API, RTM] A /me message was sent + MsgSubTypeMessageChanged = "message_changed" // [Events API, RTM] A message was changed + MsgSubTypeMessageDeleted = "message_deleted" // [Events API, RTM] A message was deleted + MsgSubTypeMessageReplied = "message_replied" // [Events API, RTM] A message thread received a reply + MsgSubTypeReplyBroadcast = "reply_broadcast" // @Deprecated (No longer served) A message thread's reply was broadcast to a channel + MsgSubTypeThreadBroadcast = "thread_broadcast" // [Events API, RTM] A message thread's reply was broadcast to a channel + MsgSubTypeChannelJoin = "channel_join" // [Events API, RTM] A member joined a channel + MsgSubTypeChannelLeave = "channel_leave" // [Events API, RTM] A member left a channel + MsgSubTypeChannelTopic = "channel_topic" // [Events API, RTM] A channel topic was updated + MsgSubTypeChannelPurpose = "channel_purpose" // [Events API, RTM] A channel purpose was updated + MsgSubTypeChannelName = "channel_name" // [Events API, RTM] A channel was renamed + MsgSubTypeChannelArchive = "channel_archive" // [Events API, RTM] A channel was archived + MsgSubTypeChannelUnarchive = "channel_unarchive" // [Events API, RTM] A channel was unarchived + MsgSubTypeGroupJoin = "group_join" // [RTM] A member joined a group + MsgSubTypeGroupLeave = "group_leave" // [RTM] A member left a group + MsgSubTypeGroupTopic = "group_topic" // [RTM] A group topic was updated + MsgSubTypeGroupPurpose = "group_purpose" // [RTM] A group purpose was updated + MsgSubTypeGroupName = "group_name" // [RTM] A group was renamed + MsgSubTypeGroupArchive = "group_archive" // [RTM] A group was archived + MsgSubTypeGroupUnarchive = "group_unarchive" // [RTM] A group was unarchived + MsgSubTypeFileShare = "file_share" // [Events API, RTM] A file was shared into a channel + MsgSubTypeFileComment = "file_comment" // [RTM] A comment was added to a file + MsgSubTypeGileMention = "file_mention" // [RTM] A file was mentioned in a channel + MsgSubTypePinnedItem = "pinned_item" // [RTM] An item was pinned in a channel + MsgSubTypeUnpinnedItem = "unpinned_item" // [RTM] An item was unpinned from a channel + MsgSubTypeEkmAccessDenied = "ekm_access_denied" // [Events API, RTM] Message content redacted due to Enterprise Key Management (EKM) + MsgSubTypeChannelPostingPermissions = "channel_posting_permissions" // [Events API, RTM] The posting permissions for a channel changed +) + // Msg contains information about a slack message type Msg struct { // Basic Message @@ -71,6 +103,7 @@ type Msg struct { ReplyCount int `json:"reply_count,omitempty"` Replies []Reply `json:"replies,omitempty"` ParentUserId string `json:"parent_user_id,omitempty"` + LatestReply string `json:"latest_reply,omitempty"` // file_share, file_comment, file_mention Files []File `json:"files,omitempty"` diff --git a/messages_test.go b/messages_test.go index d47800e47..62fd759fa 100644 --- a/messages_test.go +++ b/messages_test.go @@ -50,7 +50,7 @@ func TestStarredMessage(t *testing.T) { assert.NotNil(t, message) assert.Equal(t, "is testing", message.Text) assert.Equal(t, "message", message.Type) - assert.Equal(t, "me_message", message.SubType) + assert.Equal(t, MsgSubTypeMeMessage, message.SubType) assert.Equal(t, "U2147483697", message.User) assert.Equal(t, "1433314126.000003", message.Timestamp) assert.Equal(t, true, message.IsStarred) @@ -136,7 +136,7 @@ func TestUploadedFile(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "file_share", message.SubType) + assert.Equal(t, MsgSubTypeFileShare, message.SubType) assert.Equal(t, "<@U2147483697|tester> uploaded a file: and commented: test comment here", message.Text) // TODO: Assert File assert.Equal(t, "U2147483697", message.User) @@ -190,7 +190,7 @@ func TestPost(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "file_share", message.SubType) + assert.Equal(t, MsgSubTypeFileShare, message.SubType) assert.Equal(t, "<@U2147483697|tester> shared a file: ", message.Text) // TODO: Assert File assert.Equal(t, "U2147483697", message.User) @@ -250,7 +250,7 @@ func TestComment(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "file_comment", message.SubType) + assert.Equal(t, MsgSubTypeFileComment, message.SubType) assert.Equal(t, "<@U2147483697|tester> commented on <@U2147483697|tester>'s file : another comment", message.Text) // TODO: Assert File // TODO: Assert Comment @@ -290,7 +290,7 @@ func TestBotMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "bot_message", message.SubType) + assert.Equal(t, MsgSubTypeBotMessage, message.SubType) assert.Equal(t, "1358877455.000010", message.Timestamp) assert.Equal(t, "Pushing is the answer", message.Text) assert.Equal(t, "BB12033", message.BotID) @@ -325,7 +325,7 @@ func TestMeMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "me_message", message.SubType) + assert.Equal(t, MsgSubTypeMeMessage, message.SubType) assert.Equal(t, "C2147483705", message.Channel) assert.Equal(t, "U2147483697", message.User) assert.Equal(t, "is doing that thing", message.Text) @@ -355,7 +355,7 @@ func TestMessageChangedMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "message_changed", message.SubType) + assert.Equal(t, MsgSubTypeMessageChanged, message.SubType) assert.True(t, message.Hidden) assert.Equal(t, "C2147483705", message.Channel) assert.NotNil(t, message.SubMessage) @@ -383,7 +383,7 @@ func TestMessageDeletedMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "message_deleted", message.SubType) + assert.Equal(t, MsgSubTypeMessageDeleted, message.SubType) assert.True(t, message.Hidden) assert.Equal(t, "C2147483705", message.Channel) assert.Equal(t, "1358878755.000001", message.Timestamp) @@ -403,7 +403,7 @@ func TestChannelJoinMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "channel_join", message.SubType) + assert.Equal(t, MsgSubTypeChannelJoin, message.SubType) assert.Equal(t, "1358877458.000011", message.Timestamp) assert.Equal(t, "U2147483828", message.User) assert.Equal(t, "<@U2147483828|cal> has joined the channel", message.Text) @@ -423,7 +423,7 @@ func TestChannelJoinInvitedMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "channel_join", message.SubType) + assert.Equal(t, MsgSubTypeChannelJoin, message.SubType) assert.Equal(t, "1358877458.000011", message.Timestamp) assert.Equal(t, "U2147483828", message.User) assert.Equal(t, "<@U2147483828|cal> has joined the channel", message.Text) @@ -443,7 +443,7 @@ func TestChannelLeaveMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "channel_leave", message.SubType) + assert.Equal(t, MsgSubTypeChannelLeave, message.SubType) assert.Equal(t, "1358877455.000010", message.Timestamp) assert.Equal(t, "U2147483828", message.User) assert.Equal(t, "<@U2147483828|cal> has left the channel", message.Text) @@ -463,7 +463,7 @@ func TestChannelTopicMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "channel_topic", message.SubType) + assert.Equal(t, MsgSubTypeChannelTopic, message.SubType) assert.Equal(t, "1358877455.000010", message.Timestamp) assert.Equal(t, "U2147483828", message.User) assert.Equal(t, "hello world", message.Topic) @@ -484,7 +484,7 @@ func TestChannelPurposeMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "channel_purpose", message.SubType) + assert.Equal(t, MsgSubTypeChannelPurpose, message.SubType) assert.Equal(t, "1358877455.000010", message.Timestamp) assert.Equal(t, "U2147483828", message.User) assert.Equal(t, "whatever", message.Purpose) @@ -506,7 +506,7 @@ func TestChannelNameMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "channel_name", message.SubType) + assert.Equal(t, MsgSubTypeChannelName, message.SubType) assert.Equal(t, "1358877455.000010", message.Timestamp) assert.Equal(t, "U2147483828", message.User) assert.Equal(t, "random", message.OldName) @@ -528,7 +528,7 @@ func TestChannelArchiveMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "channel_archive", message.SubType) + assert.Equal(t, MsgSubTypeChannelArchive, message.SubType) assert.Equal(t, "1361482916.000003", message.Timestamp) assert.Equal(t, " archived the channel", message.Text) assert.Equal(t, "U1234", message.User) @@ -549,7 +549,7 @@ func TestChannelUnarchiveMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "channel_unarchive", message.SubType) + assert.Equal(t, MsgSubTypeChannelUnarchive, message.SubType) assert.Equal(t, "1361482916.000003", message.Timestamp) assert.Equal(t, " un-archived the channel", message.Text) assert.Equal(t, "U1234", message.User) @@ -627,7 +627,7 @@ func TestGroupJoinMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "group_join", message.SubType) + assert.Equal(t, MsgSubTypeGroupJoin, message.SubType) assert.Equal(t, "1358877458.000011", message.Timestamp) assert.Equal(t, "U2147483828", message.User) assert.Equal(t, "<@U2147483828|cal> has joined the group", message.Text) @@ -647,7 +647,7 @@ func TestGroupJoinInvitedMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "group_join", message.SubType) + assert.Equal(t, MsgSubTypeGroupJoin, message.SubType) assert.Equal(t, "1358877458.000011", message.Timestamp) assert.Equal(t, "U2147483828", message.User) assert.Equal(t, "<@U2147483828|cal> has joined the group", message.Text) @@ -667,7 +667,7 @@ func TestGroupLeaveMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "group_leave", message.SubType) + assert.Equal(t, MsgSubTypeGroupLeave, message.SubType) assert.Equal(t, "1358877455.000010", message.Timestamp) assert.Equal(t, "U2147483828", message.User) assert.Equal(t, "<@U2147483828|cal> has left the group", message.Text) @@ -687,7 +687,7 @@ func TestGroupTopicMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "group_topic", message.SubType) + assert.Equal(t, MsgSubTypeGroupTopic, message.SubType) assert.Equal(t, "1358877455.000010", message.Timestamp) assert.Equal(t, "U2147483828", message.User) assert.Equal(t, "hello world", message.Topic) @@ -708,7 +708,7 @@ func TestGroupPurposeMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "group_purpose", message.SubType) + assert.Equal(t, MsgSubTypeGroupPurpose, message.SubType) assert.Equal(t, "1358877455.000010", message.Timestamp) assert.Equal(t, "U2147483828", message.User) assert.Equal(t, "whatever", message.Purpose) @@ -730,7 +730,7 @@ func TestGroupNameMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "group_name", message.SubType) + assert.Equal(t, MsgSubTypeGroupName, message.SubType) assert.Equal(t, "1358877455.000010", message.Timestamp) assert.Equal(t, "U2147483828", message.User) assert.Equal(t, "random", message.OldName) @@ -752,7 +752,7 @@ func TestGroupArchiveMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "group_archive", message.SubType) + assert.Equal(t, MsgSubTypeGroupArchive, message.SubType) assert.Equal(t, "1361482916.000003", message.Timestamp) assert.Equal(t, " archived the group", message.Text) assert.Equal(t, "U1234", message.User) @@ -773,7 +773,7 @@ func TestGroupUnarchiveMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "group_unarchive", message.SubType) + assert.Equal(t, MsgSubTypeGroupUnarchive, message.SubType) assert.Equal(t, "1361482916.000003", message.Timestamp) assert.Equal(t, " un-archived the group", message.Text) assert.Equal(t, "U1234", message.User) @@ -834,7 +834,7 @@ func TestFileShareMessage(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, message) assert.Equal(t, "message", message.Type) - assert.Equal(t, "file_share", message.SubType) + assert.Equal(t, MsgSubTypeFileShare, message.SubType) assert.Equal(t, "1358877455.000010", message.Timestamp) assert.Equal(t, "<@cal> uploaded a file: ", message.Text) assert.Equal(t, "U2147483697", message.User) diff --git a/misc.go b/misc.go index 7e60b8f4d..3b85a3bea 100644 --- a/misc.go +++ b/misc.go @@ -42,7 +42,7 @@ func (t SlackResponse) Err() error { return nil } - return errors.New(t.Error) + return SlackErrorResponse{Err: t.Error, ResponseMetadata: t.ResponseMetadata} } type warner interface { @@ -66,6 +66,14 @@ func (t SlackResponse) Warn() *Warning { } } +// SlackErrorResponse brings along the metadata of errors returned by the Slack API. +type SlackErrorResponse struct { + Err string + ResponseMetadata ResponseMetadata +} + +func (r SlackErrorResponse) Error() string { return r.Err } + // RateLimitedError represents the rate limit respond from slack type RateLimitedError struct { RetryAfter time.Duration @@ -80,29 +88,27 @@ func (e *RateLimitedError) Retryable() bool { } func fileUploadReq(ctx context.Context, path string, values url.Values, r io.Reader) (*http.Request, error) { - req, err := http.NewRequest("POST", path, r) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, path, r) if err != nil { return nil, err } - req = req.WithContext(ctx) - req.URL.RawQuery = (values).Encode() + req.URL.RawQuery = values.Encode() return req, nil } -func downloadFile(client httpClient, token string, downloadURL string, writer io.Writer, d Debug) error { +func downloadFile(ctx context.Context, client httpClient, token string, downloadURL string, writer io.Writer, d Debug) error { if downloadURL == "" { return fmt.Errorf("received empty download URL") } - req, err := http.NewRequest("GET", downloadURL, &bytes.Buffer{}) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, &bytes.Buffer{}) if err != nil { return err } var bearer = "Bearer " + token req.Header.Add("Authorization", bearer) - req.WithContext(context.Background()) resp, err := client.Do(req) if err != nil { @@ -121,8 +127,8 @@ func downloadFile(client httpClient, token string, downloadURL string, writer io return err } -func formReq(endpoint string, values url.Values) (req *http.Request, err error) { - if req, err = http.NewRequest("POST", endpoint, strings.NewReader(values.Encode())); err != nil { +func formReq(ctx context.Context, endpoint string, values url.Values) (req *http.Request, err error) { + if req, err = http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(values.Encode())); err != nil { return nil, err } @@ -130,13 +136,13 @@ func formReq(endpoint string, values url.Values) (req *http.Request, err error) return req, nil } -func jsonReq(endpoint string, body interface{}) (req *http.Request, err error) { +func jsonReq(ctx context.Context, endpoint string, body interface{}) (req *http.Request, err error) { buffer := bytes.NewBuffer([]byte{}) if err = json.NewEncoder(buffer).Encode(body); err != nil { return nil, err } - if req, err = http.NewRequest("POST", endpoint, buffer); err != nil { + if req, err = http.NewRequestWithContext(ctx, http.MethodPost, endpoint, buffer); err != nil { return nil, err } @@ -157,7 +163,7 @@ func parseResponseBody(body io.ReadCloser, intf interface{}, d Debug) error { return json.Unmarshal(response, intf) } -func postLocalWithMultipartResponse(ctx context.Context, client httpClient, method, fpath, fieldname string, values url.Values, intf interface{}, d Debug) error { +func postLocalWithMultipartResponse(ctx context.Context, client httpClient, method, fpath, fieldname, token string, values url.Values, intf interface{}, d Debug) error { fullpath, err := filepath.Abs(fpath) if err != nil { return err @@ -168,10 +174,10 @@ func postLocalWithMultipartResponse(ctx context.Context, client httpClient, meth } defer file.Close() - return postWithMultipartResponse(ctx, client, method, filepath.Base(fpath), fieldname, values, file, intf, d) + return postWithMultipartResponse(ctx, client, method, filepath.Base(fpath), fieldname, token, values, file, intf, d) } -func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, d Debug) error { +func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname, token string, values url.Values, r io.Reader, intf interface{}, d Debug) error { pipeReader, pipeWriter := io.Pipe() wr := multipart.NewWriter(pipeWriter) errc := make(chan error) @@ -197,7 +203,7 @@ func postWithMultipartResponse(ctx context.Context, client httpClient, path, nam return err } req.Header.Add("Content-Type", wr.FormDataContentType()) - req = req.WithContext(ctx) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) resp, err := client.Do(req) if err != nil { @@ -219,7 +225,6 @@ func postWithMultipartResponse(ctx context.Context, client httpClient, path, nam } func doPost(ctx context.Context, client httpClient, req *http.Request, parser responseParser, d Debug) error { - req = req.WithContext(ctx) resp, err := client.Do(req) if err != nil { return err @@ -237,7 +242,7 @@ func doPost(ctx context.Context, client httpClient, req *http.Request, parser re // post JSON. func postJSON(ctx context.Context, client httpClient, endpoint, token string, json []byte, intf interface{}, d Debug) error { reqBody := bytes.NewBuffer(json) - req, err := http.NewRequest("POST", endpoint, reqBody) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, reqBody) if err != nil { return err } @@ -250,7 +255,7 @@ func postJSON(ctx context.Context, client httpClient, endpoint, token string, js // post a url encoded form. func postForm(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d Debug) error { reqBody := strings.NewReader(values.Encode()) - req, err := http.NewRequest("POST", endpoint, reqBody) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, reqBody) if err != nil { return err } @@ -258,12 +263,14 @@ func postForm(ctx context.Context, client httpClient, endpoint string, values ur return doPost(ctx, client, req, newJSONParser(intf), d) } -func getResource(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d Debug) error { - req, err := http.NewRequest("GET", endpoint, nil) +func getResource(ctx context.Context, client httpClient, endpoint, token string, values url.Values, intf interface{}, d Debug) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + req.URL.RawQuery = values.Encode() return doPost(ctx, client, req, newJSONParser(intf), d) diff --git a/oauth.go b/oauth.go index 707ccc6b5..94b6546d5 100644 --- a/oauth.go +++ b/oauth.go @@ -42,6 +42,8 @@ type OAuthV2Response struct { IncomingWebhook OAuthResponseIncomingWebhook `json:"incoming_webhook"` Enterprise OAuthV2ResponseEnterprise `json:"enterprise"` AuthedUser OAuthV2ResponseAuthedUser `json:"authed_user"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` SlackResponse } @@ -59,10 +61,12 @@ type OAuthV2ResponseEnterprise struct { // OAuthV2ResponseAuthedUser ... type OAuthV2ResponseAuthedUser struct { - ID string `json:"id"` - Scope string `json:"scope"` - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` + ID string `json:"id"` + Scope string `json:"scope"` + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` } // GetOAuthToken retrieves an AccessToken @@ -132,3 +136,23 @@ func GetOAuthV2ResponseContext(ctx context.Context, client httpClient, clientID, } return response, response.Err() } + +// RefreshOAuthV2AccessContext with a context, gets a V2 OAuth access token response +func RefreshOAuthV2Token(client httpClient, clientID, clientSecret, refreshToken string) (resp *OAuthV2Response, err error) { + return RefreshOAuthV2TokenContext(context.Background(), client, clientID, clientSecret, refreshToken) +} + +// RefreshOAuthV2AccessContext with a context, gets a V2 OAuth access token response +func RefreshOAuthV2TokenContext(ctx context.Context, client httpClient, clientID, clientSecret, refreshToken string) (resp *OAuthV2Response, err error) { + values := url.Values{ + "client_id": {clientID}, + "client_secret": {clientSecret}, + "refresh_token": {refreshToken}, + "grant_type": {"refresh_token"}, + } + response := &OAuthV2Response{} + if err = postForm(ctx, client, APIURL+"oauth.v2.access", values, response, discard{}); err != nil { + return nil, err + } + return response, response.Err() +} diff --git a/reminders.go b/reminders.go index ae1da8669..53d67c03c 100644 --- a/reminders.go +++ b/reminders.go @@ -52,10 +52,17 @@ func (api *Client) doReminders(ctx context.Context, path string, values url.Valu // // See https://api.slack.com/methods/reminders.list func (api *Client) ListReminders() ([]*Reminder, error) { + return api.ListRemindersContext(context.Background()) +} + +// ListRemindersContext lists all the reminders created by or for the authenticated user with a custom context +// +// For more details, see ListReminders documentation. +func (api *Client) ListRemindersContext(ctx context.Context) ([]*Reminder, error) { values := url.Values{ "token": {api.token}, } - return api.doReminders(context.Background(), "reminders.list", values) + return api.doReminders(ctx, "reminders.list", values) } // AddChannelReminder adds a reminder for a channel. @@ -64,13 +71,20 @@ func (api *Client) ListReminders() ([]*Reminder, error) { // reminders on a channel is currently undocumented but has been tested to // work) func (api *Client) AddChannelReminder(channelID, text, time string) (*Reminder, error) { + return api.AddChannelReminderContext(context.Background(), channelID, text, time) +} + +// AddChannelReminderContext adds a reminder for a channel with a custom context +// +// For more details, see AddChannelReminder documentation. +func (api *Client) AddChannelReminderContext(ctx context.Context, channelID, text, time string) (*Reminder, error) { values := url.Values{ "token": {api.token}, "text": {text}, "time": {time}, "channel": {channelID}, } - return api.doReminder(context.Background(), "reminders.add", values) + return api.doReminder(ctx, "reminders.add", values) } // AddUserReminder adds a reminder for a user. @@ -79,25 +93,39 @@ func (api *Client) AddChannelReminder(channelID, text, time string) (*Reminder, // reminders on a channel is currently undocumented but has been tested to // work) func (api *Client) AddUserReminder(userID, text, time string) (*Reminder, error) { + return api.AddUserReminderContext(context.Background(), userID, text, time) +} + +// AddUserReminderContext adds a reminder for a user with a custom context +// +// For more details, see AddUserReminder documentation. +func (api *Client) AddUserReminderContext(ctx context.Context, userID, text, time string) (*Reminder, error) { values := url.Values{ "token": {api.token}, "text": {text}, "time": {time}, "user": {userID}, } - return api.doReminder(context.Background(), "reminders.add", values) + return api.doReminder(ctx, "reminders.add", values) } // DeleteReminder deletes an existing reminder. // // See https://api.slack.com/methods/reminders.delete func (api *Client) DeleteReminder(id string) error { + return api.DeleteReminderContext(context.Background(), id) +} + +// DeleteReminderContext deletes an existing reminder with a custom context +// +// For more details, see DeleteReminder documentation. +func (api *Client) DeleteReminderContext(ctx context.Context, id string) error { values := url.Values{ "token": {api.token}, "reminder": {id}, } response := &SlackResponse{} - if err := api.postMethod(context.Background(), "reminders.delete", values, response); err != nil { + if err := api.postMethod(ctx, "reminders.delete", values, response); err != nil { return err } return response.Err() diff --git a/slack.go b/slack.go index 319d84d68..87a4876ca 100644 --- a/slack.go +++ b/slack.go @@ -171,8 +171,8 @@ func (api *Client) postMethod(ctx context.Context, path string, values url.Value } // get a slack web method. -func (api *Client) getMethod(ctx context.Context, path string, values url.Values, intf interface{}) error { - err := getResource(ctx, api.httpclient, api.endpoint+path, values, intf, api) +func (api *Client) getMethod(ctx context.Context, path string, token string, values url.Values, intf interface{}) error { + err := getResource(ctx, api.httpclient, api.endpoint+path, token, values, intf, api) if w, ok := intf.(warner); ok { if warning := w.Warn(); warning != nil && api.onWarning != nil { api.onWarning(warning, path, values) diff --git a/slackevents/inner_events.go b/slackevents/inner_events.go index 96d52ce1f..de98c280e 100644 --- a/slackevents/inner_events.go +++ b/slackevents/inner_events.go @@ -47,12 +47,104 @@ type AppUninstalledEvent struct { Type string `json:"type"` } -// ChannelIDChangedEvent A channel ID has changed after being shared externally. +// ChannelCreatedEvent represents the Channel created event +type ChannelCreatedEvent struct { + Type string `json:"type"` + Channel ChannelCreatedInfo `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelDeletedEvent represents the Channel deleted event +type ChannelDeletedEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` +} + +// ChannelArchiveEvent represents the Channel archive event +type ChannelArchiveEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + User string `json:"user"` +} + +// ChannelUnarchiveEvent represents the Channel unarchive event +type ChannelUnarchiveEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + User string `json:"user"` +} + +// ChannelLeftEvent represents the Channel left event +type ChannelLeftEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` +} + +// ChannelRenameEvent represents the Channel rename event +type ChannelRenameEvent struct { + Type string `json:"type"` + Channel ChannelRenameInfo `json:"channel"` +} + +// ChannelIDChangedEvent represents the Channel identifier changed event type ChannelIDChangedEvent struct { - Type string `json:"type"` - OldChannelID string `json:"old_channel_id"` - NewChannelID string `json:"new_channel_id"` - EventTimeStamp json.Number `json:"event_ts"` + Type string `json:"type"` + OldChannelID string `json:"old_channel_id"` + NewChannelID string `json:"new_channel_id"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelCreatedInfo represents the information associated with the Channel created event +type ChannelCreatedInfo struct { + ID string `json:"id"` + IsChannel bool `json:"is_channel"` + Name string `json:"name"` + Created int `json:"created"` + Creator string `json:"creator"` +} + +// ChannelRenameInfo represents the information associated with the Channel rename event +type ChannelRenameInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Created int `json:"created"` +} + +// GroupDeletedEvent represents the Group deleted event +type GroupDeletedEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` +} + +// GroupArchiveEvent represents the Group archive event +type GroupArchiveEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` +} + +// GroupUnarchiveEvent represents the Group unarchive event +type GroupUnarchiveEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` +} + +// GroupLeftEvent represents the Group left event +type GroupLeftEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` +} + +// GroupRenameEvent represents the Group rename event +type GroupRenameEvent struct { + Type string `json:"type"` + Channel GroupRenameInfo `json:"channel"` +} + +// GroupRenameInfo represents the information associated with the Group rename event +type GroupRenameInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Created int `json:"created"` } // GridMigrationFinishedEvent An enterprise grid migration has finished on this workspace. @@ -69,11 +161,14 @@ type GridMigrationStartedEvent struct { // LinkSharedEvent A message was posted containing one or more links relevant to your application type LinkSharedEvent struct { - Type string `json:"type"` - User string `json:"user"` - TimeStamp string `json:"ts"` - Channel string `json:"channel"` - MessageTimeStamp json.Number `json:"message_ts"` + Type string `json:"type"` + User string `json:"user"` + TimeStamp string `json:"ts"` + Channel string `json:"channel"` + // MessageTimeStamp can be both a numeric timestamp if the LinkSharedEvent corresponds to a sent + // message and (contrary to the field name) a uuid if the LinkSharedEvent is generated in the + // compose text area. + MessageTimeStamp string `json:"message_ts"` ThreadTimeStamp string `json:"thread_ts"` Links []sharedLinks `json:"links"` } @@ -184,6 +279,12 @@ type tokens struct { Bot []string `json:"bot"` } +// TeamJoinEvent A new member joined a workspace - https://api.slack.com/events/team_join +type TeamJoinEvent struct { + Type string `json:"type"` + User *slack.User `json:"user"` +} + // TokensRevokedEvent APP's API tokes are revoked - https://api.slack.com/events/tokens_revoked type TokensRevokedEvent struct { Type string `json:"type"` @@ -210,6 +311,23 @@ type EmojiChangedEvent struct { Value string `json:"value,omitempty"` } +// WorkflowStepExecuteEvent is fired, if a workflow step of your app is invoked +type WorkflowStepExecuteEvent struct { + Type string `json:"type"` + CallbackID string `json:"callback_id"` + WorkflowStep EventWorkflowStep `json:"workflow_step"` + EventTS string `json:"event_ts"` +} + +type EventWorkflowStep struct { + WorkflowStepExecuteID string `json:"workflow_step_execute_id"` + WorkflowID string `json:"workflow_id"` + WorkflowInstanceID string `json:"workflow_instance_id"` + StepID string `json:"step_id"` + Inputs *slack.WorkflowStepInputs `json:"inputs,omitempty"` + Outputs *[]slack.WorkflowStepOutput `json:"outputs,omitempty"` +} + // JSONTime exists so that we can have a String method converting the date type JSONTime int64 @@ -318,8 +436,30 @@ const ( AppHomeOpened = "app_home_opened" // AppUninstalled Your Slack app was uninstalled. AppUninstalled = "app_uninstalled" - // ChannelIDChangedEvent A channel ID has changed after being shared externally. + // ChannelCreated is sent when a new channel is created. + ChannelCreated = "channel_created" + // ChannelDeleted is sent when a channel is deleted. + ChannelDeleted = "channel_deleted" + // ChannelArchive is sent when a channel is archived. + ChannelArchive = "channel_archive" + // ChannelUnarchive is sent when a channel is unarchived. + ChannelUnarchive = "channel_unarchive" + // ChannelLeft is sent when a channel is left. + ChannelLeft = "channel_left" + // ChannelRename is sent when a channel is rename. + ChannelRename = "channel_rename" + // ChannelIDChanged is sent when a channel identifier is changed. ChannelIDChanged = "channel_id_changed" + // GroupDeleted is sent when a group is deleted. + GroupDeleted = "group_deleted" + // GroupArchive is sent when a group is archived. + GroupArchive = "group_archive" + // GroupUnarchive is sent when a group is unarchived. + GroupUnarchive = "group_unarchive" + // GroupLeft is sent when a group is left. + GroupLeft = "group_left" + // GroupRename is sent when a group is renamed. + GroupRename = "group_rename" // GridMigrationFinished An enterprise grid migration has finished on this workspace. GridMigrationFinished = "grid_migration_finished" // GridMigrationStarted An enterprise grid migration has started on this workspace. @@ -340,10 +480,14 @@ const ( ReactionAdded = "reaction_added" // ReactionRemoved An reaction was removed from a message ReactionRemoved = "reaction_removed" + // TeamJoin A new user joined the workspace + TeamJoin = "team_join" // TokensRevoked APP's API tokes are revoked TokensRevoked = "tokens_revoked" // EmojiChanged A custom emoji has been added or changed EmojiChanged = "emoji_changed" + // WorkflowStepExecute Happens, if a workflow step of your app is invoked + WorkflowStepExecute = "workflow_step_execute" ) // EventsAPIInnerEventMapping maps INNER Event API events to their corresponding struct @@ -353,7 +497,18 @@ var EventsAPIInnerEventMapping = map[string]interface{}{ AppMention: AppMentionEvent{}, AppHomeOpened: AppHomeOpenedEvent{}, AppUninstalled: AppUninstalledEvent{}, + ChannelCreated: ChannelCreatedEvent{}, + ChannelDeleted: ChannelDeletedEvent{}, + ChannelArchive: ChannelArchiveEvent{}, + ChannelUnarchive: ChannelUnarchiveEvent{}, + ChannelLeft: ChannelLeftEvent{}, + ChannelRename: ChannelRenameEvent{}, ChannelIDChanged: ChannelIDChangedEvent{}, + GroupDeleted: GroupDeletedEvent{}, + GroupArchive: GroupArchiveEvent{}, + GroupUnarchive: GroupUnarchiveEvent{}, + GroupLeft: GroupLeftEvent{}, + GroupRename: GroupRenameEvent{}, GridMigrationFinished: GridMigrationFinishedEvent{}, GridMigrationStarted: GridMigrationStartedEvent{}, LinkShared: LinkSharedEvent{}, @@ -364,6 +519,8 @@ var EventsAPIInnerEventMapping = map[string]interface{}{ PinRemoved: PinRemovedEvent{}, ReactionAdded: ReactionAddedEvent{}, ReactionRemoved: ReactionRemovedEvent{}, + TeamJoin: TeamJoinEvent{}, TokensRevoked: TokensRevokedEvent{}, EmojiChanged: EmojiChangedEvent{}, + WorkflowStepExecute: WorkflowStepExecuteEvent{}, } diff --git a/slackevents/inner_events_test.go b/slackevents/inner_events_test.go index 5a432f97e..92b83bdc9 100644 --- a/slackevents/inner_events_test.go +++ b/slackevents/inner_events_test.go @@ -118,6 +118,38 @@ func TestLinkSharedEvent(t *testing.T) { } } +func TestLinkSharedComposerEvent(t *testing.T) { + rawE := []byte(` + { + "type": "link_shared", + "channel": "COMPOSER", + "is_bot_user_member": true, + "user": "Uxxxxxxx", + "message_ts": "Uxxxxxxx-909b5454-75f8-4ac4-b325-1b40e230bbd8-gryl3kb80b3wm49ihzoo35fyqoq08n2y", + "unfurl_id": "Uxxxxxxx-909b5454-75f8-4ac4-b325-1b40e230bbd8-gryl3kb80b3wm49ihzoo35fyqoq08n2y", + "source": "composer", + "links": [ + { + "domain": "example.com", + "url": "https://example.com/12345" + }, + { + "domain": "example.com", + "url": "https://example.com/67890" + }, + { + "domain": "another-example.com", + "url": "https://yet.another-example.com/v/abcde" + } + ] + } + `) + err := json.Unmarshal(rawE, &LinkSharedEvent{}) + if err != nil { + t.Error(err) + } +} + func TestMessageEvent(t *testing.T) { rawE := []byte(` { @@ -415,3 +447,64 @@ func TestEmojiChanged(t *testing.T) { t.Fail() } } + +func TestWorkflowStepExecute(t *testing.T) { + // see: https://api.slack.com/events/workflow_step_execute + rawE := []byte(` + { + "type":"workflow_step_execute", + "callback_id":"open_ticket", + "workflow_step":{ + "workflow_step_execute_id":"1036669284371.19077474947.c94bcf942e047298d21f89faf24f1326", + "workflow_id":"123456789012345678", + "workflow_instance_id":"987654321098765432", + "step_id":"12a345bc-1a23-4567-8b90-1234a567b8c9", + "inputs":{ + "example-select-input":{ + "value": "value-two", + "skip_variable_replacement": false + } + }, + "outputs":[ + ] + }, + "event_ts":"1643290847.766536" + } + `) + + wse := WorkflowStepExecuteEvent{} + err := json.Unmarshal(rawE, &wse) + if err != nil { + t.Error(err) + } + + if wse.Type != "workflow_step_execute" { + t.Fail() + } + if wse.CallbackID != "open_ticket" { + t.Fail() + } + if wse.WorkflowStep.WorkflowStepExecuteID != "1036669284371.19077474947.c94bcf942e047298d21f89faf24f1326" { + t.Fail() + } + if wse.WorkflowStep.WorkflowID != "123456789012345678" { + t.Fail() + } + if wse.WorkflowStep.WorkflowInstanceID != "987654321098765432" { + t.Fail() + } + if wse.WorkflowStep.StepID != "12a345bc-1a23-4567-8b90-1234a567b8c9" { + t.Fail() + } + if len(*wse.WorkflowStep.Inputs) == 0 { + t.Fail() + } + if inputElement, ok := (*wse.WorkflowStep.Inputs)["example-select-input"]; ok { + if inputElement.Value != "value-two" { + t.Fail() + } + if inputElement.SkipVariableReplacement != false { + t.Fail() + } + } +} diff --git a/slackevents/outer_events.go b/slackevents/outer_events.go index f6df6c8aa..8d682cc32 100644 --- a/slackevents/outer_events.go +++ b/slackevents/outer_events.go @@ -8,12 +8,13 @@ import ( // EventsAPIEvent is the base EventsAPIEvent type EventsAPIEvent struct { - Token string `json:"token"` - TeamID string `json:"team_id"` - Type string `json:"type"` - APIAppID string `json:"api_app_id"` - Data interface{} - InnerEvent EventsAPIInnerEvent + Token string `json:"token"` + TeamID string `json:"team_id"` + Type string `json:"type"` + APIAppID string `json:"api_app_id"` + EnterpriseID string `json:"enterprise_id"` + Data interface{} + InnerEvent EventsAPIInnerEvent } // EventsAPIURLVerificationEvent received when configuring a EventsAPI driven app diff --git a/slackevents/parsers.go b/slackevents/parsers.go index 07f296aeb..0cc584c76 100644 --- a/slackevents/parsers.go +++ b/slackevents/parsers.go @@ -39,6 +39,7 @@ func parseOuterEvent(rawE json.RawMessage) (EventsAPIEvent, error) { "", "unmarshalling_error", "", + "", &slack.UnmarshallingErrorEvent{ErrorObj: err}, EventsAPIInnerEvent{}, }, err @@ -52,6 +53,7 @@ func parseOuterEvent(rawE json.RawMessage) (EventsAPIEvent, error) { "", "unmarshalling_error", "", + "", &slack.UnmarshallingErrorEvent{ErrorObj: err}, EventsAPIInnerEvent{}, }, err @@ -61,6 +63,7 @@ func parseOuterEvent(rawE json.RawMessage) (EventsAPIEvent, error) { e.TeamID, e.Type, e.APIAppID, + e.EnterpriseID, cbEvent, EventsAPIInnerEvent{}, }, nil @@ -73,6 +76,7 @@ func parseOuterEvent(rawE json.RawMessage) (EventsAPIEvent, error) { "", "unmarshalling_error", "", + "", &slack.UnmarshallingErrorEvent{ErrorObj: err}, EventsAPIInnerEvent{}, }, err @@ -82,6 +86,7 @@ func parseOuterEvent(rawE json.RawMessage) (EventsAPIEvent, error) { e.TeamID, e.Type, e.APIAppID, + e.EnterpriseID, urlVE, EventsAPIInnerEvent{}, }, nil @@ -97,6 +102,7 @@ func parseInnerEvent(e *EventsAPICallbackEvent) (EventsAPIEvent, error) { e.TeamID, "unmarshalling_error", e.APIAppID, + "", &slack.UnmarshallingErrorEvent{ErrorObj: err}, EventsAPIInnerEvent{}, }, err @@ -108,6 +114,7 @@ func parseInnerEvent(e *EventsAPICallbackEvent) (EventsAPIEvent, error) { e.TeamID, iE.Type, e.APIAppID, + "", nil, EventsAPIInnerEvent{}, }, fmt.Errorf("Inner Event does not exist! %s", iE.Type) @@ -121,6 +128,7 @@ func parseInnerEvent(e *EventsAPICallbackEvent) (EventsAPIEvent, error) { e.TeamID, "unmarshalling_error", e.APIAppID, + "", &slack.UnmarshallingErrorEvent{ErrorObj: err}, EventsAPIInnerEvent{}, }, err @@ -130,6 +138,7 @@ func parseInnerEvent(e *EventsAPICallbackEvent) (EventsAPIEvent, error) { e.TeamID, e.Type, e.APIAppID, + "", e, EventsAPIInnerEvent{iE.Type, recvEvent}, }, nil @@ -196,6 +205,7 @@ func ParseEvent(rawEvent json.RawMessage, opts ...Option) (EventsAPIEvent, error "", "unmarshalling_error", "", + "", &slack.UnmarshallingErrorEvent{ErrorObj: err}, EventsAPIInnerEvent{}, }, err @@ -210,6 +220,7 @@ func ParseEvent(rawEvent json.RawMessage, opts ...Option) (EventsAPIEvent, error "", "unmarshalling_error", "", + "", &slack.UnmarshallingErrorEvent{ErrorObj: err}, EventsAPIInnerEvent{}, }, err @@ -219,6 +230,7 @@ func ParseEvent(rawEvent json.RawMessage, opts ...Option) (EventsAPIEvent, error e.TeamID, e.Type, e.APIAppID, + e.EnterpriseID, urlVerificationEvent, EventsAPIInnerEvent{}, }, nil diff --git a/slacktest/README.md b/slacktest/README.md index c35d7f610..3897c062a 100644 --- a/slacktest/README.md +++ b/slacktest/README.md @@ -1,5 +1,14 @@ # slacktest -code in this package was shamelessly copied from https://github.com/lusis/slack-test. -since we were unable to get a response from the maintainer and its lack of license -have left us in an odd state. but wanted to move it here for maintenance purposes. +This package was copied from https://github.com/lusis/slack-test for historical reasons. +This package's license is the following. + +--- + +Copyright 2018 @lusis + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/slacktest/funcs.go b/slacktest/funcs.go index a5b6470eb..d083af2b5 100644 --- a/slacktest/funcs.go +++ b/slacktest/funcs.go @@ -7,6 +7,7 @@ import ( "time" websocket "github.com/gorilla/websocket" + slack "github.com/slack-go/slack" ) diff --git a/slacktest/handlers.go b/slacktest/handlers.go index 85eec5804..8dfe451df 100644 --- a/slacktest/handlers.go +++ b/slacktest/handlers.go @@ -11,6 +11,7 @@ import ( "time" websocket "github.com/gorilla/websocket" + slack "github.com/slack-go/slack" ) @@ -82,11 +83,6 @@ func (sts *Server) conversationsInfoHandler(w http.ResponseWriter, r *http.Reque _, _ = w.Write(encoded) } -// handle channels.list and conversations.list -func listChannelsHandler(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte(defaultChannelsListJSON)) -} - // handle conversations.create func createConversationHandler(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(defaultConversationJSON)) @@ -112,11 +108,6 @@ func inviteConversationHandler(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(inviteConversationJSON)) } -// handle groups.list -func listGroupsHandler(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte(defaultGroupsListJSON)) -} - // handle chat.postMessage func (sts *Server) postMessageHandler(w http.ResponseWriter, r *http.Request) { serverAddr := r.Context().Value(ServerBotHubNameContextKey).(string) @@ -136,12 +127,24 @@ func (sts *Server) postMessageHandler(w http.ResponseWriter, r *http.Request) { } ts := time.Now().Unix() - resp := fmt.Sprintf(`{"channel":"%s","ts":"%d", "text":"%s", "ok": true}`, values.Get("channel"), ts, values.Get("text")) + resp := &struct { + Ok bool `json:"ok"` + Channel string `json:"channel"` + Ts string `json:"ts"` + Text string `json:"text"` + }{ + Ok: true, + Channel: values.Get("channel"), + Ts: fmt.Sprintf("%d", ts), + Text: values.Get("text"), + } + m := slack.Message{} m.Type = "message" m.Channel = values.Get("channel") m.Timestamp = fmt.Sprintf("%d", ts) m.Text = values.Get("text") + m.ThreadTimestamp = values.Get("thread_ts") if values.Get("as_user") != "true" { m.User = defaultNonBotUserID m.Username = defaultNonBotUserName @@ -195,7 +198,7 @@ func (sts *Server) postMessageHandler(w http.ResponseWriter, r *http.Request) { return } go sts.queueForWebsocket(string(jsonMessage), serverAddr) - _, _ = w.Write([]byte(resp)) + _ = json.NewEncoder(w).Encode(resp) } // RTMConnectHandler generates a valid connection diff --git a/slacktest/handlers_test.go b/slacktest/handlers_test.go index b3ef5fefd..6678e7684 100644 --- a/slacktest/handlers_test.go +++ b/slacktest/handlers_test.go @@ -3,8 +3,9 @@ package slacktest import ( "testing" - slack "github.com/slack-go/slack" "github.com/stretchr/testify/assert" + + slack "github.com/slack-go/slack" ) func TestAuthTestHandler(t *testing.T) { @@ -31,21 +32,6 @@ func TestPostMessageHandler(t *testing.T) { assert.NotEmpty(t, tstamp, "timestamp should not be empty") } -func TestServerListChannels(t *testing.T) { - s := NewTestServer() - go s.Start() - - client := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL())) - channels, err := client.GetChannels(true) - assert.NoError(t, err) - assert.Len(t, channels, 2) - assert.Equal(t, "C024BE91L", channels[0].ID) - assert.Equal(t, "C024BE92L", channels[1].ID) - for _, channel := range channels { - assert.Equal(t, "W012A3CDE", channel.Creator) - } -} - func TestServerCreateConversationHandler(t *testing.T) { s := NewTestServer() go s.Start() @@ -137,41 +123,3 @@ func TestBotInfoHandler(t *testing.T) { assert.Equal(t, s.BotName, bot.Name) assert.False(t, bot.Deleted) } - -func TestListGroupsHandler(t *testing.T) { - s := NewTestServer() - go s.Start() - - client := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL())) - groups, err := client.GetGroups(true) - assert.NoError(t, err) - if !assert.Len(t, groups, 1, "should have one group") { - t.FailNow() - } - mygroup := groups[0] - assert.Equal(t, "G024BE91L", mygroup.ID, "id should match") - assert.Equal(t, "secretplans", mygroup.Name, "name should match") - assert.True(t, mygroup.IsGroup, "should be a group") -} - -func TestListChannelsHandler(t *testing.T) { - s := NewTestServer() - go s.Start() - - client := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL())) - channels, err := client.GetChannels(true) - assert.NoError(t, err) - if !assert.Len(t, channels, 2, "should have two channels") { - t.FailNow() - } - generalChan := channels[0] - otherChan := channels[1] - assert.Equal(t, "C024BE91L", generalChan.ID, "id should match") - assert.Equal(t, "general", generalChan.Name, "name should match") - assert.Equal(t, "Fun times", generalChan.Topic.Value) - assert.True(t, generalChan.IsMember, "should be in channel") - assert.Equal(t, "C024BE92L", otherChan.ID, "id should match") - assert.Equal(t, "bot-playground", otherChan.Name, "name should match") - assert.Equal(t, "Fun times", otherChan.Topic.Value) - assert.True(t, otherChan.IsMember, "should be in channel") -} diff --git a/slacktest/rtm_test.go b/slacktest/rtm_test.go index 00a59f5fc..4beab76c3 100644 --- a/slacktest/rtm_test.go +++ b/slacktest/rtm_test.go @@ -4,8 +4,9 @@ import ( "testing" "time" - "github.com/slack-go/slack" "github.com/stretchr/testify/assert" + + "github.com/slack-go/slack" ) func TestRTMInfo(t *testing.T) { diff --git a/slacktest/server.go b/slacktest/server.go index aae24995d..aa6c0a180 100644 --- a/slacktest/server.go +++ b/slacktest/server.go @@ -50,14 +50,11 @@ func NewTestServer(custom ...binder) *Server { s.Handle("/rtm.start", rtmStartHandler) s.Handle("/rtm.connect", RTMConnectHandler) s.Handle("/chat.postMessage", s.postMessageHandler) - s.Handle("/channels.list", listChannelsHandler) - s.Handle("/conversations.list", listChannelsHandler) s.Handle("/conversations.create", createConversationHandler) s.Handle("/conversations.setTopic", setConversationTopicHandler) s.Handle("/conversations.setPurpose", setConversationPurposeHandler) s.Handle("/conversations.rename", renameConversationHandler) s.Handle("/conversations.invite", inviteConversationHandler) - s.Handle("/groups.list", listGroupsHandler) s.Handle("/users.info", usersInfoHandler) s.Handle("/users.lookupByEmail", usersInfoHandler) s.Handle("/bots.info", botsInfoHandler) diff --git a/slacktest/server_test.go b/slacktest/server_test.go index 5902282c1..ac4244471 100644 --- a/slacktest/server_test.go +++ b/slacktest/server_test.go @@ -6,8 +6,9 @@ import ( "testing" "time" - "github.com/slack-go/slack" "github.com/stretchr/testify/assert" + + "github.com/slack-go/slack" ) func TestDefaultNewServer(t *testing.T) { diff --git a/slackutilsx/slackutilsx.go b/slackutilsx/slackutilsx.go index 1f7b2b8c2..d6c9c07ef 100644 --- a/slackutilsx/slackutilsx.go +++ b/slackutilsx/slackutilsx.go @@ -50,10 +50,12 @@ func DetectChannelType(channelID string) ChannelType { } } +// initialize replacer only once (if needed) +var escapeReplacer = strings.NewReplacer("&", "&", "<", "<", ">", ">") + // EscapeMessage text func EscapeMessage(message string) string { - replacer := strings.NewReplacer("&", "&", "<", "<", ">", ">") - return replacer.Replace(message) + return escapeReplacer.Replace(message) } // Retryable errors return true. diff --git a/slackutilsx/slackutilsx_test.go b/slackutilsx/slackutilsx_test.go index dcebb4adb..72b2ea140 100644 --- a/slackutilsx/slackutilsx_test.go +++ b/slackutilsx/slackutilsx_test.go @@ -28,3 +28,9 @@ func TestEscapeMessage(t *testing.T) { test("A < B", "A < B") test("A > B", "A > B") } + +func BenchmarkEscapeMessage(b *testing.B) { + for i := 0; i < b.N; i++ { + EscapeMessage("A & B") + } +} diff --git a/socketmode/client.go b/socketmode/client.go index cce8a703b..8fd0b9871 100644 --- a/socketmode/client.go +++ b/socketmode/client.go @@ -28,7 +28,7 @@ type ConnectionInfo struct { } type SocketModeMessagePayload struct { - Event json.RawMessage `json:"´event"` + Event json.RawMessage `json:"event"` } // Client is a Socket Mode client that allows programs to use [Events API](https://api.slack.com/events-api) @@ -44,7 +44,7 @@ type SocketModeMessagePayload struct { // Client's New() and call Run() to start it. Please see examples/socketmode for the usage. type Client struct { // Client is the main API, embedded - apiClient slack.Client + slack.Client // maxPingInterval is the maximum duration elapsed after the last WebSocket PING sent from Slack // until Client considers the WebSocket connection is dead and needs to be reopened. diff --git a/socketmode/socket_mode_managed_conn.go b/socketmode/socket_mode_managed_conn.go index 4cd649c1c..7caad5b4f 100644 --- a/socketmode/socket_mode_managed_conn.go +++ b/socketmode/socket_mode_managed_conn.go @@ -11,13 +11,13 @@ import ( "sync" "time" + "github.com/gorilla/websocket" + "github.com/slack-go/slack" "github.com/slack-go/slack/internal/backoff" "github.com/slack-go/slack/internal/misc" - "github.com/slack-go/slack/slackevents" - - "github.com/gorilla/websocket" "github.com/slack-go/slack/internal/timex" + "github.com/slack-go/slack/slackevents" ) // Run is a blocking function that connects the Slack Socket Mode API and handles all incoming @@ -31,8 +31,20 @@ import ( // If you want to retry even on reconnection failure, you'd need to write your own wrapper for this function // to do so. func (smc *Client) Run() error { - ctx := context.TODO() + return smc.RunContext(context.TODO()) +} +// RunContext is a blocking function that connects the Slack Socket Mode API and handles all incoming +// requests and outgoing responses. +// +// The consumer of the Client and this function should read the Client.Events channel to receive +// `socketmode.Event`s that includes the client-specific events that may or may not wrap Socket Mode requests. +// +// Note that this function automatically reconnect on requested by Slack through a `disconnect` message. +// This function exists with an error only when a reconnection is failued due to some reason. +// If you want to retry even on reconnection failure, you'd need to write your own wrapper for this function +// to do so. +func (smc *Client) RunContext(ctx context.Context) error { for connectionCount := 0; ; connectionCount++ { if err := smc.run(ctx, connectionCount); err != nil { return err @@ -44,6 +56,7 @@ func (smc *Client) Run() error { func (smc *Client) run(ctx context.Context, connectionCount int) error { messages := make(chan json.RawMessage) + defer close(messages) deadmanTimer := newDeadmanTimer(smc.maxPingInterval) @@ -131,7 +144,10 @@ func (smc *Client) run(ctx context.Context, connectionCount int) error { select { case <-ctx.Done(): - // Detect when the connection is dead. + // Detect when the connection is dead and try close connection. + if err = conn.Close(); err != nil { + smc.Debugf("Failed to close connection: %v", err) + } case <-deadmanTimer.Elapsed(): firstErrOnce.Do(func() { firstErr = errors.New("ping timeout: Slack did not send us WebSocket PING for more than Client.maxInterval") @@ -143,14 +159,14 @@ func (smc *Client) run(ctx context.Context, connectionCount int) error { wg.Wait() + if firstErr == context.Canceled { + return firstErr + } + // wg.Wait() finishes only after any of the above go routines finishes. // Also, we can expect firstErr to be not nil, as goroutines can finish only on error. smc.Debugf("Reconnecting due to %v", firstErr) - if err = conn.Close(); err != nil { - smc.Debugf("Failed to close connection: %v", err) - } - return nil } @@ -162,6 +178,7 @@ func (smc *Client) connect(ctx context.Context, connectionCount int, additionalP errInvalidAuth = "invalid_auth" errInactiveAccount = "account_inactive" errMissingAuthToken = "not_authed" + errTokenRevoked = "token_revoked" ) // used to provide exponential backoff wait time with jitter before trying @@ -182,14 +199,14 @@ func (smc *Client) connect(ctx context.Context, connectionCount int, additionalP }) // attempt to start the connection - info, conn, err := smc.openAndDial(additionalPingHandler) + info, conn, err := smc.openAndDial(ctx, additionalPingHandler) if err == nil { return info, conn, nil } // check for fatal errors switch err.Error() { - case errInvalidAuth, errInactiveAccount, errMissingAuthToken: + case errInvalidAuth, errInactiveAccount, errMissingAuthToken, errTokenRevoked: smc.Debugf("invalid auth when connecting with SocketMode: %s", err) return nil, nil, err default: @@ -233,13 +250,13 @@ func (smc *Client) connect(ctx context.Context, connectionCount int, additionalP // openAndDial attempts to open a Socket Mode connection and dial to the connection endpoint using WebSocket. // It returns the full information returned by the "apps.connections.open" method on the // Slack API. -func (smc *Client) openAndDial(additionalPingHandler func(string) error) (info *slack.SocketModeConnection, _ *websocket.Conn, err error) { +func (smc *Client) openAndDial(ctx context.Context, additionalPingHandler func(string) error) (info *slack.SocketModeConnection, _ *websocket.Conn, err error) { var ( url string ) smc.Debugf("Starting SocketMode") - info, url, err = smc.Open() + info, url, err = smc.OpenContext(ctx) if err != nil { smc.Debugf("Failed to start or connect with SocketMode: %s", err) @@ -254,7 +271,7 @@ func (smc *Client) openAndDial(additionalPingHandler func(string) error) (info * if smc.dialer != nil { dialer = smc.dialer } - conn, _, err := dialer.Dial(url, upgradeHeader) + conn, _, err := dialer.DialContext(ctx, url, upgradeHeader) if err != nil { smc.Debugf("Failed to dial to the websocket: %s", err) return nil, nil, err diff --git a/socketmode/socket_mode_managed_conn_test.go b/socketmode/socket_mode_managed_conn_test.go new file mode 100644 index 000000000..542ccdb45 --- /dev/null +++ b/socketmode/socket_mode_managed_conn_test.go @@ -0,0 +1,46 @@ +//go:build go1.13 +// +build go1.13 + +package socketmode + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/slack-go/slack" + "github.com/slack-go/slack/slacktest" + + "github.com/stretchr/testify/assert" +) + +func Test_passContext(t *testing.T) { + s := slacktest.NewTestServer() + go s.Start() + + api := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL())) + cli := New(api) + + ctx, cancel := context.WithTimeout(context.TODO(), time.Nanosecond) + defer cancel() + + t.Run("RunWithContext", func(t *testing.T) { + // should fail imidiatly. + assert.EqualError(t, cli.RunContext(ctx), context.DeadlineExceeded.Error()) + }) + + t.Run("openAndDial", func(t *testing.T) { + _, _, err := cli.openAndDial(ctx, func(_ string) error { return nil }) + + // should fail imidiatly. + assert.EqualError(t, errors.Unwrap(err), context.DeadlineExceeded.Error()) + }) + + t.Run("OpenWithContext", func(t *testing.T) { + _, _, err := cli.OpenContext(ctx) + + // should fail imidiatly. + assert.EqualError(t, errors.Unwrap(err), context.DeadlineExceeded.Error()) + }) +} diff --git a/socketmode/socketmode.go b/socketmode/socketmode.go index 635e818e3..1871e6763 100644 --- a/socketmode/socketmode.go +++ b/socketmode/socketmode.go @@ -6,9 +6,9 @@ import ( "os" "time" - "github.com/slack-go/slack" - "github.com/gorilla/websocket" + + "github.com/slack-go/slack" ) // EventType is the type of events that are emitted by scoketmode.Client. @@ -58,7 +58,14 @@ func (smc *Client) Open() (info *slack.SocketModeConnection, websocketURL string ctx, cancel := context.WithTimeout(context.Background(), websocketDefaultTimeout) defer cancel() - return smc.apiClient.StartSocketModeContext(ctx) + return smc.StartSocketModeContext(ctx) +} + +// OpenContext calls the "apps.connections.open" endpoint and returns the provided URL and the full Info block. +// +// To have a fully managed Websocket connection, use `New`, and call `Run()` on it. +func (smc *Client) OpenContext(ctx context.Context) (info *slack.SocketModeConnection, websocketURL string, err error) { + return smc.StartSocketModeContext(ctx) } // Option options for the managed Client. @@ -99,7 +106,7 @@ func OptionLog(l logger) func(*Client) { // Slack's Websocket-based Socket Mode. func New(api *slack.Client, options ...Option) *Client { result := &Client{ - apiClient: *api, + Client: *api, Events: make(chan Event, 50), socketModeResponses: make(chan *Response, 20), maxPingInterval: defaultMaxPingInterval, diff --git a/socketmode/socketmode_test.go b/socketmode/socketmode_test.go index b9ce77690..015a34604 100644 --- a/socketmode/socketmode_test.go +++ b/socketmode/socketmode_test.go @@ -3,12 +3,11 @@ package socketmode import ( "bytes" "encoding/json" + "errors" "reflect" "testing" "github.com/slack-go/slack/slackevents" - - "github.com/pkg/errors" ) const ( diff --git a/users.go b/users.go index 5d8cb35f8..873115690 100644 --- a/users.go +++ b/users.go @@ -469,9 +469,7 @@ func (api *Client) SetUserPhoto(image string, params UserSetPhotoParams) error { // SetUserPhotoContext changes the currently authenticated user's profile image using a custom context func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params UserSetPhotoParams) (err error) { response := &SlackResponse{} - values := url.Values{ - "token": {api.token}, - } + values := url.Values{} if params.CropX != DEFAULT_USER_PHOTO_CROP_X { values.Add("crop_x", strconv.Itoa(params.CropX)) } @@ -482,7 +480,7 @@ func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params values.Add("crop_w", strconv.Itoa(params.CropW)) } - err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"users.setPhoto", image, "image", values, response, api) + err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"users.setPhoto", image, "image", api.token, values, response, api) if err != nil { return err } @@ -632,9 +630,15 @@ func (api *Client) UnsetUserCustomStatusContext(ctx context.Context) error { return api.SetUserCustomStatusContext(ctx, "", "", 0) } +// GetUserProfileParameters are the parameters required to get user profile +type GetUserProfileParameters struct { + UserID string + IncludeLabels bool +} + // GetUserProfile retrieves a user's profile information. -func (api *Client) GetUserProfile(userID string, includeLabels bool) (*UserProfile, error) { - return api.GetUserProfileContext(context.Background(), userID, includeLabels) +func (api *Client) GetUserProfile(params *GetUserProfileParameters) (*UserProfile, error) { + return api.GetUserProfileContext(context.Background(), params) } type getUserProfileResponse struct { @@ -643,9 +647,13 @@ type getUserProfileResponse struct { } // GetUserProfileContext retrieves a user's profile information with a context. -func (api *Client) GetUserProfileContext(ctx context.Context, userID string, includeLabels bool) (*UserProfile, error) { - values := url.Values{"token": {api.token}, "user": {userID}} - if includeLabels { +func (api *Client) GetUserProfileContext(ctx context.Context, params *GetUserProfileParameters) (*UserProfile, error) { + values := url.Values{"token": {api.token}} + + if params.UserID != "" { + values.Add("user", params.UserID) + } + if params.IncludeLabels { values.Add("include_labels", "true") } resp := &getUserProfileResponse{} diff --git a/users_test.go b/users_test.go index a49208976..58b3788b5 100644 --- a/users_test.go +++ b/users_test.go @@ -13,6 +13,7 @@ import ( "os" "reflect" "strconv" + "strings" "sync/atomic" "testing" ) @@ -509,7 +510,8 @@ func setUserPhotoHandler(wantBytes []byte, wantParams UserSetPhotoParams) http.H } // Test for expected token - if v := r.Form.Get("token"); v != validToken { + actualToken := strings.Split(r.Header.Get("Authorization"), "Bearer ")[1] + if actualToken != validToken { httpTestErrReply(w, true, fmt.Sprintf("expected multipart form value token=%v", validToken)) return } @@ -598,7 +600,7 @@ func TestGetUserProfile(t *testing.T) { http.HandleFunc("/users.profile.get", getUserProfileHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) - profile, err := api.GetUserProfile("UXXXXXXXX", false) + profile, err := api.GetUserProfile(&GetUserProfileParameters{UserID: "UXXXXXXXX"}) if err != nil { t.Fatalf("Unexpected error: %s", err) } diff --git a/vendor/github.com/google/go-cmp/LICENSE b/vendor/github.com/google/go-cmp/LICENSE new file mode 100644 index 000000000..32017f8fa --- /dev/null +++ b/vendor/github.com/google/go-cmp/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2017 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/google/go-cmp/cmp/compare.go b/vendor/github.com/google/go-cmp/cmp/compare.go new file mode 100644 index 000000000..2a5446762 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/compare.go @@ -0,0 +1,665 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package cmp determines equality of values. +// +// This package is intended to be a more powerful and safer alternative to +// reflect.DeepEqual for comparing whether two values are semantically equal. +// It is intended to only be used in tests, as performance is not a goal and +// it may panic if it cannot compare the values. Its propensity towards +// panicking means that its unsuitable for production environments where a +// spurious panic may be fatal. +// +// The primary features of cmp are: +// +// • When the default behavior of equality does not suit the needs of the test, +// custom equality functions can override the equality operation. +// For example, an equality function may report floats as equal so long as they +// are within some tolerance of each other. +// +// • Types that have an Equal method may use that method to determine equality. +// This allows package authors to determine the equality operation for the types +// that they define. +// +// • If no custom equality functions are used and no Equal method is defined, +// equality is determined by recursively comparing the primitive kinds on both +// values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported +// fields are not compared by default; they result in panics unless suppressed +// by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly +// compared using the Exporter option. +package cmp + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp/internal/diff" + "github.com/google/go-cmp/cmp/internal/function" + "github.com/google/go-cmp/cmp/internal/value" +) + +// Equal reports whether x and y are equal by recursively applying the +// following rules in the given order to x and y and all of their sub-values: +// +// • Let S be the set of all Ignore, Transformer, and Comparer options that +// remain after applying all path filters, value filters, and type filters. +// If at least one Ignore exists in S, then the comparison is ignored. +// If the number of Transformer and Comparer options in S is greater than one, +// then Equal panics because it is ambiguous which option to use. +// If S contains a single Transformer, then use that to transform the current +// values and recursively call Equal on the output values. +// If S contains a single Comparer, then use that to compare the current values. +// Otherwise, evaluation proceeds to the next rule. +// +// • If the values have an Equal method of the form "(T) Equal(T) bool" or +// "(T) Equal(I) bool" where T is assignable to I, then use the result of +// x.Equal(y) even if x or y is nil. Otherwise, no such method exists and +// evaluation proceeds to the next rule. +// +// • Lastly, try to compare x and y based on their basic kinds. +// Simple kinds like booleans, integers, floats, complex numbers, strings, and +// channels are compared using the equivalent of the == operator in Go. +// Functions are only equal if they are both nil, otherwise they are unequal. +// +// Structs are equal if recursively calling Equal on all fields report equal. +// If a struct contains unexported fields, Equal panics unless an Ignore option +// (e.g., cmpopts.IgnoreUnexported) ignores that field or the Exporter option +// explicitly permits comparing the unexported field. +// +// Slices are equal if they are both nil or both non-nil, where recursively +// calling Equal on all non-ignored slice or array elements report equal. +// Empty non-nil slices and nil slices are not equal; to equate empty slices, +// consider using cmpopts.EquateEmpty. +// +// Maps are equal if they are both nil or both non-nil, where recursively +// calling Equal on all non-ignored map entries report equal. +// Map keys are equal according to the == operator. +// To use custom comparisons for map keys, consider using cmpopts.SortMaps. +// Empty non-nil maps and nil maps are not equal; to equate empty maps, +// consider using cmpopts.EquateEmpty. +// +// Pointers and interfaces are equal if they are both nil or both non-nil, +// where they have the same underlying concrete type and recursively +// calling Equal on the underlying values reports equal. +// +// Before recursing into a pointer, slice element, or map, the current path +// is checked to detect whether the address has already been visited. +// If there is a cycle, then the pointed at values are considered equal +// only if both addresses were previously visited in the same path step. +func Equal(x, y interface{}, opts ...Option) bool { + s := newState(opts) + s.compareAny(rootStep(x, y)) + return s.result.Equal() +} + +// Diff returns a human-readable report of the differences between two values: +// y - x. It returns an empty string if and only if Equal returns true for the +// same input values and options. +// +// The output is displayed as a literal in pseudo-Go syntax. +// At the start of each line, a "-" prefix indicates an element removed from x, +// a "+" prefix to indicates an element added from y, and the lack of a prefix +// indicates an element common to both x and y. If possible, the output +// uses fmt.Stringer.String or error.Error methods to produce more humanly +// readable outputs. In such cases, the string is prefixed with either an +// 's' or 'e' character, respectively, to indicate that the method was called. +// +// Do not depend on this output being stable. If you need the ability to +// programmatically interpret the difference, consider using a custom Reporter. +func Diff(x, y interface{}, opts ...Option) string { + s := newState(opts) + + // Optimization: If there are no other reporters, we can optimize for the + // common case where the result is equal (and thus no reported difference). + // This avoids the expensive construction of a difference tree. + if len(s.reporters) == 0 { + s.compareAny(rootStep(x, y)) + if s.result.Equal() { + return "" + } + s.result = diff.Result{} // Reset results + } + + r := new(defaultReporter) + s.reporters = append(s.reporters, reporter{r}) + s.compareAny(rootStep(x, y)) + d := r.String() + if (d == "") != s.result.Equal() { + panic("inconsistent difference and equality results") + } + return d +} + +// rootStep constructs the first path step. If x and y have differing types, +// then they are stored within an empty interface type. +func rootStep(x, y interface{}) PathStep { + vx := reflect.ValueOf(x) + vy := reflect.ValueOf(y) + + // If the inputs are different types, auto-wrap them in an empty interface + // so that they have the same parent type. + var t reflect.Type + if !vx.IsValid() || !vy.IsValid() || vx.Type() != vy.Type() { + t = reflect.TypeOf((*interface{})(nil)).Elem() + if vx.IsValid() { + vvx := reflect.New(t).Elem() + vvx.Set(vx) + vx = vvx + } + if vy.IsValid() { + vvy := reflect.New(t).Elem() + vvy.Set(vy) + vy = vvy + } + } else { + t = vx.Type() + } + + return &pathStep{t, vx, vy} +} + +type state struct { + // These fields represent the "comparison state". + // Calling statelessCompare must not result in observable changes to these. + result diff.Result // The current result of comparison + curPath Path // The current path in the value tree + curPtrs pointerPath // The current set of visited pointers + reporters []reporter // Optional reporters + + // recChecker checks for infinite cycles applying the same set of + // transformers upon the output of itself. + recChecker recChecker + + // dynChecker triggers pseudo-random checks for option correctness. + // It is safe for statelessCompare to mutate this value. + dynChecker dynChecker + + // These fields, once set by processOption, will not change. + exporters []exporter // List of exporters for structs with unexported fields + opts Options // List of all fundamental and filter options +} + +func newState(opts []Option) *state { + // Always ensure a validator option exists to validate the inputs. + s := &state{opts: Options{validator{}}} + s.curPtrs.Init() + s.processOption(Options(opts)) + return s +} + +func (s *state) processOption(opt Option) { + switch opt := opt.(type) { + case nil: + case Options: + for _, o := range opt { + s.processOption(o) + } + case coreOption: + type filtered interface { + isFiltered() bool + } + if fopt, ok := opt.(filtered); ok && !fopt.isFiltered() { + panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt)) + } + s.opts = append(s.opts, opt) + case exporter: + s.exporters = append(s.exporters, opt) + case reporter: + s.reporters = append(s.reporters, opt) + default: + panic(fmt.Sprintf("unknown option %T", opt)) + } +} + +// statelessCompare compares two values and returns the result. +// This function is stateless in that it does not alter the current result, +// or output to any registered reporters. +func (s *state) statelessCompare(step PathStep) diff.Result { + // We do not save and restore curPath and curPtrs because all of the + // compareX methods should properly push and pop from them. + // It is an implementation bug if the contents of the paths differ from + // when calling this function to when returning from it. + + oldResult, oldReporters := s.result, s.reporters + s.result = diff.Result{} // Reset result + s.reporters = nil // Remove reporters to avoid spurious printouts + s.compareAny(step) + res := s.result + s.result, s.reporters = oldResult, oldReporters + return res +} + +func (s *state) compareAny(step PathStep) { + // Update the path stack. + s.curPath.push(step) + defer s.curPath.pop() + for _, r := range s.reporters { + r.PushStep(step) + defer r.PopStep() + } + s.recChecker.Check(s.curPath) + + // Cycle-detection for slice elements (see NOTE in compareSlice). + t := step.Type() + vx, vy := step.Values() + if si, ok := step.(SliceIndex); ok && si.isSlice && vx.IsValid() && vy.IsValid() { + px, py := vx.Addr(), vy.Addr() + if eq, visited := s.curPtrs.Push(px, py); visited { + s.report(eq, reportByCycle) + return + } + defer s.curPtrs.Pop(px, py) + } + + // Rule 1: Check whether an option applies on this node in the value tree. + if s.tryOptions(t, vx, vy) { + return + } + + // Rule 2: Check whether the type has a valid Equal method. + if s.tryMethod(t, vx, vy) { + return + } + + // Rule 3: Compare based on the underlying kind. + switch t.Kind() { + case reflect.Bool: + s.report(vx.Bool() == vy.Bool(), 0) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + s.report(vx.Int() == vy.Int(), 0) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + s.report(vx.Uint() == vy.Uint(), 0) + case reflect.Float32, reflect.Float64: + s.report(vx.Float() == vy.Float(), 0) + case reflect.Complex64, reflect.Complex128: + s.report(vx.Complex() == vy.Complex(), 0) + case reflect.String: + s.report(vx.String() == vy.String(), 0) + case reflect.Chan, reflect.UnsafePointer: + s.report(vx.Pointer() == vy.Pointer(), 0) + case reflect.Func: + s.report(vx.IsNil() && vy.IsNil(), 0) + case reflect.Struct: + s.compareStruct(t, vx, vy) + case reflect.Slice, reflect.Array: + s.compareSlice(t, vx, vy) + case reflect.Map: + s.compareMap(t, vx, vy) + case reflect.Ptr: + s.comparePtr(t, vx, vy) + case reflect.Interface: + s.compareInterface(t, vx, vy) + default: + panic(fmt.Sprintf("%v kind not handled", t.Kind())) + } +} + +func (s *state) tryOptions(t reflect.Type, vx, vy reflect.Value) bool { + // Evaluate all filters and apply the remaining options. + if opt := s.opts.filter(s, t, vx, vy); opt != nil { + opt.apply(s, vx, vy) + return true + } + return false +} + +func (s *state) tryMethod(t reflect.Type, vx, vy reflect.Value) bool { + // Check if this type even has an Equal method. + m, ok := t.MethodByName("Equal") + if !ok || !function.IsType(m.Type, function.EqualAssignable) { + return false + } + + eq := s.callTTBFunc(m.Func, vx, vy) + s.report(eq, reportByMethod) + return true +} + +func (s *state) callTRFunc(f, v reflect.Value, step Transform) reflect.Value { + if !s.dynChecker.Next() { + return f.Call([]reflect.Value{v})[0] + } + + // Run the function twice and ensure that we get the same results back. + // We run in goroutines so that the race detector (if enabled) can detect + // unsafe mutations to the input. + c := make(chan reflect.Value) + go detectRaces(c, f, v) + got := <-c + want := f.Call([]reflect.Value{v})[0] + if step.vx, step.vy = got, want; !s.statelessCompare(step).Equal() { + // To avoid false-positives with non-reflexive equality operations, + // we sanity check whether a value is equal to itself. + if step.vx, step.vy = want, want; !s.statelessCompare(step).Equal() { + return want + } + panic(fmt.Sprintf("non-deterministic function detected: %s", function.NameOf(f))) + } + return want +} + +func (s *state) callTTBFunc(f, x, y reflect.Value) bool { + if !s.dynChecker.Next() { + return f.Call([]reflect.Value{x, y})[0].Bool() + } + + // Swapping the input arguments is sufficient to check that + // f is symmetric and deterministic. + // We run in goroutines so that the race detector (if enabled) can detect + // unsafe mutations to the input. + c := make(chan reflect.Value) + go detectRaces(c, f, y, x) + got := <-c + want := f.Call([]reflect.Value{x, y})[0].Bool() + if !got.IsValid() || got.Bool() != want { + panic(fmt.Sprintf("non-deterministic or non-symmetric function detected: %s", function.NameOf(f))) + } + return want +} + +func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) { + var ret reflect.Value + defer func() { + recover() // Ignore panics, let the other call to f panic instead + c <- ret + }() + ret = f.Call(vs)[0] +} + +func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) { + var addr bool + var vax, vay reflect.Value // Addressable versions of vx and vy + + var mayForce, mayForceInit bool + step := StructField{&structField{}} + for i := 0; i < t.NumField(); i++ { + step.typ = t.Field(i).Type + step.vx = vx.Field(i) + step.vy = vy.Field(i) + step.name = t.Field(i).Name + step.idx = i + step.unexported = !isExported(step.name) + if step.unexported { + if step.name == "_" { + continue + } + // Defer checking of unexported fields until later to give an + // Ignore a chance to ignore the field. + if !vax.IsValid() || !vay.IsValid() { + // For retrieveUnexportedField to work, the parent struct must + // be addressable. Create a new copy of the values if + // necessary to make them addressable. + addr = vx.CanAddr() || vy.CanAddr() + vax = makeAddressable(vx) + vay = makeAddressable(vy) + } + if !mayForceInit { + for _, xf := range s.exporters { + mayForce = mayForce || xf(t) + } + mayForceInit = true + } + step.mayForce = mayForce + step.paddr = addr + step.pvx = vax + step.pvy = vay + step.field = t.Field(i) + } + s.compareAny(step) + } +} + +func (s *state) compareSlice(t reflect.Type, vx, vy reflect.Value) { + isSlice := t.Kind() == reflect.Slice + if isSlice && (vx.IsNil() || vy.IsNil()) { + s.report(vx.IsNil() && vy.IsNil(), 0) + return + } + + // NOTE: It is incorrect to call curPtrs.Push on the slice header pointer + // since slices represents a list of pointers, rather than a single pointer. + // The pointer checking logic must be handled on a per-element basis + // in compareAny. + // + // A slice header (see reflect.SliceHeader) in Go is a tuple of a starting + // pointer P, a length N, and a capacity C. Supposing each slice element has + // a memory size of M, then the slice is equivalent to the list of pointers: + // [P+i*M for i in range(N)] + // + // For example, v[:0] and v[:1] are slices with the same starting pointer, + // but they are clearly different values. Using the slice pointer alone + // violates the assumption that equal pointers implies equal values. + + step := SliceIndex{&sliceIndex{pathStep: pathStep{typ: t.Elem()}, isSlice: isSlice}} + withIndexes := func(ix, iy int) SliceIndex { + if ix >= 0 { + step.vx, step.xkey = vx.Index(ix), ix + } else { + step.vx, step.xkey = reflect.Value{}, -1 + } + if iy >= 0 { + step.vy, step.ykey = vy.Index(iy), iy + } else { + step.vy, step.ykey = reflect.Value{}, -1 + } + return step + } + + // Ignore options are able to ignore missing elements in a slice. + // However, detecting these reliably requires an optimal differencing + // algorithm, for which diff.Difference is not. + // + // Instead, we first iterate through both slices to detect which elements + // would be ignored if standing alone. The index of non-discarded elements + // are stored in a separate slice, which diffing is then performed on. + var indexesX, indexesY []int + var ignoredX, ignoredY []bool + for ix := 0; ix < vx.Len(); ix++ { + ignored := s.statelessCompare(withIndexes(ix, -1)).NumDiff == 0 + if !ignored { + indexesX = append(indexesX, ix) + } + ignoredX = append(ignoredX, ignored) + } + for iy := 0; iy < vy.Len(); iy++ { + ignored := s.statelessCompare(withIndexes(-1, iy)).NumDiff == 0 + if !ignored { + indexesY = append(indexesY, iy) + } + ignoredY = append(ignoredY, ignored) + } + + // Compute an edit-script for slices vx and vy (excluding ignored elements). + edits := diff.Difference(len(indexesX), len(indexesY), func(ix, iy int) diff.Result { + return s.statelessCompare(withIndexes(indexesX[ix], indexesY[iy])) + }) + + // Replay the ignore-scripts and the edit-script. + var ix, iy int + for ix < vx.Len() || iy < vy.Len() { + var e diff.EditType + switch { + case ix < len(ignoredX) && ignoredX[ix]: + e = diff.UniqueX + case iy < len(ignoredY) && ignoredY[iy]: + e = diff.UniqueY + default: + e, edits = edits[0], edits[1:] + } + switch e { + case diff.UniqueX: + s.compareAny(withIndexes(ix, -1)) + ix++ + case diff.UniqueY: + s.compareAny(withIndexes(-1, iy)) + iy++ + default: + s.compareAny(withIndexes(ix, iy)) + ix++ + iy++ + } + } +} + +func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) { + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), 0) + return + } + + // Cycle-detection for maps. + if eq, visited := s.curPtrs.Push(vx, vy); visited { + s.report(eq, reportByCycle) + return + } + defer s.curPtrs.Pop(vx, vy) + + // We combine and sort the two map keys so that we can perform the + // comparisons in a deterministic order. + step := MapIndex{&mapIndex{pathStep: pathStep{typ: t.Elem()}}} + for _, k := range value.SortKeys(append(vx.MapKeys(), vy.MapKeys()...)) { + step.vx = vx.MapIndex(k) + step.vy = vy.MapIndex(k) + step.key = k + if !step.vx.IsValid() && !step.vy.IsValid() { + // It is possible for both vx and vy to be invalid if the + // key contained a NaN value in it. + // + // Even with the ability to retrieve NaN keys in Go 1.12, + // there still isn't a sensible way to compare the values since + // a NaN key may map to multiple unordered values. + // The most reasonable way to compare NaNs would be to compare the + // set of values. However, this is impossible to do efficiently + // since set equality is provably an O(n^2) operation given only + // an Equal function. If we had a Less function or Hash function, + // this could be done in O(n*log(n)) or O(n), respectively. + // + // Rather than adding complex logic to deal with NaNs, make it + // the user's responsibility to compare such obscure maps. + const help = "consider providing a Comparer to compare the map" + panic(fmt.Sprintf("%#v has map key with NaNs\n%s", s.curPath, help)) + } + s.compareAny(step) + } +} + +func (s *state) comparePtr(t reflect.Type, vx, vy reflect.Value) { + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), 0) + return + } + + // Cycle-detection for pointers. + if eq, visited := s.curPtrs.Push(vx, vy); visited { + s.report(eq, reportByCycle) + return + } + defer s.curPtrs.Pop(vx, vy) + + vx, vy = vx.Elem(), vy.Elem() + s.compareAny(Indirect{&indirect{pathStep{t.Elem(), vx, vy}}}) +} + +func (s *state) compareInterface(t reflect.Type, vx, vy reflect.Value) { + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), 0) + return + } + vx, vy = vx.Elem(), vy.Elem() + if vx.Type() != vy.Type() { + s.report(false, 0) + return + } + s.compareAny(TypeAssertion{&typeAssertion{pathStep{vx.Type(), vx, vy}}}) +} + +func (s *state) report(eq bool, rf resultFlags) { + if rf&reportByIgnore == 0 { + if eq { + s.result.NumSame++ + rf |= reportEqual + } else { + s.result.NumDiff++ + rf |= reportUnequal + } + } + for _, r := range s.reporters { + r.Report(Result{flags: rf}) + } +} + +// recChecker tracks the state needed to periodically perform checks that +// user provided transformers are not stuck in an infinitely recursive cycle. +type recChecker struct{ next int } + +// Check scans the Path for any recursive transformers and panics when any +// recursive transformers are detected. Note that the presence of a +// recursive Transformer does not necessarily imply an infinite cycle. +// As such, this check only activates after some minimal number of path steps. +func (rc *recChecker) Check(p Path) { + const minLen = 1 << 16 + if rc.next == 0 { + rc.next = minLen + } + if len(p) < rc.next { + return + } + rc.next <<= 1 + + // Check whether the same transformer has appeared at least twice. + var ss []string + m := map[Option]int{} + for _, ps := range p { + if t, ok := ps.(Transform); ok { + t := t.Option() + if m[t] == 1 { // Transformer was used exactly once before + tf := t.(*transformer).fnc.Type() + ss = append(ss, fmt.Sprintf("%v: %v => %v", t, tf.In(0), tf.Out(0))) + } + m[t]++ + } + } + if len(ss) > 0 { + const warning = "recursive set of Transformers detected" + const help = "consider using cmpopts.AcyclicTransformer" + set := strings.Join(ss, "\n\t") + panic(fmt.Sprintf("%s:\n\t%s\n%s", warning, set, help)) + } +} + +// dynChecker tracks the state needed to periodically perform checks that +// user provided functions are symmetric and deterministic. +// The zero value is safe for immediate use. +type dynChecker struct{ curr, next int } + +// Next increments the state and reports whether a check should be performed. +// +// Checks occur every Nth function call, where N is a triangular number: +// 0 1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136 153 171 190 ... +// See https://en.wikipedia.org/wiki/Triangular_number +// +// This sequence ensures that the cost of checks drops significantly as +// the number of functions calls grows larger. +func (dc *dynChecker) Next() bool { + ok := dc.curr == dc.next + if ok { + dc.curr = 0 + dc.next++ + } + dc.curr++ + return ok +} + +// makeAddressable returns a value that is always addressable. +// It returns the input verbatim if it is already addressable, +// otherwise it creates a new value and returns an addressable copy. +func makeAddressable(v reflect.Value) reflect.Value { + if v.CanAddr() { + return v + } + vc := reflect.New(v.Type()).Elem() + vc.Set(v) + return vc +} diff --git a/vendor/github.com/google/go-cmp/cmp/export_panic.go b/vendor/github.com/google/go-cmp/cmp/export_panic.go new file mode 100644 index 000000000..ae851fe53 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/export_panic.go @@ -0,0 +1,16 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build purego +// +build purego + +package cmp + +import "reflect" + +const supportExporters = false + +func retrieveUnexportedField(reflect.Value, reflect.StructField, bool) reflect.Value { + panic("no support for forcibly accessing unexported fields") +} diff --git a/vendor/github.com/google/go-cmp/cmp/export_unsafe.go b/vendor/github.com/google/go-cmp/cmp/export_unsafe.go new file mode 100644 index 000000000..e2c0f74e8 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/export_unsafe.go @@ -0,0 +1,36 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !purego +// +build !purego + +package cmp + +import ( + "reflect" + "unsafe" +) + +const supportExporters = true + +// retrieveUnexportedField uses unsafe to forcibly retrieve any field from +// a struct such that the value has read-write permissions. +// +// The parent struct, v, must be addressable, while f must be a StructField +// describing the field to retrieve. If addr is false, +// then the returned value will be shallowed copied to be non-addressable. +func retrieveUnexportedField(v reflect.Value, f reflect.StructField, addr bool) reflect.Value { + ve := reflect.NewAt(f.Type, unsafe.Pointer(uintptr(unsafe.Pointer(v.UnsafeAddr()))+f.Offset)).Elem() + if !addr { + // A field is addressable if and only if the struct is addressable. + // If the original parent value was not addressable, shallow copy the + // value to make it non-addressable to avoid leaking an implementation + // detail of how forcibly exporting a field works. + if ve.Kind() == reflect.Interface && ve.IsNil() { + return reflect.Zero(f.Type) + } + return reflect.ValueOf(ve.Interface()).Convert(f.Type) + } + return ve +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go b/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go new file mode 100644 index 000000000..36062a604 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go @@ -0,0 +1,18 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !cmp_debug +// +build !cmp_debug + +package diff + +var debug debugger + +type debugger struct{} + +func (debugger) Begin(_, _ int, f EqualFunc, _, _ *EditScript) EqualFunc { + return f +} +func (debugger) Update() {} +func (debugger) Finish() {} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go b/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go new file mode 100644 index 000000000..a3b97a1ad --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go @@ -0,0 +1,123 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build cmp_debug +// +build cmp_debug + +package diff + +import ( + "fmt" + "strings" + "sync" + "time" +) + +// The algorithm can be seen running in real-time by enabling debugging: +// go test -tags=cmp_debug -v +// +// Example output: +// === RUN TestDifference/#34 +// ┌───────────────────────────────┐ +// │ \ · · · · · · · · · · · · · · │ +// │ · # · · · · · · · · · · · · · │ +// │ · \ · · · · · · · · · · · · · │ +// │ · · \ · · · · · · · · · · · · │ +// │ · · · X # · · · · · · · · · · │ +// │ · · · # \ · · · · · · · · · · │ +// │ · · · · · # # · · · · · · · · │ +// │ · · · · · # \ · · · · · · · · │ +// │ · · · · · · · \ · · · · · · · │ +// │ · · · · · · · · \ · · · · · · │ +// │ · · · · · · · · · \ · · · · · │ +// │ · · · · · · · · · · \ · · # · │ +// │ · · · · · · · · · · · \ # # · │ +// │ · · · · · · · · · · · # # # · │ +// │ · · · · · · · · · · # # # # · │ +// │ · · · · · · · · · # # # # # · │ +// │ · · · · · · · · · · · · · · \ │ +// └───────────────────────────────┘ +// [.Y..M.XY......YXYXY.|] +// +// The grid represents the edit-graph where the horizontal axis represents +// list X and the vertical axis represents list Y. The start of the two lists +// is the top-left, while the ends are the bottom-right. The '·' represents +// an unexplored node in the graph. The '\' indicates that the two symbols +// from list X and Y are equal. The 'X' indicates that two symbols are similar +// (but not exactly equal) to each other. The '#' indicates that the two symbols +// are different (and not similar). The algorithm traverses this graph trying to +// make the paths starting in the top-left and the bottom-right connect. +// +// The series of '.', 'X', 'Y', and 'M' characters at the bottom represents +// the currently established path from the forward and reverse searches, +// separated by a '|' character. + +const ( + updateDelay = 100 * time.Millisecond + finishDelay = 500 * time.Millisecond + ansiTerminal = true // ANSI escape codes used to move terminal cursor +) + +var debug debugger + +type debugger struct { + sync.Mutex + p1, p2 EditScript + fwdPath, revPath *EditScript + grid []byte + lines int +} + +func (dbg *debugger) Begin(nx, ny int, f EqualFunc, p1, p2 *EditScript) EqualFunc { + dbg.Lock() + dbg.fwdPath, dbg.revPath = p1, p2 + top := "┌─" + strings.Repeat("──", nx) + "┐\n" + row := "│ " + strings.Repeat("· ", nx) + "│\n" + btm := "└─" + strings.Repeat("──", nx) + "┘\n" + dbg.grid = []byte(top + strings.Repeat(row, ny) + btm) + dbg.lines = strings.Count(dbg.String(), "\n") + fmt.Print(dbg) + + // Wrap the EqualFunc so that we can intercept each result. + return func(ix, iy int) (r Result) { + cell := dbg.grid[len(top)+iy*len(row):][len("│ ")+len("· ")*ix:][:len("·")] + for i := range cell { + cell[i] = 0 // Zero out the multiple bytes of UTF-8 middle-dot + } + switch r = f(ix, iy); { + case r.Equal(): + cell[0] = '\\' + case r.Similar(): + cell[0] = 'X' + default: + cell[0] = '#' + } + return + } +} + +func (dbg *debugger) Update() { + dbg.print(updateDelay) +} + +func (dbg *debugger) Finish() { + dbg.print(finishDelay) + dbg.Unlock() +} + +func (dbg *debugger) String() string { + dbg.p1, dbg.p2 = *dbg.fwdPath, dbg.p2[:0] + for i := len(*dbg.revPath) - 1; i >= 0; i-- { + dbg.p2 = append(dbg.p2, (*dbg.revPath)[i]) + } + return fmt.Sprintf("%s[%v|%v]\n\n", dbg.grid, dbg.p1, dbg.p2) +} + +func (dbg *debugger) print(d time.Duration) { + if ansiTerminal { + fmt.Printf("\x1b[%dA", dbg.lines) // Reset terminal cursor + } + fmt.Print(dbg) + time.Sleep(d) +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go b/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go new file mode 100644 index 000000000..bc196b16c --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go @@ -0,0 +1,398 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package diff implements an algorithm for producing edit-scripts. +// The edit-script is a sequence of operations needed to transform one list +// of symbols into another (or vice-versa). The edits allowed are insertions, +// deletions, and modifications. The summation of all edits is called the +// Levenshtein distance as this problem is well-known in computer science. +// +// This package prioritizes performance over accuracy. That is, the run time +// is more important than obtaining a minimal Levenshtein distance. +package diff + +import ( + "math/rand" + "time" + + "github.com/google/go-cmp/cmp/internal/flags" +) + +// EditType represents a single operation within an edit-script. +type EditType uint8 + +const ( + // Identity indicates that a symbol pair is identical in both list X and Y. + Identity EditType = iota + // UniqueX indicates that a symbol only exists in X and not Y. + UniqueX + // UniqueY indicates that a symbol only exists in Y and not X. + UniqueY + // Modified indicates that a symbol pair is a modification of each other. + Modified +) + +// EditScript represents the series of differences between two lists. +type EditScript []EditType + +// String returns a human-readable string representing the edit-script where +// Identity, UniqueX, UniqueY, and Modified are represented by the +// '.', 'X', 'Y', and 'M' characters, respectively. +func (es EditScript) String() string { + b := make([]byte, len(es)) + for i, e := range es { + switch e { + case Identity: + b[i] = '.' + case UniqueX: + b[i] = 'X' + case UniqueY: + b[i] = 'Y' + case Modified: + b[i] = 'M' + default: + panic("invalid edit-type") + } + } + return string(b) +} + +// stats returns a histogram of the number of each type of edit operation. +func (es EditScript) stats() (s struct{ NI, NX, NY, NM int }) { + for _, e := range es { + switch e { + case Identity: + s.NI++ + case UniqueX: + s.NX++ + case UniqueY: + s.NY++ + case Modified: + s.NM++ + default: + panic("invalid edit-type") + } + } + return +} + +// Dist is the Levenshtein distance and is guaranteed to be 0 if and only if +// lists X and Y are equal. +func (es EditScript) Dist() int { return len(es) - es.stats().NI } + +// LenX is the length of the X list. +func (es EditScript) LenX() int { return len(es) - es.stats().NY } + +// LenY is the length of the Y list. +func (es EditScript) LenY() int { return len(es) - es.stats().NX } + +// EqualFunc reports whether the symbols at indexes ix and iy are equal. +// When called by Difference, the index is guaranteed to be within nx and ny. +type EqualFunc func(ix int, iy int) Result + +// Result is the result of comparison. +// NumSame is the number of sub-elements that are equal. +// NumDiff is the number of sub-elements that are not equal. +type Result struct{ NumSame, NumDiff int } + +// BoolResult returns a Result that is either Equal or not Equal. +func BoolResult(b bool) Result { + if b { + return Result{NumSame: 1} // Equal, Similar + } else { + return Result{NumDiff: 2} // Not Equal, not Similar + } +} + +// Equal indicates whether the symbols are equal. Two symbols are equal +// if and only if NumDiff == 0. If Equal, then they are also Similar. +func (r Result) Equal() bool { return r.NumDiff == 0 } + +// Similar indicates whether two symbols are similar and may be represented +// by using the Modified type. As a special case, we consider binary comparisons +// (i.e., those that return Result{1, 0} or Result{0, 1}) to be similar. +// +// The exact ratio of NumSame to NumDiff to determine similarity may change. +func (r Result) Similar() bool { + // Use NumSame+1 to offset NumSame so that binary comparisons are similar. + return r.NumSame+1 >= r.NumDiff +} + +var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0 + +// Difference reports whether two lists of lengths nx and ny are equal +// given the definition of equality provided as f. +// +// This function returns an edit-script, which is a sequence of operations +// needed to convert one list into the other. The following invariants for +// the edit-script are maintained: +// • eq == (es.Dist()==0) +// • nx == es.LenX() +// • ny == es.LenY() +// +// This algorithm is not guaranteed to be an optimal solution (i.e., one that +// produces an edit-script with a minimal Levenshtein distance). This algorithm +// favors performance over optimality. The exact output is not guaranteed to +// be stable and may change over time. +func Difference(nx, ny int, f EqualFunc) (es EditScript) { + // This algorithm is based on traversing what is known as an "edit-graph". + // See Figure 1 from "An O(ND) Difference Algorithm and Its Variations" + // by Eugene W. Myers. Since D can be as large as N itself, this is + // effectively O(N^2). Unlike the algorithm from that paper, we are not + // interested in the optimal path, but at least some "decent" path. + // + // For example, let X and Y be lists of symbols: + // X = [A B C A B B A] + // Y = [C B A B A C] + // + // The edit-graph can be drawn as the following: + // A B C A B B A + // ┌─────────────┐ + // C │_|_|\|_|_|_|_│ 0 + // B │_|\|_|_|\|\|_│ 1 + // A │\|_|_|\|_|_|\│ 2 + // B │_|\|_|_|\|\|_│ 3 + // A │\|_|_|\|_|_|\│ 4 + // C │ | |\| | | | │ 5 + // └─────────────┘ 6 + // 0 1 2 3 4 5 6 7 + // + // List X is written along the horizontal axis, while list Y is written + // along the vertical axis. At any point on this grid, if the symbol in + // list X matches the corresponding symbol in list Y, then a '\' is drawn. + // The goal of any minimal edit-script algorithm is to find a path from the + // top-left corner to the bottom-right corner, while traveling through the + // fewest horizontal or vertical edges. + // A horizontal edge is equivalent to inserting a symbol from list X. + // A vertical edge is equivalent to inserting a symbol from list Y. + // A diagonal edge is equivalent to a matching symbol between both X and Y. + + // Invariants: + // • 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx + // • 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny + // + // In general: + // • fwdFrontier.X < revFrontier.X + // • fwdFrontier.Y < revFrontier.Y + // Unless, it is time for the algorithm to terminate. + fwdPath := path{+1, point{0, 0}, make(EditScript, 0, (nx+ny)/2)} + revPath := path{-1, point{nx, ny}, make(EditScript, 0)} + fwdFrontier := fwdPath.point // Forward search frontier + revFrontier := revPath.point // Reverse search frontier + + // Search budget bounds the cost of searching for better paths. + // The longest sequence of non-matching symbols that can be tolerated is + // approximately the square-root of the search budget. + searchBudget := 4 * (nx + ny) // O(n) + + // Running the tests with the "cmp_debug" build tag prints a visualization + // of the algorithm running in real-time. This is educational for + // understanding how the algorithm works. See debug_enable.go. + f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es) + + // The algorithm below is a greedy, meet-in-the-middle algorithm for + // computing sub-optimal edit-scripts between two lists. + // + // The algorithm is approximately as follows: + // • Searching for differences switches back-and-forth between + // a search that starts at the beginning (the top-left corner), and + // a search that starts at the end (the bottom-right corner). The goal of + // the search is connect with the search from the opposite corner. + // • As we search, we build a path in a greedy manner, where the first + // match seen is added to the path (this is sub-optimal, but provides a + // decent result in practice). When matches are found, we try the next pair + // of symbols in the lists and follow all matches as far as possible. + // • When searching for matches, we search along a diagonal going through + // through the "frontier" point. If no matches are found, we advance the + // frontier towards the opposite corner. + // • This algorithm terminates when either the X coordinates or the + // Y coordinates of the forward and reverse frontier points ever intersect. + + // This algorithm is correct even if searching only in the forward direction + // or in the reverse direction. We do both because it is commonly observed + // that two lists commonly differ because elements were added to the front + // or end of the other list. + // + // Non-deterministically start with either the forward or reverse direction + // to introduce some deliberate instability so that we have the flexibility + // to change this algorithm in the future. + if flags.Deterministic || randBool { + goto forwardSearch + } else { + goto reverseSearch + } + +forwardSearch: + { + // Forward search from the beginning. + if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { + goto finishSearch + } + for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { + // Search in a diagonal pattern for a match. + z := zigzag(i) + p := point{fwdFrontier.X + z, fwdFrontier.Y - z} + switch { + case p.X >= revPath.X || p.Y < fwdPath.Y: + stop1 = true // Hit top-right corner + case p.Y >= revPath.Y || p.X < fwdPath.X: + stop2 = true // Hit bottom-left corner + case f(p.X, p.Y).Equal(): + // Match found, so connect the path to this point. + fwdPath.connect(p, f) + fwdPath.append(Identity) + // Follow sequence of matches as far as possible. + for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y { + if !f(fwdPath.X, fwdPath.Y).Equal() { + break + } + fwdPath.append(Identity) + } + fwdFrontier = fwdPath.point + stop1, stop2 = true, true + default: + searchBudget-- // Match not found + } + debug.Update() + } + // Advance the frontier towards reverse point. + if revPath.X-fwdFrontier.X >= revPath.Y-fwdFrontier.Y { + fwdFrontier.X++ + } else { + fwdFrontier.Y++ + } + goto reverseSearch + } + +reverseSearch: + { + // Reverse search from the end. + if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { + goto finishSearch + } + for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { + // Search in a diagonal pattern for a match. + z := zigzag(i) + p := point{revFrontier.X - z, revFrontier.Y + z} + switch { + case fwdPath.X >= p.X || revPath.Y < p.Y: + stop1 = true // Hit bottom-left corner + case fwdPath.Y >= p.Y || revPath.X < p.X: + stop2 = true // Hit top-right corner + case f(p.X-1, p.Y-1).Equal(): + // Match found, so connect the path to this point. + revPath.connect(p, f) + revPath.append(Identity) + // Follow sequence of matches as far as possible. + for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y { + if !f(revPath.X-1, revPath.Y-1).Equal() { + break + } + revPath.append(Identity) + } + revFrontier = revPath.point + stop1, stop2 = true, true + default: + searchBudget-- // Match not found + } + debug.Update() + } + // Advance the frontier towards forward point. + if revFrontier.X-fwdPath.X >= revFrontier.Y-fwdPath.Y { + revFrontier.X-- + } else { + revFrontier.Y-- + } + goto forwardSearch + } + +finishSearch: + // Join the forward and reverse paths and then append the reverse path. + fwdPath.connect(revPath.point, f) + for i := len(revPath.es) - 1; i >= 0; i-- { + t := revPath.es[i] + revPath.es = revPath.es[:i] + fwdPath.append(t) + } + debug.Finish() + return fwdPath.es +} + +type path struct { + dir int // +1 if forward, -1 if reverse + point // Leading point of the EditScript path + es EditScript +} + +// connect appends any necessary Identity, Modified, UniqueX, or UniqueY types +// to the edit-script to connect p.point to dst. +func (p *path) connect(dst point, f EqualFunc) { + if p.dir > 0 { + // Connect in forward direction. + for dst.X > p.X && dst.Y > p.Y { + switch r := f(p.X, p.Y); { + case r.Equal(): + p.append(Identity) + case r.Similar(): + p.append(Modified) + case dst.X-p.X >= dst.Y-p.Y: + p.append(UniqueX) + default: + p.append(UniqueY) + } + } + for dst.X > p.X { + p.append(UniqueX) + } + for dst.Y > p.Y { + p.append(UniqueY) + } + } else { + // Connect in reverse direction. + for p.X > dst.X && p.Y > dst.Y { + switch r := f(p.X-1, p.Y-1); { + case r.Equal(): + p.append(Identity) + case r.Similar(): + p.append(Modified) + case p.Y-dst.Y >= p.X-dst.X: + p.append(UniqueY) + default: + p.append(UniqueX) + } + } + for p.X > dst.X { + p.append(UniqueX) + } + for p.Y > dst.Y { + p.append(UniqueY) + } + } +} + +func (p *path) append(t EditType) { + p.es = append(p.es, t) + switch t { + case Identity, Modified: + p.add(p.dir, p.dir) + case UniqueX: + p.add(p.dir, 0) + case UniqueY: + p.add(0, p.dir) + } + debug.Update() +} + +type point struct{ X, Y int } + +func (p *point) add(dx, dy int) { p.X += dx; p.Y += dy } + +// zigzag maps a consecutive sequence of integers to a zig-zag sequence. +// [0 1 2 3 4 5 ...] => [0 -1 +1 -2 +2 ...] +func zigzag(x int) int { + if x&1 != 0 { + x = ^x + } + return x >> 1 +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/flags/flags.go b/vendor/github.com/google/go-cmp/cmp/internal/flags/flags.go new file mode 100644 index 000000000..d8e459c9b --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/flags/flags.go @@ -0,0 +1,9 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package flags + +// Deterministic controls whether the output of Diff should be deterministic. +// This is only used for testing. +var Deterministic bool diff --git a/vendor/github.com/google/go-cmp/cmp/internal/function/func.go b/vendor/github.com/google/go-cmp/cmp/internal/function/func.go new file mode 100644 index 000000000..d127d4362 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/function/func.go @@ -0,0 +1,99 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package function provides functionality for identifying function types. +package function + +import ( + "reflect" + "regexp" + "runtime" + "strings" +) + +type funcType int + +const ( + _ funcType = iota + + tbFunc // func(T) bool + ttbFunc // func(T, T) bool + trbFunc // func(T, R) bool + tibFunc // func(T, I) bool + trFunc // func(T) R + + Equal = ttbFunc // func(T, T) bool + EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool + Transformer = trFunc // func(T) R + ValueFilter = ttbFunc // func(T, T) bool + Less = ttbFunc // func(T, T) bool + ValuePredicate = tbFunc // func(T) bool + KeyValuePredicate = trbFunc // func(T, R) bool +) + +var boolType = reflect.TypeOf(true) + +// IsType reports whether the reflect.Type is of the specified function type. +func IsType(t reflect.Type, ft funcType) bool { + if t == nil || t.Kind() != reflect.Func || t.IsVariadic() { + return false + } + ni, no := t.NumIn(), t.NumOut() + switch ft { + case tbFunc: // func(T) bool + if ni == 1 && no == 1 && t.Out(0) == boolType { + return true + } + case ttbFunc: // func(T, T) bool + if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == boolType { + return true + } + case trbFunc: // func(T, R) bool + if ni == 2 && no == 1 && t.Out(0) == boolType { + return true + } + case tibFunc: // func(T, I) bool + if ni == 2 && no == 1 && t.In(0).AssignableTo(t.In(1)) && t.Out(0) == boolType { + return true + } + case trFunc: // func(T) R + if ni == 1 && no == 1 { + return true + } + } + return false +} + +var lastIdentRx = regexp.MustCompile(`[_\p{L}][_\p{L}\p{N}]*$`) + +// NameOf returns the name of the function value. +func NameOf(v reflect.Value) string { + fnc := runtime.FuncForPC(v.Pointer()) + if fnc == nil { + return "" + } + fullName := fnc.Name() // e.g., "long/path/name/mypkg.(*MyType).(long/path/name/mypkg.myMethod)-fm" + + // Method closures have a "-fm" suffix. + fullName = strings.TrimSuffix(fullName, "-fm") + + var name string + for len(fullName) > 0 { + inParen := strings.HasSuffix(fullName, ")") + fullName = strings.TrimSuffix(fullName, ")") + + s := lastIdentRx.FindString(fullName) + if s == "" { + break + } + name = s + "." + name + fullName = strings.TrimSuffix(fullName, s) + + if i := strings.LastIndexByte(fullName, '('); inParen && i >= 0 { + fullName = fullName[:i] + } + fullName = strings.TrimSuffix(fullName, ".") + } + return strings.TrimSuffix(name, ".") +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/name.go b/vendor/github.com/google/go-cmp/cmp/internal/value/name.go new file mode 100644 index 000000000..7b498bb2c --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/value/name.go @@ -0,0 +1,164 @@ +// Copyright 2020, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package value + +import ( + "reflect" + "strconv" +) + +var anyType = reflect.TypeOf((*interface{})(nil)).Elem() + +// TypeString is nearly identical to reflect.Type.String, +// but has an additional option to specify that full type names be used. +func TypeString(t reflect.Type, qualified bool) string { + return string(appendTypeName(nil, t, qualified, false)) +} + +func appendTypeName(b []byte, t reflect.Type, qualified, elideFunc bool) []byte { + // BUG: Go reflection provides no way to disambiguate two named types + // of the same name and within the same package, + // but declared within the namespace of different functions. + + // Use the "any" alias instead of "interface{}" for better readability. + if t == anyType { + return append(b, "any"...) + } + + // Named type. + if t.Name() != "" { + if qualified && t.PkgPath() != "" { + b = append(b, '"') + b = append(b, t.PkgPath()...) + b = append(b, '"') + b = append(b, '.') + b = append(b, t.Name()...) + } else { + b = append(b, t.String()...) + } + return b + } + + // Unnamed type. + switch k := t.Kind(); k { + case reflect.Bool, reflect.String, reflect.UnsafePointer, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: + b = append(b, k.String()...) + case reflect.Chan: + if t.ChanDir() == reflect.RecvDir { + b = append(b, "<-"...) + } + b = append(b, "chan"...) + if t.ChanDir() == reflect.SendDir { + b = append(b, "<-"...) + } + b = append(b, ' ') + b = appendTypeName(b, t.Elem(), qualified, false) + case reflect.Func: + if !elideFunc { + b = append(b, "func"...) + } + b = append(b, '(') + for i := 0; i < t.NumIn(); i++ { + if i > 0 { + b = append(b, ", "...) + } + if i == t.NumIn()-1 && t.IsVariadic() { + b = append(b, "..."...) + b = appendTypeName(b, t.In(i).Elem(), qualified, false) + } else { + b = appendTypeName(b, t.In(i), qualified, false) + } + } + b = append(b, ')') + switch t.NumOut() { + case 0: + // Do nothing + case 1: + b = append(b, ' ') + b = appendTypeName(b, t.Out(0), qualified, false) + default: + b = append(b, " ("...) + for i := 0; i < t.NumOut(); i++ { + if i > 0 { + b = append(b, ", "...) + } + b = appendTypeName(b, t.Out(i), qualified, false) + } + b = append(b, ')') + } + case reflect.Struct: + b = append(b, "struct{ "...) + for i := 0; i < t.NumField(); i++ { + if i > 0 { + b = append(b, "; "...) + } + sf := t.Field(i) + if !sf.Anonymous { + if qualified && sf.PkgPath != "" { + b = append(b, '"') + b = append(b, sf.PkgPath...) + b = append(b, '"') + b = append(b, '.') + } + b = append(b, sf.Name...) + b = append(b, ' ') + } + b = appendTypeName(b, sf.Type, qualified, false) + if sf.Tag != "" { + b = append(b, ' ') + b = strconv.AppendQuote(b, string(sf.Tag)) + } + } + if b[len(b)-1] == ' ' { + b = b[:len(b)-1] + } else { + b = append(b, ' ') + } + b = append(b, '}') + case reflect.Slice, reflect.Array: + b = append(b, '[') + if k == reflect.Array { + b = strconv.AppendUint(b, uint64(t.Len()), 10) + } + b = append(b, ']') + b = appendTypeName(b, t.Elem(), qualified, false) + case reflect.Map: + b = append(b, "map["...) + b = appendTypeName(b, t.Key(), qualified, false) + b = append(b, ']') + b = appendTypeName(b, t.Elem(), qualified, false) + case reflect.Ptr: + b = append(b, '*') + b = appendTypeName(b, t.Elem(), qualified, false) + case reflect.Interface: + b = append(b, "interface{ "...) + for i := 0; i < t.NumMethod(); i++ { + if i > 0 { + b = append(b, "; "...) + } + m := t.Method(i) + if qualified && m.PkgPath != "" { + b = append(b, '"') + b = append(b, m.PkgPath...) + b = append(b, '"') + b = append(b, '.') + } + b = append(b, m.Name...) + b = appendTypeName(b, m.Type, qualified, true) + } + if b[len(b)-1] == ' ' { + b = b[:len(b)-1] + } else { + b = append(b, ' ') + } + b = append(b, '}') + default: + panic("invalid kind: " + k.String()) + } + return b +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/pointer_purego.go b/vendor/github.com/google/go-cmp/cmp/internal/value/pointer_purego.go new file mode 100644 index 000000000..1a71bfcbd --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/value/pointer_purego.go @@ -0,0 +1,34 @@ +// Copyright 2018, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build purego +// +build purego + +package value + +import "reflect" + +// Pointer is an opaque typed pointer and is guaranteed to be comparable. +type Pointer struct { + p uintptr + t reflect.Type +} + +// PointerOf returns a Pointer from v, which must be a +// reflect.Ptr, reflect.Slice, or reflect.Map. +func PointerOf(v reflect.Value) Pointer { + // NOTE: Storing a pointer as an uintptr is technically incorrect as it + // assumes that the GC implementation does not use a moving collector. + return Pointer{v.Pointer(), v.Type()} +} + +// IsNil reports whether the pointer is nil. +func (p Pointer) IsNil() bool { + return p.p == 0 +} + +// Uintptr returns the pointer as a uintptr. +func (p Pointer) Uintptr() uintptr { + return p.p +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/pointer_unsafe.go b/vendor/github.com/google/go-cmp/cmp/internal/value/pointer_unsafe.go new file mode 100644 index 000000000..16e6860af --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/value/pointer_unsafe.go @@ -0,0 +1,37 @@ +// Copyright 2018, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !purego +// +build !purego + +package value + +import ( + "reflect" + "unsafe" +) + +// Pointer is an opaque typed pointer and is guaranteed to be comparable. +type Pointer struct { + p unsafe.Pointer + t reflect.Type +} + +// PointerOf returns a Pointer from v, which must be a +// reflect.Ptr, reflect.Slice, or reflect.Map. +func PointerOf(v reflect.Value) Pointer { + // The proper representation of a pointer is unsafe.Pointer, + // which is necessary if the GC ever uses a moving collector. + return Pointer{unsafe.Pointer(v.Pointer()), v.Type()} +} + +// IsNil reports whether the pointer is nil. +func (p Pointer) IsNil() bool { + return p.p == nil +} + +// Uintptr returns the pointer as a uintptr. +func (p Pointer) Uintptr() uintptr { + return uintptr(p.p) +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go b/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go new file mode 100644 index 000000000..98533b036 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go @@ -0,0 +1,106 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package value + +import ( + "fmt" + "math" + "reflect" + "sort" +) + +// SortKeys sorts a list of map keys, deduplicating keys if necessary. +// The type of each value must be comparable. +func SortKeys(vs []reflect.Value) []reflect.Value { + if len(vs) == 0 { + return vs + } + + // Sort the map keys. + sort.SliceStable(vs, func(i, j int) bool { return isLess(vs[i], vs[j]) }) + + // Deduplicate keys (fails for NaNs). + vs2 := vs[:1] + for _, v := range vs[1:] { + if isLess(vs2[len(vs2)-1], v) { + vs2 = append(vs2, v) + } + } + return vs2 +} + +// isLess is a generic function for sorting arbitrary map keys. +// The inputs must be of the same type and must be comparable. +func isLess(x, y reflect.Value) bool { + switch x.Type().Kind() { + case reflect.Bool: + return !x.Bool() && y.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return x.Int() < y.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return x.Uint() < y.Uint() + case reflect.Float32, reflect.Float64: + // NOTE: This does not sort -0 as less than +0 + // since Go maps treat -0 and +0 as equal keys. + fx, fy := x.Float(), y.Float() + return fx < fy || math.IsNaN(fx) && !math.IsNaN(fy) + case reflect.Complex64, reflect.Complex128: + cx, cy := x.Complex(), y.Complex() + rx, ix, ry, iy := real(cx), imag(cx), real(cy), imag(cy) + if rx == ry || (math.IsNaN(rx) && math.IsNaN(ry)) { + return ix < iy || math.IsNaN(ix) && !math.IsNaN(iy) + } + return rx < ry || math.IsNaN(rx) && !math.IsNaN(ry) + case reflect.Ptr, reflect.UnsafePointer, reflect.Chan: + return x.Pointer() < y.Pointer() + case reflect.String: + return x.String() < y.String() + case reflect.Array: + for i := 0; i < x.Len(); i++ { + if isLess(x.Index(i), y.Index(i)) { + return true + } + if isLess(y.Index(i), x.Index(i)) { + return false + } + } + return false + case reflect.Struct: + for i := 0; i < x.NumField(); i++ { + if isLess(x.Field(i), y.Field(i)) { + return true + } + if isLess(y.Field(i), x.Field(i)) { + return false + } + } + return false + case reflect.Interface: + vx, vy := x.Elem(), y.Elem() + if !vx.IsValid() || !vy.IsValid() { + return !vx.IsValid() && vy.IsValid() + } + tx, ty := vx.Type(), vy.Type() + if tx == ty { + return isLess(x.Elem(), y.Elem()) + } + if tx.Kind() != ty.Kind() { + return vx.Kind() < vy.Kind() + } + if tx.String() != ty.String() { + return tx.String() < ty.String() + } + if tx.PkgPath() != ty.PkgPath() { + return tx.PkgPath() < ty.PkgPath() + } + // This can happen in rare situations, so we fallback to just comparing + // the unique pointer for a reflect.Type. This guarantees deterministic + // ordering within a program, but it is obviously not stable. + return reflect.ValueOf(vx.Type()).Pointer() < reflect.ValueOf(vy.Type()).Pointer() + default: + // Must be Func, Map, or Slice; which are not comparable. + panic(fmt.Sprintf("%T is not comparable", x.Type())) + } +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/zero.go b/vendor/github.com/google/go-cmp/cmp/internal/value/zero.go new file mode 100644 index 000000000..9147a2997 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/value/zero.go @@ -0,0 +1,48 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package value + +import ( + "math" + "reflect" +) + +// IsZero reports whether v is the zero value. +// This does not rely on Interface and so can be used on unexported fields. +func IsZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Bool: + return v.Bool() == false + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return math.Float64bits(v.Float()) == 0 + case reflect.Complex64, reflect.Complex128: + return math.Float64bits(real(v.Complex())) == 0 && math.Float64bits(imag(v.Complex())) == 0 + case reflect.String: + return v.String() == "" + case reflect.UnsafePointer: + return v.Pointer() == 0 + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: + return v.IsNil() + case reflect.Array: + for i := 0; i < v.Len(); i++ { + if !IsZero(v.Index(i)) { + return false + } + } + return true + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + if !IsZero(v.Field(i)) { + return false + } + } + return true + } + return false +} diff --git a/vendor/github.com/google/go-cmp/cmp/options.go b/vendor/github.com/google/go-cmp/cmp/options.go new file mode 100644 index 000000000..e57b9eb53 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/options.go @@ -0,0 +1,552 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import ( + "fmt" + "reflect" + "regexp" + "strings" + + "github.com/google/go-cmp/cmp/internal/function" +) + +// Option configures for specific behavior of Equal and Diff. In particular, +// the fundamental Option functions (Ignore, Transformer, and Comparer), +// configure how equality is determined. +// +// The fundamental options may be composed with filters (FilterPath and +// FilterValues) to control the scope over which they are applied. +// +// The cmp/cmpopts package provides helper functions for creating options that +// may be used with Equal and Diff. +type Option interface { + // filter applies all filters and returns the option that remains. + // Each option may only read s.curPath and call s.callTTBFunc. + // + // An Options is returned only if multiple comparers or transformers + // can apply simultaneously and will only contain values of those types + // or sub-Options containing values of those types. + filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption +} + +// applicableOption represents the following types: +// Fundamental: ignore | validator | *comparer | *transformer +// Grouping: Options +type applicableOption interface { + Option + + // apply executes the option, which may mutate s or panic. + apply(s *state, vx, vy reflect.Value) +} + +// coreOption represents the following types: +// Fundamental: ignore | validator | *comparer | *transformer +// Filters: *pathFilter | *valuesFilter +type coreOption interface { + Option + isCore() +} + +type core struct{} + +func (core) isCore() {} + +// Options is a list of Option values that also satisfies the Option interface. +// Helper comparison packages may return an Options value when packing multiple +// Option values into a single Option. When this package processes an Options, +// it will be implicitly expanded into a flat list. +// +// Applying a filter on an Options is equivalent to applying that same filter +// on all individual options held within. +type Options []Option + +func (opts Options) filter(s *state, t reflect.Type, vx, vy reflect.Value) (out applicableOption) { + for _, opt := range opts { + switch opt := opt.filter(s, t, vx, vy); opt.(type) { + case ignore: + return ignore{} // Only ignore can short-circuit evaluation + case validator: + out = validator{} // Takes precedence over comparer or transformer + case *comparer, *transformer, Options: + switch out.(type) { + case nil: + out = opt + case validator: + // Keep validator + case *comparer, *transformer, Options: + out = Options{out, opt} // Conflicting comparers or transformers + } + } + } + return out +} + +func (opts Options) apply(s *state, _, _ reflect.Value) { + const warning = "ambiguous set of applicable options" + const help = "consider using filters to ensure at most one Comparer or Transformer may apply" + var ss []string + for _, opt := range flattenOptions(nil, opts) { + ss = append(ss, fmt.Sprint(opt)) + } + set := strings.Join(ss, "\n\t") + panic(fmt.Sprintf("%s at %#v:\n\t%s\n%s", warning, s.curPath, set, help)) +} + +func (opts Options) String() string { + var ss []string + for _, opt := range opts { + ss = append(ss, fmt.Sprint(opt)) + } + return fmt.Sprintf("Options{%s}", strings.Join(ss, ", ")) +} + +// FilterPath returns a new Option where opt is only evaluated if filter f +// returns true for the current Path in the value tree. +// +// This filter is called even if a slice element or map entry is missing and +// provides an opportunity to ignore such cases. The filter function must be +// symmetric such that the filter result is identical regardless of whether the +// missing value is from x or y. +// +// The option passed in may be an Ignore, Transformer, Comparer, Options, or +// a previously filtered Option. +func FilterPath(f func(Path) bool, opt Option) Option { + if f == nil { + panic("invalid path filter function") + } + if opt := normalizeOption(opt); opt != nil { + return &pathFilter{fnc: f, opt: opt} + } + return nil +} + +type pathFilter struct { + core + fnc func(Path) bool + opt Option +} + +func (f pathFilter) filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption { + if f.fnc(s.curPath) { + return f.opt.filter(s, t, vx, vy) + } + return nil +} + +func (f pathFilter) String() string { + return fmt.Sprintf("FilterPath(%s, %v)", function.NameOf(reflect.ValueOf(f.fnc)), f.opt) +} + +// FilterValues returns a new Option where opt is only evaluated if filter f, +// which is a function of the form "func(T, T) bool", returns true for the +// current pair of values being compared. If either value is invalid or +// the type of the values is not assignable to T, then this filter implicitly +// returns false. +// +// The filter function must be +// symmetric (i.e., agnostic to the order of the inputs) and +// deterministic (i.e., produces the same result when given the same inputs). +// If T is an interface, it is possible that f is called with two values with +// different concrete types that both implement T. +// +// The option passed in may be an Ignore, Transformer, Comparer, Options, or +// a previously filtered Option. +func FilterValues(f interface{}, opt Option) Option { + v := reflect.ValueOf(f) + if !function.IsType(v.Type(), function.ValueFilter) || v.IsNil() { + panic(fmt.Sprintf("invalid values filter function: %T", f)) + } + if opt := normalizeOption(opt); opt != nil { + vf := &valuesFilter{fnc: v, opt: opt} + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { + vf.typ = ti + } + return vf + } + return nil +} + +type valuesFilter struct { + core + typ reflect.Type // T + fnc reflect.Value // func(T, T) bool + opt Option +} + +func (f valuesFilter) filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption { + if !vx.IsValid() || !vx.CanInterface() || !vy.IsValid() || !vy.CanInterface() { + return nil + } + if (f.typ == nil || t.AssignableTo(f.typ)) && s.callTTBFunc(f.fnc, vx, vy) { + return f.opt.filter(s, t, vx, vy) + } + return nil +} + +func (f valuesFilter) String() string { + return fmt.Sprintf("FilterValues(%s, %v)", function.NameOf(f.fnc), f.opt) +} + +// Ignore is an Option that causes all comparisons to be ignored. +// This value is intended to be combined with FilterPath or FilterValues. +// It is an error to pass an unfiltered Ignore option to Equal. +func Ignore() Option { return ignore{} } + +type ignore struct{ core } + +func (ignore) isFiltered() bool { return false } +func (ignore) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { return ignore{} } +func (ignore) apply(s *state, _, _ reflect.Value) { s.report(true, reportByIgnore) } +func (ignore) String() string { return "Ignore()" } + +// validator is a sentinel Option type to indicate that some options could not +// be evaluated due to unexported fields, missing slice elements, or +// missing map entries. Both values are validator only for unexported fields. +type validator struct{ core } + +func (validator) filter(_ *state, _ reflect.Type, vx, vy reflect.Value) applicableOption { + if !vx.IsValid() || !vy.IsValid() { + return validator{} + } + if !vx.CanInterface() || !vy.CanInterface() { + return validator{} + } + return nil +} +func (validator) apply(s *state, vx, vy reflect.Value) { + // Implies missing slice element or map entry. + if !vx.IsValid() || !vy.IsValid() { + s.report(vx.IsValid() == vy.IsValid(), 0) + return + } + + // Unable to Interface implies unexported field without visibility access. + if !vx.CanInterface() || !vy.CanInterface() { + help := "consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported" + var name string + if t := s.curPath.Index(-2).Type(); t.Name() != "" { + // Named type with unexported fields. + name = fmt.Sprintf("%q.%v", t.PkgPath(), t.Name()) // e.g., "path/to/package".MyType + if _, ok := reflect.New(t).Interface().(error); ok { + help = "consider using cmpopts.EquateErrors to compare error values" + } + } else { + // Unnamed type with unexported fields. Derive PkgPath from field. + var pkgPath string + for i := 0; i < t.NumField() && pkgPath == ""; i++ { + pkgPath = t.Field(i).PkgPath + } + name = fmt.Sprintf("%q.(%v)", pkgPath, t.String()) // e.g., "path/to/package".(struct { a int }) + } + panic(fmt.Sprintf("cannot handle unexported field at %#v:\n\t%v\n%s", s.curPath, name, help)) + } + + panic("not reachable") +} + +// identRx represents a valid identifier according to the Go specification. +const identRx = `[_\p{L}][_\p{L}\p{N}]*` + +var identsRx = regexp.MustCompile(`^` + identRx + `(\.` + identRx + `)*$`) + +// Transformer returns an Option that applies a transformation function that +// converts values of a certain type into that of another. +// +// The transformer f must be a function "func(T) R" that converts values of +// type T to those of type R and is implicitly filtered to input values +// assignable to T. The transformer must not mutate T in any way. +// +// To help prevent some cases of infinite recursive cycles applying the +// same transform to the output of itself (e.g., in the case where the +// input and output types are the same), an implicit filter is added such that +// a transformer is applicable only if that exact transformer is not already +// in the tail of the Path since the last non-Transform step. +// For situations where the implicit filter is still insufficient, +// consider using cmpopts.AcyclicTransformer, which adds a filter +// to prevent the transformer from being recursively applied upon itself. +// +// The name is a user provided label that is used as the Transform.Name in the +// transformation PathStep (and eventually shown in the Diff output). +// The name must be a valid identifier or qualified identifier in Go syntax. +// If empty, an arbitrary name is used. +func Transformer(name string, f interface{}) Option { + v := reflect.ValueOf(f) + if !function.IsType(v.Type(), function.Transformer) || v.IsNil() { + panic(fmt.Sprintf("invalid transformer function: %T", f)) + } + if name == "" { + name = function.NameOf(v) + if !identsRx.MatchString(name) { + name = "λ" // Lambda-symbol as placeholder name + } + } else if !identsRx.MatchString(name) { + panic(fmt.Sprintf("invalid name: %q", name)) + } + tr := &transformer{name: name, fnc: reflect.ValueOf(f)} + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { + tr.typ = ti + } + return tr +} + +type transformer struct { + core + name string + typ reflect.Type // T + fnc reflect.Value // func(T) R +} + +func (tr *transformer) isFiltered() bool { return tr.typ != nil } + +func (tr *transformer) filter(s *state, t reflect.Type, _, _ reflect.Value) applicableOption { + for i := len(s.curPath) - 1; i >= 0; i-- { + if t, ok := s.curPath[i].(Transform); !ok { + break // Hit most recent non-Transform step + } else if tr == t.trans { + return nil // Cannot directly use same Transform + } + } + if tr.typ == nil || t.AssignableTo(tr.typ) { + return tr + } + return nil +} + +func (tr *transformer) apply(s *state, vx, vy reflect.Value) { + step := Transform{&transform{pathStep{typ: tr.fnc.Type().Out(0)}, tr}} + vvx := s.callTRFunc(tr.fnc, vx, step) + vvy := s.callTRFunc(tr.fnc, vy, step) + step.vx, step.vy = vvx, vvy + s.compareAny(step) +} + +func (tr transformer) String() string { + return fmt.Sprintf("Transformer(%s, %s)", tr.name, function.NameOf(tr.fnc)) +} + +// Comparer returns an Option that determines whether two values are equal +// to each other. +// +// The comparer f must be a function "func(T, T) bool" and is implicitly +// filtered to input values assignable to T. If T is an interface, it is +// possible that f is called with two values of different concrete types that +// both implement T. +// +// The equality function must be: +// • Symmetric: equal(x, y) == equal(y, x) +// • Deterministic: equal(x, y) == equal(x, y) +// • Pure: equal(x, y) does not modify x or y +func Comparer(f interface{}) Option { + v := reflect.ValueOf(f) + if !function.IsType(v.Type(), function.Equal) || v.IsNil() { + panic(fmt.Sprintf("invalid comparer function: %T", f)) + } + cm := &comparer{fnc: v} + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { + cm.typ = ti + } + return cm +} + +type comparer struct { + core + typ reflect.Type // T + fnc reflect.Value // func(T, T) bool +} + +func (cm *comparer) isFiltered() bool { return cm.typ != nil } + +func (cm *comparer) filter(_ *state, t reflect.Type, _, _ reflect.Value) applicableOption { + if cm.typ == nil || t.AssignableTo(cm.typ) { + return cm + } + return nil +} + +func (cm *comparer) apply(s *state, vx, vy reflect.Value) { + eq := s.callTTBFunc(cm.fnc, vx, vy) + s.report(eq, reportByFunc) +} + +func (cm comparer) String() string { + return fmt.Sprintf("Comparer(%s)", function.NameOf(cm.fnc)) +} + +// Exporter returns an Option that specifies whether Equal is allowed to +// introspect into the unexported fields of certain struct types. +// +// Users of this option must understand that comparing on unexported fields +// from external packages is not safe since changes in the internal +// implementation of some external package may cause the result of Equal +// to unexpectedly change. However, it may be valid to use this option on types +// defined in an internal package where the semantic meaning of an unexported +// field is in the control of the user. +// +// In many cases, a custom Comparer should be used instead that defines +// equality as a function of the public API of a type rather than the underlying +// unexported implementation. +// +// For example, the reflect.Type documentation defines equality to be determined +// by the == operator on the interface (essentially performing a shallow pointer +// comparison) and most attempts to compare *regexp.Regexp types are interested +// in only checking that the regular expression strings are equal. +// Both of these are accomplished using Comparers: +// +// Comparer(func(x, y reflect.Type) bool { return x == y }) +// Comparer(func(x, y *regexp.Regexp) bool { return x.String() == y.String() }) +// +// In other cases, the cmpopts.IgnoreUnexported option can be used to ignore +// all unexported fields on specified struct types. +func Exporter(f func(reflect.Type) bool) Option { + if !supportExporters { + panic("Exporter is not supported on purego builds") + } + return exporter(f) +} + +type exporter func(reflect.Type) bool + +func (exporter) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { + panic("not implemented") +} + +// AllowUnexported returns an Options that allows Equal to forcibly introspect +// unexported fields of the specified struct types. +// +// See Exporter for the proper use of this option. +func AllowUnexported(types ...interface{}) Option { + m := make(map[reflect.Type]bool) + for _, typ := range types { + t := reflect.TypeOf(typ) + if t.Kind() != reflect.Struct { + panic(fmt.Sprintf("invalid struct type: %T", typ)) + } + m[t] = true + } + return exporter(func(t reflect.Type) bool { return m[t] }) +} + +// Result represents the comparison result for a single node and +// is provided by cmp when calling Result (see Reporter). +type Result struct { + _ [0]func() // Make Result incomparable + flags resultFlags +} + +// Equal reports whether the node was determined to be equal or not. +// As a special case, ignored nodes are considered equal. +func (r Result) Equal() bool { + return r.flags&(reportEqual|reportByIgnore) != 0 +} + +// ByIgnore reports whether the node is equal because it was ignored. +// This never reports true if Equal reports false. +func (r Result) ByIgnore() bool { + return r.flags&reportByIgnore != 0 +} + +// ByMethod reports whether the Equal method determined equality. +func (r Result) ByMethod() bool { + return r.flags&reportByMethod != 0 +} + +// ByFunc reports whether a Comparer function determined equality. +func (r Result) ByFunc() bool { + return r.flags&reportByFunc != 0 +} + +// ByCycle reports whether a reference cycle was detected. +func (r Result) ByCycle() bool { + return r.flags&reportByCycle != 0 +} + +type resultFlags uint + +const ( + _ resultFlags = (1 << iota) / 2 + + reportEqual + reportUnequal + reportByIgnore + reportByMethod + reportByFunc + reportByCycle +) + +// Reporter is an Option that can be passed to Equal. When Equal traverses +// the value trees, it calls PushStep as it descends into each node in the +// tree and PopStep as it ascend out of the node. The leaves of the tree are +// either compared (determined to be equal or not equal) or ignored and reported +// as such by calling the Report method. +func Reporter(r interface { + // PushStep is called when a tree-traversal operation is performed. + // The PathStep itself is only valid until the step is popped. + // The PathStep.Values are valid for the duration of the entire traversal + // and must not be mutated. + // + // Equal always calls PushStep at the start to provide an operation-less + // PathStep used to report the root values. + // + // Within a slice, the exact set of inserted, removed, or modified elements + // is unspecified and may change in future implementations. + // The entries of a map are iterated through in an unspecified order. + PushStep(PathStep) + + // Report is called exactly once on leaf nodes to report whether the + // comparison identified the node as equal, unequal, or ignored. + // A leaf node is one that is immediately preceded by and followed by + // a pair of PushStep and PopStep calls. + Report(Result) + + // PopStep ascends back up the value tree. + // There is always a matching pop call for every push call. + PopStep() +}) Option { + return reporter{r} +} + +type reporter struct{ reporterIface } +type reporterIface interface { + PushStep(PathStep) + Report(Result) + PopStep() +} + +func (reporter) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { + panic("not implemented") +} + +// normalizeOption normalizes the input options such that all Options groups +// are flattened and groups with a single element are reduced to that element. +// Only coreOptions and Options containing coreOptions are allowed. +func normalizeOption(src Option) Option { + switch opts := flattenOptions(nil, Options{src}); len(opts) { + case 0: + return nil + case 1: + return opts[0] + default: + return opts + } +} + +// flattenOptions copies all options in src to dst as a flat list. +// Only coreOptions and Options containing coreOptions are allowed. +func flattenOptions(dst, src Options) Options { + for _, opt := range src { + switch opt := opt.(type) { + case nil: + continue + case Options: + dst = flattenOptions(dst, opt) + case coreOption: + dst = append(dst, opt) + default: + panic(fmt.Sprintf("invalid option type: %T", opt)) + } + } + return dst +} diff --git a/vendor/github.com/google/go-cmp/cmp/path.go b/vendor/github.com/google/go-cmp/cmp/path.go new file mode 100644 index 000000000..c71003463 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/path.go @@ -0,0 +1,378 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import ( + "fmt" + "reflect" + "strings" + "unicode" + "unicode/utf8" + + "github.com/google/go-cmp/cmp/internal/value" +) + +// Path is a list of PathSteps describing the sequence of operations to get +// from some root type to the current position in the value tree. +// The first Path element is always an operation-less PathStep that exists +// simply to identify the initial type. +// +// When traversing structs with embedded structs, the embedded struct will +// always be accessed as a field before traversing the fields of the +// embedded struct themselves. That is, an exported field from the +// embedded struct will never be accessed directly from the parent struct. +type Path []PathStep + +// PathStep is a union-type for specific operations to traverse +// a value's tree structure. Users of this package never need to implement +// these types as values of this type will be returned by this package. +// +// Implementations of this interface are +// StructField, SliceIndex, MapIndex, Indirect, TypeAssertion, and Transform. +type PathStep interface { + String() string + + // Type is the resulting type after performing the path step. + Type() reflect.Type + + // Values is the resulting values after performing the path step. + // The type of each valid value is guaranteed to be identical to Type. + // + // In some cases, one or both may be invalid or have restrictions: + // • For StructField, both are not interface-able if the current field + // is unexported and the struct type is not explicitly permitted by + // an Exporter to traverse unexported fields. + // • For SliceIndex, one may be invalid if an element is missing from + // either the x or y slice. + // • For MapIndex, one may be invalid if an entry is missing from + // either the x or y map. + // + // The provided values must not be mutated. + Values() (vx, vy reflect.Value) +} + +var ( + _ PathStep = StructField{} + _ PathStep = SliceIndex{} + _ PathStep = MapIndex{} + _ PathStep = Indirect{} + _ PathStep = TypeAssertion{} + _ PathStep = Transform{} +) + +func (pa *Path) push(s PathStep) { + *pa = append(*pa, s) +} + +func (pa *Path) pop() { + *pa = (*pa)[:len(*pa)-1] +} + +// Last returns the last PathStep in the Path. +// If the path is empty, this returns a non-nil PathStep that reports a nil Type. +func (pa Path) Last() PathStep { + return pa.Index(-1) +} + +// Index returns the ith step in the Path and supports negative indexing. +// A negative index starts counting from the tail of the Path such that -1 +// refers to the last step, -2 refers to the second-to-last step, and so on. +// If index is invalid, this returns a non-nil PathStep that reports a nil Type. +func (pa Path) Index(i int) PathStep { + if i < 0 { + i = len(pa) + i + } + if i < 0 || i >= len(pa) { + return pathStep{} + } + return pa[i] +} + +// String returns the simplified path to a node. +// The simplified path only contains struct field accesses. +// +// For example: +// MyMap.MySlices.MyField +func (pa Path) String() string { + var ss []string + for _, s := range pa { + if _, ok := s.(StructField); ok { + ss = append(ss, s.String()) + } + } + return strings.TrimPrefix(strings.Join(ss, ""), ".") +} + +// GoString returns the path to a specific node using Go syntax. +// +// For example: +// (*root.MyMap["key"].(*mypkg.MyStruct).MySlices)[2][3].MyField +func (pa Path) GoString() string { + var ssPre, ssPost []string + var numIndirect int + for i, s := range pa { + var nextStep PathStep + if i+1 < len(pa) { + nextStep = pa[i+1] + } + switch s := s.(type) { + case Indirect: + numIndirect++ + pPre, pPost := "(", ")" + switch nextStep.(type) { + case Indirect: + continue // Next step is indirection, so let them batch up + case StructField: + numIndirect-- // Automatic indirection on struct fields + case nil: + pPre, pPost = "", "" // Last step; no need for parenthesis + } + if numIndirect > 0 { + ssPre = append(ssPre, pPre+strings.Repeat("*", numIndirect)) + ssPost = append(ssPost, pPost) + } + numIndirect = 0 + continue + case Transform: + ssPre = append(ssPre, s.trans.name+"(") + ssPost = append(ssPost, ")") + continue + } + ssPost = append(ssPost, s.String()) + } + for i, j := 0, len(ssPre)-1; i < j; i, j = i+1, j-1 { + ssPre[i], ssPre[j] = ssPre[j], ssPre[i] + } + return strings.Join(ssPre, "") + strings.Join(ssPost, "") +} + +type pathStep struct { + typ reflect.Type + vx, vy reflect.Value +} + +func (ps pathStep) Type() reflect.Type { return ps.typ } +func (ps pathStep) Values() (vx, vy reflect.Value) { return ps.vx, ps.vy } +func (ps pathStep) String() string { + if ps.typ == nil { + return "" + } + s := ps.typ.String() + if s == "" || strings.ContainsAny(s, "{}\n") { + return "root" // Type too simple or complex to print + } + return fmt.Sprintf("{%s}", s) +} + +// StructField represents a struct field access on a field called Name. +type StructField struct{ *structField } +type structField struct { + pathStep + name string + idx int + + // These fields are used for forcibly accessing an unexported field. + // pvx, pvy, and field are only valid if unexported is true. + unexported bool + mayForce bool // Forcibly allow visibility + paddr bool // Was parent addressable? + pvx, pvy reflect.Value // Parent values (always addressable) + field reflect.StructField // Field information +} + +func (sf StructField) Type() reflect.Type { return sf.typ } +func (sf StructField) Values() (vx, vy reflect.Value) { + if !sf.unexported { + return sf.vx, sf.vy // CanInterface reports true + } + + // Forcibly obtain read-write access to an unexported struct field. + if sf.mayForce { + vx = retrieveUnexportedField(sf.pvx, sf.field, sf.paddr) + vy = retrieveUnexportedField(sf.pvy, sf.field, sf.paddr) + return vx, vy // CanInterface reports true + } + return sf.vx, sf.vy // CanInterface reports false +} +func (sf StructField) String() string { return fmt.Sprintf(".%s", sf.name) } + +// Name is the field name. +func (sf StructField) Name() string { return sf.name } + +// Index is the index of the field in the parent struct type. +// See reflect.Type.Field. +func (sf StructField) Index() int { return sf.idx } + +// SliceIndex is an index operation on a slice or array at some index Key. +type SliceIndex struct{ *sliceIndex } +type sliceIndex struct { + pathStep + xkey, ykey int + isSlice bool // False for reflect.Array +} + +func (si SliceIndex) Type() reflect.Type { return si.typ } +func (si SliceIndex) Values() (vx, vy reflect.Value) { return si.vx, si.vy } +func (si SliceIndex) String() string { + switch { + case si.xkey == si.ykey: + return fmt.Sprintf("[%d]", si.xkey) + case si.ykey == -1: + // [5->?] means "I don't know where X[5] went" + return fmt.Sprintf("[%d->?]", si.xkey) + case si.xkey == -1: + // [?->3] means "I don't know where Y[3] came from" + return fmt.Sprintf("[?->%d]", si.ykey) + default: + // [5->3] means "X[5] moved to Y[3]" + return fmt.Sprintf("[%d->%d]", si.xkey, si.ykey) + } +} + +// Key is the index key; it may return -1 if in a split state +func (si SliceIndex) Key() int { + if si.xkey != si.ykey { + return -1 + } + return si.xkey +} + +// SplitKeys are the indexes for indexing into slices in the +// x and y values, respectively. These indexes may differ due to the +// insertion or removal of an element in one of the slices, causing +// all of the indexes to be shifted. If an index is -1, then that +// indicates that the element does not exist in the associated slice. +// +// Key is guaranteed to return -1 if and only if the indexes returned +// by SplitKeys are not the same. SplitKeys will never return -1 for +// both indexes. +func (si SliceIndex) SplitKeys() (ix, iy int) { return si.xkey, si.ykey } + +// MapIndex is an index operation on a map at some index Key. +type MapIndex struct{ *mapIndex } +type mapIndex struct { + pathStep + key reflect.Value +} + +func (mi MapIndex) Type() reflect.Type { return mi.typ } +func (mi MapIndex) Values() (vx, vy reflect.Value) { return mi.vx, mi.vy } +func (mi MapIndex) String() string { return fmt.Sprintf("[%#v]", mi.key) } + +// Key is the value of the map key. +func (mi MapIndex) Key() reflect.Value { return mi.key } + +// Indirect represents pointer indirection on the parent type. +type Indirect struct{ *indirect } +type indirect struct { + pathStep +} + +func (in Indirect) Type() reflect.Type { return in.typ } +func (in Indirect) Values() (vx, vy reflect.Value) { return in.vx, in.vy } +func (in Indirect) String() string { return "*" } + +// TypeAssertion represents a type assertion on an interface. +type TypeAssertion struct{ *typeAssertion } +type typeAssertion struct { + pathStep +} + +func (ta TypeAssertion) Type() reflect.Type { return ta.typ } +func (ta TypeAssertion) Values() (vx, vy reflect.Value) { return ta.vx, ta.vy } +func (ta TypeAssertion) String() string { return fmt.Sprintf(".(%v)", ta.typ) } + +// Transform is a transformation from the parent type to the current type. +type Transform struct{ *transform } +type transform struct { + pathStep + trans *transformer +} + +func (tf Transform) Type() reflect.Type { return tf.typ } +func (tf Transform) Values() (vx, vy reflect.Value) { return tf.vx, tf.vy } +func (tf Transform) String() string { return fmt.Sprintf("%s()", tf.trans.name) } + +// Name is the name of the Transformer. +func (tf Transform) Name() string { return tf.trans.name } + +// Func is the function pointer to the transformer function. +func (tf Transform) Func() reflect.Value { return tf.trans.fnc } + +// Option returns the originally constructed Transformer option. +// The == operator can be used to detect the exact option used. +func (tf Transform) Option() Option { return tf.trans } + +// pointerPath represents a dual-stack of pointers encountered when +// recursively traversing the x and y values. This data structure supports +// detection of cycles and determining whether the cycles are equal. +// In Go, cycles can occur via pointers, slices, and maps. +// +// The pointerPath uses a map to represent a stack; where descension into a +// pointer pushes the address onto the stack, and ascension from a pointer +// pops the address from the stack. Thus, when traversing into a pointer from +// reflect.Ptr, reflect.Slice element, or reflect.Map, we can detect cycles +// by checking whether the pointer has already been visited. The cycle detection +// uses a separate stack for the x and y values. +// +// If a cycle is detected we need to determine whether the two pointers +// should be considered equal. The definition of equality chosen by Equal +// requires two graphs to have the same structure. To determine this, both the +// x and y values must have a cycle where the previous pointers were also +// encountered together as a pair. +// +// Semantically, this is equivalent to augmenting Indirect, SliceIndex, and +// MapIndex with pointer information for the x and y values. +// Suppose px and py are two pointers to compare, we then search the +// Path for whether px was ever encountered in the Path history of x, and +// similarly so with py. If either side has a cycle, the comparison is only +// equal if both px and py have a cycle resulting from the same PathStep. +// +// Using a map as a stack is more performant as we can perform cycle detection +// in O(1) instead of O(N) where N is len(Path). +type pointerPath struct { + // mx is keyed by x pointers, where the value is the associated y pointer. + mx map[value.Pointer]value.Pointer + // my is keyed by y pointers, where the value is the associated x pointer. + my map[value.Pointer]value.Pointer +} + +func (p *pointerPath) Init() { + p.mx = make(map[value.Pointer]value.Pointer) + p.my = make(map[value.Pointer]value.Pointer) +} + +// Push indicates intent to descend into pointers vx and vy where +// visited reports whether either has been seen before. If visited before, +// equal reports whether both pointers were encountered together. +// Pop must be called if and only if the pointers were never visited. +// +// The pointers vx and vy must be a reflect.Ptr, reflect.Slice, or reflect.Map +// and be non-nil. +func (p pointerPath) Push(vx, vy reflect.Value) (equal, visited bool) { + px := value.PointerOf(vx) + py := value.PointerOf(vy) + _, ok1 := p.mx[px] + _, ok2 := p.my[py] + if ok1 || ok2 { + equal = p.mx[px] == py && p.my[py] == px // Pointers paired together + return equal, true + } + p.mx[px] = py + p.my[py] = px + return false, false +} + +// Pop ascends from pointers vx and vy. +func (p pointerPath) Pop(vx, vy reflect.Value) { + delete(p.mx, value.PointerOf(vx)) + delete(p.my, value.PointerOf(vy)) +} + +// isExported reports whether the identifier is exported. +func isExported(id string) bool { + r, _ := utf8.DecodeRuneInString(id) + return unicode.IsUpper(r) +} diff --git a/vendor/github.com/google/go-cmp/cmp/report.go b/vendor/github.com/google/go-cmp/cmp/report.go new file mode 100644 index 000000000..f43cd12eb --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/report.go @@ -0,0 +1,54 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +// defaultReporter implements the reporter interface. +// +// As Equal serially calls the PushStep, Report, and PopStep methods, the +// defaultReporter constructs a tree-based representation of the compared value +// and the result of each comparison (see valueNode). +// +// When the String method is called, the FormatDiff method transforms the +// valueNode tree into a textNode tree, which is a tree-based representation +// of the textual output (see textNode). +// +// Lastly, the textNode.String method produces the final report as a string. +type defaultReporter struct { + root *valueNode + curr *valueNode +} + +func (r *defaultReporter) PushStep(ps PathStep) { + r.curr = r.curr.PushStep(ps) + if r.root == nil { + r.root = r.curr + } +} +func (r *defaultReporter) Report(rs Result) { + r.curr.Report(rs) +} +func (r *defaultReporter) PopStep() { + r.curr = r.curr.PopStep() +} + +// String provides a full report of the differences detected as a structured +// literal in pseudo-Go syntax. String may only be called after the entire tree +// has been traversed. +func (r *defaultReporter) String() string { + assert(r.root != nil && r.curr == nil) + if r.root.NumDiff == 0 { + return "" + } + ptrs := new(pointerReferences) + text := formatOptions{}.FormatDiff(r.root, ptrs) + resolveReferences(text) + return text.String() +} + +func assert(ok bool) { + if !ok { + panic("assertion failure") + } +} diff --git a/vendor/github.com/google/go-cmp/cmp/report_compare.go b/vendor/github.com/google/go-cmp/cmp/report_compare.go new file mode 100644 index 000000000..104bb3053 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/report_compare.go @@ -0,0 +1,432 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import ( + "fmt" + "reflect" + + "github.com/google/go-cmp/cmp/internal/value" +) + +// numContextRecords is the number of surrounding equal records to print. +const numContextRecords = 2 + +type diffMode byte + +const ( + diffUnknown diffMode = 0 + diffIdentical diffMode = ' ' + diffRemoved diffMode = '-' + diffInserted diffMode = '+' +) + +type typeMode int + +const ( + // emitType always prints the type. + emitType typeMode = iota + // elideType never prints the type. + elideType + // autoType prints the type only for composite kinds + // (i.e., structs, slices, arrays, and maps). + autoType +) + +type formatOptions struct { + // DiffMode controls the output mode of FormatDiff. + // + // If diffUnknown, then produce a diff of the x and y values. + // If diffIdentical, then emit values as if they were equal. + // If diffRemoved, then only emit x values (ignoring y values). + // If diffInserted, then only emit y values (ignoring x values). + DiffMode diffMode + + // TypeMode controls whether to print the type for the current node. + // + // As a general rule of thumb, we always print the type of the next node + // after an interface, and always elide the type of the next node after + // a slice or map node. + TypeMode typeMode + + // formatValueOptions are options specific to printing reflect.Values. + formatValueOptions +} + +func (opts formatOptions) WithDiffMode(d diffMode) formatOptions { + opts.DiffMode = d + return opts +} +func (opts formatOptions) WithTypeMode(t typeMode) formatOptions { + opts.TypeMode = t + return opts +} +func (opts formatOptions) WithVerbosity(level int) formatOptions { + opts.VerbosityLevel = level + opts.LimitVerbosity = true + return opts +} +func (opts formatOptions) verbosity() uint { + switch { + case opts.VerbosityLevel < 0: + return 0 + case opts.VerbosityLevel > 16: + return 16 // some reasonable maximum to avoid shift overflow + default: + return uint(opts.VerbosityLevel) + } +} + +const maxVerbosityPreset = 6 + +// verbosityPreset modifies the verbosity settings given an index +// between 0 and maxVerbosityPreset, inclusive. +func verbosityPreset(opts formatOptions, i int) formatOptions { + opts.VerbosityLevel = int(opts.verbosity()) + 2*i + if i > 0 { + opts.AvoidStringer = true + } + if i >= maxVerbosityPreset { + opts.PrintAddresses = true + opts.QualifiedNames = true + } + return opts +} + +// FormatDiff converts a valueNode tree into a textNode tree, where the later +// is a textual representation of the differences detected in the former. +func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out textNode) { + if opts.DiffMode == diffIdentical { + opts = opts.WithVerbosity(1) + } else if opts.verbosity() < 3 { + opts = opts.WithVerbosity(3) + } + + // Check whether we have specialized formatting for this node. + // This is not necessary, but helpful for producing more readable outputs. + if opts.CanFormatDiffSlice(v) { + return opts.FormatDiffSlice(v) + } + + var parentKind reflect.Kind + if v.parent != nil && v.parent.TransformerName == "" { + parentKind = v.parent.Type.Kind() + } + + // For leaf nodes, format the value based on the reflect.Values alone. + if v.MaxDepth == 0 { + switch opts.DiffMode { + case diffUnknown, diffIdentical: + // Format Equal. + if v.NumDiff == 0 { + outx := opts.FormatValue(v.ValueX, parentKind, ptrs) + outy := opts.FormatValue(v.ValueY, parentKind, ptrs) + if v.NumIgnored > 0 && v.NumSame == 0 { + return textEllipsis + } else if outx.Len() < outy.Len() { + return outx + } else { + return outy + } + } + + // Format unequal. + assert(opts.DiffMode == diffUnknown) + var list textList + outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, parentKind, ptrs) + outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, parentKind, ptrs) + for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ { + opts2 := verbosityPreset(opts, i).WithTypeMode(elideType) + outx = opts2.FormatValue(v.ValueX, parentKind, ptrs) + outy = opts2.FormatValue(v.ValueY, parentKind, ptrs) + } + if outx != nil { + list = append(list, textRecord{Diff: '-', Value: outx}) + } + if outy != nil { + list = append(list, textRecord{Diff: '+', Value: outy}) + } + return opts.WithTypeMode(emitType).FormatType(v.Type, list) + case diffRemoved: + return opts.FormatValue(v.ValueX, parentKind, ptrs) + case diffInserted: + return opts.FormatValue(v.ValueY, parentKind, ptrs) + default: + panic("invalid diff mode") + } + } + + // Register slice element to support cycle detection. + if parentKind == reflect.Slice { + ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, true) + defer ptrs.Pop() + defer func() { out = wrapTrunkReferences(ptrRefs, out) }() + } + + // Descend into the child value node. + if v.TransformerName != "" { + out := opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs) + out = &textWrap{Prefix: "Inverse(" + v.TransformerName + ", ", Value: out, Suffix: ")"} + return opts.FormatType(v.Type, out) + } else { + switch k := v.Type.Kind(); k { + case reflect.Struct, reflect.Array, reflect.Slice: + out = opts.formatDiffList(v.Records, k, ptrs) + out = opts.FormatType(v.Type, out) + case reflect.Map: + // Register map to support cycle detection. + ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false) + defer ptrs.Pop() + + out = opts.formatDiffList(v.Records, k, ptrs) + out = wrapTrunkReferences(ptrRefs, out) + out = opts.FormatType(v.Type, out) + case reflect.Ptr: + // Register pointer to support cycle detection. + ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false) + defer ptrs.Pop() + + out = opts.FormatDiff(v.Value, ptrs) + out = wrapTrunkReferences(ptrRefs, out) + out = &textWrap{Prefix: "&", Value: out} + case reflect.Interface: + out = opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs) + default: + panic(fmt.Sprintf("%v cannot have children", k)) + } + return out + } +} + +func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, ptrs *pointerReferences) textNode { + // Derive record name based on the data structure kind. + var name string + var formatKey func(reflect.Value) string + switch k { + case reflect.Struct: + name = "field" + opts = opts.WithTypeMode(autoType) + formatKey = func(v reflect.Value) string { return v.String() } + case reflect.Slice, reflect.Array: + name = "element" + opts = opts.WithTypeMode(elideType) + formatKey = func(reflect.Value) string { return "" } + case reflect.Map: + name = "entry" + opts = opts.WithTypeMode(elideType) + formatKey = func(v reflect.Value) string { return formatMapKey(v, false, ptrs) } + } + + maxLen := -1 + if opts.LimitVerbosity { + if opts.DiffMode == diffIdentical { + maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... + } else { + maxLen = (1 << opts.verbosity()) << 1 // 2, 4, 8, 16, 32, 64, etc... + } + opts.VerbosityLevel-- + } + + // Handle unification. + switch opts.DiffMode { + case diffIdentical, diffRemoved, diffInserted: + var list textList + var deferredEllipsis bool // Add final "..." to indicate records were dropped + for _, r := range recs { + if len(list) == maxLen { + deferredEllipsis = true + break + } + + // Elide struct fields that are zero value. + if k == reflect.Struct { + var isZero bool + switch opts.DiffMode { + case diffIdentical: + isZero = value.IsZero(r.Value.ValueX) || value.IsZero(r.Value.ValueY) + case diffRemoved: + isZero = value.IsZero(r.Value.ValueX) + case diffInserted: + isZero = value.IsZero(r.Value.ValueY) + } + if isZero { + continue + } + } + // Elide ignored nodes. + if r.Value.NumIgnored > 0 && r.Value.NumSame+r.Value.NumDiff == 0 { + deferredEllipsis = !(k == reflect.Slice || k == reflect.Array) + if !deferredEllipsis { + list.AppendEllipsis(diffStats{}) + } + continue + } + if out := opts.FormatDiff(r.Value, ptrs); out != nil { + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + } + } + if deferredEllipsis { + list.AppendEllipsis(diffStats{}) + } + return &textWrap{Prefix: "{", Value: list, Suffix: "}"} + case diffUnknown: + default: + panic("invalid diff mode") + } + + // Handle differencing. + var numDiffs int + var list textList + var keys []reflect.Value // invariant: len(list) == len(keys) + groups := coalesceAdjacentRecords(name, recs) + maxGroup := diffStats{Name: name} + for i, ds := range groups { + if maxLen >= 0 && numDiffs >= maxLen { + maxGroup = maxGroup.Append(ds) + continue + } + + // Handle equal records. + if ds.NumDiff() == 0 { + // Compute the number of leading and trailing records to print. + var numLo, numHi int + numEqual := ds.NumIgnored + ds.NumIdentical + for numLo < numContextRecords && numLo+numHi < numEqual && i != 0 { + if r := recs[numLo].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 { + break + } + numLo++ + } + for numHi < numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 { + if r := recs[numEqual-numHi-1].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 { + break + } + numHi++ + } + if numEqual-(numLo+numHi) == 1 && ds.NumIgnored == 0 { + numHi++ // Avoid pointless coalescing of a single equal record + } + + // Format the equal values. + for _, r := range recs[:numLo] { + out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs) + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + keys = append(keys, r.Key) + } + if numEqual > numLo+numHi { + ds.NumIdentical -= numLo + numHi + list.AppendEllipsis(ds) + for len(keys) < len(list) { + keys = append(keys, reflect.Value{}) + } + } + for _, r := range recs[numEqual-numHi : numEqual] { + out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs) + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + keys = append(keys, r.Key) + } + recs = recs[numEqual:] + continue + } + + // Handle unequal records. + for _, r := range recs[:ds.NumDiff()] { + switch { + case opts.CanFormatDiffSlice(r.Value): + out := opts.FormatDiffSlice(r.Value) + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + keys = append(keys, r.Key) + case r.Value.NumChildren == r.Value.MaxDepth: + outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs) + outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs) + for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ { + opts2 := verbosityPreset(opts, i) + outx = opts2.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs) + outy = opts2.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs) + } + if outx != nil { + list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx}) + keys = append(keys, r.Key) + } + if outy != nil { + list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy}) + keys = append(keys, r.Key) + } + default: + out := opts.FormatDiff(r.Value, ptrs) + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + keys = append(keys, r.Key) + } + } + recs = recs[ds.NumDiff():] + numDiffs += ds.NumDiff() + } + if maxGroup.IsZero() { + assert(len(recs) == 0) + } else { + list.AppendEllipsis(maxGroup) + for len(keys) < len(list) { + keys = append(keys, reflect.Value{}) + } + } + assert(len(list) == len(keys)) + + // For maps, the default formatting logic uses fmt.Stringer which may + // produce ambiguous output. Avoid calling String to disambiguate. + if k == reflect.Map { + var ambiguous bool + seenKeys := map[string]reflect.Value{} + for i, currKey := range keys { + if currKey.IsValid() { + strKey := list[i].Key + prevKey, seen := seenKeys[strKey] + if seen && prevKey.CanInterface() && currKey.CanInterface() { + ambiguous = prevKey.Interface() != currKey.Interface() + if ambiguous { + break + } + } + seenKeys[strKey] = currKey + } + } + if ambiguous { + for i, k := range keys { + if k.IsValid() { + list[i].Key = formatMapKey(k, true, ptrs) + } + } + } + } + + return &textWrap{Prefix: "{", Value: list, Suffix: "}"} +} + +// coalesceAdjacentRecords coalesces the list of records into groups of +// adjacent equal, or unequal counts. +func coalesceAdjacentRecords(name string, recs []reportRecord) (groups []diffStats) { + var prevCase int // Arbitrary index into which case last occurred + lastStats := func(i int) *diffStats { + if prevCase != i { + groups = append(groups, diffStats{Name: name}) + prevCase = i + } + return &groups[len(groups)-1] + } + for _, r := range recs { + switch rv := r.Value; { + case rv.NumIgnored > 0 && rv.NumSame+rv.NumDiff == 0: + lastStats(1).NumIgnored++ + case rv.NumDiff == 0: + lastStats(1).NumIdentical++ + case rv.NumDiff > 0 && !rv.ValueY.IsValid(): + lastStats(2).NumRemoved++ + case rv.NumDiff > 0 && !rv.ValueX.IsValid(): + lastStats(2).NumInserted++ + default: + lastStats(2).NumModified++ + } + } + return groups +} diff --git a/vendor/github.com/google/go-cmp/cmp/report_references.go b/vendor/github.com/google/go-cmp/cmp/report_references.go new file mode 100644 index 000000000..be31b33a9 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/report_references.go @@ -0,0 +1,264 @@ +// Copyright 2020, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp/internal/flags" + "github.com/google/go-cmp/cmp/internal/value" +) + +const ( + pointerDelimPrefix = "⟪" + pointerDelimSuffix = "⟫" +) + +// formatPointer prints the address of the pointer. +func formatPointer(p value.Pointer, withDelims bool) string { + v := p.Uintptr() + if flags.Deterministic { + v = 0xdeadf00f // Only used for stable testing purposes + } + if withDelims { + return pointerDelimPrefix + formatHex(uint64(v)) + pointerDelimSuffix + } + return formatHex(uint64(v)) +} + +// pointerReferences is a stack of pointers visited so far. +type pointerReferences [][2]value.Pointer + +func (ps *pointerReferences) PushPair(vx, vy reflect.Value, d diffMode, deref bool) (pp [2]value.Pointer) { + if deref && vx.IsValid() { + vx = vx.Addr() + } + if deref && vy.IsValid() { + vy = vy.Addr() + } + switch d { + case diffUnknown, diffIdentical: + pp = [2]value.Pointer{value.PointerOf(vx), value.PointerOf(vy)} + case diffRemoved: + pp = [2]value.Pointer{value.PointerOf(vx), value.Pointer{}} + case diffInserted: + pp = [2]value.Pointer{value.Pointer{}, value.PointerOf(vy)} + } + *ps = append(*ps, pp) + return pp +} + +func (ps *pointerReferences) Push(v reflect.Value) (p value.Pointer, seen bool) { + p = value.PointerOf(v) + for _, pp := range *ps { + if p == pp[0] || p == pp[1] { + return p, true + } + } + *ps = append(*ps, [2]value.Pointer{p, p}) + return p, false +} + +func (ps *pointerReferences) Pop() { + *ps = (*ps)[:len(*ps)-1] +} + +// trunkReferences is metadata for a textNode indicating that the sub-tree +// represents the value for either pointer in a pair of references. +type trunkReferences struct{ pp [2]value.Pointer } + +// trunkReference is metadata for a textNode indicating that the sub-tree +// represents the value for the given pointer reference. +type trunkReference struct{ p value.Pointer } + +// leafReference is metadata for a textNode indicating that the value is +// truncated as it refers to another part of the tree (i.e., a trunk). +type leafReference struct{ p value.Pointer } + +func wrapTrunkReferences(pp [2]value.Pointer, s textNode) textNode { + switch { + case pp[0].IsNil(): + return &textWrap{Value: s, Metadata: trunkReference{pp[1]}} + case pp[1].IsNil(): + return &textWrap{Value: s, Metadata: trunkReference{pp[0]}} + case pp[0] == pp[1]: + return &textWrap{Value: s, Metadata: trunkReference{pp[0]}} + default: + return &textWrap{Value: s, Metadata: trunkReferences{pp}} + } +} +func wrapTrunkReference(p value.Pointer, printAddress bool, s textNode) textNode { + var prefix string + if printAddress { + prefix = formatPointer(p, true) + } + return &textWrap{Prefix: prefix, Value: s, Metadata: trunkReference{p}} +} +func makeLeafReference(p value.Pointer, printAddress bool) textNode { + out := &textWrap{Prefix: "(", Value: textEllipsis, Suffix: ")"} + var prefix string + if printAddress { + prefix = formatPointer(p, true) + } + return &textWrap{Prefix: prefix, Value: out, Metadata: leafReference{p}} +} + +// resolveReferences walks the textNode tree searching for any leaf reference +// metadata and resolves each against the corresponding trunk references. +// Since pointer addresses in memory are not particularly readable to the user, +// it replaces each pointer value with an arbitrary and unique reference ID. +func resolveReferences(s textNode) { + var walkNodes func(textNode, func(textNode)) + walkNodes = func(s textNode, f func(textNode)) { + f(s) + switch s := s.(type) { + case *textWrap: + walkNodes(s.Value, f) + case textList: + for _, r := range s { + walkNodes(r.Value, f) + } + } + } + + // Collect all trunks and leaves with reference metadata. + var trunks, leaves []*textWrap + walkNodes(s, func(s textNode) { + if s, ok := s.(*textWrap); ok { + switch s.Metadata.(type) { + case leafReference: + leaves = append(leaves, s) + case trunkReference, trunkReferences: + trunks = append(trunks, s) + } + } + }) + + // No leaf references to resolve. + if len(leaves) == 0 { + return + } + + // Collect the set of all leaf references to resolve. + leafPtrs := make(map[value.Pointer]bool) + for _, leaf := range leaves { + leafPtrs[leaf.Metadata.(leafReference).p] = true + } + + // Collect the set of trunk pointers that are always paired together. + // This allows us to assign a single ID to both pointers for brevity. + // If a pointer in a pair ever occurs by itself or as a different pair, + // then the pair is broken. + pairedTrunkPtrs := make(map[value.Pointer]value.Pointer) + unpair := func(p value.Pointer) { + if !pairedTrunkPtrs[p].IsNil() { + pairedTrunkPtrs[pairedTrunkPtrs[p]] = value.Pointer{} // invalidate other half + } + pairedTrunkPtrs[p] = value.Pointer{} // invalidate this half + } + for _, trunk := range trunks { + switch p := trunk.Metadata.(type) { + case trunkReference: + unpair(p.p) // standalone pointer cannot be part of a pair + case trunkReferences: + p0, ok0 := pairedTrunkPtrs[p.pp[0]] + p1, ok1 := pairedTrunkPtrs[p.pp[1]] + switch { + case !ok0 && !ok1: + // Register the newly seen pair. + pairedTrunkPtrs[p.pp[0]] = p.pp[1] + pairedTrunkPtrs[p.pp[1]] = p.pp[0] + case ok0 && ok1 && p0 == p.pp[1] && p1 == p.pp[0]: + // Exact pair already seen; do nothing. + default: + // Pair conflicts with some other pair; break all pairs. + unpair(p.pp[0]) + unpair(p.pp[1]) + } + } + } + + // Correlate each pointer referenced by leaves to a unique identifier, + // and print the IDs for each trunk that matches those pointers. + var nextID uint + ptrIDs := make(map[value.Pointer]uint) + newID := func() uint { + id := nextID + nextID++ + return id + } + for _, trunk := range trunks { + switch p := trunk.Metadata.(type) { + case trunkReference: + if print := leafPtrs[p.p]; print { + id, ok := ptrIDs[p.p] + if !ok { + id = newID() + ptrIDs[p.p] = id + } + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id)) + } + case trunkReferences: + print0 := leafPtrs[p.pp[0]] + print1 := leafPtrs[p.pp[1]] + if print0 || print1 { + id0, ok0 := ptrIDs[p.pp[0]] + id1, ok1 := ptrIDs[p.pp[1]] + isPair := pairedTrunkPtrs[p.pp[0]] == p.pp[1] && pairedTrunkPtrs[p.pp[1]] == p.pp[0] + if isPair { + var id uint + assert(ok0 == ok1) // must be seen together or not at all + if ok0 { + assert(id0 == id1) // must have the same ID + id = id0 + } else { + id = newID() + ptrIDs[p.pp[0]] = id + ptrIDs[p.pp[1]] = id + } + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id)) + } else { + if print0 && !ok0 { + id0 = newID() + ptrIDs[p.pp[0]] = id0 + } + if print1 && !ok1 { + id1 = newID() + ptrIDs[p.pp[1]] = id1 + } + switch { + case print0 && print1: + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id0)+","+formatReference(id1)) + case print0: + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id0)) + case print1: + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id1)) + } + } + } + } + } + + // Update all leaf references with the unique identifier. + for _, leaf := range leaves { + if id, ok := ptrIDs[leaf.Metadata.(leafReference).p]; ok { + leaf.Prefix = updateReferencePrefix(leaf.Prefix, formatReference(id)) + } + } +} + +func formatReference(id uint) string { + return fmt.Sprintf("ref#%d", id) +} + +func updateReferencePrefix(prefix, ref string) string { + if prefix == "" { + return pointerDelimPrefix + ref + pointerDelimSuffix + } + suffix := strings.TrimPrefix(prefix, pointerDelimPrefix) + return pointerDelimPrefix + ref + ": " + suffix +} diff --git a/vendor/github.com/google/go-cmp/cmp/report_reflect.go b/vendor/github.com/google/go-cmp/cmp/report_reflect.go new file mode 100644 index 000000000..76c04fdbd --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/report_reflect.go @@ -0,0 +1,403 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "strings" + "unicode" + "unicode/utf8" + + "github.com/google/go-cmp/cmp/internal/value" +) + +type formatValueOptions struct { + // AvoidStringer controls whether to avoid calling custom stringer + // methods like error.Error or fmt.Stringer.String. + AvoidStringer bool + + // PrintAddresses controls whether to print the address of all pointers, + // slice elements, and maps. + PrintAddresses bool + + // QualifiedNames controls whether FormatType uses the fully qualified name + // (including the full package path as opposed to just the package name). + QualifiedNames bool + + // VerbosityLevel controls the amount of output to produce. + // A higher value produces more output. A value of zero or lower produces + // no output (represented using an ellipsis). + // If LimitVerbosity is false, then the level is treated as infinite. + VerbosityLevel int + + // LimitVerbosity specifies that formatting should respect VerbosityLevel. + LimitVerbosity bool +} + +// FormatType prints the type as if it were wrapping s. +// This may return s as-is depending on the current type and TypeMode mode. +func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode { + // Check whether to emit the type or not. + switch opts.TypeMode { + case autoType: + switch t.Kind() { + case reflect.Struct, reflect.Slice, reflect.Array, reflect.Map: + if s.Equal(textNil) { + return s + } + default: + return s + } + if opts.DiffMode == diffIdentical { + return s // elide type for identical nodes + } + case elideType: + return s + } + + // Determine the type label, applying special handling for unnamed types. + typeName := value.TypeString(t, opts.QualifiedNames) + if t.Name() == "" { + // According to Go grammar, certain type literals contain symbols that + // do not strongly bind to the next lexicographical token (e.g., *T). + switch t.Kind() { + case reflect.Chan, reflect.Func, reflect.Ptr: + typeName = "(" + typeName + ")" + } + } + return &textWrap{Prefix: typeName, Value: wrapParens(s)} +} + +// wrapParens wraps s with a set of parenthesis, but avoids it if the +// wrapped node itself is already surrounded by a pair of parenthesis or braces. +// It handles unwrapping one level of pointer-reference nodes. +func wrapParens(s textNode) textNode { + var refNode *textWrap + if s2, ok := s.(*textWrap); ok { + // Unwrap a single pointer reference node. + switch s2.Metadata.(type) { + case leafReference, trunkReference, trunkReferences: + refNode = s2 + if s3, ok := refNode.Value.(*textWrap); ok { + s2 = s3 + } + } + + // Already has delimiters that make parenthesis unnecessary. + hasParens := strings.HasPrefix(s2.Prefix, "(") && strings.HasSuffix(s2.Suffix, ")") + hasBraces := strings.HasPrefix(s2.Prefix, "{") && strings.HasSuffix(s2.Suffix, "}") + if hasParens || hasBraces { + return s + } + } + if refNode != nil { + refNode.Value = &textWrap{Prefix: "(", Value: refNode.Value, Suffix: ")"} + return s + } + return &textWrap{Prefix: "(", Value: s, Suffix: ")"} +} + +// FormatValue prints the reflect.Value, taking extra care to avoid descending +// into pointers already in ptrs. As pointers are visited, ptrs is also updated. +func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, ptrs *pointerReferences) (out textNode) { + if !v.IsValid() { + return nil + } + t := v.Type() + + // Check slice element for cycles. + if parentKind == reflect.Slice { + ptrRef, visited := ptrs.Push(v.Addr()) + if visited { + return makeLeafReference(ptrRef, false) + } + defer ptrs.Pop() + defer func() { out = wrapTrunkReference(ptrRef, false, out) }() + } + + // Check whether there is an Error or String method to call. + if !opts.AvoidStringer && v.CanInterface() { + // Avoid calling Error or String methods on nil receivers since many + // implementations crash when doing so. + if (t.Kind() != reflect.Ptr && t.Kind() != reflect.Interface) || !v.IsNil() { + var prefix, strVal string + func() { + // Swallow and ignore any panics from String or Error. + defer func() { recover() }() + switch v := v.Interface().(type) { + case error: + strVal = v.Error() + prefix = "e" + case fmt.Stringer: + strVal = v.String() + prefix = "s" + } + }() + if prefix != "" { + return opts.formatString(prefix, strVal) + } + } + } + + // Check whether to explicitly wrap the result with the type. + var skipType bool + defer func() { + if !skipType { + out = opts.FormatType(t, out) + } + }() + + switch t.Kind() { + case reflect.Bool: + return textLine(fmt.Sprint(v.Bool())) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return textLine(fmt.Sprint(v.Int())) + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return textLine(fmt.Sprint(v.Uint())) + case reflect.Uint8: + if parentKind == reflect.Slice || parentKind == reflect.Array { + return textLine(formatHex(v.Uint())) + } + return textLine(fmt.Sprint(v.Uint())) + case reflect.Uintptr: + return textLine(formatHex(v.Uint())) + case reflect.Float32, reflect.Float64: + return textLine(fmt.Sprint(v.Float())) + case reflect.Complex64, reflect.Complex128: + return textLine(fmt.Sprint(v.Complex())) + case reflect.String: + return opts.formatString("", v.String()) + case reflect.UnsafePointer, reflect.Chan, reflect.Func: + return textLine(formatPointer(value.PointerOf(v), true)) + case reflect.Struct: + var list textList + v := makeAddressable(v) // needed for retrieveUnexportedField + maxLen := v.NumField() + if opts.LimitVerbosity { + maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... + opts.VerbosityLevel-- + } + for i := 0; i < v.NumField(); i++ { + vv := v.Field(i) + if value.IsZero(vv) { + continue // Elide fields with zero values + } + if len(list) == maxLen { + list.AppendEllipsis(diffStats{}) + break + } + sf := t.Field(i) + if supportExporters && !isExported(sf.Name) { + vv = retrieveUnexportedField(v, sf, true) + } + s := opts.WithTypeMode(autoType).FormatValue(vv, t.Kind(), ptrs) + list = append(list, textRecord{Key: sf.Name, Value: s}) + } + return &textWrap{Prefix: "{", Value: list, Suffix: "}"} + case reflect.Slice: + if v.IsNil() { + return textNil + } + + // Check whether this is a []byte of text data. + if t.Elem() == reflect.TypeOf(byte(0)) { + b := v.Bytes() + isPrintSpace := func(r rune) bool { return unicode.IsPrint(r) || unicode.IsSpace(r) } + if len(b) > 0 && utf8.Valid(b) && len(bytes.TrimFunc(b, isPrintSpace)) == 0 { + out = opts.formatString("", string(b)) + skipType = true + return opts.WithTypeMode(emitType).FormatType(t, out) + } + } + + fallthrough + case reflect.Array: + maxLen := v.Len() + if opts.LimitVerbosity { + maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... + opts.VerbosityLevel-- + } + var list textList + for i := 0; i < v.Len(); i++ { + if len(list) == maxLen { + list.AppendEllipsis(diffStats{}) + break + } + s := opts.WithTypeMode(elideType).FormatValue(v.Index(i), t.Kind(), ptrs) + list = append(list, textRecord{Value: s}) + } + + out = &textWrap{Prefix: "{", Value: list, Suffix: "}"} + if t.Kind() == reflect.Slice && opts.PrintAddresses { + header := fmt.Sprintf("ptr:%v, len:%d, cap:%d", formatPointer(value.PointerOf(v), false), v.Len(), v.Cap()) + out = &textWrap{Prefix: pointerDelimPrefix + header + pointerDelimSuffix, Value: out} + } + return out + case reflect.Map: + if v.IsNil() { + return textNil + } + + // Check pointer for cycles. + ptrRef, visited := ptrs.Push(v) + if visited { + return makeLeafReference(ptrRef, opts.PrintAddresses) + } + defer ptrs.Pop() + + maxLen := v.Len() + if opts.LimitVerbosity { + maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... + opts.VerbosityLevel-- + } + var list textList + for _, k := range value.SortKeys(v.MapKeys()) { + if len(list) == maxLen { + list.AppendEllipsis(diffStats{}) + break + } + sk := formatMapKey(k, false, ptrs) + sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), t.Kind(), ptrs) + list = append(list, textRecord{Key: sk, Value: sv}) + } + + out = &textWrap{Prefix: "{", Value: list, Suffix: "}"} + out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out) + return out + case reflect.Ptr: + if v.IsNil() { + return textNil + } + + // Check pointer for cycles. + ptrRef, visited := ptrs.Push(v) + if visited { + out = makeLeafReference(ptrRef, opts.PrintAddresses) + return &textWrap{Prefix: "&", Value: out} + } + defer ptrs.Pop() + + skipType = true // Let the underlying value print the type instead + out = opts.FormatValue(v.Elem(), t.Kind(), ptrs) + out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out) + out = &textWrap{Prefix: "&", Value: out} + return out + case reflect.Interface: + if v.IsNil() { + return textNil + } + // Interfaces accept different concrete types, + // so configure the underlying value to explicitly print the type. + skipType = true // Print the concrete type instead + return opts.WithTypeMode(emitType).FormatValue(v.Elem(), t.Kind(), ptrs) + default: + panic(fmt.Sprintf("%v kind not handled", v.Kind())) + } +} + +func (opts formatOptions) formatString(prefix, s string) textNode { + maxLen := len(s) + maxLines := strings.Count(s, "\n") + 1 + if opts.LimitVerbosity { + maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc... + maxLines = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc... + } + + // For multiline strings, use the triple-quote syntax, + // but only use it when printing removed or inserted nodes since + // we only want the extra verbosity for those cases. + lines := strings.Split(strings.TrimSuffix(s, "\n"), "\n") + isTripleQuoted := len(lines) >= 4 && (opts.DiffMode == '-' || opts.DiffMode == '+') + for i := 0; i < len(lines) && isTripleQuoted; i++ { + lines[i] = strings.TrimPrefix(strings.TrimSuffix(lines[i], "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support + isPrintable := func(r rune) bool { + return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable + } + line := lines[i] + isTripleQuoted = !strings.HasPrefix(strings.TrimPrefix(line, prefix), `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == "" && len(line) <= maxLen + } + if isTripleQuoted { + var list textList + list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true}) + for i, line := range lines { + if numElided := len(lines) - i; i == maxLines-1 && numElided > 1 { + comment := commentString(fmt.Sprintf("%d elided lines", numElided)) + list = append(list, textRecord{Diff: opts.DiffMode, Value: textEllipsis, ElideComma: true, Comment: comment}) + break + } + list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(line), ElideComma: true}) + } + list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true}) + return &textWrap{Prefix: "(", Value: list, Suffix: ")"} + } + + // Format the string as a single-line quoted string. + if len(s) > maxLen+len(textEllipsis) { + return textLine(prefix + formatString(s[:maxLen]) + string(textEllipsis)) + } + return textLine(prefix + formatString(s)) +} + +// formatMapKey formats v as if it were a map key. +// The result is guaranteed to be a single line. +func formatMapKey(v reflect.Value, disambiguate bool, ptrs *pointerReferences) string { + var opts formatOptions + opts.DiffMode = diffIdentical + opts.TypeMode = elideType + opts.PrintAddresses = disambiguate + opts.AvoidStringer = disambiguate + opts.QualifiedNames = disambiguate + opts.VerbosityLevel = maxVerbosityPreset + opts.LimitVerbosity = true + s := opts.FormatValue(v, reflect.Map, ptrs).String() + return strings.TrimSpace(s) +} + +// formatString prints s as a double-quoted or backtick-quoted string. +func formatString(s string) string { + // Use quoted string if it the same length as a raw string literal. + // Otherwise, attempt to use the raw string form. + qs := strconv.Quote(s) + if len(qs) == 1+len(s)+1 { + return qs + } + + // Disallow newlines to ensure output is a single line. + // Only allow printable runes for readability purposes. + rawInvalid := func(r rune) bool { + return r == '`' || r == '\n' || !(unicode.IsPrint(r) || r == '\t') + } + if utf8.ValidString(s) && strings.IndexFunc(s, rawInvalid) < 0 { + return "`" + s + "`" + } + return qs +} + +// formatHex prints u as a hexadecimal integer in Go notation. +func formatHex(u uint64) string { + var f string + switch { + case u <= 0xff: + f = "0x%02x" + case u <= 0xffff: + f = "0x%04x" + case u <= 0xffffff: + f = "0x%06x" + case u <= 0xffffffff: + f = "0x%08x" + case u <= 0xffffffffff: + f = "0x%010x" + case u <= 0xffffffffffff: + f = "0x%012x" + case u <= 0xffffffffffffff: + f = "0x%014x" + case u <= 0xffffffffffffffff: + f = "0x%016x" + } + return fmt.Sprintf(f, u) +} diff --git a/vendor/github.com/google/go-cmp/cmp/report_slices.go b/vendor/github.com/google/go-cmp/cmp/report_slices.go new file mode 100644 index 000000000..68b5c1ae1 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/report_slices.go @@ -0,0 +1,613 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import ( + "bytes" + "fmt" + "math" + "reflect" + "strconv" + "strings" + "unicode" + "unicode/utf8" + + "github.com/google/go-cmp/cmp/internal/diff" +) + +// CanFormatDiffSlice reports whether we support custom formatting for nodes +// that are slices of primitive kinds or strings. +func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool { + switch { + case opts.DiffMode != diffUnknown: + return false // Must be formatting in diff mode + case v.NumDiff == 0: + return false // No differences detected + case !v.ValueX.IsValid() || !v.ValueY.IsValid(): + return false // Both values must be valid + case v.NumIgnored > 0: + return false // Some ignore option was used + case v.NumTransformed > 0: + return false // Some transform option was used + case v.NumCompared > 1: + return false // More than one comparison was used + case v.NumCompared == 1 && v.Type.Name() != "": + // The need for cmp to check applicability of options on every element + // in a slice is a significant performance detriment for large []byte. + // The workaround is to specify Comparer(bytes.Equal), + // which enables cmp to compare []byte more efficiently. + // If they differ, we still want to provide batched diffing. + // The logic disallows named types since they tend to have their own + // String method, with nicer formatting than what this provides. + return false + } + + // Check whether this is an interface with the same concrete types. + t := v.Type + vx, vy := v.ValueX, v.ValueY + if t.Kind() == reflect.Interface && !vx.IsNil() && !vy.IsNil() && vx.Elem().Type() == vy.Elem().Type() { + vx, vy = vx.Elem(), vy.Elem() + t = vx.Type() + } + + // Check whether we provide specialized diffing for this type. + switch t.Kind() { + case reflect.String: + case reflect.Array, reflect.Slice: + // Only slices of primitive types have specialized handling. + switch t.Elem().Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: + default: + return false + } + + // Both slice values have to be non-empty. + if t.Kind() == reflect.Slice && (vx.Len() == 0 || vy.Len() == 0) { + return false + } + + // If a sufficient number of elements already differ, + // use specialized formatting even if length requirement is not met. + if v.NumDiff > v.NumSame { + return true + } + default: + return false + } + + // Use specialized string diffing for longer slices or strings. + const minLength = 32 + return vx.Len() >= minLength && vy.Len() >= minLength +} + +// FormatDiffSlice prints a diff for the slices (or strings) represented by v. +// This provides custom-tailored logic to make printing of differences in +// textual strings and slices of primitive kinds more readable. +func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { + assert(opts.DiffMode == diffUnknown) + t, vx, vy := v.Type, v.ValueX, v.ValueY + if t.Kind() == reflect.Interface { + vx, vy = vx.Elem(), vy.Elem() + t = vx.Type() + opts = opts.WithTypeMode(emitType) + } + + // Auto-detect the type of the data. + var sx, sy string + var ssx, ssy []string + var isString, isMostlyText, isPureLinedText, isBinary bool + switch { + case t.Kind() == reflect.String: + sx, sy = vx.String(), vy.String() + isString = true + case t.Kind() == reflect.Slice && t.Elem() == reflect.TypeOf(byte(0)): + sx, sy = string(vx.Bytes()), string(vy.Bytes()) + isString = true + case t.Kind() == reflect.Array: + // Arrays need to be addressable for slice operations to work. + vx2, vy2 := reflect.New(t).Elem(), reflect.New(t).Elem() + vx2.Set(vx) + vy2.Set(vy) + vx, vy = vx2, vy2 + } + if isString { + var numTotalRunes, numValidRunes, numLines, lastLineIdx, maxLineLen int + for i, r := range sx + sy { + numTotalRunes++ + if (unicode.IsPrint(r) || unicode.IsSpace(r)) && r != utf8.RuneError { + numValidRunes++ + } + if r == '\n' { + if maxLineLen < i-lastLineIdx { + maxLineLen = i - lastLineIdx + } + lastLineIdx = i + 1 + numLines++ + } + } + isPureText := numValidRunes == numTotalRunes + isMostlyText = float64(numValidRunes) > math.Floor(0.90*float64(numTotalRunes)) + isPureLinedText = isPureText && numLines >= 4 && maxLineLen <= 1024 + isBinary = !isMostlyText + + // Avoid diffing by lines if it produces a significantly more complex + // edit script than diffing by bytes. + if isPureLinedText { + ssx = strings.Split(sx, "\n") + ssy = strings.Split(sy, "\n") + esLines := diff.Difference(len(ssx), len(ssy), func(ix, iy int) diff.Result { + return diff.BoolResult(ssx[ix] == ssy[iy]) + }) + esBytes := diff.Difference(len(sx), len(sy), func(ix, iy int) diff.Result { + return diff.BoolResult(sx[ix] == sy[iy]) + }) + efficiencyLines := float64(esLines.Dist()) / float64(len(esLines)) + efficiencyBytes := float64(esBytes.Dist()) / float64(len(esBytes)) + isPureLinedText = efficiencyLines < 4*efficiencyBytes + } + } + + // Format the string into printable records. + var list textList + var delim string + switch { + // If the text appears to be multi-lined text, + // then perform differencing across individual lines. + case isPureLinedText: + list = opts.formatDiffSlice( + reflect.ValueOf(ssx), reflect.ValueOf(ssy), 1, "line", + func(v reflect.Value, d diffMode) textRecord { + s := formatString(v.Index(0).String()) + return textRecord{Diff: d, Value: textLine(s)} + }, + ) + delim = "\n" + + // If possible, use a custom triple-quote (""") syntax for printing + // differences in a string literal. This format is more readable, + // but has edge-cases where differences are visually indistinguishable. + // This format is avoided under the following conditions: + // • A line starts with `"""` + // • A line starts with "..." + // • A line contains non-printable characters + // • Adjacent different lines differ only by whitespace + // + // For example: + // """ + // ... // 3 identical lines + // foo + // bar + // - baz + // + BAZ + // """ + isTripleQuoted := true + prevRemoveLines := map[string]bool{} + prevInsertLines := map[string]bool{} + var list2 textList + list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true}) + for _, r := range list { + if !r.Value.Equal(textEllipsis) { + line, _ := strconv.Unquote(string(r.Value.(textLine))) + line = strings.TrimPrefix(strings.TrimSuffix(line, "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support + normLine := strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return -1 // drop whitespace to avoid visually indistinguishable output + } + return r + }, line) + isPrintable := func(r rune) bool { + return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable + } + isTripleQuoted = !strings.HasPrefix(line, `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == "" + switch r.Diff { + case diffRemoved: + isTripleQuoted = isTripleQuoted && !prevInsertLines[normLine] + prevRemoveLines[normLine] = true + case diffInserted: + isTripleQuoted = isTripleQuoted && !prevRemoveLines[normLine] + prevInsertLines[normLine] = true + } + if !isTripleQuoted { + break + } + r.Value = textLine(line) + r.ElideComma = true + } + if !(r.Diff == diffRemoved || r.Diff == diffInserted) { // start a new non-adjacent difference group + prevRemoveLines = map[string]bool{} + prevInsertLines = map[string]bool{} + } + list2 = append(list2, r) + } + if r := list2[len(list2)-1]; r.Diff == diffIdentical && len(r.Value.(textLine)) == 0 { + list2 = list2[:len(list2)-1] // elide single empty line at the end + } + list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true}) + if isTripleQuoted { + var out textNode = &textWrap{Prefix: "(", Value: list2, Suffix: ")"} + switch t.Kind() { + case reflect.String: + if t != reflect.TypeOf(string("")) { + out = opts.FormatType(t, out) + } + case reflect.Slice: + // Always emit type for slices since the triple-quote syntax + // looks like a string (not a slice). + opts = opts.WithTypeMode(emitType) + out = opts.FormatType(t, out) + } + return out + } + + // If the text appears to be single-lined text, + // then perform differencing in approximately fixed-sized chunks. + // The output is printed as quoted strings. + case isMostlyText: + list = opts.formatDiffSlice( + reflect.ValueOf(sx), reflect.ValueOf(sy), 64, "byte", + func(v reflect.Value, d diffMode) textRecord { + s := formatString(v.String()) + return textRecord{Diff: d, Value: textLine(s)} + }, + ) + + // If the text appears to be binary data, + // then perform differencing in approximately fixed-sized chunks. + // The output is inspired by hexdump. + case isBinary: + list = opts.formatDiffSlice( + reflect.ValueOf(sx), reflect.ValueOf(sy), 16, "byte", + func(v reflect.Value, d diffMode) textRecord { + var ss []string + for i := 0; i < v.Len(); i++ { + ss = append(ss, formatHex(v.Index(i).Uint())) + } + s := strings.Join(ss, ", ") + comment := commentString(fmt.Sprintf("%c|%v|", d, formatASCII(v.String()))) + return textRecord{Diff: d, Value: textLine(s), Comment: comment} + }, + ) + + // For all other slices of primitive types, + // then perform differencing in approximately fixed-sized chunks. + // The size of each chunk depends on the width of the element kind. + default: + var chunkSize int + if t.Elem().Kind() == reflect.Bool { + chunkSize = 16 + } else { + switch t.Elem().Bits() { + case 8: + chunkSize = 16 + case 16: + chunkSize = 12 + case 32: + chunkSize = 8 + default: + chunkSize = 8 + } + } + list = opts.formatDiffSlice( + vx, vy, chunkSize, t.Elem().Kind().String(), + func(v reflect.Value, d diffMode) textRecord { + var ss []string + for i := 0; i < v.Len(); i++ { + switch t.Elem().Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + ss = append(ss, fmt.Sprint(v.Index(i).Int())) + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + ss = append(ss, fmt.Sprint(v.Index(i).Uint())) + case reflect.Uint8, reflect.Uintptr: + ss = append(ss, formatHex(v.Index(i).Uint())) + case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: + ss = append(ss, fmt.Sprint(v.Index(i).Interface())) + } + } + s := strings.Join(ss, ", ") + return textRecord{Diff: d, Value: textLine(s)} + }, + ) + } + + // Wrap the output with appropriate type information. + var out textNode = &textWrap{Prefix: "{", Value: list, Suffix: "}"} + if !isMostlyText { + // The "{...}" byte-sequence literal is not valid Go syntax for strings. + // Emit the type for extra clarity (e.g. "string{...}"). + if t.Kind() == reflect.String { + opts = opts.WithTypeMode(emitType) + } + return opts.FormatType(t, out) + } + switch t.Kind() { + case reflect.String: + out = &textWrap{Prefix: "strings.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)} + if t != reflect.TypeOf(string("")) { + out = opts.FormatType(t, out) + } + case reflect.Slice: + out = &textWrap{Prefix: "bytes.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)} + if t != reflect.TypeOf([]byte(nil)) { + out = opts.FormatType(t, out) + } + } + return out +} + +// formatASCII formats s as an ASCII string. +// This is useful for printing binary strings in a semi-legible way. +func formatASCII(s string) string { + b := bytes.Repeat([]byte{'.'}, len(s)) + for i := 0; i < len(s); i++ { + if ' ' <= s[i] && s[i] <= '~' { + b[i] = s[i] + } + } + return string(b) +} + +func (opts formatOptions) formatDiffSlice( + vx, vy reflect.Value, chunkSize int, name string, + makeRec func(reflect.Value, diffMode) textRecord, +) (list textList) { + eq := func(ix, iy int) bool { + return vx.Index(ix).Interface() == vy.Index(iy).Interface() + } + es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result { + return diff.BoolResult(eq(ix, iy)) + }) + + appendChunks := func(v reflect.Value, d diffMode) int { + n0 := v.Len() + for v.Len() > 0 { + n := chunkSize + if n > v.Len() { + n = v.Len() + } + list = append(list, makeRec(v.Slice(0, n), d)) + v = v.Slice(n, v.Len()) + } + return n0 - v.Len() + } + + var numDiffs int + maxLen := -1 + if opts.LimitVerbosity { + maxLen = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc... + opts.VerbosityLevel-- + } + + groups := coalesceAdjacentEdits(name, es) + groups = coalesceInterveningIdentical(groups, chunkSize/4) + groups = cleanupSurroundingIdentical(groups, eq) + maxGroup := diffStats{Name: name} + for i, ds := range groups { + if maxLen >= 0 && numDiffs >= maxLen { + maxGroup = maxGroup.Append(ds) + continue + } + + // Print equal. + if ds.NumDiff() == 0 { + // Compute the number of leading and trailing equal bytes to print. + var numLo, numHi int + numEqual := ds.NumIgnored + ds.NumIdentical + for numLo < chunkSize*numContextRecords && numLo+numHi < numEqual && i != 0 { + numLo++ + } + for numHi < chunkSize*numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 { + numHi++ + } + if numEqual-(numLo+numHi) <= chunkSize && ds.NumIgnored == 0 { + numHi = numEqual - numLo // Avoid pointless coalescing of single equal row + } + + // Print the equal bytes. + appendChunks(vx.Slice(0, numLo), diffIdentical) + if numEqual > numLo+numHi { + ds.NumIdentical -= numLo + numHi + list.AppendEllipsis(ds) + } + appendChunks(vx.Slice(numEqual-numHi, numEqual), diffIdentical) + vx = vx.Slice(numEqual, vx.Len()) + vy = vy.Slice(numEqual, vy.Len()) + continue + } + + // Print unequal. + len0 := len(list) + nx := appendChunks(vx.Slice(0, ds.NumIdentical+ds.NumRemoved+ds.NumModified), diffRemoved) + vx = vx.Slice(nx, vx.Len()) + ny := appendChunks(vy.Slice(0, ds.NumIdentical+ds.NumInserted+ds.NumModified), diffInserted) + vy = vy.Slice(ny, vy.Len()) + numDiffs += len(list) - len0 + } + if maxGroup.IsZero() { + assert(vx.Len() == 0 && vy.Len() == 0) + } else { + list.AppendEllipsis(maxGroup) + } + return list +} + +// coalesceAdjacentEdits coalesces the list of edits into groups of adjacent +// equal or unequal counts. +// +// Example: +// +// Input: "..XXY...Y" +// Output: [ +// {NumIdentical: 2}, +// {NumRemoved: 2, NumInserted 1}, +// {NumIdentical: 3}, +// {NumInserted: 1}, +// ] +// +func coalesceAdjacentEdits(name string, es diff.EditScript) (groups []diffStats) { + var prevMode byte + lastStats := func(mode byte) *diffStats { + if prevMode != mode { + groups = append(groups, diffStats{Name: name}) + prevMode = mode + } + return &groups[len(groups)-1] + } + for _, e := range es { + switch e { + case diff.Identity: + lastStats('=').NumIdentical++ + case diff.UniqueX: + lastStats('!').NumRemoved++ + case diff.UniqueY: + lastStats('!').NumInserted++ + case diff.Modified: + lastStats('!').NumModified++ + } + } + return groups +} + +// coalesceInterveningIdentical coalesces sufficiently short (<= windowSize) +// equal groups into adjacent unequal groups that currently result in a +// dual inserted/removed printout. This acts as a high-pass filter to smooth +// out high-frequency changes within the windowSize. +// +// Example: +// +// WindowSize: 16, +// Input: [ +// {NumIdentical: 61}, // group 0 +// {NumRemoved: 3, NumInserted: 1}, // group 1 +// {NumIdentical: 6}, // ├── coalesce +// {NumInserted: 2}, // ├── coalesce +// {NumIdentical: 1}, // ├── coalesce +// {NumRemoved: 9}, // └── coalesce +// {NumIdentical: 64}, // group 2 +// {NumRemoved: 3, NumInserted: 1}, // group 3 +// {NumIdentical: 6}, // ├── coalesce +// {NumInserted: 2}, // ├── coalesce +// {NumIdentical: 1}, // ├── coalesce +// {NumRemoved: 7}, // ├── coalesce +// {NumIdentical: 1}, // ├── coalesce +// {NumRemoved: 2}, // └── coalesce +// {NumIdentical: 63}, // group 4 +// ] +// Output: [ +// {NumIdentical: 61}, +// {NumIdentical: 7, NumRemoved: 12, NumInserted: 3}, +// {NumIdentical: 64}, +// {NumIdentical: 8, NumRemoved: 12, NumInserted: 3}, +// {NumIdentical: 63}, +// ] +// +func coalesceInterveningIdentical(groups []diffStats, windowSize int) []diffStats { + groups, groupsOrig := groups[:0], groups + for i, ds := range groupsOrig { + if len(groups) >= 2 && ds.NumDiff() > 0 { + prev := &groups[len(groups)-2] // Unequal group + curr := &groups[len(groups)-1] // Equal group + next := &groupsOrig[i] // Unequal group + hadX, hadY := prev.NumRemoved > 0, prev.NumInserted > 0 + hasX, hasY := next.NumRemoved > 0, next.NumInserted > 0 + if ((hadX || hasX) && (hadY || hasY)) && curr.NumIdentical <= windowSize { + *prev = prev.Append(*curr).Append(*next) + groups = groups[:len(groups)-1] // Truncate off equal group + continue + } + } + groups = append(groups, ds) + } + return groups +} + +// cleanupSurroundingIdentical scans through all unequal groups, and +// moves any leading sequence of equal elements to the preceding equal group and +// moves and trailing sequence of equal elements to the succeeding equal group. +// +// This is necessary since coalesceInterveningIdentical may coalesce edit groups +// together such that leading/trailing spans of equal elements becomes possible. +// Note that this can occur even with an optimal diffing algorithm. +// +// Example: +// +// Input: [ +// {NumIdentical: 61}, +// {NumIdentical: 1 , NumRemoved: 11, NumInserted: 2}, // assume 3 leading identical elements +// {NumIdentical: 67}, +// {NumIdentical: 7, NumRemoved: 12, NumInserted: 3}, // assume 10 trailing identical elements +// {NumIdentical: 54}, +// ] +// Output: [ +// {NumIdentical: 64}, // incremented by 3 +// {NumRemoved: 9}, +// {NumIdentical: 67}, +// {NumRemoved: 9}, +// {NumIdentical: 64}, // incremented by 10 +// ] +// +func cleanupSurroundingIdentical(groups []diffStats, eq func(i, j int) bool) []diffStats { + var ix, iy int // indexes into sequence x and y + for i, ds := range groups { + // Handle equal group. + if ds.NumDiff() == 0 { + ix += ds.NumIdentical + iy += ds.NumIdentical + continue + } + + // Handle unequal group. + nx := ds.NumIdentical + ds.NumRemoved + ds.NumModified + ny := ds.NumIdentical + ds.NumInserted + ds.NumModified + var numLeadingIdentical, numTrailingIdentical int + for j := 0; j < nx && j < ny && eq(ix+j, iy+j); j++ { + numLeadingIdentical++ + } + for j := 0; j < nx && j < ny && eq(ix+nx-1-j, iy+ny-1-j); j++ { + numTrailingIdentical++ + } + if numIdentical := numLeadingIdentical + numTrailingIdentical; numIdentical > 0 { + if numLeadingIdentical > 0 { + // Remove leading identical span from this group and + // insert it into the preceding group. + if i-1 >= 0 { + groups[i-1].NumIdentical += numLeadingIdentical + } else { + // No preceding group exists, so prepend a new group, + // but do so after we finish iterating over all groups. + defer func() { + groups = append([]diffStats{{Name: groups[0].Name, NumIdentical: numLeadingIdentical}}, groups...) + }() + } + // Increment indexes since the preceding group would have handled this. + ix += numLeadingIdentical + iy += numLeadingIdentical + } + if numTrailingIdentical > 0 { + // Remove trailing identical span from this group and + // insert it into the succeeding group. + if i+1 < len(groups) { + groups[i+1].NumIdentical += numTrailingIdentical + } else { + // No succeeding group exists, so append a new group, + // but do so after we finish iterating over all groups. + defer func() { + groups = append(groups, diffStats{Name: groups[len(groups)-1].Name, NumIdentical: numTrailingIdentical}) + }() + } + // Do not increment indexes since the succeeding group will handle this. + } + + // Update this group since some identical elements were removed. + nx -= numIdentical + ny -= numIdentical + groups[i] = diffStats{Name: ds.Name, NumRemoved: nx, NumInserted: ny} + } + ix += nx + iy += ny + } + return groups +} diff --git a/vendor/github.com/google/go-cmp/cmp/report_text.go b/vendor/github.com/google/go-cmp/cmp/report_text.go new file mode 100644 index 000000000..0fd46d7ff --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/report_text.go @@ -0,0 +1,431 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import ( + "bytes" + "fmt" + "math/rand" + "strings" + "time" + "unicode/utf8" + + "github.com/google/go-cmp/cmp/internal/flags" +) + +var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0 + +const maxColumnLength = 80 + +type indentMode int + +func (n indentMode) appendIndent(b []byte, d diffMode) []byte { + // The output of Diff is documented as being unstable to provide future + // flexibility in changing the output for more humanly readable reports. + // This logic intentionally introduces instability to the exact output + // so that users can detect accidental reliance on stability early on, + // rather than much later when an actual change to the format occurs. + if flags.Deterministic || randBool { + // Use regular spaces (U+0020). + switch d { + case diffUnknown, diffIdentical: + b = append(b, " "...) + case diffRemoved: + b = append(b, "- "...) + case diffInserted: + b = append(b, "+ "...) + } + } else { + // Use non-breaking spaces (U+00a0). + switch d { + case diffUnknown, diffIdentical: + b = append(b, "  "...) + case diffRemoved: + b = append(b, "- "...) + case diffInserted: + b = append(b, "+ "...) + } + } + return repeatCount(n).appendChar(b, '\t') +} + +type repeatCount int + +func (n repeatCount) appendChar(b []byte, c byte) []byte { + for ; n > 0; n-- { + b = append(b, c) + } + return b +} + +// textNode is a simplified tree-based representation of structured text. +// Possible node types are textWrap, textList, or textLine. +type textNode interface { + // Len reports the length in bytes of a single-line version of the tree. + // Nested textRecord.Diff and textRecord.Comment fields are ignored. + Len() int + // Equal reports whether the two trees are structurally identical. + // Nested textRecord.Diff and textRecord.Comment fields are compared. + Equal(textNode) bool + // String returns the string representation of the text tree. + // It is not guaranteed that len(x.String()) == x.Len(), + // nor that x.String() == y.String() implies that x.Equal(y). + String() string + + // formatCompactTo formats the contents of the tree as a single-line string + // to the provided buffer. Any nested textRecord.Diff and textRecord.Comment + // fields are ignored. + // + // However, not all nodes in the tree should be collapsed as a single-line. + // If a node can be collapsed as a single-line, it is replaced by a textLine + // node. Since the top-level node cannot replace itself, this also returns + // the current node itself. + // + // This does not mutate the receiver. + formatCompactTo([]byte, diffMode) ([]byte, textNode) + // formatExpandedTo formats the contents of the tree as a multi-line string + // to the provided buffer. In order for column alignment to operate well, + // formatCompactTo must be called before calling formatExpandedTo. + formatExpandedTo([]byte, diffMode, indentMode) []byte +} + +// textWrap is a wrapper that concatenates a prefix and/or a suffix +// to the underlying node. +type textWrap struct { + Prefix string // e.g., "bytes.Buffer{" + Value textNode // textWrap | textList | textLine + Suffix string // e.g., "}" + Metadata interface{} // arbitrary metadata; has no effect on formatting +} + +func (s *textWrap) Len() int { + return len(s.Prefix) + s.Value.Len() + len(s.Suffix) +} +func (s1 *textWrap) Equal(s2 textNode) bool { + if s2, ok := s2.(*textWrap); ok { + return s1.Prefix == s2.Prefix && s1.Value.Equal(s2.Value) && s1.Suffix == s2.Suffix + } + return false +} +func (s *textWrap) String() string { + var d diffMode + var n indentMode + _, s2 := s.formatCompactTo(nil, d) + b := n.appendIndent(nil, d) // Leading indent + b = s2.formatExpandedTo(b, d, n) // Main body + b = append(b, '\n') // Trailing newline + return string(b) +} +func (s *textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { + n0 := len(b) // Original buffer length + b = append(b, s.Prefix...) + b, s.Value = s.Value.formatCompactTo(b, d) + b = append(b, s.Suffix...) + if _, ok := s.Value.(textLine); ok { + return b, textLine(b[n0:]) + } + return b, s +} +func (s *textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte { + b = append(b, s.Prefix...) + b = s.Value.formatExpandedTo(b, d, n) + b = append(b, s.Suffix...) + return b +} + +// textList is a comma-separated list of textWrap or textLine nodes. +// The list may be formatted as multi-lines or single-line at the discretion +// of the textList.formatCompactTo method. +type textList []textRecord +type textRecord struct { + Diff diffMode // e.g., 0 or '-' or '+' + Key string // e.g., "MyField" + Value textNode // textWrap | textLine + ElideComma bool // avoid trailing comma + Comment fmt.Stringer // e.g., "6 identical fields" +} + +// AppendEllipsis appends a new ellipsis node to the list if none already +// exists at the end. If cs is non-zero it coalesces the statistics with the +// previous diffStats. +func (s *textList) AppendEllipsis(ds diffStats) { + hasStats := !ds.IsZero() + if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) { + if hasStats { + *s = append(*s, textRecord{Value: textEllipsis, ElideComma: true, Comment: ds}) + } else { + *s = append(*s, textRecord{Value: textEllipsis, ElideComma: true}) + } + return + } + if hasStats { + (*s)[len(*s)-1].Comment = (*s)[len(*s)-1].Comment.(diffStats).Append(ds) + } +} + +func (s textList) Len() (n int) { + for i, r := range s { + n += len(r.Key) + if r.Key != "" { + n += len(": ") + } + n += r.Value.Len() + if i < len(s)-1 { + n += len(", ") + } + } + return n +} + +func (s1 textList) Equal(s2 textNode) bool { + if s2, ok := s2.(textList); ok { + if len(s1) != len(s2) { + return false + } + for i := range s1 { + r1, r2 := s1[i], s2[i] + if !(r1.Diff == r2.Diff && r1.Key == r2.Key && r1.Value.Equal(r2.Value) && r1.Comment == r2.Comment) { + return false + } + } + return true + } + return false +} + +func (s textList) String() string { + return (&textWrap{Prefix: "{", Value: s, Suffix: "}"}).String() +} + +func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { + s = append(textList(nil), s...) // Avoid mutating original + + // Determine whether we can collapse this list as a single line. + n0 := len(b) // Original buffer length + var multiLine bool + for i, r := range s { + if r.Diff == diffInserted || r.Diff == diffRemoved { + multiLine = true + } + b = append(b, r.Key...) + if r.Key != "" { + b = append(b, ": "...) + } + b, s[i].Value = r.Value.formatCompactTo(b, d|r.Diff) + if _, ok := s[i].Value.(textLine); !ok { + multiLine = true + } + if r.Comment != nil { + multiLine = true + } + if i < len(s)-1 { + b = append(b, ", "...) + } + } + // Force multi-lined output when printing a removed/inserted node that + // is sufficiently long. + if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > maxColumnLength { + multiLine = true + } + if !multiLine { + return b, textLine(b[n0:]) + } + return b, s +} + +func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte { + alignKeyLens := s.alignLens( + func(r textRecord) bool { + _, isLine := r.Value.(textLine) + return r.Key == "" || !isLine + }, + func(r textRecord) int { return utf8.RuneCountInString(r.Key) }, + ) + alignValueLens := s.alignLens( + func(r textRecord) bool { + _, isLine := r.Value.(textLine) + return !isLine || r.Value.Equal(textEllipsis) || r.Comment == nil + }, + func(r textRecord) int { return utf8.RuneCount(r.Value.(textLine)) }, + ) + + // Format lists of simple lists in a batched form. + // If the list is sequence of only textLine values, + // then batch multiple values on a single line. + var isSimple bool + for _, r := range s { + _, isLine := r.Value.(textLine) + isSimple = r.Diff == 0 && r.Key == "" && isLine && r.Comment == nil + if !isSimple { + break + } + } + if isSimple { + n++ + var batch []byte + emitBatch := func() { + if len(batch) > 0 { + b = n.appendIndent(append(b, '\n'), d) + b = append(b, bytes.TrimRight(batch, " ")...) + batch = batch[:0] + } + } + for _, r := range s { + line := r.Value.(textLine) + if len(batch)+len(line)+len(", ") > maxColumnLength { + emitBatch() + } + batch = append(batch, line...) + batch = append(batch, ", "...) + } + emitBatch() + n-- + return n.appendIndent(append(b, '\n'), d) + } + + // Format the list as a multi-lined output. + n++ + for i, r := range s { + b = n.appendIndent(append(b, '\n'), d|r.Diff) + if r.Key != "" { + b = append(b, r.Key+": "...) + } + b = alignKeyLens[i].appendChar(b, ' ') + + b = r.Value.formatExpandedTo(b, d|r.Diff, n) + if !r.ElideComma { + b = append(b, ',') + } + b = alignValueLens[i].appendChar(b, ' ') + + if r.Comment != nil { + b = append(b, " // "+r.Comment.String()...) + } + } + n-- + + return n.appendIndent(append(b, '\n'), d) +} + +func (s textList) alignLens( + skipFunc func(textRecord) bool, + lenFunc func(textRecord) int, +) []repeatCount { + var startIdx, endIdx, maxLen int + lens := make([]repeatCount, len(s)) + for i, r := range s { + if skipFunc(r) { + for j := startIdx; j < endIdx && j < len(s); j++ { + lens[j] = repeatCount(maxLen - lenFunc(s[j])) + } + startIdx, endIdx, maxLen = i+1, i+1, 0 + } else { + if maxLen < lenFunc(r) { + maxLen = lenFunc(r) + } + endIdx = i + 1 + } + } + for j := startIdx; j < endIdx && j < len(s); j++ { + lens[j] = repeatCount(maxLen - lenFunc(s[j])) + } + return lens +} + +// textLine is a single-line segment of text and is always a leaf node +// in the textNode tree. +type textLine []byte + +var ( + textNil = textLine("nil") + textEllipsis = textLine("...") +) + +func (s textLine) Len() int { + return len(s) +} +func (s1 textLine) Equal(s2 textNode) bool { + if s2, ok := s2.(textLine); ok { + return bytes.Equal([]byte(s1), []byte(s2)) + } + return false +} +func (s textLine) String() string { + return string(s) +} +func (s textLine) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { + return append(b, s...), s +} +func (s textLine) formatExpandedTo(b []byte, _ diffMode, _ indentMode) []byte { + return append(b, s...) +} + +type diffStats struct { + Name string + NumIgnored int + NumIdentical int + NumRemoved int + NumInserted int + NumModified int +} + +func (s diffStats) IsZero() bool { + s.Name = "" + return s == diffStats{} +} + +func (s diffStats) NumDiff() int { + return s.NumRemoved + s.NumInserted + s.NumModified +} + +func (s diffStats) Append(ds diffStats) diffStats { + assert(s.Name == ds.Name) + s.NumIgnored += ds.NumIgnored + s.NumIdentical += ds.NumIdentical + s.NumRemoved += ds.NumRemoved + s.NumInserted += ds.NumInserted + s.NumModified += ds.NumModified + return s +} + +// String prints a humanly-readable summary of coalesced records. +// +// Example: +// diffStats{Name: "Field", NumIgnored: 5}.String() => "5 ignored fields" +func (s diffStats) String() string { + var ss []string + var sum int + labels := [...]string{"ignored", "identical", "removed", "inserted", "modified"} + counts := [...]int{s.NumIgnored, s.NumIdentical, s.NumRemoved, s.NumInserted, s.NumModified} + for i, n := range counts { + if n > 0 { + ss = append(ss, fmt.Sprintf("%d %v", n, labels[i])) + } + sum += n + } + + // Pluralize the name (adjusting for some obscure English grammar rules). + name := s.Name + if sum > 1 { + name += "s" + if strings.HasSuffix(name, "ys") { + name = name[:len(name)-2] + "ies" // e.g., "entrys" => "entries" + } + } + + // Format the list according to English grammar (with Oxford comma). + switch n := len(ss); n { + case 0: + return "" + case 1, 2: + return strings.Join(ss, " and ") + " " + name + default: + return strings.Join(ss[:n-1], ", ") + ", and " + ss[n-1] + " " + name + } +} + +type commentString string + +func (s commentString) String() string { return string(s) } diff --git a/vendor/github.com/google/go-cmp/cmp/report_value.go b/vendor/github.com/google/go-cmp/cmp/report_value.go new file mode 100644 index 000000000..668d470fd --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/report_value.go @@ -0,0 +1,121 @@ +// Copyright 2019, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmp + +import "reflect" + +// valueNode represents a single node within a report, which is a +// structured representation of the value tree, containing information +// regarding which nodes are equal or not. +type valueNode struct { + parent *valueNode + + Type reflect.Type + ValueX reflect.Value + ValueY reflect.Value + + // NumSame is the number of leaf nodes that are equal. + // All descendants are equal only if NumDiff is 0. + NumSame int + // NumDiff is the number of leaf nodes that are not equal. + NumDiff int + // NumIgnored is the number of leaf nodes that are ignored. + NumIgnored int + // NumCompared is the number of leaf nodes that were compared + // using an Equal method or Comparer function. + NumCompared int + // NumTransformed is the number of non-leaf nodes that were transformed. + NumTransformed int + // NumChildren is the number of transitive descendants of this node. + // This counts from zero; thus, leaf nodes have no descendants. + NumChildren int + // MaxDepth is the maximum depth of the tree. This counts from zero; + // thus, leaf nodes have a depth of zero. + MaxDepth int + + // Records is a list of struct fields, slice elements, or map entries. + Records []reportRecord // If populated, implies Value is not populated + + // Value is the result of a transformation, pointer indirect, of + // type assertion. + Value *valueNode // If populated, implies Records is not populated + + // TransformerName is the name of the transformer. + TransformerName string // If non-empty, implies Value is populated +} +type reportRecord struct { + Key reflect.Value // Invalid for slice element + Value *valueNode +} + +func (parent *valueNode) PushStep(ps PathStep) (child *valueNode) { + vx, vy := ps.Values() + child = &valueNode{parent: parent, Type: ps.Type(), ValueX: vx, ValueY: vy} + switch s := ps.(type) { + case StructField: + assert(parent.Value == nil) + parent.Records = append(parent.Records, reportRecord{Key: reflect.ValueOf(s.Name()), Value: child}) + case SliceIndex: + assert(parent.Value == nil) + parent.Records = append(parent.Records, reportRecord{Value: child}) + case MapIndex: + assert(parent.Value == nil) + parent.Records = append(parent.Records, reportRecord{Key: s.Key(), Value: child}) + case Indirect: + assert(parent.Value == nil && parent.Records == nil) + parent.Value = child + case TypeAssertion: + assert(parent.Value == nil && parent.Records == nil) + parent.Value = child + case Transform: + assert(parent.Value == nil && parent.Records == nil) + parent.Value = child + parent.TransformerName = s.Name() + parent.NumTransformed++ + default: + assert(parent == nil) // Must be the root step + } + return child +} + +func (r *valueNode) Report(rs Result) { + assert(r.MaxDepth == 0) // May only be called on leaf nodes + + if rs.ByIgnore() { + r.NumIgnored++ + } else { + if rs.Equal() { + r.NumSame++ + } else { + r.NumDiff++ + } + } + assert(r.NumSame+r.NumDiff+r.NumIgnored == 1) + + if rs.ByMethod() { + r.NumCompared++ + } + if rs.ByFunc() { + r.NumCompared++ + } + assert(r.NumCompared <= 1) +} + +func (child *valueNode) PopStep() (parent *valueNode) { + if child.parent == nil { + return nil + } + parent = child.parent + parent.NumSame += child.NumSame + parent.NumDiff += child.NumDiff + parent.NumIgnored += child.NumIgnored + parent.NumCompared += child.NumCompared + parent.NumTransformed += child.NumTransformed + parent.NumChildren += child.NumChildren + 1 + if parent.MaxDepth < child.MaxDepth+1 { + parent.MaxDepth = child.MaxDepth + 1 + } + return parent +} diff --git a/vendor/github.com/pkg/errors/.gitignore b/vendor/github.com/pkg/errors/.gitignore deleted file mode 100644 index daf913b1b..000000000 --- a/vendor/github.com/pkg/errors/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof diff --git a/vendor/github.com/pkg/errors/.travis.yml b/vendor/github.com/pkg/errors/.travis.yml deleted file mode 100644 index 588ceca18..000000000 --- a/vendor/github.com/pkg/errors/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -language: go -go_import_path: github.com/pkg/errors -go: - - 1.4.3 - - 1.5.4 - - 1.6.2 - - 1.7.1 - - tip - -script: - - go test -v ./... diff --git a/vendor/github.com/pkg/errors/LICENSE b/vendor/github.com/pkg/errors/LICENSE deleted file mode 100644 index 835ba3e75..000000000 --- a/vendor/github.com/pkg/errors/LICENSE +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) 2015, Dave Cheney -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/pkg/errors/README.md b/vendor/github.com/pkg/errors/README.md deleted file mode 100644 index 273db3c98..000000000 --- a/vendor/github.com/pkg/errors/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) - -Package errors provides simple error handling primitives. - -`go get github.com/pkg/errors` - -The traditional error handling idiom in Go is roughly akin to -```go -if err != nil { - return err -} -``` -which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error. - -## Adding context to an error - -The errors.Wrap function returns a new error that adds context to the original error. For example -```go -_, err := ioutil.ReadAll(r) -if err != nil { - return errors.Wrap(err, "read failed") -} -``` -## Retrieving the cause of an error - -Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`. -```go -type causer interface { - Cause() error -} -``` -`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example: -```go -switch err := errors.Cause(err).(type) { -case *MyError: - // handle specifically -default: - // unknown error -} -``` - -[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors). - -## Contributing - -We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high. - -Before proposing a change, please discuss your change by raising an issue. - -## Licence - -BSD-2-Clause diff --git a/vendor/github.com/pkg/errors/appveyor.yml b/vendor/github.com/pkg/errors/appveyor.yml deleted file mode 100644 index a932eade0..000000000 --- a/vendor/github.com/pkg/errors/appveyor.yml +++ /dev/null @@ -1,32 +0,0 @@ -version: build-{build}.{branch} - -clone_folder: C:\gopath\src\github.com\pkg\errors -shallow_clone: true # for startup speed - -environment: - GOPATH: C:\gopath - -platform: - - x64 - -# http://www.appveyor.com/docs/installed-software -install: - # some helpful output for debugging builds - - go version - - go env - # pre-installed MinGW at C:\MinGW is 32bit only - # but MSYS2 at C:\msys64 has mingw64 - - set PATH=C:\msys64\mingw64\bin;%PATH% - - gcc --version - - g++ --version - -build_script: - - go install -v ./... - -test_script: - - set PATH=C:\gopath\bin;%PATH% - - go test -v ./... - -#artifacts: -# - path: '%GOPATH%\bin\*.exe' -deploy: off diff --git a/vendor/github.com/pkg/errors/errors.go b/vendor/github.com/pkg/errors/errors.go deleted file mode 100644 index 842ee8045..000000000 --- a/vendor/github.com/pkg/errors/errors.go +++ /dev/null @@ -1,269 +0,0 @@ -// Package errors provides simple error handling primitives. -// -// The traditional error handling idiom in Go is roughly akin to -// -// if err != nil { -// return err -// } -// -// which applied recursively up the call stack results in error reports -// without context or debugging information. The errors package allows -// programmers to add context to the failure path in their code in a way -// that does not destroy the original value of the error. -// -// Adding context to an error -// -// The errors.Wrap function returns a new error that adds context to the -// original error by recording a stack trace at the point Wrap is called, -// and the supplied message. For example -// -// _, err := ioutil.ReadAll(r) -// if err != nil { -// return errors.Wrap(err, "read failed") -// } -// -// If additional control is required the errors.WithStack and errors.WithMessage -// functions destructure errors.Wrap into its component operations of annotating -// an error with a stack trace and an a message, respectively. -// -// Retrieving the cause of an error -// -// Using errors.Wrap constructs a stack of errors, adding context to the -// preceding error. Depending on the nature of the error it may be necessary -// to reverse the operation of errors.Wrap to retrieve the original error -// for inspection. Any error value which implements this interface -// -// type causer interface { -// Cause() error -// } -// -// can be inspected by errors.Cause. errors.Cause will recursively retrieve -// the topmost error which does not implement causer, which is assumed to be -// the original cause. For example: -// -// switch err := errors.Cause(err).(type) { -// case *MyError: -// // handle specifically -// default: -// // unknown error -// } -// -// causer interface is not exported by this package, but is considered a part -// of stable public API. -// -// Formatted printing of errors -// -// All error values returned from this package implement fmt.Formatter and can -// be formatted by the fmt package. The following verbs are supported -// -// %s print the error. If the error has a Cause it will be -// printed recursively -// %v see %s -// %+v extended format. Each Frame of the error's StackTrace will -// be printed in detail. -// -// Retrieving the stack trace of an error or wrapper -// -// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are -// invoked. This information can be retrieved with the following interface. -// -// type stackTracer interface { -// StackTrace() errors.StackTrace -// } -// -// Where errors.StackTrace is defined as -// -// type StackTrace []Frame -// -// The Frame type represents a call site in the stack trace. Frame supports -// the fmt.Formatter interface that can be used for printing information about -// the stack trace of this error. For example: -// -// if err, ok := err.(stackTracer); ok { -// for _, f := range err.StackTrace() { -// fmt.Printf("%+s:%d", f) -// } -// } -// -// stackTracer interface is not exported by this package, but is considered a part -// of stable public API. -// -// See the documentation for Frame.Format for more details. -package errors - -import ( - "fmt" - "io" -) - -// New returns an error with the supplied message. -// New also records the stack trace at the point it was called. -func New(message string) error { - return &fundamental{ - msg: message, - stack: callers(), - } -} - -// Errorf formats according to a format specifier and returns the string -// as a value that satisfies error. -// Errorf also records the stack trace at the point it was called. -func Errorf(format string, args ...interface{}) error { - return &fundamental{ - msg: fmt.Sprintf(format, args...), - stack: callers(), - } -} - -// fundamental is an error that has a message and a stack, but no caller. -type fundamental struct { - msg string - *stack -} - -func (f *fundamental) Error() string { return f.msg } - -func (f *fundamental) Format(s fmt.State, verb rune) { - switch verb { - case 'v': - if s.Flag('+') { - io.WriteString(s, f.msg) - f.stack.Format(s, verb) - return - } - fallthrough - case 's': - io.WriteString(s, f.msg) - case 'q': - fmt.Fprintf(s, "%q", f.msg) - } -} - -// WithStack annotates err with a stack trace at the point WithStack was called. -// If err is nil, WithStack returns nil. -func WithStack(err error) error { - if err == nil { - return nil - } - return &withStack{ - err, - callers(), - } -} - -type withStack struct { - error - *stack -} - -func (w *withStack) Cause() error { return w.error } - -func (w *withStack) Format(s fmt.State, verb rune) { - switch verb { - case 'v': - if s.Flag('+') { - fmt.Fprintf(s, "%+v", w.Cause()) - w.stack.Format(s, verb) - return - } - fallthrough - case 's': - io.WriteString(s, w.Error()) - case 'q': - fmt.Fprintf(s, "%q", w.Error()) - } -} - -// Wrap returns an error annotating err with a stack trace -// at the point Wrap is called, and the supplied message. -// If err is nil, Wrap returns nil. -func Wrap(err error, message string) error { - if err == nil { - return nil - } - err = &withMessage{ - cause: err, - msg: message, - } - return &withStack{ - err, - callers(), - } -} - -// Wrapf returns an error annotating err with a stack trace -// at the point Wrapf is call, and the format specifier. -// If err is nil, Wrapf returns nil. -func Wrapf(err error, format string, args ...interface{}) error { - if err == nil { - return nil - } - err = &withMessage{ - cause: err, - msg: fmt.Sprintf(format, args...), - } - return &withStack{ - err, - callers(), - } -} - -// WithMessage annotates err with a new message. -// If err is nil, WithMessage returns nil. -func WithMessage(err error, message string) error { - if err == nil { - return nil - } - return &withMessage{ - cause: err, - msg: message, - } -} - -type withMessage struct { - cause error - msg string -} - -func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } -func (w *withMessage) Cause() error { return w.cause } - -func (w *withMessage) Format(s fmt.State, verb rune) { - switch verb { - case 'v': - if s.Flag('+') { - fmt.Fprintf(s, "%+v\n", w.Cause()) - io.WriteString(s, w.msg) - return - } - fallthrough - case 's', 'q': - io.WriteString(s, w.Error()) - } -} - -// Cause returns the underlying cause of the error, if possible. -// An error value has a cause if it implements the following -// interface: -// -// type causer interface { -// Cause() error -// } -// -// If the error does not implement Cause, the original error will -// be returned. If the error is nil, nil will be returned without further -// investigation. -func Cause(err error) error { - type causer interface { - Cause() error - } - - for err != nil { - cause, ok := err.(causer) - if !ok { - break - } - err = cause.Cause() - } - return err -} diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go deleted file mode 100644 index 6b1f2891a..000000000 --- a/vendor/github.com/pkg/errors/stack.go +++ /dev/null @@ -1,178 +0,0 @@ -package errors - -import ( - "fmt" - "io" - "path" - "runtime" - "strings" -) - -// Frame represents a program counter inside a stack frame. -type Frame uintptr - -// pc returns the program counter for this frame; -// multiple frames may have the same PC value. -func (f Frame) pc() uintptr { return uintptr(f) - 1 } - -// file returns the full path to the file that contains the -// function for this Frame's pc. -func (f Frame) file() string { - fn := runtime.FuncForPC(f.pc()) - if fn == nil { - return "unknown" - } - file, _ := fn.FileLine(f.pc()) - return file -} - -// line returns the line number of source code of the -// function for this Frame's pc. -func (f Frame) line() int { - fn := runtime.FuncForPC(f.pc()) - if fn == nil { - return 0 - } - _, line := fn.FileLine(f.pc()) - return line -} - -// Format formats the frame according to the fmt.Formatter interface. -// -// %s source file -// %d source line -// %n function name -// %v equivalent to %s:%d -// -// Format accepts flags that alter the printing of some verbs, as follows: -// -// %+s path of source file relative to the compile time GOPATH -// %+v equivalent to %+s:%d -func (f Frame) Format(s fmt.State, verb rune) { - switch verb { - case 's': - switch { - case s.Flag('+'): - pc := f.pc() - fn := runtime.FuncForPC(pc) - if fn == nil { - io.WriteString(s, "unknown") - } else { - file, _ := fn.FileLine(pc) - fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file) - } - default: - io.WriteString(s, path.Base(f.file())) - } - case 'd': - fmt.Fprintf(s, "%d", f.line()) - case 'n': - name := runtime.FuncForPC(f.pc()).Name() - io.WriteString(s, funcname(name)) - case 'v': - f.Format(s, 's') - io.WriteString(s, ":") - f.Format(s, 'd') - } -} - -// StackTrace is stack of Frames from innermost (newest) to outermost (oldest). -type StackTrace []Frame - -func (st StackTrace) Format(s fmt.State, verb rune) { - switch verb { - case 'v': - switch { - case s.Flag('+'): - for _, f := range st { - fmt.Fprintf(s, "\n%+v", f) - } - case s.Flag('#'): - fmt.Fprintf(s, "%#v", []Frame(st)) - default: - fmt.Fprintf(s, "%v", []Frame(st)) - } - case 's': - fmt.Fprintf(s, "%s", []Frame(st)) - } -} - -// stack represents a stack of program counters. -type stack []uintptr - -func (s *stack) Format(st fmt.State, verb rune) { - switch verb { - case 'v': - switch { - case st.Flag('+'): - for _, pc := range *s { - f := Frame(pc) - fmt.Fprintf(st, "\n%+v", f) - } - } - } -} - -func (s *stack) StackTrace() StackTrace { - f := make([]Frame, len(*s)) - for i := 0; i < len(f); i++ { - f[i] = Frame((*s)[i]) - } - return f -} - -func callers() *stack { - const depth = 32 - var pcs [depth]uintptr - n := runtime.Callers(3, pcs[:]) - var st stack = pcs[0:n] - return &st -} - -// funcname removes the path prefix component of a function's name reported by func.Name(). -func funcname(name string) string { - i := strings.LastIndex(name, "/") - name = name[i+1:] - i = strings.Index(name, ".") - return name[i+1:] -} - -func trimGOPATH(name, file string) string { - // Here we want to get the source file path relative to the compile time - // GOPATH. As of Go 1.6.x there is no direct way to know the compiled - // GOPATH at runtime, but we can infer the number of path segments in the - // GOPATH. We note that fn.Name() returns the function name qualified by - // the import path, which does not include the GOPATH. Thus we can trim - // segments from the beginning of the file path until the number of path - // separators remaining is one more than the number of path separators in - // the function name. For example, given: - // - // GOPATH /home/user - // file /home/user/src/pkg/sub/file.go - // fn.Name() pkg/sub.Type.Method - // - // We want to produce: - // - // pkg/sub/file.go - // - // From this we can easily see that fn.Name() has one less path separator - // than our desired output. We count separators from the end of the file - // path until it finds two more than in the function name and then move - // one character forward to preserve the initial path segment without a - // leading separator. - const sep = "/" - goal := strings.Count(name, sep) + 2 - i := len(file) - for n := 0; n < goal; n++ { - i = strings.LastIndex(file[:i], sep) - if i == -1 { - // not enough separators found, set i so that the slice expression - // below leaves file unmodified - i = -len(sep) - break - } - } - // get back to 0 or trim the leading separator - file = file[i+len(sep):] - return file -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 4f05cfa3d..cca25c04d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,12 +1,22 @@ # github.com/davecgh/go-spew v1.1.1 +## explicit github.com/davecgh/go-spew/spew # github.com/go-test/deep v1.0.4 +## explicit github.com/go-test/deep +# github.com/google/go-cmp v0.5.7 +## explicit +github.com/google/go-cmp/cmp +github.com/google/go-cmp/cmp/internal/diff +github.com/google/go-cmp/cmp/internal/flags +github.com/google/go-cmp/cmp/internal/function +github.com/google/go-cmp/cmp/internal/value # github.com/gorilla/websocket v1.4.2 +## explicit github.com/gorilla/websocket -# github.com/pkg/errors v0.8.0 -github.com/pkg/errors # github.com/pmezard/go-difflib v1.0.0 +## explicit github.com/pmezard/go-difflib/difflib # github.com/stretchr/testify v1.2.2 +## explicit github.com/stretchr/testify/assert diff --git a/views.go b/views.go index ee197ad84..a3a1bd056 100644 --- a/views.go +++ b/views.go @@ -38,8 +38,16 @@ type View struct { BotID string `json:"bot_id"` } +type ViewSubmissionCallbackResponseURL struct { + BlockID string `json:"block_id"` + ActionID string `json:"action_id"` + ChannelID string `json:"channel_id"` + ResponseURL string `json:"response_url"` +} + type ViewSubmissionCallback struct { - Hash string `json:"hash"` + Hash string `json:"hash"` + ResponseURLs []ViewSubmissionCallbackResponseURL `json:"response_urls,omitempty"` } type ViewClosedCallback struct { @@ -90,7 +98,7 @@ func NewErrorsViewSubmissionResponse(errors map[string]string) *ViewSubmissionRe type ModalViewRequest struct { Type ViewType `json:"type"` - Title *TextBlockObject `json:"title"` + Title *TextBlockObject `json:"title,omitempty"` Blocks Blocks `json:"blocks"` Close *TextBlockObject `json:"close,omitempty"` Submit *TextBlockObject `json:"submit,omitempty"` diff --git a/views_test.go b/views_test.go index 59312c567..85da29d0a 100644 --- a/views_test.go +++ b/views_test.go @@ -6,8 +6,9 @@ import ( "reflect" "testing" - "github.com/slack-go/slack/internal/errorsx" "github.com/stretchr/testify/assert" + + "github.com/slack-go/slack/internal/errorsx" ) var dummySlackErr = errorsx.String("dummy_error_from_slack") diff --git a/webhooks.go b/webhooks.go index 39fff441e..15097f03e 100644 --- a/webhooks.go +++ b/webhooks.go @@ -1,7 +1,10 @@ package slack import ( + "bytes" "context" + "encoding/json" + "fmt" "net/http" ) @@ -15,6 +18,9 @@ type WebhookMessage struct { Attachments []Attachment `json:"attachments,omitempty"` Parse string `json:"parse,omitempty"` Blocks *Blocks `json:"blocks,omitempty"` + ResponseType string `json:"response_type,omitempty"` + ReplaceOriginal bool `json:"replace_original,omitempty"` + DeleteOriginal bool `json:"delete_original,omitempty"` } func PostWebhook(url string, msg *WebhookMessage) error { @@ -28,3 +34,24 @@ func PostWebhookContext(ctx context.Context, url string, msg *WebhookMessage) er func PostWebhookCustomHTTP(url string, httpClient *http.Client, msg *WebhookMessage) error { return PostWebhookCustomHTTPContext(context.Background(), url, httpClient, msg) } + +func PostWebhookCustomHTTPContext(ctx context.Context, url string, httpClient *http.Client, msg *WebhookMessage) error { + raw, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("marshal failed: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(raw)) + if err != nil { + return fmt.Errorf("failed new request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to post webhook: %w", err) + } + defer resp.Body.Close() + + return checkStatusCode(resp, discard{}) +} diff --git a/webhooks_go112.go b/webhooks_go112.go deleted file mode 100644 index 4e0db0e41..000000000 --- a/webhooks_go112.go +++ /dev/null @@ -1,34 +0,0 @@ -// +build !go1.13 - -package slack - -import ( - "bytes" - "context" - "encoding/json" - "net/http" - - "github.com/pkg/errors" -) - -func PostWebhookCustomHTTPContext(ctx context.Context, url string, httpClient *http.Client, msg *WebhookMessage) error { - raw, err := json.Marshal(msg) - if err != nil { - return errors.Wrap(err, "marshal failed") - } - - req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(raw)) - if err != nil { - return errors.Wrap(err, "failed new request") - } - req = req.WithContext(ctx) - req.Header.Set("Content-Type", "application/json") - - resp, err := httpClient.Do(req) - if err != nil { - return errors.Wrap(err, "failed to post webhook") - } - defer resp.Body.Close() - - return checkStatusCode(resp, discard{}) -} diff --git a/webhooks_go113.go b/webhooks_go113.go deleted file mode 100644 index 99c243f59..000000000 --- a/webhooks_go113.go +++ /dev/null @@ -1,33 +0,0 @@ -// +build go1.13 - -package slack - -import ( - "bytes" - "context" - "encoding/json" - "net/http" - - "github.com/pkg/errors" -) - -func PostWebhookCustomHTTPContext(ctx context.Context, url string, httpClient *http.Client, msg *WebhookMessage) error { - raw, err := json.Marshal(msg) - if err != nil { - return errors.Wrap(err, "marshal failed") - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(raw)) - if err != nil { - return errors.Wrap(err, "failed new request") - } - req.Header.Set("Content-Type", "application/json") - - resp, err := httpClient.Do(req) - if err != nil { - return errors.Wrap(err, "failed to post webhook") - } - defer resp.Body.Close() - - return checkStatusCode(resp, discard{}) -} diff --git a/websocket_internals.go b/websocket_internals.go index 3e1906eee..454a21331 100644 --- a/websocket_internals.go +++ b/websocket_internals.go @@ -94,6 +94,7 @@ func (i *IncomingEventError) Error() string { // AckErrorEvent i type AckErrorEvent struct { ErrorObj error + ReplyTo int } func (a *AckErrorEvent) Error() string { diff --git a/websocket_managed_conn.go b/websocket_managed_conn.go index fe6802e49..92536171f 100644 --- a/websocket_managed_conn.go +++ b/websocket_managed_conn.go @@ -9,11 +9,11 @@ import ( "reflect" "time" - "github.com/slack-go/slack/internal/backoff" - "github.com/slack-go/slack/internal/misc" - "github.com/gorilla/websocket" + + "github.com/slack-go/slack/internal/backoff" "github.com/slack-go/slack/internal/errorsx" + "github.com/slack-go/slack/internal/misc" "github.com/slack-go/slack/internal/timex" ) @@ -91,6 +91,7 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke errInvalidAuth = "invalid_auth" errInactiveAccount = "account_inactive" errMissingAuthToken = "not_authed" + errTokenRevoked = "token_revoked" ) // used to provide exponential backoff wait time with jitter before trying @@ -118,7 +119,7 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke // check for fatal errors switch err.Error() { - case errInvalidAuth, errInactiveAccount, errMissingAuthToken: + case errInvalidAuth, errInactiveAccount, errMissingAuthToken, errTokenRevoked: rtm.Debugf("invalid auth when connecting with RTM: %s", err) rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}} return nil, nil, err @@ -438,10 +439,10 @@ func (rtm *RTM) handleAck(event json.RawMessage) { if ack.RTMResponse.Error.Code == -1 && ack.RTMResponse.Error.Msg == "slow down, too many messages..." { rtm.IncomingEvents <- RTMEvent{"ack_error", &RateLimitEvent{}} } else { - rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{ack.Error}} + rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{ack.Error, ack.ReplyTo}} } } else { - rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{fmt.Errorf("ack decode failure")}} + rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{ErrorObj: fmt.Errorf("ack decode failure")}} } } diff --git a/websocket_managed_conn_test.go b/websocket_managed_conn_test.go index bb4e3e4d5..429ce8d52 100644 --- a/websocket_managed_conn_test.go +++ b/websocket_managed_conn_test.go @@ -8,9 +8,10 @@ import ( "time" websocket "github.com/gorilla/websocket" + "github.com/stretchr/testify/assert" + "github.com/slack-go/slack" "github.com/slack-go/slack/slacktest" - "github.com/stretchr/testify/assert" ) const ( diff --git a/workflow_step.go b/workflow_step.go new file mode 100644 index 000000000..bcc892c5a --- /dev/null +++ b/workflow_step.go @@ -0,0 +1,98 @@ +package slack + +import ( + "context" + "encoding/json" +) + +const VTWorkflowStep ViewType = "workflow_step" + +type ( + ConfigurationModalRequest struct { + ModalViewRequest + } + + WorkflowStepCompleteResponse struct { + WorkflowStepEditID string `json:"workflow_step_edit_id"` + Inputs *WorkflowStepInputs `json:"inputs,omitempty"` + Outputs *[]WorkflowStepOutput `json:"outputs,omitempty"` + } + + WorkflowStepInputElement struct { + Value string `json:"value"` + SkipVariableReplacement bool `json:"skip_variable_replacement"` + } + + WorkflowStepInputs map[string]WorkflowStepInputElement + + WorkflowStepOutput struct { + Name string `json:"name"` + Type string `json:"type"` + Label string `json:"label"` + } +) + +func NewConfigurationModalRequest(blocks Blocks, privateMetaData string, externalID string) *ConfigurationModalRequest { + return &ConfigurationModalRequest{ + ModalViewRequest{ + Type: VTWorkflowStep, + Title: nil, // slack configuration modal must not have a title! + Blocks: blocks, + PrivateMetadata: privateMetaData, + ExternalID: externalID, + }, + } +} + +func (api *Client) SaveWorkflowStepConfiguration(workflowStepEditID string, inputs *WorkflowStepInputs, outputs *[]WorkflowStepOutput) error { + return api.SaveWorkflowStepConfigurationContext(context.Background(), workflowStepEditID, inputs, outputs) +} + +func (api *Client) SaveWorkflowStepConfigurationContext(ctx context.Context, workflowStepEditID string, inputs *WorkflowStepInputs, outputs *[]WorkflowStepOutput) error { + // More information: https://api.slack.com/methods/workflows.updateStep + wscr := WorkflowStepCompleteResponse{ + WorkflowStepEditID: workflowStepEditID, + Inputs: inputs, + Outputs: outputs, + } + + endpoint := api.endpoint + "workflows.updateStep" + jsonData, err := json.Marshal(wscr) + if err != nil { + return err + } + + response := &SlackResponse{} + if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonData, response, api); err != nil { + return err + } + + if !response.Ok { + return response.Err() + } + + return nil +} + +func GetInitialOptionFromWorkflowStepInput(selection *SelectBlockElement, inputs *WorkflowStepInputs, options []*OptionBlockObject) (*OptionBlockObject, bool) { + if len(*inputs) == 0 { + return &OptionBlockObject{}, false + } + if len(options) == 0 { + return &OptionBlockObject{}, false + } + + if val, ok := (*inputs)[selection.ActionID]; ok { + if val.SkipVariableReplacement { + return &OptionBlockObject{}, false + } + + for _, option := range options { + if option.Value == val.Value { + return option, true + } + } + } + + return &OptionBlockObject{}, false +} diff --git a/workflow_step_test.go b/workflow_step_test.go new file mode 100644 index 000000000..daa3b6da2 --- /dev/null +++ b/workflow_step_test.go @@ -0,0 +1,189 @@ +package slack + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +const ( + IDExampleSelectInput = "ae9642ae-a9ef-4394-904b-a5c7a83bf4a6" + IDSelectOptionBlock = "832bf7af-22ea-4acb-82e3-a0cc3722052b" +) + +func TestNewConfigurationModalRequest(t *testing.T) { + blocks := configModalBlocks() + privateMetaData := "An optional string that will be sent to your app in view_submission and block_actions events. Max length of 3000 characters." + externalID := "c4baf441-fbc1-4131-b349-7c8df0ae7df6" + + result := NewConfigurationModalRequest(blocks, privateMetaData, externalID) + + if result.ModalViewRequest.Title != nil { + t.Fail() + } + if result.PrivateMetadata != privateMetaData { + t.Fail() + } + if result.ExternalID != externalID { + t.Fail() + } +} + +func TestGetInitialOptionFromWorkflowStepInput(t *testing.T) { + options, testOption := createOptionBlockObjects() + selection := createSelection(options) + + scenarios := []struct { + options []*OptionBlockObject + inputs *WorkflowStepInputs + expectedResult *OptionBlockObject + expectedFlag bool + }{ + { + options: options, + inputs: createWorkflowStepInputs1(), + expectedResult: &OptionBlockObject{}, + expectedFlag: false, + }, + { + options: []*OptionBlockObject{}, + inputs: createWorkflowStepInputs4(testOption.Value), + expectedResult: &OptionBlockObject{}, + expectedFlag: false, + }, + { + options: options, + inputs: createWorkflowStepInputs2(), + expectedResult: &OptionBlockObject{}, + expectedFlag: false, + }, + { + options: options, + inputs: createWorkflowStepInputs3(), + expectedResult: &OptionBlockObject{}, + expectedFlag: false, + }, + { + options: options, + inputs: createWorkflowStepInputs4(testOption.Value), + expectedResult: testOption, + expectedFlag: true, + }, + } + + for _, scenario := range scenarios { + result, ok := GetInitialOptionFromWorkflowStepInput(selection, scenario.inputs, scenario.options) + if ok != scenario.expectedFlag { + t.Fail() + } + + if !cmp.Equal(result, scenario.expectedResult) { + t.Fail() + } + } +} + +func createOptionBlockObjects() ([]*OptionBlockObject, *OptionBlockObject) { + var options []*OptionBlockObject + options = append( + options, + NewOptionBlockObject("one", NewTextBlockObject("plain_text", "One", false, false), nil), + ) + + option2 := NewOptionBlockObject("two", NewTextBlockObject("plain_text", "Two", false, false), nil) + options = append( + options, + option2, + ) + + options = append( + options, + NewOptionBlockObject("three", NewTextBlockObject("plain_text", "Three", false, false), nil), + ) + + return options, option2 +} + +func createSelection(options []*OptionBlockObject) *SelectBlockElement { + return NewOptionsSelectBlockElement( + "static_select", + NewTextBlockObject("plain_text", "your choice", false, false), + IDExampleSelectInput, + options..., + ) +} + +func configModalBlocks() Blocks { + headerText := NewTextBlockObject("mrkdwn", "Hello World!\nThis is your workflow step app configuration view", false, false) + headerSection := NewSectionBlock(headerText, nil, nil) + + options, _ := createOptionBlockObjects() + + selection := createSelection(options) + + inputBlock := NewInputBlock( + IDSelectOptionBlock, + NewTextBlockObject("plain_text", "Select an option", false, false), + selection, + ) + + blocks := Blocks{ + BlockSet: []Block{ + headerSection, + inputBlock, + }, + } + + return blocks +} + +func createWorkflowStepInputs1() *WorkflowStepInputs { + return &WorkflowStepInputs{} +} + +func createWorkflowStepInputs2() *WorkflowStepInputs { + return &WorkflowStepInputs{ + "test": WorkflowStepInputElement{ + Value: "random-string", + SkipVariableReplacement: false, + }, + "123-test": WorkflowStepInputElement{ + Value: "another-string", + SkipVariableReplacement: false, + }, + } +} + +func createWorkflowStepInputs3() *WorkflowStepInputs { + return &WorkflowStepInputs{ + "test": WorkflowStepInputElement{ + Value: "random-string", + SkipVariableReplacement: false, + }, + "123-test": WorkflowStepInputElement{ + Value: "another-string", + SkipVariableReplacement: false, + }, + IDExampleSelectInput: WorkflowStepInputElement{ + Value: "lorem-ipsum", + SkipVariableReplacement: true, + }, + } +} + +func createWorkflowStepInputs4(optionValue string) *WorkflowStepInputs { + return &WorkflowStepInputs{ + "test": WorkflowStepInputElement{ + Value: "random-string", + SkipVariableReplacement: false, + }, + "123-test": WorkflowStepInputElement{ + Value: "another-string", + SkipVariableReplacement: false, + }, + IDExampleSelectInput: WorkflowStepInputElement{ + Value: optionValue, + SkipVariableReplacement: false, + }, + } +}