From 04344fd03a609af37ec95a6c8279a45c17d5de70 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Thu, 24 Nov 2016 10:37:54 -0800 Subject: [PATCH] adding initial support to generate content type from more descriptive cli args --- cmd/ponzu/cli_test.go | 65 ++++++++++++++ cmd/ponzu/generate.go | 192 ++++++++++++++++++++++++++++++++++++++++++ cmd/ponzu/main.go | 5 +- cmd/ponzu/options.go | 153 +-------------------------------- 4 files changed, 262 insertions(+), 153 deletions(-) create mode 100644 cmd/ponzu/cli_test.go create mode 100644 cmd/ponzu/generate.go diff --git a/cmd/ponzu/cli_test.go b/cmd/ponzu/cli_test.go new file mode 100644 index 00000000..76feac3c --- /dev/null +++ b/cmd/ponzu/cli_test.go @@ -0,0 +1,65 @@ +package main + +import "testing" + +func TestParseType(t *testing.T) { + // blog title:string Author:string PostCategory:string content:string some_thing:int + args := []string{ + "blog", "title:string", "Author:string", + "PostCategory:string", "content:string", + "some_thing:int", "Some_otherThing:float64", + } + + gt, err := parseType(args) + if err != nil { + t.Errorf("Failed: %s", err.Error()) + } + + if gt.Name != "Blog" { + t.Errorf("Expected %s, got: %s", "Blog", gt.Name) + } +} + +func TestFieldJSONName(t *testing.T) { + cases := map[string]string{ + "_T": "t", + "T": "t", + "_tT_": "t_t_", + "TestCapsNoSym": "test_caps_no_sym", + "test_Some_caps_Sym": "test_some_caps_sym", + "testnocaps": "testnocaps", + "_Test_Caps_Sym_odd": "test_caps_sym_odd", + "test-hyphen": "test-hyphen", + "Test-hyphen-Caps": "test-hyphen-caps", + "Test-Hyphen_Sym-Caps": "test-hyphen_sym-caps", + } + + for input, expected := range cases { + output := fieldJSONName(input) + if output != expected { + t.Errorf("Expected: %s, got: %s", expected, output) + } + } +} + +func TestFieldName(t *testing.T) { + cases := map[string]string{ + "_T": "T", + "T": "T", + "_tT_": "TT", + "TestCapsNoSym": "TestCapsNoSym", + "test_Some_caps_Sym": "TestSomeCapsSym", + "testnocaps": "Testnocaps", + "_Test_Caps_Sym_odd": "TestCapsSymOdd", + "test-hyphen": "TestHyphen", + "Test-hyphen-Caps": "TestHyphenCaps", + "Test-Hyphen_Sym-Caps": "TestHyphenSymCaps", + } + + for input, expected := range cases { + output := fieldName(input) + if output != expected { + t.Errorf("Expected: %s, got: %s", expected, output) + } + } +} diff --git a/cmd/ponzu/generate.go b/cmd/ponzu/generate.go new file mode 100644 index 00000000..9325d1d6 --- /dev/null +++ b/cmd/ponzu/generate.go @@ -0,0 +1,192 @@ +package main + +import ( + "bytes" + "fmt" + "go/format" + "html/template" + "os" + "path/filepath" + "strings" +) + +type generateType struct { + Name string + Initial string + Fields []generateField +} + +type generateField struct { + Name string + TypeName string + JSONName string +} + +// blog title:string Author:string PostCategory:string content:string some_thing:int +func parseType(args []string) (generateType, error) { + t := generateType{ + Name: fieldName(args[0]), + } + t.Initial = strings.ToLower(string(t.Name[0])) + + fields := args[1:] + for _, field := range fields { + f, err := parseField(field) + if err != nil { + return generateType{}, err + } + + t.Fields = append(t.Fields, f) + } + + return t, nil +} + +func parseField(raw string) (generateField, error) { + // title:string + if !strings.Contains(raw, ":") { + return generateField{}, fmt.Errorf("Invalid generate argument. [%s]", raw) + } + + pair := strings.Split(raw, ":") + field := generateField{ + Name: fieldName(pair[0]), + TypeName: strings.ToLower(pair[1]), + JSONName: fieldJSONName(pair[0]), + } + + return field, nil +} + +// get the initial field name passed and check it for all possible cases +// MyTitle:string myTitle:string my_title:string -> MyTitle +// error-message:string -> ErrorMessage +func fieldName(name string) string { + // remove _ or - if first character + if name[0] == '-' || name[0] == '_' { + name = name[1:] + } + + // remove _ or - if last character + if name[len(name)-1] == '-' || name[len(name)-1] == '_' { + name = name[:len(name)-1] + } + + // upcase the first character + name = strings.ToUpper(string(name[0])) + name[1:] + + // remove _ or - character, and upcase the character immediately following + for i := 0; i < len(name); i++ { + r := rune(name[i]) + if isUnderscore(r) || isHyphen(r) { + up := strings.ToUpper(string(name[i+1])) + name = name[:i] + up + name[i+2:] + } + } + + return name +} + +// get the initial field name passed and convert to json-like name +// MyTitle:string myTitle:string my_title:string -> my_title +// error-message:string -> error-message +func fieldJSONName(name string) string { + // remove _ or - if first character + if name[0] == '-' || name[0] == '_' { + name = name[1:] + } + + // downcase the first character + name = strings.ToLower(string(name[0])) + name[1:] + + // check for uppercase character, downcase and insert _ before it if i-1 + // isn't already _ or - + for i := 0; i < len(name); i++ { + r := rune(name[i]) + if isUpper(r) { + low := strings.ToLower(string(r)) + if name[i-1] == '_' || name[i-1] == '-' { + name = name[:i] + low + name[i+1:] + } else { + name = name[:i] + "_" + low + name[i+1:] + } + } + } + + return name +} + +func isUpper(char rune) bool { + if char >= 'A' && char <= 'Z' { + return true + } + + return false +} + +func isUnderscore(char rune) bool { + return char == '_' +} + +func isHyphen(char rune) bool { + return char == '-' +} + +func generateContentType(args []string, path string) error { + name := args[0] + fileName := strings.ToLower(name) + ".go" + + // open file in ./content/ dir + // if exists, alert user of conflict + pwd, err := os.Getwd() + if err != nil { + return err + } + + if path != "" { + pwd = path + } + + contentDir := filepath.Join(pwd, "content") + filePath := filepath.Join(contentDir, fileName) + + if _, err := os.Stat(filePath); !os.IsNotExist(err) { + return fmt.Errorf("Please remove '%s' before executing this command.", fileName) + } + + // no file exists.. ok to write new one + file, err := os.Create(filePath) + defer file.Close() + if err != nil { + return err + } + + // parse type info from args + gt, err := parseType(args) + if err != nil { + return fmt.Errorf("Failed to parse type args: %s", err.Error()) + } + + tmpl, err := template.ParseFiles("contentType.tmpl") + if err != nil { + return fmt.Errorf("Failed to parse template: %s", err.Error()) + } + + buf := &bytes.Buffer{} + err = tmpl.Execute(buf, gt) + if err != nil { + return fmt.Errorf("Failed to execute template: %s", err.Error()) + } + + fmtBuf, err := format.Source(buf.Bytes()) + if err != nil { + return fmt.Errorf("Failed to format template: %s", err.Error()) + } + + _, err = file.Write(fmtBuf) + if err != nil { + return fmt.Errorf("Failed to write generated file buffer: %s", err.Error()) + } + + return nil +} diff --git a/cmd/ponzu/main.go b/cmd/ponzu/main.go index 234d3b61..88355f47 100644 --- a/cmd/ponzu/main.go +++ b/cmd/ponzu/main.go @@ -120,11 +120,12 @@ func main() { os.Exit(0) } - err := generateContentType(args[1], "") + err := generateContentType(args[1:], "") if err != nil { fmt.Println(err) os.Exit(1) } + case "build": err := buildPonzuServer(args) if err != nil { @@ -197,8 +198,10 @@ func main() { } log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) + case "": flag.PrintDefaults() + default: flag.PrintDefaults() } diff --git a/cmd/ponzu/options.go b/cmd/ponzu/options.go index 244a759e..375893d1 100644 --- a/cmd/ponzu/options.go +++ b/cmd/ponzu/options.go @@ -3,7 +3,6 @@ package main import ( "errors" "fmt" - "html/template" "io" "io/ioutil" "os" @@ -12,143 +11,6 @@ import ( "strings" ) -func generateContentType(name, path string) error { - fileName := strings.ToLower(name) + ".go" - typeName := strings.ToUpper(string(name[0])) + string(name[1:]) - - // contain processed name an info for template - data := map[string]string{ - "name": typeName, - "initial": string(fileName[0]), - } - - // open file in ./content/ dir - // if exists, alert user of conflict - pwd, err := os.Getwd() - if err != nil { - return err - } - - if path != "" { - pwd = path - } - - contentDir := filepath.Join(pwd, "content") - filePath := filepath.Join(contentDir, fileName) - - if _, err := os.Stat(filePath); !os.IsNotExist(err) { - return fmt.Errorf("Please remove '%s' before executing this command.", fileName) - } - - // no file exists.. ok to write new one - file, err := os.Create(filePath) - defer file.Close() - if err != nil { - return err - } - - // execute template - tmpl := template.Must(template.New("content").Parse(contentTypeTmpl)) - err = tmpl.Execute(file, data) - if err != nil { - return err - } - - return nil -} - -const contentTypeTmpl = ` -package content - -import ( - "fmt" - - "github.com/bosssauce/ponzu/management/editor" -) - -// {{ .name }} is the generic content struct -type {{ .name }} struct { - Item - editor editor.Editor - - // required: all maintained {{ .name }} fields must have json tags! - Title string ` + "`json:" + `"title"` + "`" + ` - Content string ` + "`json:" + `"content"` + "`" + ` - Author string ` + "`json:" + `"author"` + "`" + ` - Photo string ` + "`json:" + `"photo"` + "`" + ` - Category []string ` + "`json:" + `"category"` + "`" + ` - Theme string ` + "`json:" + `"theme"` + "`" + ` -} - -// MarshalEditor writes a buffer of html to edit a {{ .name }} -// partially implements editor.Editable -func ({{ .initial }} *{{ .name }}) MarshalEditor() ([]byte, error) { - view, err := editor.Form({{ .initial }}, - editor.Field{ - // Take note that the first argument to these Input-like methods - // is the string version of each {{ .name }} field, and must follow - // this pattern for auto-decoding and auto-encoding reasons. - View: editor.Input("Title", {{ .initial }}, map[string]string{ - "label": "{{ .name }} Title", - "type": "text", - "placeholder": "Enter your {{ .name }} Title here", - }), - }, - editor.Field{ - View: editor.Richtext("Content", {{ .initial }}, map[string]string{ - "label": "Content", - "placeholder": "Add the content of your {{ .name }} here", - }), - }, - editor.Field{ - View: editor.Input("Author", {{ .initial }}, map[string]string{ - "label": "Author", - "type": "text", - "placeholder": "Enter the author name here", - }), - }, - editor.Field{ - View: editor.File("Photo", {{ .initial }}, map[string]string{ - "label": "Author Photo", - "placeholder": "Upload a profile picture for the author", - }), - }, - editor.Field{ - View: editor.Tags("Category", {{ .initial }}, map[string]string{ - "label": "{{ .name }} Category", - }), - }, - editor.Field{ - View: editor.Select("Theme", {{ .initial }}, map[string]string{ - "label": "Theme Style", - }, map[string]string{ - "dark": "Dark", - "light": "Light", - }), - }, - ) - - if err != nil { - return nil, fmt.Errorf("Failed to render {{ .name }} editor view: %s", err.Error()) - } - - return view, nil -} - -func init() { - Types["{{ .name }}"] = func() interface{} { return new({{ .name }}) } -} - -// ContentName is required to set the display name for a piece of content in the editor -// Partially implements editor.Editable -func ({{ .initial }} *{{ .name }}) ContentName() string { return {{ .initial }}.Title } - -// Editor is a buffer of bytes for the Form function to write input views -// partially implements editor.Editable -func ({{ .initial }} *{{ .name }}) Editor() *editor.Editor { return &{{ .initial }}.editor } - -` - func newProjectInDir(path string) error { // set path to be nested inside $GOPATH/src gopath := os.Getenv("GOPATH") @@ -230,12 +92,6 @@ func createProjInDir(path string) error { return err } - err = generateContentType("post", path) - if err != nil { - // TODO: rollback, remove ponzu project from path - return err - } - fmt.Println("Dev build cloned from " + local + ":ponzu-dev") return nil } @@ -278,12 +134,6 @@ func createProjInDir(path string) error { return err } - err = generateContentType("post", path) - if err != nil { - // TODO: rollback, remove ponzu project from path - return err - } - gitDir := filepath.Join(path, ".git") err = os.RemoveAll(gitDir) if err != nil { @@ -311,8 +161,7 @@ func vendorCorePackages(path string) error { } } - // create a user 'content' package, and give it a single 'post.go' file - // using generateContentType("post") + // create a user 'content' package contentPath := filepath.Join(path, "content") err = os.Mkdir(contentPath, os.ModeDir|os.ModePerm) if err != nil {