1// Copyright (c) Microsoft Corporation. All rights reserved. 2// Licensed under the MIT License. See License.txt in the project root for license information. 3 4package request 5 6import ( 7 "context" 8 "fmt" 9 "regexp" 10 "strings" 11 "time" 12 13 "github.com/Azure/azure-sdk-for-go/tools/generator/cmd/issue/link" 14 "github.com/Azure/azure-sdk-for-go/tools/generator/cmd/issue/query" 15 "github.com/google/go-github/v32/github" 16) 17 18var ( 19 resultHandlerMap = map[link.Code]resultHandlerFunc{ 20 link.CodeSuccess: handleSuccess, 21 link.CodeDataPlane: handleDataPlane, 22 link.CodePRNotMerged: handlePRNotMerged, 23 } 24) 25 26// ParsingOptions ... 27type ParsingOptions struct { 28 IncludeDataPlaneRequests bool 29} 30 31type resultHandlerFunc func(ctx context.Context, client *query.Client, reqIssue ReleaseRequestIssue, result link.ResolveResult) (*Request, error) 32 33// Request represents a parsed SDK release request 34type Request struct { 35 RequestLink string 36 TargetDate time.Time 37 ReadmePath string 38 Tag string 39 Track Track 40} 41 42// Track ... 43type Track string 44 45const ( 46 // Track1 ... 47 Track1 Track = "Track1" 48 // Track2 ... 49 Track2 Track = "Track2" 50) 51 52const ( 53 linkKeyword = "**Link**: " 54 tagKeyword = "**Readme Tag**: " 55 releaseDateKeyword = "**Target release date**: " 56) 57 58type issueError struct { 59 issue github.Issue 60 err error 61} 62 63// Error ... 64func (e *issueError) Error() string { 65 return fmt.Sprintf("cannot parse release request from issue %s: %+v", e.issue.GetHTMLURL(), e.err) 66} 67 68func initializeHandlers(options ParsingOptions) { 69 if options.IncludeDataPlaneRequests { 70 resultHandlerMap[link.CodeDataPlane] = handleSuccess 71 } 72} 73 74// ParseIssue parses the release request issues to release requests 75func ParseIssue(ctx context.Context, client *query.Client, issue github.Issue, options ParsingOptions) (*Request, error) { 76 initializeHandlers(options) 77 78 reqIssue, err := NewReleaseRequestIssue(issue) 79 if err != nil { 80 return nil, err 81 } 82 result, err := ParseReadmeFromLink(ctx, client, *reqIssue) 83 if err != nil { 84 return nil, err 85 } 86 handler := resultHandlerMap[result.GetCode()] 87 if handler == nil { 88 panic(fmt.Sprintf("unhandled code '%s'", result.GetCode())) 89 } 90 return handler(ctx, client, *reqIssue, result) 91} 92 93// ReleaseRequestIssue represents a release request issue 94type ReleaseRequestIssue struct { 95 IssueLink string 96 TargetLink string 97 Tag string 98 ReleaseDate time.Time 99 Labels []*github.Label 100} 101 102// NewReleaseRequestIssue ... 103func NewReleaseRequestIssue(issue github.Issue) (*ReleaseRequestIssue, error) { 104 body := issue.GetBody() 105 contents := getRawContent(strings.Split(body, "\n"), []string{ 106 linkKeyword, tagKeyword, releaseDateKeyword, 107 }) 108 // get release date 109 releaseDate, err := time.Parse("2006-01-02", contents[releaseDateKeyword]) 110 if err != nil { 111 return nil, &issueError{ 112 issue: issue, 113 err: err, 114 } 115 } 116 return &ReleaseRequestIssue{ 117 IssueLink: issue.GetHTMLURL(), 118 TargetLink: parseLink(contents[linkKeyword]), 119 Tag: contents[tagKeyword], 120 ReleaseDate: releaseDate, 121 Labels: issue.Labels, 122 }, nil 123} 124 125func getRawContent(lines []string, keywords []string) map[string]string { 126 result := make(map[string]string) 127 for _, line := range lines { 128 for _, keyword := range keywords { 129 raw := getContentByPrefix(line, keyword) 130 if raw != "" { 131 result[keyword] = raw 132 } 133 } 134 } 135 return result 136} 137 138func getContentByPrefix(line, prefix string) string { 139 if strings.HasPrefix(line, prefix) { 140 return strings.TrimSpace(strings.TrimPrefix(line, prefix)) 141 } 142 return "" 143} 144 145func parseLink(rawLink string) string { 146 regex := regexp.MustCompile(`^\[.+\]\((.+)\)$`) 147 r := regex.FindStringSubmatch(rawLink) 148 if len(r) < 1 { 149 return "" 150 } 151 return r[1] 152} 153 154// ParseReadmeFromLink ... 155func ParseReadmeFromLink(ctx context.Context, client *query.Client, reqIssue ReleaseRequestIssue) (link.ResolveResult, error) { 156 // check if invalid characters in a url 157 regex := regexp.MustCompile(`^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:/\?#\[\]@!\$&'\(\)\*\+,;=]+$`) 158 if !regex.MatchString(reqIssue.TargetLink) { 159 return nil, fmt.Errorf("link '%s' contains invalid characters", reqIssue.TargetLink) 160 } 161 r, err := parseResolver(ctx, client, reqIssue.IssueLink, reqIssue.TargetLink) 162 if err != nil { 163 return nil, err 164 } 165 return r.Resolve() 166} 167 168func parseResolver(ctx context.Context, client *query.Client, requestLink, releaseLink string) (link.Resolver, error) { 169 if !strings.HasPrefix(releaseLink, link.SpecRepoPrefix) { 170 return nil, fmt.Errorf("link '%s' is not from '%s'", releaseLink, link.SpecRepoPrefix) 171 } 172 releaseLink = strings.TrimPrefix(releaseLink, link.SpecRepoPrefix) 173 prefix, err := getLinkPrefix(releaseLink) 174 if err != nil { 175 return nil, fmt.Errorf("cannot resolve link '%s': %+v", releaseLink, err) 176 } 177 switch prefix { 178 case link.PullRequestPrefix: 179 return link.NewPullRequestLink(ctx, client, requestLink, releaseLink), nil 180 case link.DirectoryPrefix: 181 return link.NewDirectoryLink(ctx, client, requestLink, releaseLink), nil 182 case link.FilePrefix: 183 return link.NewFileLink(ctx, client, requestLink, releaseLink), nil 184 case link.CommitPrefix: 185 return link.NewCommitLink(ctx, client, requestLink, releaseLink), nil 186 default: 187 return nil, fmt.Errorf("prefix '%s' of link '%s' not supported yet", prefix, releaseLink) 188 } 189} 190 191func getLinkPrefix(link string) (string, error) { 192 segments := strings.Split(link, "/") 193 if len(segments) < 2 { 194 return "", fmt.Errorf("cannot determine the prefix of link") 195 } 196 return segments[0] + "/", nil 197} 198