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 autorest
5
6import (
7	"bufio"
8	"encoding/json"
9	"fmt"
10	"io"
11	"log"
12	"os"
13	"path/filepath"
14	"strings"
15	"time"
16
17	"github.com/Azure/azure-sdk-for-go/tools/generator/autorest/model"
18	"github.com/Azure/azure-sdk-for-go/tools/internal/exports"
19	"github.com/Azure/azure-sdk-for-go/tools/internal/utils"
20)
21
22// GenerateContext describes the context that would be used in an autorest generation task
23type GenerateContext interface {
24	SDKRoot() string
25	SpecRoot() string
26	RepoContent() map[string]exports.Content
27}
28
29// GenerateInput describes the input information for a package generation
30type GenerateInput struct {
31	// Readme is the relative path of the readme file to the root directory of azure-sdk-for-go
32	Readme string
33	// Tag is the readme tag to be generated
34	Tag string
35	// CommitHash is the head commit hash of azure-rest-api-specs
36	CommitHash string
37	// Options specifies the options that this generation task will be using
38	Options model.Options
39}
40
41// GenerateOptions describes the options for a package generation
42type GenerateOptions struct {
43	// MetadataOutputRoot specifies the root directory of all the metadata goes.
44	// Metadata will be generated to a temp directory if not specified.
45	// The metadataOutput directory will not be removed after the generation succeeded
46	MetadataOutputRoot string
47	// Stderr ...
48	Stderr io.Writer
49	// Stdout ...
50	Stdout io.Writer
51	// AutoRestLogPrefix ...
52	AutoRestLogPrefix string
53	// ChangelogTitle
54	ChangelogTitle string
55	// Validators ...
56	Validators []MetadataValidateFunc
57}
58
59// GenerateResult describes the result of a generation task
60type GenerateResult struct {
61	// MetadataOutputRoot stores the metadata output root which is the same as in options, or randomly generated if not specified in options
62	MetadataOutputRoot string
63	// Metadata is the GenerationMetadata of the generated package
64	Metadata GenerationMetadata
65	// Package is the changelog information of the generated package
66	Package ChangelogResult
67}
68
69// GeneratePackage is a wrapper function of the autorest execution task
70func GeneratePackage(ctx GenerateContext, input GenerateInput, options GenerateOptions) (*GenerateResult, error) {
71	if err := input.validate(); err != nil {
72		return nil, err
73	}
74	if err := options.validate(); err != nil {
75		return nil, err
76	}
77
78	absReadme := filepath.Join(ctx.SpecRoot(), input.Readme)
79	metadataOutput := filepath.Join(options.MetadataOutputRoot, input.Tag)
80	g := NewGeneratorFromOptions(input.Options).WithTag(input.Tag).WithMetadataOutput(metadataOutput).WithReadme(absReadme)
81
82	// generate
83	if err := generate(g, options.Stdout, options.Stderr, options.AutoRestLogPrefix); err != nil {
84		return nil, fmt.Errorf("failed to execute autorest: %+v", err)
85	}
86
87	// parse the metadata from autorest
88	metadataMap, err := NewMetadataProcessorFromLocation(metadataOutput).Process()
89	if err != nil {
90		return nil, fmt.Errorf("failed to parse metadata in '%s': %+v", metadataOutput, err)
91	}
92
93	// validate
94	if err := validate(input.Readme, metadataMap, options.Validators); err != nil {
95		return nil, fmt.Errorf("failed in validation: %+v", err)
96	}
97
98	// write the changelog and metadata file
99	result, metadata, err := changelogAndMetadata(ctx, input, metadataMap, options.ChangelogTitle, g.Arguments())
100	if err != nil {
101		return nil, err
102	}
103
104	return &GenerateResult{
105		MetadataOutputRoot: options.MetadataOutputRoot,
106		Metadata:           *metadata,
107		Package:            *result,
108	}, nil
109}
110
111func generate(generator *Generator, stdout, stderr io.Writer, prefix string) error {
112	stdoutPipe, _ := generator.StdoutPipe()
113	stderrPipe, _ := generator.StderrPipe()
114	defer stdoutPipe.Close()
115	defer stderrPipe.Close()
116	var arguments []string
117	for _, o := range generator.Arguments() {
118		arguments = append(arguments, o.Format())
119	}
120	log.Printf("Generation parameters: %s", strings.Join(arguments, ", "))
121	_ = generator.Start()
122	// we put all the output from autorest to stderr since those are logs in order not to interrupt the proper output of the release command
123	go scannerPrint(bufio.NewScanner(stdoutPipe), stdout, prefix)
124	go scannerPrint(bufio.NewScanner(stderrPipe), stderr, prefix)
125	return generator.Wait()
126}
127
128func validate(readme string, metadataMap map[string]model.Metadata, validators []MetadataValidateFunc) error {
129	builder := validationErrorBuilder{
130		readme: readme,
131	}
132
133	for tag, metadata := range metadataMap {
134		errors := ValidateMetadata(validators, tag, metadata)
135		if len(errors) != 0 {
136			builder.add(errors...)
137		}
138	}
139
140	return builder.build()
141}
142
143type validationErrorBuilder struct {
144	readme string
145	errors []error
146}
147
148func (b *validationErrorBuilder) add(errors ...error) {
149	b.errors = append(b.errors, errors...)
150}
151
152func (b *validationErrorBuilder) build() error {
153	if len(b.errors) == 0 {
154		return nil
155	}
156	var messages []string
157	for _, e := range b.errors {
158		messages = append(messages, e.Error())
159	}
160	return fmt.Errorf("validation failed in readme '%s' with %d error(s): \n%s", b.readme, len(b.errors), strings.Join(messages, "\n"))
161}
162
163func changelogAndMetadata(ctx GenerateContext, input GenerateInput, metadataMap map[string]model.Metadata, changelogTitle string, argument []model.Option) (*ChangelogResult, *GenerationMetadata, error) {
164	result, err := changelog(ctx, metadataMap, changelogTitle)
165	if err != nil {
166		return nil, nil, fmt.Errorf("failed to write changelog file: %+v", err)
167	}
168
169	// write the metadata file
170	metadata, err := metadata(input, *result, argument)
171	if err != nil {
172		return nil, nil, fmt.Errorf("failed to write metadata file: %+v", err)
173	}
174
175	return result, metadata, nil
176}
177
178func changelog(ctx GenerateContext, metadataMap map[string]model.Metadata, changelogTitle string) (*ChangelogResult, error) {
179	// process the changelog
180	changelogResults, err := NewChangelogProcessorFromContext(ctx).Process(metadataMap)
181	if err != nil {
182		return nil, fmt.Errorf("failed to process the changelog: %+v", err)
183	}
184	// we should only have one changelog
185	if len(changelogResults) != 1 {
186		return nil, fmt.Errorf("expecting 1 changelog result, but got %d", len(changelogResults))
187	}
188
189	changelogPath, err := WriteChangelogFile(changelogTitle, changelogResults[0])
190	if err != nil {
191		return nil, fmt.Errorf("failed to write changelog file: %+v", err)
192	}
193	log.Printf("changelog file writes to '%s'", changelogPath)
194	return &changelogResults[0], nil
195}
196
197func metadata(input GenerateInput, result ChangelogResult, arguments []model.Option) (*GenerationMetadata, error) {
198	metadata := getMetadata(input, result, arguments)
199	metadataPath, err := WriteMetadataFile(result.PackageFullPath, metadata)
200	if err != nil {
201		return nil, err
202	}
203	log.Printf("metadata file writes to '%s'", metadataPath)
204	return &metadata, nil
205}
206
207func getMetadata(input GenerateInput, result ChangelogResult, arguments []model.Option) GenerationMetadata {
208	options := AdditionalOptionsToString(arguments)
209	codeGenVersion := input.Options.CodeGeneratorVersion()
210	return GenerationMetadata{
211		CommitHash:     input.CommitHash,
212		Readme:         NormalizedSpecRoot + utils.NormalizePath(input.Readme),
213		Tag:            input.Tag,
214		CodeGenVersion: codeGenVersion,
215		RepositoryURL:  "https://github.com/Azure/azure-rest-api-specs.git",
216		AutorestCommand: fmt.Sprintf("autorest --use=%s --tag=%s --go-sdk-folder=/_/azure-sdk-for-go %s /_/azure-rest-api-specs/%s",
217			codeGenVersion, result.Tag, strings.Join(options, " "), utils.NormalizePath(input.Readme)),
218		AdditionalProperties: GenerationMetadataAdditionalProperties{
219			AdditionalOptions: strings.Join(options, " "),
220		},
221	}
222}
223
224func (input GenerateInput) validate() error {
225	if input.Readme == "" {
226		return fmt.Errorf("`Readme` cannot be empty in input")
227	}
228	if filepath.IsAbs(input.Readme) {
229		return fmt.Errorf("`Readme` must be a relative path")
230	}
231	if input.Tag == "" {
232		return fmt.Errorf("`Tag` cannot be empty in input")
233	}
234	if input.Options == nil {
235		return fmt.Errorf("`Options` cannot be nil")
236	}
237	return nil
238}
239
240func (options *GenerateOptions) validate() error {
241	if options.MetadataOutputRoot == "" {
242		options.MetadataOutputRoot = filepath.Join(os.TempDir(), fmt.Sprintf("generation-metadata-%v", time.Now().Unix()))
243	}
244	if options.ChangelogTitle == "" {
245		options.ChangelogTitle = "Change History"
246	}
247	return nil
248}
249
250// WriteChangelogFile writes the changelog to the disk
251func WriteChangelogFile(title string, result ChangelogResult) (string, error) {
252	fileContent := fmt.Sprintf(`# %s
253
254%s`, title, result.Changelog.ToMarkdown())
255	path := filepath.Join(result.PackageFullPath, ChangelogFilename)
256	changelogFile, err := os.Create(path)
257	if err != nil {
258		return "", err
259	}
260	defer changelogFile.Close()
261	if _, err := changelogFile.WriteString(fileContent); err != nil {
262		return "", err
263	}
264	return path, nil
265}
266
267// WriteMetadataFile writes the metadata to the disk
268func WriteMetadataFile(packagePath string, metadata GenerationMetadata) (string, error) {
269	metadataFilepath := filepath.Join(packagePath, MetadataFilename)
270	metadataFile, err := os.Create(metadataFilepath)
271	if err != nil {
272		return "", err
273	}
274	defer metadataFile.Close()
275
276	// marshal metadata
277	b, err := json.MarshalIndent(metadata, "", "  ")
278	if err != nil {
279		return "", fmt.Errorf("cannot marshal metadata: %+v", err)
280	}
281
282	if _, err := metadataFile.Write(b); err != nil {
283		return "", err
284	}
285	return metadataFilepath, nil
286}
287
288// scannerPrint prints the scanner to writer with a specified prefix
289func scannerPrint(scanner *bufio.Scanner, writer io.Writer, prefix string) error {
290	if writer == nil {
291		return nil
292	}
293	for scanner.Scan() {
294		line := scanner.Text()
295		if _, err := fmt.Fprintln(writer, fmt.Sprintf("%s%s", prefix, line)); err != nil {
296			return err
297		}
298	}
299	return nil
300}
301