Skip to content

Commit

Permalink
Add WebhookTypes and EventForType methods (google#2865)
Browse files Browse the repository at this point in the history
Fixes: google#2863.
  • Loading branch information
evankanderson authored Aug 16, 2023
1 parent b9027bd commit 1a4b106
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 200 deletions.
141 changes: 9 additions & 132 deletions github/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,139 +27,16 @@ func (e Event) String() string {

// ParsePayload parses the event payload. For recognized event types,
// a value of the corresponding struct type will be returned.
func (e *Event) ParsePayload() (payload interface{}, err error) {
switch *e.Type {
case "BranchProtectionRuleEvent":
payload = &BranchProtectionRuleEvent{}
case "CheckRunEvent":
payload = &CheckRunEvent{}
case "CheckSuiteEvent":
payload = &CheckSuiteEvent{}
case "CodeScanningAlertEvent":
payload = &CodeScanningAlertEvent{}
case "CommitCommentEvent":
payload = &CommitCommentEvent{}
case "ContentReferenceEvent":
payload = &ContentReferenceEvent{}
case "CreateEvent":
payload = &CreateEvent{}
case "DeleteEvent":
payload = &DeleteEvent{}
case "DeployKeyEvent":
payload = &DeployKeyEvent{}
case "DeploymentEvent":
payload = &DeploymentEvent{}
case "DeploymentProtectionRuleEvent":
payload = &DeploymentProtectionRuleEvent{}
case "DeploymentStatusEvent":
payload = &DeploymentStatusEvent{}
case "DiscussionEvent":
payload = &DiscussionEvent{}
case "DiscussionCommentEvent":
payload = &DiscussionCommentEvent{}
case "ForkEvent":
payload = &ForkEvent{}
case "GitHubAppAuthorizationEvent":
payload = &GitHubAppAuthorizationEvent{}
case "GollumEvent":
payload = &GollumEvent{}
case "InstallationEvent":
payload = &InstallationEvent{}
case "InstallationRepositoriesEvent":
payload = &InstallationRepositoriesEvent{}
case "InstallationTargetEvent":
payload = &InstallationTargetEvent{}
case "IssueCommentEvent":
payload = &IssueCommentEvent{}
case "IssuesEvent":
payload = &IssuesEvent{}
case "LabelEvent":
payload = &LabelEvent{}
case "MarketplacePurchaseEvent":
payload = &MarketplacePurchaseEvent{}
case "MemberEvent":
payload = &MemberEvent{}
case "MembershipEvent":
payload = &MembershipEvent{}
case "MergeGroupEvent":
payload = &MergeGroupEvent{}
case "MetaEvent":
payload = &MetaEvent{}
case "MilestoneEvent":
payload = &MilestoneEvent{}
case "OrganizationEvent":
payload = &OrganizationEvent{}
case "OrgBlockEvent":
payload = &OrgBlockEvent{}
case "PackageEvent":
payload = &PackageEvent{}
case "PageBuildEvent":
payload = &PageBuildEvent{}
case "PersonalAccessTokenRequestEvent":
payload = &PersonalAccessTokenRequestEvent{}
case "PingEvent":
payload = &PingEvent{}
case "ProjectEvent":
payload = &ProjectEvent{}
case "ProjectCardEvent":
payload = &ProjectCardEvent{}
case "ProjectColumnEvent":
payload = &ProjectColumnEvent{}
case "ProjectV2Event":
payload = &ProjectV2Event{}
case "ProjectV2ItemEvent":
payload = &ProjectV2ItemEvent{}
case "PublicEvent":
payload = &PublicEvent{}
case "PullRequestEvent":
payload = &PullRequestEvent{}
case "PullRequestReviewEvent":
payload = &PullRequestReviewEvent{}
case "PullRequestReviewCommentEvent":
payload = &PullRequestReviewCommentEvent{}
case "PullRequestReviewThreadEvent":
payload = &PullRequestReviewThreadEvent{}
case "PullRequestTargetEvent":
payload = &PullRequestTargetEvent{}
case "PushEvent":
payload = &PushEvent{}
case "ReleaseEvent":
payload = &ReleaseEvent{}
case "RepositoryEvent":
payload = &RepositoryEvent{}
case "RepositoryDispatchEvent":
payload = &RepositoryDispatchEvent{}
case "RepositoryImportEvent":
payload = &RepositoryImportEvent{}
case "RepositoryVulnerabilityAlertEvent":
payload = &RepositoryVulnerabilityAlertEvent{}
case "SecretScanningAlertEvent":
payload = &SecretScanningAlertEvent{}
case "SecurityAdvisoryEvent":
payload = &SecurityAdvisoryEvent{}
case "SecurityAndAnalysisEvent":
payload = &SecurityAndAnalysisEvent{}
case "StarEvent":
payload = &StarEvent{}
case "StatusEvent":
payload = &StatusEvent{}
case "TeamEvent":
payload = &TeamEvent{}
case "TeamAddEvent":
payload = &TeamAddEvent{}
case "UserEvent":
payload = &UserEvent{}
case "WatchEvent":
payload = &WatchEvent{}
case "WorkflowDispatchEvent":
payload = &WorkflowDispatchEvent{}
case "WorkflowJobEvent":
payload = &WorkflowJobEvent{}
case "WorkflowRunEvent":
payload = &WorkflowRunEvent{}
func (e *Event) ParsePayload() (interface{}, error) {
// It would be nice if e.Type were the snake_case name of the event,
// but the existing interface uses the struct name instead.
payload := EventForType(typeToMessageMapping[e.GetType()])

if err := json.Unmarshal(e.GetRawPayload(), &payload); err != nil {
return nil, err
}
err = json.Unmarshal(*e.RawPayload, &payload)
return payload, err

return payload, nil
}

// Payload returns the parsed event payload. For recognized event types,
Expand Down
12 changes: 12 additions & 0 deletions github/event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ func TestPayload_NoPanic(t *testing.T) {
e.Payload()
}

func TestEmptyEvent_NoPanic(t *testing.T) {
e := &Event{}
if _, err := e.ParsePayload(); err == nil {
t.Error("ParsePayload unexpectedly succeeded on empty event")
}

e = nil
if _, err := e.ParsePayload(); err == nil {
t.Error("ParsePayload unexpectedly succeeded on nil event")
}
}

func TestEvent_Marshal(t *testing.T) {
testJSONMarshal(t, &Event{}, "{}")

Expand Down
171 changes: 105 additions & 66 deletions github/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"mime"
"net/http"
"net/url"
"reflect"
"sort"
"strings"
)

Expand All @@ -43,74 +45,86 @@ const (

var (
// eventTypeMapping maps webhooks types to their corresponding go-github struct types.
eventTypeMapping = map[string]string{
"branch_protection_rule": "BranchProtectionRuleEvent",
"check_run": "CheckRunEvent",
"check_suite": "CheckSuiteEvent",
"code_scanning_alert": "CodeScanningAlertEvent",
"commit_comment": "CommitCommentEvent",
"content_reference": "ContentReferenceEvent",
"create": "CreateEvent",
"delete": "DeleteEvent",
"deploy_key": "DeployKeyEvent",
"deployment": "DeploymentEvent",
"deployment_status": "DeploymentStatusEvent",
"deployment_protection_rule": "DeploymentProtectionRuleEvent",
"discussion": "DiscussionEvent",
"discussion_comment": "DiscussionCommentEvent",
"fork": "ForkEvent",
"github_app_authorization": "GitHubAppAuthorizationEvent",
"gollum": "GollumEvent",
"installation": "InstallationEvent",
"installation_repositories": "InstallationRepositoriesEvent",
"installation_target": "InstallationTargetEvent",
"issue_comment": "IssueCommentEvent",
"issues": "IssuesEvent",
"label": "LabelEvent",
"marketplace_purchase": "MarketplacePurchaseEvent",
"member": "MemberEvent",
"membership": "MembershipEvent",
"merge_group": "MergeGroupEvent",
"meta": "MetaEvent",
"milestone": "MilestoneEvent",
"organization": "OrganizationEvent",
"org_block": "OrgBlockEvent",
"package": "PackageEvent",
"page_build": "PageBuildEvent",
"personal_access_token_request": "PersonalAccessTokenRequestEvent",
"ping": "PingEvent",
"project": "ProjectEvent",
"project_card": "ProjectCardEvent",
"project_column": "ProjectColumnEvent",
"projects_v2": "ProjectV2Event",
"projects_v2_item": "ProjectV2ItemEvent",
"public": "PublicEvent",
"pull_request": "PullRequestEvent",
"pull_request_review": "PullRequestReviewEvent",
"pull_request_review_comment": "PullRequestReviewCommentEvent",
"pull_request_review_thread": "PullRequestReviewThreadEvent",
"pull_request_target": "PullRequestTargetEvent",
"push": "PushEvent",
"repository": "RepositoryEvent",
"repository_dispatch": "RepositoryDispatchEvent",
"repository_import": "RepositoryImportEvent",
"repository_vulnerability_alert": "RepositoryVulnerabilityAlertEvent",
"release": "ReleaseEvent",
"secret_scanning_alert": "SecretScanningAlertEvent",
"security_advisory": "SecurityAdvisoryEvent",
"security_and_analysis": "SecurityAndAnalysisEvent",
"star": "StarEvent",
"status": "StatusEvent",
"team": "TeamEvent",
"team_add": "TeamAddEvent",
"user": "UserEvent",
"watch": "WatchEvent",
"workflow_dispatch": "WorkflowDispatchEvent",
"workflow_job": "WorkflowJobEvent",
"workflow_run": "WorkflowRunEvent",
eventTypeMapping = map[string]interface{}{
"branch_protection_rule": &BranchProtectionRuleEvent{},
"check_run": &CheckRunEvent{},
"check_suite": &CheckSuiteEvent{},
"code_scanning_alert": &CodeScanningAlertEvent{},
"commit_comment": &CommitCommentEvent{},
"content_reference": &ContentReferenceEvent{},
"create": &CreateEvent{},
"delete": &DeleteEvent{},
"deploy_key": &DeployKeyEvent{},
"deployment": &DeploymentEvent{},
"deployment_status": &DeploymentStatusEvent{},
"deployment_protection_rule": &DeploymentProtectionRuleEvent{},
"discussion": &DiscussionEvent{},
"discussion_comment": &DiscussionCommentEvent{},
"fork": &ForkEvent{},
"github_app_authorization": &GitHubAppAuthorizationEvent{},
"gollum": &GollumEvent{},
"installation": &InstallationEvent{},
"installation_repositories": &InstallationRepositoriesEvent{},
"installation_target": &InstallationTargetEvent{},
"issue_comment": &IssueCommentEvent{},
"issues": &IssuesEvent{},
"label": &LabelEvent{},
"marketplace_purchase": &MarketplacePurchaseEvent{},
"member": &MemberEvent{},
"membership": &MembershipEvent{},
"merge_group": &MergeGroupEvent{},
"meta": &MetaEvent{},
"milestone": &MilestoneEvent{},
"organization": &OrganizationEvent{},
"org_block": &OrgBlockEvent{},
"package": &PackageEvent{},
"page_build": &PageBuildEvent{},
"personal_access_token_request": &PersonalAccessTokenRequestEvent{},
"ping": &PingEvent{},
"project": &ProjectEvent{},
"project_card": &ProjectCardEvent{},
"project_column": &ProjectColumnEvent{},
"projects_v2": &ProjectV2Event{},
"projects_v2_item": &ProjectV2ItemEvent{},
"public": &PublicEvent{},
"pull_request": &PullRequestEvent{},
"pull_request_review": &PullRequestReviewEvent{},
"pull_request_review_comment": &PullRequestReviewCommentEvent{},
"pull_request_review_thread": &PullRequestReviewThreadEvent{},
"pull_request_target": &PullRequestTargetEvent{},
"push": &PushEvent{},
"repository": &RepositoryEvent{},
"repository_dispatch": &RepositoryDispatchEvent{},
"repository_import": &RepositoryImportEvent{},
"repository_vulnerability_alert": &RepositoryVulnerabilityAlertEvent{},
"release": &ReleaseEvent{},
"secret_scanning_alert": &SecretScanningAlertEvent{},
"security_advisory": &SecurityAdvisoryEvent{},
"security_and_analysis": &SecurityAndAnalysisEvent{},
"star": &StarEvent{},
"status": &StatusEvent{},
"team": &TeamEvent{},
"team_add": &TeamAddEvent{},
"user": &UserEvent{},
"watch": &WatchEvent{},
"workflow_dispatch": &WorkflowDispatchEvent{},
"workflow_job": &WorkflowJobEvent{},
"workflow_run": &WorkflowRunEvent{},
}
// forward mapping of event types to the string names of the structs
messageToTypeName = make(map[string]string, len(eventTypeMapping))
// Inverse map of the above
typeToMessageMapping = make(map[string]string, len(eventTypeMapping))
)

func init() {
for k, v := range eventTypeMapping {
typename := reflect.TypeOf(v).Elem().Name()
messageToTypeName[k] = typename
typeToMessageMapping[typename] = k
}
}

// genMAC generates the HMAC signature for a message provided the secret key
// and hashFunc.
func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte {
Expand Down Expand Up @@ -299,7 +313,7 @@ func DeliveryID(r *http.Request) string {
// }
// }
func ParseWebHook(messageType string, payload []byte) (interface{}, error) {
eventType, ok := eventTypeMapping[messageType]
eventType, ok := messageToTypeName[messageType]
if !ok {
return nil, fmt.Errorf("unknown X-Github-Event in message: %v", messageType)
}
Expand All @@ -310,3 +324,28 @@ func ParseWebHook(messageType string, payload []byte) (interface{}, error) {
}
return event.ParsePayload()
}

// WebhookTypes returns a sorted list of all the known GitHub event type strings
// supported by go-github.
func MessageTypes() []string {
types := make([]string, 0, len(eventTypeMapping))
for t := range eventTypeMapping {
types = append(types, t)
}
sort.Strings(types)
return types
}

// EventForType returns an empty struct matching the specified GitHub event type.
// If messageType does not match any known event types, it returns nil.
func EventForType(messageType string) interface{} {
prototype := eventTypeMapping[messageType]
if prototype == nil {
return nil
}
// return a _copy_ of the pointed-to-object. Unfortunately, for this we
// need to use reflection. If we store the actual objects in the map,
// we still need to use reflection to convert from `any` to the actual
// type, so this was deemed the lesser of two evils. (#2865)
return reflect.New(reflect.TypeOf(prototype).Elem()).Interface()
}
17 changes: 17 additions & 0 deletions github/messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,23 @@ func TestParseWebHook(t *testing.T) {
}
}

func TestAllMessageTypesMapped(t *testing.T) {
for _, mt := range MessageTypes() {
if obj := EventForType(mt); obj == nil {
t.Errorf("messageMap missing message type %q", mt)
}
}
}

func TestUnknownMessageType(t *testing.T) {
if obj := EventForType("unknown"); obj != nil {
t.Errorf("EventForType(unknown) = %#v, want nil", obj)
}
if obj := EventForType(""); obj != nil {
t.Errorf(`EventForType("") = %#v, want nil`, obj)
}
}

func TestParseWebHook_BadMessageType(t *testing.T) {
if _, err := ParseWebHook("bogus message type", []byte("{}")); err == nil {
t.Fatal("ParseWebHook returned nil; wanted error")
Expand Down
Loading

0 comments on commit 1a4b106

Please sign in to comment.