github.com/bosssauce/ponzu@v0.11.1-0.20200102001432-9bc41b703131/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  }