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