diff --git a/FAQ.md b/FAQ.md index b6d813a..6ec6f61 100644 --- a/FAQ.md +++ b/FAQ.md @@ -48,20 +48,21 @@ See the [documentation](https://golang.org/pkg/text/template/) for every availab For a full list, see [helper.go](registry/helper.go) -| Helper | Example | Output | -|-------------|----------------------------------|-------------------------------------------------------------------------------------------------------| -| upper | `{{upper project}}` | `MY-PROJECT` | -| lower | `{{lower project}}` | `my-project` | -| title | `{{title project}}` | `My-Project` | -| snake | `{{snake project}}` | `my_project` | -| kebab | `{{kebab project}}` | `my-project` | -| pascal | `{{pascal project}}` | `MyProject` | -| camel | `{{camel project}}` | `myProject` | -| env | `{{env "USER"}}` | The current user | -| sep | `{{sep}}` | Filepath separator for current OS | -| time | `{{time "01/02/2006"}}` | `11/21/2020` - The time according to the given [format](https://flaviocopes.com/go-date-time-format/) | -| trim_prefix | `{{trim_prefix "foobar" "foo"}}` | `bar` | -| trim_suffix | `{{trim_suffix "foobar" "bar"}}` | `foo` | +| Helper | Example | Output | +|-------------|------------------------------------|-------------------------------------------------------------------------------------------------------| +| upper | `{{upper project}}` | `MY-PROJECT` | +| lower | `{{lower project}}` | `my-project` | +| title | `{{title project}}` | `My-Project` | +| snake | `{{snake project}}` | `my_project` | +| kebab | `{{kebab project}}` | `my-project` | +| pascal | `{{pascal project}}` | `MyProject` | +| camel | `{{camel project}}` | `myProject` | +| env | `{{env "USER"}}` | The current user | +| sep | `{{sep}}` | Filepath separator for current OS | +| time | `{{time "01/02/2006"}}` | `11/21/2020` - The time according to the given [format](https://flaviocopes.com/go-date-time-format/) | +| trim_prefix | `{{trim_prefix "foobar" "foo"}}` | `bar` | +| trim_suffix | `{{trim_suffix "foobar" "bar"}}` | `foo` | +| replace | `{{replace "foobar" "bar" "baz"}}` | `foobaz` | ## Sources diff --git a/cmd/init.go b/cmd/init.go index 61c916c..371e25f 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -36,7 +36,7 @@ func runInit(_ *cli.Context) error { if err != nil { return err } - if _, err := fi.WriteString(comments); err != nil { + if _, err := fi.WriteString(initConfig); err != nil { return err } if err := os.Mkdir("template", os.ModePerm); err != nil { @@ -46,12 +46,15 @@ func runInit(_ *cli.Context) error { return fi.Close() } -var comments = `# tmpl.yaml +var initConfig = `# tmpl.yaml # Write any template args here to prompt the user for, giving any defaults/options as applicable prompts: - id: name # The unique ID for the prompt - label: Project Name # The prompt message/label - help: The name to use in the project # Optional help message for the prompt - default: tmpl # Prompt default + label: Project Name # (Optional) Prompt message/label, defaults to id + help: The name to use in the project # (Optional) Help message for the prompt + default: tmpl # (Optional) Prompt default + options: # (Optional) Set of options the user can choose from + - coolproject123 + - ${USER}'s cool project ` diff --git a/cmd/init_test.go b/cmd/init_test.go new file mode 100644 index 0000000..6771ab4 --- /dev/null +++ b/cmd/init_test.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "strings" + "testing" + + "go.jolheiser.com/tmpl/schema" + + "github.com/matryer/is" +) + +func TestInitSchema(t *testing.T) { + assert := is.New(t) + + err := schema.Lint(strings.NewReader(initConfig)) + assert.NoErr(err) // Init config should conform to schema +} diff --git a/config/config.go b/config/config.go index c306743..fa7eb12 100644 --- a/config/config.go +++ b/config/config.go @@ -16,10 +16,11 @@ type Config struct { // Prompt is a tmpl prompt type Prompt struct { - ID string `yaml:"id"` - Label string `yaml:"label"` - Help string `yaml:"help"` - Default any `yaml:"default"` + ID string `yaml:"id"` + Label string `yaml:"label"` + Help string `yaml:"help"` + Default string `yaml:"default"` + Options []string `yaml:"options"` } // Load loads a tmpl config diff --git a/registry/error.go b/registry/error.go index 168ed92..35985ef 100644 --- a/registry/error.go +++ b/registry/error.go @@ -10,11 +10,6 @@ func (e ErrTemplateExists) Error() string { return fmt.Sprintf("template %s already exists", e.Name) } -func IsErrTemplateExists(err error) bool { - _, ok := err.(ErrTemplateExists) - return ok -} - type ErrTemplateNotFound struct { Name string } @@ -30,8 +25,3 @@ type ErrSourceNotFound struct { func (e ErrSourceNotFound) Error() string { return fmt.Sprintf("Source not found for %s", e.Name) } - -func IsErrSourceNotFound(err error) bool { - _, ok := err.(ErrSourceNotFound) - return ok -} diff --git a/registry/helper.go b/registry/helper.go index a9bb49d..dc9dfff 100644 --- a/registry/helper.go +++ b/registry/helper.go @@ -10,7 +10,6 @@ import ( ) var funcMap = map[string]any{ - // String conversions "upper": strings.ToUpper, "lower": strings.ToLower, "title": strings.Title, @@ -20,15 +19,10 @@ var funcMap = map[string]any{ "camel": func(in string) string { return xstrings.FirstRuneToLower(xstrings.ToCamelCase(in)) }, - "trim_prefix": func(in, trim string) string { - return strings.TrimPrefix(in, trim) - }, - "trim_suffix": func(in, trim string) string { - return strings.TrimSuffix(in, trim) - }, - - // Other - "env": os.Getenv, + "trim_prefix": strings.TrimPrefix, + "trim_suffix": strings.TrimSuffix, + "replace": strings.ReplaceAll, + "env": os.Getenv, "sep": func() string { return string(filepath.Separator) }, diff --git a/registry/prompt.go b/registry/prompt.go index e12adce..9f731eb 100644 --- a/registry/prompt.go +++ b/registry/prompt.go @@ -33,9 +33,6 @@ func prompt(dir string, defaults bool) (templatePrompts, error) { if tp.Label == "" { tp.Label = tp.ID } - if tp.Default == nil { - tp.Default = "" - } prompts = append(prompts, tp) } @@ -50,52 +47,32 @@ func prompt(dir string, defaults bool) (templatePrompts, error) { // Check if we are using defaults if defaults { - val := prompt.Default - switch t := prompt.Default.(type) { - case []string: - for idy, s := range t { - t[idy] = os.ExpandEnv(s) - } - val = t - case string: - val = os.ExpandEnv(t) - } - s := fmt.Sprint(val) - prompts[idx].Value = s - os.Setenv(fmt.Sprintf("TMPL_PROMPT_%s", envKey), s) + val := os.ExpandEnv(prompt.Default) + prompts[idx].Value = val + os.Setenv(fmt.Sprintf("TMPL_PROMPT_%s", envKey), val) continue } + // Otherwise, prompt var p survey.Prompt - switch t := prompt.Default.(type) { - case []string: - for idy, s := range t { - t[idy] = os.ExpandEnv(s) + if len(prompt.Options) > 0 { + opts := make([]string, 0, len(prompt.Options)) + for idy, opt := range prompt.Options { + opts[idy] = os.ExpandEnv(opt) } p = &survey.Select{ Message: prompt.Label, - Options: t, + Options: opts, Help: prompt.Help, } - case bool: - p = &survey.Confirm{ - Message: prompt.Label, - Default: t, - Help: prompt.Help, - } - case string: + } else { p = &survey.Input{ Message: prompt.Label, - Default: os.ExpandEnv(t), - Help: prompt.Help, - } - default: - p = &survey.Input{ - Message: prompt.Label, - Default: fmt.Sprint(t), + Default: os.ExpandEnv(prompt.Default), Help: prompt.Help, } } + var a string if err := survey.AskOne(p, &a); err != nil { return nil, err diff --git a/schema/schema_test.go b/schema/schema_test.go new file mode 100644 index 0000000..5d7daec --- /dev/null +++ b/schema/schema_test.go @@ -0,0 +1,42 @@ +package schema + +import ( + "embed" + "errors" + "fmt" + "testing" + + "github.com/matryer/is" +) + +//go:embed testdata +var testdata embed.FS + +func TestSchema(t *testing.T) { + tt := []struct { + Name string + NumErr int + }{ + {Name: "good", NumErr: 0}, + {Name: "bad", NumErr: 10}, + {Name: "empty", NumErr: 1}, + } + + for _, tc := range tt { + t.Run(tc.Name, func(t *testing.T) { + assert := is.New(t) + + fi, err := testdata.Open(fmt.Sprintf("testdata/%s.yaml", tc.Name)) + assert.NoErr(err) // Should open test file + + err = Lint(fi) + if tc.NumErr > 0 { + var rerrs ResultErrors + assert.True(errors.As(err, &rerrs)) // Error should be ResultErrors + assert.True(len(rerrs) == tc.NumErr) // Number of errors should match test case + } else { + assert.NoErr(err) // Good schemas shouldn't return errors + } + }) + } +} diff --git a/schema/testdata/bad.yaml b/schema/testdata/bad.yaml new file mode 100644 index 0000000..7a4055d --- /dev/null +++ b/schema/testdata/bad.yaml @@ -0,0 +1,20 @@ +prompts: + - label: Bar + default: baz + help: | + This is a foobar! + options: + - "1" + - bonk + - "false" + - id: test + label: 1234 + - id: test123 + options: [] + - label: 1234 + default: false + help: # nil + options: + - 1 + - 2 + - true \ No newline at end of file diff --git a/schema/testdata/empty.yaml b/schema/testdata/empty.yaml new file mode 100644 index 0000000..e69de29 diff --git a/schema/testdata/good.yaml b/schema/testdata/good.yaml new file mode 100644 index 0000000..e5bb858 --- /dev/null +++ b/schema/testdata/good.yaml @@ -0,0 +1,10 @@ +prompts: + - id: foo + label: Bar + default: baz + help: | + This is a foobar! + options: + - "1" + - bonk + - "false" \ No newline at end of file diff --git a/schema/tmpl.json b/schema/tmpl.json index b8acf98..a8fdb7f 100644 --- a/schema/tmpl.json +++ b/schema/tmpl.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://git.jojodev.com/jolheiser/tmpl/src/branch/main/schema/tmpl.json", + "$id": "https://git.jojodev.com/jolheiser/tmpl/raw/branch/main/schema/tmpl.json", "title": "tmpl template", "description": "A template for tmpl", "type": "object", @@ -28,16 +28,16 @@ "description": "A label to show instead of the ID when prompting", "type": "string" }, - "default": { - "description": "A default value for the prompt", - "type": "string" - }, "help": { "description": "A help message for more information on a prompt", "type": "string" }, - "depends_on": { - "description": "A list of prompt IDs that this prompt depends on", + "default": { + "description": "A default value for the prompt", + "type": "string" + }, + "options": { + "description": "A set of options for this prompt", "type": "array", "minItems": 1, "items": {