github.com/rpdict/ponzu@v0.10.1-0.20190226054626-477f29d6bf5e/cmd/ponzu/generate.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "fmt" 6 "go/format" 7 "os" 8 "path/filepath" 9 "strings" 10 "text/template" 11 12 "github.com/spf13/cobra" 13 ) 14 15 type generateType struct { 16 Name string 17 Initial string 18 Fields []generateField 19 HasReferences bool 20 } 21 22 type generateField struct { 23 Name string 24 Initial string 25 TypeName string 26 JSONName string 27 View string 28 29 IsReference bool 30 ReferenceName string 31 ReferenceJSONTags []string 32 } 33 34 var reservedFieldNames = map[string]string{ 35 "uuid": "UUID", 36 "item": "Item", 37 "id": "ID", 38 "slug": "Slug", 39 "timestamp": "Timestamp", 40 "updated": "Updated", 41 } 42 43 func legalFieldNames(fields ...generateField) (bool, map[string]string) { 44 conflicts := make(map[string]string) 45 for _, field := range fields { 46 for jsonName, fieldName := range reservedFieldNames { 47 if field.JSONName == jsonName || field.Name == fieldName { 48 conflicts[jsonName] = fieldName 49 } 50 } 51 } 52 53 if len(conflicts) > 0 { 54 return false, conflicts 55 } 56 57 return true, conflicts 58 } 59 60 // blog title:string Author:string PostCategory:string content:string some_thing:int 61 func parseType(args []string) (generateType, error) { 62 t := generateType{ 63 Name: fieldName(args[0]), 64 } 65 t.Initial = strings.ToLower(string(t.Name[0])) 66 67 fields := args[1:] 68 for _, field := range fields { 69 f, err := parseField(field, &t) 70 if err != nil { 71 return generateType{}, err 72 } 73 74 // set initial (1st character of the type's name) on field so we don't need 75 // to set the template variable like was done in prior version 76 f.Initial = t.Initial 77 78 t.Fields = append(t.Fields, f) 79 } 80 81 if ok, nameConflicts := legalFieldNames(t.Fields...); !ok { 82 for jsonName, fieldName := range nameConflicts { 83 fmt.Println(fmt.Sprintf("reserved field name: %s (%s)", jsonName, fieldName)) 84 } 85 86 count := len(nameConflicts) 87 var c = "conflict" 88 if count > 1 { 89 c = "conflicts" 90 } 91 92 return generateType{}, fmt.Errorf("You have (%d) naming %s - please rename and try again", count, c) 93 } 94 95 return t, nil 96 } 97 98 func parseField(raw string, gt *generateType) (generateField, error) { 99 // contents:string 100 // contents:string:richtext 101 // author:@author,name,age 102 // authors:[]@author,name,age 103 104 if !strings.Contains(raw, ":") { 105 return generateField{}, fmt.Errorf("Invalid generate argument. [%s]", raw) 106 } 107 108 data := strings.Split(raw, ":") 109 110 field := generateField{ 111 Name: fieldName(data[0]), 112 Initial: gt.Initial, 113 JSONName: fieldJSONName(data[0]), 114 } 115 116 setFieldTypeName(&field, data[1], gt) 117 118 viewType := "input" 119 if len(data) == 3 { 120 viewType = data[2] 121 } 122 123 err := setFieldView(&field, viewType) 124 if err != nil { 125 return generateField{}, err 126 } 127 128 return field, nil 129 } 130 131 // parse the field's type name and check if it is a special reference type, or 132 // a slice of reference types, which we'll set their underlying type to string 133 // or []string respectively 134 func setFieldTypeName(field *generateField, fieldType string, gt *generateType) { 135 if !strings.Contains(fieldType, "@") { 136 // not a reference, set as-is downcased 137 field.TypeName = strings.ToLower(fieldType) 138 field.IsReference = false 139 return 140 } 141 142 // some possibilities are 143 // @author,name,age 144 // []@author,name,age 145 // ------------------- 146 // [] = slice of author 147 // @author = reference to Author struct 148 // ,name,age = JSON tag names from Author struct fields to use as select option display 149 150 if strings.Contains(fieldType, ",") { 151 referenceConf := strings.Split(fieldType, ",") 152 fieldType = referenceConf[0] 153 field.ReferenceJSONTags = referenceConf[1:] 154 } 155 156 var referenceType string 157 if strings.HasPrefix(fieldType, "[]") { 158 referenceType = strings.TrimPrefix(fieldType, "[]@") 159 fieldType = "[]string" 160 } else { 161 referenceType = strings.TrimPrefix(fieldType, "@") 162 fieldType = "string" 163 } 164 165 field.TypeName = strings.ToLower(fieldType) 166 field.ReferenceName = fieldName(referenceType) 167 field.IsReference = true 168 gt.HasReferences = true 169 return 170 } 171 172 // get the initial field name passed and check it for all possible cases 173 // MyTitle:string myTitle:string my_title:string -> MyTitle 174 // error-message:string -> ErrorMessage 175 func fieldName(name string) string { 176 // remove _ or - if first character 177 if name[0] == '-' || name[0] == '_' { 178 name = name[1:] 179 } 180 181 // remove _ or - if last character 182 if name[len(name)-1] == '-' || name[len(name)-1] == '_' { 183 name = name[:len(name)-1] 184 } 185 186 // upcase the first character 187 name = strings.ToUpper(string(name[0])) + name[1:] 188 189 // remove _ or - character, and upcase the character immediately following 190 for i := 0; i < len(name); i++ { 191 r := rune(name[i]) 192 if isUnderscore(r) || isHyphen(r) { 193 up := strings.ToUpper(string(name[i+1])) 194 name = name[:i] + up + name[i+2:] 195 } 196 } 197 198 return name 199 } 200 201 // get the initial field name passed and convert to json-like name 202 // MyTitle:string myTitle:string my_title:string -> my_title 203 // error-message:string -> error-message 204 func fieldJSONName(name string) string { 205 // remove _ or - if first character 206 if name[0] == '-' || name[0] == '_' { 207 name = name[1:] 208 } 209 210 // downcase the first character 211 name = strings.ToLower(string(name[0])) + name[1:] 212 213 // check for uppercase character, downcase and insert _ before it if i-1 214 // isn't already _ or - 215 for i := 0; i < len(name); i++ { 216 r := rune(name[i]) 217 if isUpper(r) { 218 low := strings.ToLower(string(r)) 219 if name[i-1] == '_' || name[i-1] == '-' { 220 name = name[:i] + low + name[i+1:] 221 } else { 222 name = name[:i] + "_" + low + name[i+1:] 223 } 224 } 225 } 226 227 return name 228 } 229 230 func optimizeFieldView(field *generateField, viewType string) string { 231 viewType = strings.ToLower(viewType) 232 233 if field.IsReference { 234 viewType = "reference" 235 } 236 237 // if we have a []T field type, automatically make the input view a repeater 238 // as long as a repeater exists for the input type 239 repeaterElements := []string{"input", "select", "file", "reference"} 240 if strings.HasPrefix(field.TypeName, "[]") { 241 for _, el := range repeaterElements { 242 // if the viewType already is declared to be a -repeater 243 // the comparison below will fail but the switch will 244 // still find the right generator template 245 // ex. authors:"[]string":select 246 // ex. authors:string:select-repeater 247 if viewType == el { 248 viewType = viewType + "-repeater" 249 } 250 } 251 } else { 252 // if the viewType is already declared as a -repeater, but 253 // the TypeName is not of []T, add the [] prefix so the user 254 // code is correct 255 // ex. authors:string:select-repeater 256 // ex. authors:@author:select-repeater 257 if strings.HasSuffix(viewType, "-repeater") { 258 field.TypeName = "[]" + field.TypeName 259 } 260 } 261 262 return viewType 263 } 264 265 // set the specified view inside the editor field for a generated field for a type 266 func setFieldView(field *generateField, viewType string) error { 267 var err error 268 var tmpl *template.Template 269 buf := &bytes.Buffer{} 270 271 pwd, err := os.Getwd() 272 if err != nil { 273 return err 274 } 275 276 tmplDir := filepath.Join(pwd, "cmd", "ponzu", "templates") 277 tmplFromWithDelims := func(filename string, delim [2]string) (*template.Template, error) { 278 if delim[0] == "" || delim[1] == "" { 279 delim = [2]string{"{{", "}}"} 280 } 281 282 return template.New(filename).Delims(delim[0], delim[1]).ParseFiles(filepath.Join(tmplDir, filename)) 283 } 284 285 viewType = optimizeFieldView(field, viewType) 286 switch viewType { 287 case "checkbox": 288 tmpl, err = tmplFromWithDelims("gen-checkbox.tmpl", [2]string{}) 289 case "custom": 290 tmpl, err = tmplFromWithDelims("gen-custom.tmpl", [2]string{}) 291 case "file": 292 tmpl, err = tmplFromWithDelims("gen-file.tmpl", [2]string{}) 293 case "hidden": 294 tmpl, err = tmplFromWithDelims("gen-hidden.tmpl", [2]string{}) 295 case "input", "text": 296 tmpl, err = tmplFromWithDelims("gen-input.tmpl", [2]string{}) 297 case "richtext": 298 tmpl, err = tmplFromWithDelims("gen-richtext.tmpl", [2]string{}) 299 case "select": 300 tmpl, err = tmplFromWithDelims("gen-select.tmpl", [2]string{}) 301 case "textarea": 302 tmpl, err = tmplFromWithDelims("gen-textarea.tmpl", [2]string{}) 303 case "tags": 304 tmpl, err = tmplFromWithDelims("gen-tags.tmpl", [2]string{}) 305 306 case "input-repeater": 307 tmpl, err = tmplFromWithDelims("gen-input-repeater.tmpl", [2]string{}) 308 case "select-repeater": 309 tmpl, err = tmplFromWithDelims("gen-select-repeater.tmpl", [2]string{}) 310 case "file-repeater": 311 tmpl, err = tmplFromWithDelims("gen-file-repeater.tmpl", [2]string{}) 312 313 // use [[ and ]] as delimeters since reference views need to generate 314 // display names containing {{ and }} 315 case "reference": 316 tmpl, err = tmplFromWithDelims("gen-reference.tmpl", [2]string{"[[", "]]"}) 317 if err != nil { 318 return err 319 } 320 case "reference-repeater": 321 tmpl, err = tmplFromWithDelims("gen-reference-repeater.tmpl", [2]string{"[[", "]]"}) 322 if err != nil { 323 return err 324 } 325 326 default: 327 msg := fmt.Sprintf("'%s' is not a recognized view type. Using 'input' instead.", viewType) 328 fmt.Println(msg) 329 tmpl, err = tmplFromWithDelims("gen-input.tmpl", [2]string{}) 330 } 331 332 if err != nil { 333 return err 334 } 335 336 err = tmpl.Execute(buf, field) 337 if err != nil { 338 return err 339 } 340 341 field.View = buf.String() 342 343 return nil 344 } 345 346 func isUpper(char rune) bool { 347 if char >= 'A' && char <= 'Z' { 348 return true 349 } 350 351 return false 352 } 353 354 func isUnderscore(char rune) bool { 355 return char == '_' 356 } 357 358 func isHyphen(char rune) bool { 359 return char == '-' 360 } 361 362 func generateContentType(args []string) error { 363 name := args[0] 364 fileName := strings.ToLower(name) + ".go" 365 366 // open file in ./content/ dir 367 // if exists, alert user of conflict 368 pwd, err := os.Getwd() 369 if err != nil { 370 return err 371 } 372 373 contentDir := filepath.Join(pwd, "content") 374 filePath := filepath.Join(contentDir, fileName) 375 376 if _, err := os.Stat(filePath); !os.IsNotExist(err) { 377 localFile := filepath.Join("content", fileName) 378 return fmt.Errorf("Please remove '%s' before executing this command", localFile) 379 } 380 381 // parse type info from args 382 gt, err := parseType(args) 383 if err != nil { 384 return fmt.Errorf("Failed to parse type args: %s", err.Error()) 385 } 386 387 tmplPath := filepath.Join(pwd, "cmd", "ponzu", "templates", "gen-content.tmpl") 388 tmpl, err := template.ParseFiles(tmplPath) 389 if err != nil { 390 return fmt.Errorf("Failed to parse template: %s", err.Error()) 391 } 392 393 buf := &bytes.Buffer{} 394 err = tmpl.Execute(buf, gt) 395 if err != nil { 396 return fmt.Errorf("Failed to execute template: %s", err.Error()) 397 } 398 399 fmtBuf, err := format.Source(buf.Bytes()) 400 if err != nil { 401 return fmt.Errorf("Failed to format template: %s", err.Error()) 402 } 403 404 // no file exists.. ok to write new one 405 file, err := os.Create(filePath) 406 defer file.Close() 407 if err != nil { 408 return err 409 } 410 411 _, err = file.Write(fmtBuf) 412 if err != nil { 413 return fmt.Errorf("Failed to write generated file buffer: %s", err.Error()) 414 } 415 416 return nil 417 } 418 419 var generateCmd = &cobra.Command{ 420 Use: "generate <generator type (,...fields)>", 421 Aliases: []string{"gen", "g"}, 422 Short: "generate boilerplate code for various Ponzu components", 423 Long: `Generate boilerplate code for various Ponzu components, such as 'content'. 424 425 The command above will generate a file 'content/review.go' with boilerplate 426 methods, as well as struct definition, and corresponding field tags like: 427 428 type Review struct { 429 Title string ` + "`json:" + `"title"` + "`" + ` 430 Body string ` + "`json:" + `"body"` + "`" + ` 431 Rating int ` + "`json:" + `"rating"` + "`" + ` 432 Tags []string ` + "`json:" + `"tags"` + "`" + ` 433 } 434 435 The generate command will intelligently parse more sophisticated field names 436 such as 'field_name' and convert it to 'FieldName' and vice versa, only where 437 appropriate as per common Go idioms. Errors will be reported, but successful 438 generate commands return nothing.`, 439 Example: `$ ponzu gen content review title:"string" body:"string" rating:"int" tags:"[]string"`, 440 } 441 442 var contentCmd = &cobra.Command{ 443 Use: "content <namespace> <field> <field>...", 444 Aliases: []string{"c"}, 445 Short: "generates a new content type", 446 RunE: func(cmd *cobra.Command, args []string) error { 447 return generateContentType(args) 448 }, 449 } 450 451 func init() { 452 generateCmd.AddCommand(contentCmd) 453 RegisterCmdlineCommand(generateCmd) 454 }