-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathapply.go
More file actions
130 lines (117 loc) · 3.95 KB
/
apply.go
File metadata and controls
130 lines (117 loc) · 3.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package main
import (
"bytes"
"encoding/json"
"fmt"
"os"
"strings"
"text/template"
"github.com/caarlos0/log"
"github.com/spf13/cobra"
)
const (
defaultBeginMarker = "<!-- sponsors:begin -->"
defaultEndMarker = "<!-- sponsors:end -->"
)
func newApplyCmd() *cobra.Command {
var beginMarker, endMarker string
cmd := &cobra.Command{
Use: "apply sponsors.json template.tpl output.md",
Short: "Render a template and update a file between markers",
Long: `Render a Go template using the sponsors data from the given JSON file,
then replace the content between the begin and end markers in the output file.
The template receives a TemplateData value with the following fields:
.Sponsors []Sponsor — all sponsors, sorted by tier rank then name
.Tiers []Tier — tiers sorted by monthly_rate descending
.ByTier map[string][]Sponsor — sponsors grouped by tier ID
Template functions:
imageURL url size — append a size hint to a GitHub or OpenCollective avatar URL`,
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
return apply(args[0], args[1], args[2], beginMarker, endMarker)
},
}
cmd.Flags().StringVar(&beginMarker, "begin-marker", defaultBeginMarker, "begin marker in the target file")
cmd.Flags().StringVar(&endMarker, "end-marker", defaultEndMarker, "end marker in the target file")
return cmd
}
func apply(sponsorsPath, templatePath, outputPath, beginMarker, endMarker string) error {
sfData, err := os.ReadFile(sponsorsPath)
if err != nil {
return fmt.Errorf("read %s: %w", sponsorsPath, err)
}
var sf SponsorFile
if err := json.Unmarshal(sfData, &sf); err != nil {
return fmt.Errorf("parse %s: %w", sponsorsPath, err)
}
byTier := make(map[string][]Sponsor)
for _, s := range sf.Sponsors {
byTier[s.Tier] = append(byTier[s.Tier], s)
}
data := TemplateData{
Sponsors: sf.Sponsors,
Tiers: sf.Tiers,
ByTier: byTier,
}
tmplSrc, err := readFileOrURL(templatePath)
if err != nil {
return fmt.Errorf("read template %s: %w", templatePath, err)
}
funcMap := template.FuncMap{
"dict": func(kv ...any) (map[string]any, error) {
if len(kv)%2 != 0 {
return nil, fmt.Errorf("dict requires an even number of arguments")
}
m := make(map[string]any, len(kv)/2)
for i := 0; i < len(kv); i += 2 {
k, ok := kv[i].(string)
if !ok {
return nil, fmt.Errorf("dict keys must be strings, got %T", kv[i])
}
m[k] = kv[i+1]
}
return m, nil
},
"imageURL": imageURL,
}
tmpl, err := template.New("sponsors").Funcs(funcMap).Parse(string(tmplSrc))
if err != nil {
return fmt.Errorf("parse template %s: %w", templatePath, err)
}
var rendered bytes.Buffer
if err := tmpl.Execute(&rendered, data); err != nil {
return fmt.Errorf("execute template: %w", err)
}
existing, err := os.ReadFile(outputPath)
if err != nil {
return fmt.Errorf("read %s: %w", outputPath, err)
}
updated, err := replaceMarkers(string(existing), beginMarker, endMarker, rendered.String())
if err != nil {
return fmt.Errorf("update %s: %w", outputPath, err)
}
if err := os.WriteFile(outputPath, []byte(updated), 0o644); err != nil {
return fmt.Errorf("write %s: %w", outputPath, err)
}
log.WithField("output", outputPath).Info("updated file")
return nil
}
// replaceMarkers replaces the content between begin and end markers.
func replaceMarkers(content, begin, end, replacement string) (string, error) {
startIdx := strings.Index(content, begin)
if startIdx == -1 {
return "", fmt.Errorf("begin marker %q not found", begin)
}
afterBegin := startIdx + len(begin)
endIdx := strings.Index(content[afterBegin:], end)
if endIdx == -1 {
return "", fmt.Errorf("end marker %q not found", end)
}
endIdx += afterBegin
// Walk back to the start of the end-marker's line to preserve its indentation.
lineStart := endIdx
for lineStart > 0 && content[lineStart-1] != '\n' {
lineStart--
}
return content[:afterBegin] + "\n" + replacement + "\n" + content[lineStart:], nil
}