Add options support
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details

Signed-off-by: jolheiser <john.olheiser@gmail.com>
pull/24/head
jolheiser 2022-06-15 09:53:04 -05:00
parent c348255c37
commit 9a396ece19
Signed by: jolheiser
GPG Key ID: B853ADA5DA7BBF7A
12 changed files with 140 additions and 85 deletions

29
FAQ.md
View File

@ -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) For a full list, see [helper.go](registry/helper.go)
| Helper | Example | Output | | Helper | Example | Output |
|-------------|----------------------------------|-------------------------------------------------------------------------------------------------------| |-------------|------------------------------------|-------------------------------------------------------------------------------------------------------|
| upper | `{{upper project}}` | `MY-PROJECT` | | upper | `{{upper project}}` | `MY-PROJECT` |
| lower | `{{lower project}}` | `my-project` | | lower | `{{lower project}}` | `my-project` |
| title | `{{title project}}` | `My-Project` | | title | `{{title project}}` | `My-Project` |
| snake | `{{snake project}}` | `my_project` | | snake | `{{snake project}}` | `my_project` |
| kebab | `{{kebab project}}` | `my-project` | | kebab | `{{kebab project}}` | `my-project` |
| pascal | `{{pascal project}}` | `MyProject` | | pascal | `{{pascal project}}` | `MyProject` |
| camel | `{{camel project}}` | `myProject` | | camel | `{{camel project}}` | `myProject` |
| env | `{{env "USER"}}` | The current user | | env | `{{env "USER"}}` | The current user |
| sep | `{{sep}}` | Filepath separator for current OS | | 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/) | | 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_prefix | `{{trim_prefix "foobar" "foo"}}` | `bar` |
| trim_suffix | `{{trim_suffix "foobar" "bar"}}` | `foo` | | trim_suffix | `{{trim_suffix "foobar" "bar"}}` | `foo` |
| replace | `{{replace "foobar" "bar" "baz"}}` | `foobaz` |
## Sources ## Sources

View File

@ -36,7 +36,7 @@ func runInit(_ *cli.Context) error {
if err != nil { if err != nil {
return err return err
} }
if _, err := fi.WriteString(comments); err != nil { if _, err := fi.WriteString(initConfig); err != nil {
return err return err
} }
if err := os.Mkdir("template", os.ModePerm); err != nil { if err := os.Mkdir("template", os.ModePerm); err != nil {
@ -46,12 +46,15 @@ func runInit(_ *cli.Context) error {
return fi.Close() 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 # Write any template args here to prompt the user for, giving any defaults/options as applicable
prompts: prompts:
- id: name # The unique ID for the prompt - id: name # The unique ID for the prompt
label: Project Name # The prompt message/label label: Project Name # (Optional) Prompt message/label, defaults to id
help: The name to use in the project # Optional help message for the prompt help: The name to use in the project # (Optional) Help message for the prompt
default: tmpl # Prompt default default: tmpl # (Optional) Prompt default
options: # (Optional) Set of options the user can choose from
- coolproject123
- ${USER}'s cool project
` `

17
cmd/init_test.go 100644
View File

@ -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
}

View File

@ -16,10 +16,11 @@ type Config struct {
// Prompt is a tmpl prompt // Prompt is a tmpl prompt
type Prompt struct { type Prompt struct {
ID string `yaml:"id"` ID string `yaml:"id"`
Label string `yaml:"label"` Label string `yaml:"label"`
Help string `yaml:"help"` Help string `yaml:"help"`
Default any `yaml:"default"` Default string `yaml:"default"`
Options []string `yaml:"options"`
} }
// Load loads a tmpl config // Load loads a tmpl config

View File

@ -10,11 +10,6 @@ func (e ErrTemplateExists) Error() string {
return fmt.Sprintf("template %s already exists", e.Name) return fmt.Sprintf("template %s already exists", e.Name)
} }
func IsErrTemplateExists(err error) bool {
_, ok := err.(ErrTemplateExists)
return ok
}
type ErrTemplateNotFound struct { type ErrTemplateNotFound struct {
Name string Name string
} }
@ -30,8 +25,3 @@ type ErrSourceNotFound struct {
func (e ErrSourceNotFound) Error() string { func (e ErrSourceNotFound) Error() string {
return fmt.Sprintf("Source not found for %s", e.Name) return fmt.Sprintf("Source not found for %s", e.Name)
} }
func IsErrSourceNotFound(err error) bool {
_, ok := err.(ErrSourceNotFound)
return ok
}

View File

@ -10,7 +10,6 @@ import (
) )
var funcMap = map[string]any{ var funcMap = map[string]any{
// String conversions
"upper": strings.ToUpper, "upper": strings.ToUpper,
"lower": strings.ToLower, "lower": strings.ToLower,
"title": strings.Title, "title": strings.Title,
@ -20,15 +19,10 @@ var funcMap = map[string]any{
"camel": func(in string) string { "camel": func(in string) string {
return xstrings.FirstRuneToLower(xstrings.ToCamelCase(in)) return xstrings.FirstRuneToLower(xstrings.ToCamelCase(in))
}, },
"trim_prefix": func(in, trim string) string { "trim_prefix": strings.TrimPrefix,
return strings.TrimPrefix(in, trim) "trim_suffix": strings.TrimSuffix,
}, "replace": strings.ReplaceAll,
"trim_suffix": func(in, trim string) string { "env": os.Getenv,
return strings.TrimSuffix(in, trim)
},
// Other
"env": os.Getenv,
"sep": func() string { "sep": func() string {
return string(filepath.Separator) return string(filepath.Separator)
}, },

View File

@ -33,9 +33,6 @@ func prompt(dir string, defaults bool) (templatePrompts, error) {
if tp.Label == "" { if tp.Label == "" {
tp.Label = tp.ID tp.Label = tp.ID
} }
if tp.Default == nil {
tp.Default = ""
}
prompts = append(prompts, tp) prompts = append(prompts, tp)
} }
@ -50,52 +47,32 @@ func prompt(dir string, defaults bool) (templatePrompts, error) {
// Check if we are using defaults // Check if we are using defaults
if defaults { if defaults {
val := prompt.Default val := os.ExpandEnv(prompt.Default)
switch t := prompt.Default.(type) { prompts[idx].Value = val
case []string: os.Setenv(fmt.Sprintf("TMPL_PROMPT_%s", envKey), val)
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)
continue continue
} }
// Otherwise, prompt
var p survey.Prompt var p survey.Prompt
switch t := prompt.Default.(type) { if len(prompt.Options) > 0 {
case []string: opts := make([]string, 0, len(prompt.Options))
for idy, s := range t { for idy, opt := range prompt.Options {
t[idy] = os.ExpandEnv(s) opts[idy] = os.ExpandEnv(opt)
} }
p = &survey.Select{ p = &survey.Select{
Message: prompt.Label, Message: prompt.Label,
Options: t, Options: opts,
Help: prompt.Help, Help: prompt.Help,
} }
case bool: } else {
p = &survey.Confirm{
Message: prompt.Label,
Default: t,
Help: prompt.Help,
}
case string:
p = &survey.Input{ p = &survey.Input{
Message: prompt.Label, Message: prompt.Label,
Default: os.ExpandEnv(t), Default: os.ExpandEnv(prompt.Default),
Help: prompt.Help,
}
default:
p = &survey.Input{
Message: prompt.Label,
Default: fmt.Sprint(t),
Help: prompt.Help, Help: prompt.Help,
} }
} }
var a string var a string
if err := survey.AskOne(p, &a); err != nil { if err := survey.AskOne(p, &a); err != nil {
return nil, err return nil, err

View File

@ -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
}
})
}
}

20
schema/testdata/bad.yaml vendored 100644
View File

@ -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

0
schema/testdata/empty.yaml vendored 100644
View File

10
schema/testdata/good.yaml vendored 100644
View File

@ -0,0 +1,10 @@
prompts:
- id: foo
label: Bar
default: baz
help: |
This is a foobar!
options:
- "1"
- bonk
- "false"

View File

@ -1,6 +1,6 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$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", "title": "tmpl template",
"description": "A template for tmpl", "description": "A template for tmpl",
"type": "object", "type": "object",
@ -28,16 +28,16 @@
"description": "A label to show instead of the ID when prompting", "description": "A label to show instead of the ID when prompting",
"type": "string" "type": "string"
}, },
"default": {
"description": "A default value for the prompt",
"type": "string"
},
"help": { "help": {
"description": "A help message for more information on a prompt", "description": "A help message for more information on a prompt",
"type": "string" "type": "string"
}, },
"depends_on": { "default": {
"description": "A list of prompt IDs that this prompt depends on", "description": "A default value for the prompt",
"type": "string"
},
"options": {
"description": "A set of options for this prompt",
"type": "array", "type": "array",
"minItems": 1, "minItems": 1,
"items": { "items": {