1package changes
2
3import (
4	"bytes"
5	"fmt"
6	"strings"
7	"text/template"
8)
9
10// Release represents a single SDK release, which contains all change metadata and their resulting version bumps.
11type Release struct {
12	ID            string
13	SchemaVersion int
14	VersionBumps  map[string]VersionBump
15	Changes       []Change
16}
17
18type changelogModuleEntry struct {
19	Module    string
20	Version   string
21	Sections  map[ChangeType][]Change
22	TopLevel  bool
23	ReleaseID string
24}
25
26func (e changelogModuleEntry) Link() string {
27	anchor := "Release-" + strings.ReplaceAll(e.ReleaseID, " ", "-")
28	return fmt.Sprintf("[%s](%s/CHANGELOG.md#%s)", e.Module, e.Module, anchor)
29}
30
31const changelogModule = `* ` + "`" + `{{.Module}}` + "`" + `{{with .Version}} - {{.}}{{end}}
32{{- range $key, $section := .Sections -}}
33{{range $section}}
34  * {{ $key.ChangelogPrefix }}{{.IndentedDescription "  "}}
35{{- end -}}
36{{- end -}}`
37
38var changelogTemplate *template.Template
39var rootChangelogTemplate *template.Template
40
41func init() {
42	var err error
43
44	changelogTemplate, err = template.New("changelog-entry").Parse(changelogModule)
45	if err != nil {
46		panic(err)
47	}
48
49	rootChangelogTemplate, err = template.New("root-changelog").Parse(rootChangelogTemplateContents)
50	if err != nil {
51		panic(err)
52	}
53}
54
55// RenderChangelogForModule returns a new markdown section of a module's CHANGELOG based on the Changes in the Release.
56func (r *Release) RenderChangelogForModule(module string, topLevel bool) (string, error) {
57	sections := map[ChangeType][]Change{}
58
59	for _, c := range r.Changes {
60		if topLevel && c.Module == module {
61			sections[c.Type] = append(sections[c.Type], c)
62		} else if !topLevel && c.matches(module) {
63			sections[c.Type] = append(sections[c.Type], c)
64		}
65	}
66
67	if len(sections) == 0 {
68		return "", nil
69	}
70
71	var version string
72	if bump, ok := r.VersionBumps[module]; ok {
73		version = bump.To
74	}
75
76	buff := new(bytes.Buffer)
77
78	err := changelogTemplate.Execute(buff, changelogModuleEntry{
79		Module:    module,
80		Version:   version,
81		Sections:  sections,
82		ReleaseID: r.ID,
83		TopLevel:  topLevel,
84	})
85	if err != nil {
86		return "", fmt.Errorf("failed to render module %s's changelog entry: %v", module, err)
87	}
88
89	return buff.String(), nil
90}
91
92const rootChangelogTemplateContents = `# Release {{.ID}}
93{{with .AnnouncementsSection}}## Announcements
94{{range .}}{{.}}
95{{end -}}
96{{end}}{{with .ServiceSection}}## Service Client Highlights
97{{range .}}{{.}}
98{{end -}}
99{{end}}{{with .CoreSection}}## Core SDK Highlights
100{{range .}}{{.}}
101{{end -}}
102{{end}}`
103
104// RenderChangelog generates a top level CHANGELOG.md for the Release r.
105func (r *Release) RenderChangelog() (string, error) {
106	buff := new(bytes.Buffer)
107
108	err := rootChangelogTemplate.Execute(buff, r)
109	if err != nil {
110		return "", err
111	}
112
113	return buff.String(), nil
114}
115
116// AffectedModules returns a sorted list of all modules affected by this Release. A module is considered affected if
117// it is the Module of one or more Changes in the Release.
118func (r *Release) AffectedModules() []string {
119	return AffectedModules(r.Changes)
120}
121
122// wildcards returns a sorted list of wildcards Changes whose Module begin with the given prefix.
123func (r *Release) wildcards() []Change {
124	var changes []Change
125
126	for _, c := range r.Changes {
127		if c.isWildcard() {
128			changes = append(changes, c)
129		}
130	}
131
132	return changes
133}
134
135// splitSections groups entries (including wildcard Changes and module Changelog entries) into three groups: Announcements,
136// Services, and Core SDK modules.
137func (r *Release) splitSections() ([]string, []string, []string, error) {
138	const servicePrefix = "service/"
139
140	var announcements []string
141	var services []string
142	var core []string
143
144	for _, c := range r.wildcards() {
145		if c.Type == AnnouncementChangeType {
146			announcements = append(announcements, c.String())
147		} else if strings.HasPrefix(c.Module, servicePrefix) {
148			services = append(services, c.String())
149		} else {
150			core = append(core, c.String())
151		}
152	}
153
154	mods := r.AffectedModules()
155
156	for _, m := range mods {
157		entry, err := r.RenderChangelogForModule(m, true)
158		if err != nil {
159			return nil, nil, nil, err
160		}
161		if entry == "" {
162			continue
163		}
164
165		if strings.HasPrefix(m, servicePrefix) {
166			services = append(services, entry)
167		} else {
168			core = append(core, entry)
169		}
170	}
171
172	return announcements, services, core, nil
173}
174
175// AnnouncementsSection returns a list of Changelog bullet entries that should be included under the Announcements header.
176func (r *Release) AnnouncementsSection() ([]string, error) {
177	announcements, _, _, err := r.splitSections()
178	return announcements, err
179}
180
181// ServiceSection returns a list of Changelog bullet entries that should be included under the Service Clients header.
182func (r *Release) ServiceSection() ([]string, error) {
183	_, services, _, err := r.splitSections()
184	return services, err
185}
186
187// CoreSection returns a list of Changelog bullet entries that should be included under the Core SDK header.
188func (r *Release) CoreSection() ([]string, error) {
189	_, _, core, err := r.splitSections()
190	return core, err
191}
192