@@ -3,9 +3,12 @@ package main
3
3
import (
4
4
"bytes"
5
5
"encoding/json"
6
+ "errors"
7
+ "flag"
6
8
"fmt"
7
9
"go/parser"
8
10
"go/token"
11
+ "io"
9
12
"io/fs"
10
13
"os"
11
14
"path"
@@ -24,122 +27,196 @@ const (
24
27
)
25
28
26
29
func main () {
27
- if err := run (); err != nil {
28
- panic (err )
30
+ lint := flag .Bool ("lint" , false , "Lint **all** the examples instead of generating the examples.gen.json file" )
31
+ flag .Parse ()
32
+
33
+ if err := run (* lint ); err != nil {
34
+ _ , _ = fmt .Fprintf (os .Stderr , "error: %+v\n " , err )
35
+ os .Exit (1 )
29
36
}
30
37
}
31
38
32
- func run () error {
39
+ //nolint:revive // This is a script, not a library.
40
+ func run (lint bool ) error {
33
41
fset := token .NewFileSet ()
34
42
src , err := parser .ParseFile (fset , filepath .Join (examplesDir , examplesSrc ), nil , parser .ParseComments )
35
43
if err != nil {
36
44
return err
37
45
}
38
46
47
+ projectFS := os .DirFS ("." )
48
+ examplesFS := os .DirFS (examplesDir )
49
+
39
50
var paths []string
40
- for _ , comment := range src .Comments {
41
- for _ , line := range comment .List {
42
- if s , ok := parseEmbedTag (line .Text ); ok && ! strings .HasSuffix (s , ".json" ) {
43
- paths = append (paths , s )
51
+ if lint {
52
+ files , err := fs .ReadDir (examplesFS , "templates" )
53
+ if err != nil {
54
+ return err
55
+ }
56
+
57
+ for _ , f := range files {
58
+ if ! f .IsDir () {
59
+ continue
60
+ }
61
+ paths = append (paths , filepath .Join ("templates" , f .Name ()))
62
+ }
63
+ } else {
64
+ for _ , comment := range src .Comments {
65
+ for _ , line := range comment .List {
66
+ if s , ok := parseEmbedTag (line .Text ); ok && ! strings .HasSuffix (s , ".json" ) {
67
+ paths = append (paths , s )
68
+ }
44
69
}
45
70
}
46
71
}
47
72
48
73
var examples []codersdk.TemplateExample
49
- files := os . DirFS ( examplesDir )
74
+ var errs [] error
50
75
for _ , name := range paths {
51
- dir , err := fs . Stat ( files , name )
76
+ te , err := parseTemplateExample ( projectFS , examplesFS , name )
52
77
if err != nil {
53
- return err
54
- }
55
- if ! dir .IsDir () {
78
+ errs = append (errs , err )
56
79
continue
57
80
}
58
- exampleID := dir .Name ()
59
- // Each one of these is a example!
60
- readme , err := fs .ReadFile (files , path .Join (name , "README.md" ))
61
- if err != nil {
62
- return xerrors .Errorf ("example %q does not contain README.md" , exampleID )
81
+ if te != nil {
82
+ examples = append (examples , * te )
63
83
}
84
+ }
85
+
86
+ if len (errs ) > 0 {
87
+ return xerrors .Errorf ("parse failed: %w" , errors .Join (errs ... ))
88
+ }
89
+
90
+ var w io.Writer = os .Stdout
91
+ if lint {
92
+ w = io .Discard
93
+ }
64
94
65
- frontMatter , err := pageparser .ParseFrontMatterAndContent (bytes .NewReader (readme ))
95
+ _ , err = fmt .Fprint (w , "// Code generated by examplegen. DO NOT EDIT.\n " )
96
+ if err != nil {
97
+ return err
98
+ }
99
+
100
+ enc := json .NewEncoder (os .Stdout )
101
+ enc .SetIndent ("" , " " )
102
+ return enc .Encode (examples )
103
+ }
104
+
105
+ func parseTemplateExample (projectFS , examplesFS fs.FS , name string ) (te * codersdk.TemplateExample , err error ) {
106
+ var errs []error
107
+ defer func () {
66
108
if err != nil {
67
- return xerrors . Errorf ( "parse example %q front matter: %w" , exampleID , err )
109
+ errs = append ([] error { err }, errs ... )
68
110
}
69
-
70
- nameRaw , exists := frontMatter .FrontMatter ["display_name" ]
71
- if ! exists {
72
- return xerrors .Errorf ("example %q front matter does not contain name" , exampleID )
111
+ if len (errs ) > 0 {
112
+ err = xerrors .Errorf ("example %q has errors" , name )
113
+ for _ , e := range errs {
114
+ err = errors .Join (err , e )
115
+ }
73
116
}
117
+ }()
74
118
75
- name , valid := nameRaw .(string )
76
- if ! valid {
77
- return xerrors .Errorf ("example %q name isn't a string" , exampleID )
78
- }
119
+ dir , err := fs .Stat (examplesFS , name )
120
+ if err != nil {
121
+ return nil , err
122
+ }
123
+ if ! dir .IsDir () {
124
+ return nil , nil
125
+ }
79
126
80
- descriptionRaw , exists := frontMatter .FrontMatter ["description" ]
81
- if ! exists {
82
- return xerrors .Errorf ("example %q front matter does not contain name" , exampleID )
83
- }
127
+ exampleID := dir .Name ()
128
+ // Each one of these is a example!
129
+ readme , err := fs .ReadFile (examplesFS , path .Join (name , "README.md" ))
130
+ if err != nil {
131
+ return nil , xerrors .New ("missing README.md" )
132
+ }
84
133
85
- description , valid := descriptionRaw .( string )
86
- if ! valid {
87
- return xerrors .Errorf ("example %q description isn't a string " , exampleID )
88
- }
134
+ frontMatter , err := pageparser . ParseFrontMatterAndContent ( bytes . NewReader ( readme ) )
135
+ if err != nil {
136
+ return nil , xerrors .Errorf ("parse front matter: %w " , err )
137
+ }
89
138
90
- tags := []string {}
91
- tagsRaw , exists := frontMatter .FrontMatter ["tags" ]
92
- if exists {
93
- tagsI , valid := tagsRaw .([]interface {})
94
- if ! valid {
95
- return xerrors .Errorf ("example %q tags isn't a slice: type %T" , exampleID , tagsRaw )
96
- }
139
+ // Make sure validation here is in sync with requirements for
140
+ // coder/registry.
141
+ displayName , err := getString (frontMatter .FrontMatter , "display_name" )
142
+ if err != nil {
143
+ errs = append (errs , err )
144
+ }
145
+
146
+ description , err := getString (frontMatter .FrontMatter , "description" )
147
+ if err != nil {
148
+ errs = append (errs , err )
149
+ }
150
+
151
+ _ , err = getString (frontMatter .FrontMatter , "maintainer_github" )
152
+ if err != nil {
153
+ errs = append (errs , err )
154
+ }
155
+
156
+ tags := []string {}
157
+ tagsRaw , exists := frontMatter .FrontMatter ["tags" ]
158
+ if exists {
159
+ tagsI , valid := tagsRaw .([]interface {})
160
+ if ! valid {
161
+ errs = append (errs , xerrors .Errorf ("tags isn't a slice: type %T" , tagsRaw ))
162
+ } else {
97
163
for _ , tagI := range tagsI {
98
164
tag , valid := tagI .(string )
99
165
if ! valid {
100
- return xerrors .Errorf ("example %q tag isn't a string: type %T" , exampleID , tagI )
166
+ errs = append (errs , xerrors .Errorf ("tag isn't a string: type %T" , tagI ))
167
+ continue
101
168
}
102
169
tags = append (tags , tag )
103
170
}
104
171
}
172
+ }
105
173
106
- var icon string
107
- iconRaw , exists := frontMatter .FrontMatter ["icon" ]
108
- if exists {
109
- icon , valid = iconRaw .(string )
110
- if ! valid {
111
- return xerrors .Errorf ("example %q icon isn't a string" , exampleID )
174
+ var icon string
175
+ iconRaw , exists := frontMatter .FrontMatter ["icon" ]
176
+ if exists {
177
+ var valid bool
178
+ icon , valid = iconRaw .(string )
179
+ if ! valid {
180
+ errs = append (errs , xerrors .Errorf ("icon isn't a string: type %T" , iconRaw ))
181
+ } else {
182
+ cleanPath := filepath .Clean (filepath .Join (examplesDir , name , icon ))
183
+ _ , err := fs .Stat (projectFS , cleanPath )
184
+ if err != nil {
185
+ errs = append (errs , xerrors .Errorf ("icon does not exist: %w" , err ))
112
186
}
113
- icon , err = filepath .Rel ("../ site/static/" , filepath . Join ( examplesDir , name , icon ) )
187
+ icon , err = filepath .Rel ("site/static/" , cleanPath )
114
188
if err != nil {
115
- return xerrors .Errorf ("example %q icon is not in site/static: %w" , exampleID , err )
189
+ errs = append ( errs , xerrors .Errorf ("icon is not in site/static: %w" , err ) )
116
190
}
117
- // The FE needs a static path!
118
- icon = "/" + icon
119
191
}
192
+ }
120
193
121
- examples = append (examples , codersdk.TemplateExample {
122
- ID : exampleID ,
123
- Name : name ,
124
- Description : description ,
125
- Icon : icon ,
126
- Tags : tags ,
127
- Markdown : string (frontMatter .Content ),
128
-
129
- // URL is set by examples/examples.go.
130
- })
194
+ if len (errs ) > 0 {
195
+ return nil , xerrors .New ("front matter validation failed" )
131
196
}
132
197
133
- w := os .Stdout
198
+ return & codersdk.TemplateExample {
199
+ ID : exampleID ,
200
+ Name : displayName ,
201
+ Description : description ,
202
+ Icon : "/" + icon , // The FE needs a static path!
203
+ Tags : tags ,
204
+ Markdown : string (frontMatter .Content ),
134
205
135
- _ , err = fmt .Fprint (w , "// Code generated by examplegen. DO NOT EDIT.\n " )
136
- if err != nil {
137
- return err
138
- }
206
+ // URL is set by examples/examples.go.
207
+ }, nil
208
+ }
139
209
140
- enc := json .NewEncoder (os .Stdout )
141
- enc .SetIndent ("" , " " )
142
- return enc .Encode (examples )
210
+ func getString (m map [string ]any , key string ) (string , error ) {
211
+ v , ok := m [key ]
212
+ if ! ok {
213
+ return "" , xerrors .Errorf ("front matter does not contain %q" , key )
214
+ }
215
+ vv , ok := v .(string )
216
+ if ! ok {
217
+ return "" , xerrors .Errorf ("%q isn't a string" , key )
218
+ }
219
+ return vv , nil
143
220
}
144
221
145
222
func parseEmbedTag (s string ) (string , bool ) {
0 commit comments