1package main
2
3//go:generate go run .
4
5import (
6	"bufio"
7	"bytes"
8	"errors"
9	"fmt"
10	"go/format"
11	"io"
12	"log"
13	"os"
14	"path/filepath"
15	"sort"
16	"strings"
17	"text/template"
18
19	"github.com/BurntSushi/toml"
20)
21
22const (
23	root        = "../../"
24	dnsPackage  = root + "providers/dns"
25	mdTemplate  = root + "internal/dnsdocs/dns.md.tmpl"
26	cliTemplate = root + "internal/dnsdocs/dns.go.tmpl"
27	cliOutput   = root + "cmd/zz_gen_cmd_dnshelp.go"
28	docOutput   = root + "docs/content/dns"
29	readmePath  = root + "README.md"
30)
31
32const (
33	startLine = "<!-- START DNS PROVIDERS LIST -->"
34	endLine   = "<!-- END DNS PROVIDERS LIST -->"
35)
36
37type Model struct {
38	Name          string         // Real name of the DNS provider
39	Code          string         // DNS code
40	Since         string         // First lego version
41	URL           string         // DNS provider URL
42	Description   string         // Provider summary
43	Example       string         // CLI example
44	Configuration *Configuration // Environment variables
45	Links         *Links         // Links
46	Additional    string         // Extra documentation
47	GeneratedFrom string         // Source file
48}
49
50type Configuration struct {
51	Credentials map[string]string
52	Additional  map[string]string
53}
54
55type Links struct {
56	API      string
57	GoClient string
58}
59
60type Providers struct {
61	Providers []Model
62}
63
64func main() {
65	models := &Providers{}
66
67	err := filepath.Walk(dnsPackage, walker(models))
68	if err != nil {
69		log.Fatal(err)
70	}
71
72	// generate CLI help
73	err = generateCLIHelp(models)
74	if err != nil {
75		log.Fatal(err)
76	}
77
78	// generate README.md
79	err = generateReadMe(models)
80	if err != nil {
81		log.Fatal(err)
82	}
83
84	fmt.Printf("Documentation for %d DNS providers has been generated.\n", len(models.Providers)+1)
85}
86
87func walker(prs *Providers) func(string, os.FileInfo, error) error {
88	return func(path string, _ os.FileInfo, err error) error {
89		if err != nil {
90			return err
91		}
92
93		if filepath.Ext(path) == ".toml" {
94			m := Model{}
95
96			m.GeneratedFrom, err = filepath.Rel(root, path)
97			if err != nil {
98				return err
99			}
100
101			_, err := toml.DecodeFile(path, &m)
102			if err != nil {
103				return err
104			}
105
106			prs.Providers = append(prs.Providers, m)
107
108			// generate documentation
109			return generateDocumentation(m)
110		}
111
112		return nil
113	}
114}
115
116func generateDocumentation(m Model) error {
117	filename := filepath.Join(docOutput, "zz_gen_"+m.Code+".md")
118
119	file, err := os.Create(filename)
120	if err != nil {
121		return err
122	}
123
124	return template.Must(template.ParseFiles(mdTemplate)).Execute(file, m)
125}
126
127func generateCLIHelp(models *Providers) error {
128	filename := filepath.Join(cliOutput)
129
130	file, err := os.Create(filename)
131	if err != nil {
132		return err
133	}
134
135	tlt := template.New(filepath.Base(cliTemplate)).Funcs(map[string]interface{}{
136		"safe": func(src string) string {
137			return strings.ReplaceAll(src, "`", "'")
138		},
139	})
140
141	b := &bytes.Buffer{}
142	err = template.Must(tlt.ParseFiles(cliTemplate)).Execute(b, models)
143	if err != nil {
144		return err
145	}
146
147	// gofmt
148	source, err := format.Source(b.Bytes())
149	if err != nil {
150		return err
151	}
152
153	_, err = file.Write(source)
154	return err
155}
156
157func generateReadMe(models *Providers) error {
158	max, lines := extractTableData(models)
159
160	file, err := os.Open(readmePath)
161	if err != nil {
162		return err
163	}
164
165	defer func() { _ = file.Close() }()
166
167	var skip bool
168
169	buffer := bytes.NewBufferString("")
170
171	fileScanner := bufio.NewScanner(file)
172	for fileScanner.Scan() {
173		text := fileScanner.Text()
174
175		if text == startLine {
176			_, _ = fmt.Fprintln(buffer, text)
177			err = writeDNSTable(buffer, lines, max)
178			if err != nil {
179				return err
180			}
181			skip = true
182		}
183
184		if text == endLine {
185			skip = false
186		}
187
188		if skip {
189			continue
190		}
191
192		_, _ = fmt.Fprintln(buffer, text)
193	}
194
195	if fileScanner.Err() != nil {
196		return fileScanner.Err()
197	}
198
199	if skip {
200		return errors.New("missing end tag")
201	}
202
203	return os.WriteFile(readmePath, buffer.Bytes(), 0o666)
204}
205
206func extractTableData(models *Providers) (int, [][]string) {
207	readmePattern := "[%s](https://go-acme.github.io/lego/dns/%s/)"
208
209	items := []string{fmt.Sprintf(readmePattern, "Manual", "manual")}
210
211	var max int
212
213	for _, pvd := range models.Providers {
214		item := fmt.Sprintf(readmePattern, strings.ReplaceAll(pvd.Name, "|", "/"), pvd.Code)
215		items = append(items, item)
216
217		if max < len(item) {
218			max = len(item)
219		}
220	}
221
222	const nbCol = 4
223
224	sort.Slice(items, func(i, j int) bool {
225		return strings.ToLower(items[i]) < strings.ToLower(items[j])
226	})
227
228	var lines [][]string
229	var line []string
230
231	for i, item := range items {
232		switch {
233		case len(line) == nbCol:
234			lines = append(lines, line)
235			line = []string{item}
236
237		case i == len(items)-1:
238			line = append(line, item)
239			for j := len(line); j < nbCol; j++ {
240				line = append(line, "")
241			}
242			lines = append(lines, line)
243
244		default:
245			line = append(line, item)
246		}
247	}
248
249	if len(line) < nbCol {
250		for j := len(line); j < nbCol; j++ {
251			line = append(line, "")
252		}
253		lines = append(lines, line)
254	}
255
256	return max, lines
257}
258
259func writeDNSTable(w io.Writer, lines [][]string, size int) error {
260	_, err := fmt.Fprintf(w, "\n")
261	if err != nil {
262		return err
263	}
264
265	_, err = fmt.Fprintf(w, "|%[1]s|%[1]s|%[1]s|%[1]s|\n", strings.Repeat(" ", size+2))
266	if err != nil {
267		return err
268	}
269
270	_, err = fmt.Fprintf(w, "|%[1]s|%[1]s|%[1]s|%[1]s|\n", strings.Repeat("-", size+2))
271	if err != nil {
272		return err
273	}
274
275	linePattern := fmt.Sprintf("| %%-%[1]ds | %%-%[1]ds | %%-%[1]ds | %%-%[1]ds |\n", size)
276	for _, line := range lines {
277		_, err = fmt.Fprintf(w, linePattern, line[0], line[1], line[2], line[3])
278		if err != nil {
279			return err
280		}
281	}
282
283	_, err = fmt.Fprintf(w, "\n")
284	return err
285}
286