diff --git a/cmd/templit/templit.go b/cmd/templit/templit.go index ac8bedd..a903f36 100644 --- a/cmd/templit/templit.go +++ b/cmd/templit/templit.go @@ -1,7 +1,9 @@ +//nolint:gochecknoglobals,gochecknoinits package main import ( "encoding/json" + "errors" "fmt" "html/template" "maps" @@ -11,6 +13,8 @@ import ( "github.com/spf13/cobra" ) +var ErrMissingToken = errors.New("embed and import functions requires a GitHub token") + // flagValues stores the values of command-line flags var flagValues = struct { token string @@ -23,7 +27,8 @@ var templitCmd = &cobra.Command{ Use: "templit ", Short: "A CLI tool for rendering templates from remote repositories", Long: `templit is a CLI tool for rendering templates from remote repositories.`, - Run: func(cmd *cobra.Command, args []string) { + Args: cobra.MinimumNArgs(3), //nolint:mnd + Run: func(cmd *cobra.Command, _ []string) { if err := cmd.Help(); err != nil { fmt.Fprintln(os.Stderr, err) } @@ -35,15 +40,7 @@ var renderCmd = &cobra.Command{ Use: "render ", Short: "A CLI tool for rendering templates from remote repositories", Long: `generate is for rendering templates.`, - Run: func(cmd *cobra.Command, args []string) { - // Check for the correct number of command-line arguments - if len(args) < 3 { - if err := cmd.Help(); err != nil { - fmt.Fprintln(os.Stderr, err) - } - return - } - + Run: func(_ *cobra.Command, args []string) { if flagValues.token == "" { flagValues.token = os.Getenv("GIT_TOKEN") } @@ -57,6 +54,7 @@ var renderCmd = &cobra.Command{ // Parse the JSON data from the command-line argument if err := json.Unmarshal([]byte(inputData), &values); err != nil { fmt.Fprintf(os.Stderr, "Error parsing JSON data: %s\n", err) + return } @@ -64,13 +62,13 @@ var renderCmd = &cobra.Command{ executor := templit.NewExecutor(templit.NewDefaultGitClient(flagValues.branch, flagValues.token)) // funcMap defines the custom functions that can be used in templates - var funcMap = template.FuncMap{ + funcMap := template.FuncMap{ // return an error for the embed and import functions if no GitHub token is provided - "embed": func(repoAndPath string, ctx interface{}) (string, error) { - return "", fmt.Errorf("embed function requires a GitHub token") + "embed": func(string, interface{}) (string, error) { + return "", ErrMissingToken }, - "import": func(repoAndPath string, destPath string, ctx interface{}) (string, error) { - return "", fmt.Errorf("import function requires a GitHub token") + "import": func(string, string, interface{}) (string, error) { + return "", ErrMissingToken }, } @@ -80,7 +78,7 @@ var renderCmd = &cobra.Command{ } // Copy the default function map from the templit package - maps.Copy(funcMap, templit.DefaultFuncMap) + executor.Funcs(templit.DefaultFuncMap()) executor.Funcs(funcMap) // If a remote repository is specified, process the template and write it to the output directory @@ -88,6 +86,7 @@ var renderCmd = &cobra.Command{ importParts, err := templit.ParseDepURL(flagValues.remote) if err != nil { fmt.Fprintf(os.Stderr, "Error parsing remote: %s\n", err) + return } @@ -96,11 +95,12 @@ var renderCmd = &cobra.Command{ if _, err := executor.ImportFunc(outputPath)(importParts.String(), "./", values); err != nil { fmt.Fprintf(os.Stderr, "Error processing template: %s\n", err) } + return } // Copy the default function map from the templit package - maps.Copy(funcMap, templit.DefaultFuncMap) + maps.Copy(funcMap, templit.DefaultFuncMap()) executor.Funcs(funcMap) // Process the templates in the input directory and write them to the output directory diff --git a/dep.go b/dep.go index b69bfb6..5bddf7f 100644 --- a/dep.go +++ b/dep.go @@ -1,6 +1,7 @@ package templit import ( + "errors" "fmt" "net/url" "strings" @@ -20,6 +21,9 @@ type DepInfo struct { Tag string } +// ErrInvalidPath is returned when the path is invalid. +var ErrInvalidPath = errors.New("invalid path") + // String returns the string representation of a DepInfo. func (d DepInfo) String() string { var builder strings.Builder @@ -56,7 +60,7 @@ func ParseDepURL(rawURL string) (*DepInfo, error) { u, err := url.Parse(rawURL) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse URL: %w", err) } // Extract the tag if it exists before splitting the path @@ -68,14 +72,15 @@ func ParseDepURL(rawURL string) (*DepInfo, error) { // Split path into components pathParts := strings.Split(fullPath, "/") - if len(pathParts) < 2 { - return nil, fmt.Errorf("invalid path format in embed URL") + minParts := 2 + if len(pathParts) < minParts { + return nil, ErrInvalidPath } owner := pathParts[0] repo := pathParts[1] path := "" - if len(pathParts) > 2 { + if len(pathParts) > minParts { path = strings.Join(pathParts[2:], "/") } @@ -101,6 +106,7 @@ func splitAtSign(s string) (string, string) { if len(parts) > 1 { return parts[0], parts[1] } + return parts[0], "" } @@ -109,9 +115,12 @@ func extractBlockAndTag(fragment string) (string, string) { parts := strings.Split(fragment, "@") if len(parts) > 1 { return parts[0], parts[1] - } else if len(parts) == 1 { + } + + if len(parts) == 1 { return parts[0], "" } + return "", "" } @@ -147,9 +156,9 @@ func (d *DefaultGitClient) DefaultBranch() string { // Clone clones a Git repository to the given destination. func (d *DefaultGitClient) Clone(host, owner, repo, dest string) error { - repoURL := fmt.Sprintf("%s/%s/%s.git", host, owner, repo) + repoURL := host + "/" + owner + "/" + repo + ".git" if !strings.HasPrefix(repoURL, "https://") { - repoURL = fmt.Sprintf("https://%s", repoURL) + repoURL = "https://" + repoURL } var auth *http.BasicAuth @@ -164,7 +173,6 @@ func (d *DefaultGitClient) Clone(host, owner, repo, dest string) error { URL: repoURL, Auth: auth, }) - if err != nil { return fmt.Errorf("failed to clone repo %s: %w", repoURL, err) } @@ -176,12 +184,12 @@ func (d *DefaultGitClient) Clone(host, owner, repo, dest string) error { func (d *DefaultGitClient) Checkout(path, ref string) error { r, err := git.PlainOpen(path) if err != nil { - return err + return fmt.Errorf("failed to open repository: %w", err) } w, err := r.Worktree() if err != nil { - return err + return fmt.Errorf("failed to get worktree: %w", err) } // Try to checkout branch diff --git a/docs/main.go b/docs/main.go index 5cb3411..6a8443d 100644 --- a/docs/main.go +++ b/docs/main.go @@ -113,13 +113,14 @@ func (app *App) renderTemplate(this js.Value, args []js.Value) interface{} { jsonStr := args[1].String() executor := templit.NewExecutor(templit.NewDefaultGitClient("main", "")) - funcs := templit.DefaultFuncMap + funcs := templit.DefaultFuncMap() funcs["embed"] = func(name string) string { return fmt.Sprintf("Embed (not enabled): %s", name) } funcs["import"] = func(name string) string { return fmt.Sprintf("Import (not enabled): %s", name) } + executor.Template.Funcs(funcs) var data interface{} diff --git a/docs/main.wasm b/docs/main.wasm index 0b87d71..482e01e 100755 Binary files a/docs/main.wasm and b/docs/main.wasm differ diff --git a/embed_func_test.go b/embed_func_test.go index 1284b3f..0247651 100644 --- a/embed_func_test.go +++ b/embed_func_test.go @@ -36,7 +36,7 @@ func TestEmbedFunc(t *testing.T) { repoAndPath: "invalidpath", ctx: nil, expectedText: "", - expectedError: fmt.Errorf("invalid path format in embed URL"), + expectedError: fmt.Errorf("invalid path"), }, { name: "Invalid repo", diff --git a/funcs.go b/funcs.go index 64efd8f..7b675a7 100644 --- a/funcs.go +++ b/funcs.go @@ -6,34 +6,36 @@ import ( ) // DefaultFuncMap is the default function map for templates. -var DefaultFuncMap = template.FuncMap{ - "lower": strings.ToLower, - "upper": strings.ToUpper, - "trim": strings.TrimSpace, - "split": strings.Split, - "join": strings.Join, - "replace": strings.ReplaceAll, - "contains": strings.Contains, - "hasPrefix": strings.HasPrefix, - "hasSuffix": strings.HasSuffix, - "trimPrefix": strings.TrimPrefix, - "trimSuffix": strings.TrimSuffix, - "trimSpace": strings.TrimSpace, - "trimLeft": strings.TrimLeft, - "trimRight": strings.TrimRight, - "count": strings.Count, - "repeat": strings.Repeat, - "equalFold": strings.EqualFold, - "splitN": strings.SplitN, - "splitAfter": strings.SplitAfter, - "splitAfterN": strings.SplitAfterN, - "fields": strings.Fields, - "toTitle": strings.ToTitle, - "toSnakeCase": ToSnakeCase, - "toCamelCase": ToCamelCase, - "toKebabCase": ToKebabCase, - "toPascalCase": ToPascalCase, - "default": defaultVal, +func DefaultFuncMap() template.FuncMap { + return template.FuncMap{ + "lower": strings.ToLower, + "upper": strings.ToUpper, + "trim": strings.TrimSpace, + "split": strings.Split, + "join": strings.Join, + "replace": strings.ReplaceAll, + "contains": strings.Contains, + "has_prefix": strings.HasPrefix, + "has_suffix": strings.HasSuffix, + "trim_prefix": strings.TrimPrefix, + "trim_suffix": strings.TrimSuffix, + "trim_space": strings.TrimSpace, + "trim_left": strings.TrimLeft, + "trim_right": strings.TrimRight, + "count": strings.Count, + "repeat": strings.Repeat, + "equal_fold": strings.EqualFold, + "split_n": strings.SplitN, + "split_after": strings.SplitAfter, + "split_after_n": strings.SplitAfterN, + "fields": strings.Fields, + "title_case": strings.ToTitle, + "snake_case": ToSnakeCase, + "camel_case": ToCamelCase, + "kebab_case": ToKebabCase, + "pascal_case": ToPascalCase, + "default": defaultVal, + } } // defaultVal returns defaultValue if value is nil, otherwise value. @@ -41,5 +43,6 @@ func defaultVal(value, defaultValue interface{}) interface{} { if value == nil { return defaultValue } + return value } diff --git a/go.work.sum b/go.work.sum index 434a8d3..3df0819 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,21 +1,16 @@ github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= @@ -25,13 +20,11 @@ golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfS golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/import_func.go b/import_func.go index e04077e..797160b 100644 --- a/import_func.go +++ b/import_func.go @@ -54,19 +54,23 @@ func (e *Executor) ImportFunc(outputDir string) func(repoAndTag, destPath string // check if path is a file if info, err := os.Stat(sourcePath); err == nil && !info.IsDir() { + if os.IsNotExist(err) { + return "", fmt.Errorf("file does not exist %s, %w", sourcePath, os.ErrNotExist) + } // parse the file if err := e.ParsePath(filepath.Dir(sourcePath)); err != nil { return "", fmt.Errorf("failed to create executor: %w", err) } // render the file - string, err := e.Render(sourcePath, data) + output, err := e.Render(sourcePath, data) if err != nil { return "", fmt.Errorf("failed to render template: %w", err) } // write the file - if err := os.WriteFile(filepath.Join(outputPath, filepath.Base(depInfo.Path)), []byte(string), 0644); err != nil { + const perms = os.FileMode(0o644) + if err := os.WriteFile(filepath.Join(outputPath, filepath.Base(depInfo.Path)), []byte(output), perms); err != nil { return "", fmt.Errorf("failed to write file: %w", err) } diff --git a/import_func_test.go b/import_func_test.go index 780950f..0fd53f9 100644 --- a/import_func_test.go +++ b/import_func_test.go @@ -1,6 +1,7 @@ package templit_test import ( + "errors" "fmt" "os" "strings" @@ -11,7 +12,6 @@ import ( // TestImportFunc tests the ImportFunc function. func TestImportFunc(t *testing.T) { - tests := []struct { name string repoAndTag string @@ -46,7 +46,7 @@ func TestImportFunc(t *testing.T) { executor := templit.NewExecutor(client) fn := executor.ImportFunc(destPath) if _, err := fn(tt.repoAndTag, "./", tt.data); err != nil { - if tt.expectedError == nil || err.Error() != tt.expectedError.Error() { + if tt.expectedError == nil || errors.Is(err, tt.expectedError) { t.Fatalf("expected error %v, got %v", tt.expectedError, err) } return diff --git a/string_funcs.go b/string_funcs.go index f2eb27e..e6954d6 100644 --- a/string_funcs.go +++ b/string_funcs.go @@ -15,6 +15,7 @@ func ToCamelCase(s string) string { parts[i] = capitalizeFirstLetter(part) } } + return strings.Join(parts, "") } @@ -40,6 +41,7 @@ func ToSnakeCase(s string) string { previousIsLower = false } } + return result.String() } @@ -65,6 +67,7 @@ func ToKebabCase(s string) string { previousIsLower = false } } + return result.String() } @@ -74,6 +77,7 @@ func ToPascalCase(s string) string { for i, part := range parts { parts[i] = capitalizeFirstLetter(part) } + return strings.Join(parts, "") } @@ -90,6 +94,7 @@ func splitAndFilter(s string) []string { } parts[i] = string(filtered) } + return parts } @@ -98,6 +103,7 @@ func splitByMultipleDelimiters(s string, delimiters []string) []string { for _, delimiter := range delimiters { s = strings.ReplaceAll(s, delimiter, " ") } + return strings.Fields(s) } @@ -106,5 +112,6 @@ func capitalizeFirstLetter(s string) string { if s == "" { return s } + return strings.ToUpper(string(s[0])) + strings.ToLower(s[1:]) } diff --git a/templit.go b/templit.go index 26a8ad2..0682835 100644 --- a/templit.go +++ b/templit.go @@ -17,7 +17,7 @@ type Executor struct { // New returns a new Executor func NewExecutor(gitClient GitClient) *Executor { return &Executor{ - Template: template.New("main").Funcs(DefaultFuncMap), + Template: template.New("main").Funcs(DefaultFuncMap()), git: gitClient, } } @@ -38,6 +38,7 @@ func (e *Executor) ParsePath(inputPath string) error { if _, err := e.New(inputPath).Parse(string(content)); err != nil { return fmt.Errorf("failed to parse template: %w", err) } + return nil } @@ -62,7 +63,6 @@ func (e *Executor) ParsePath(inputPath string) error { return nil }) - if err != nil { return fmt.Errorf("failed to parse templates: %w", err) } @@ -76,6 +76,7 @@ func (e Executor) Render(name string, data interface{}) (string, error) { if err := e.ExecuteTemplate(&buf, name, data); err != nil { return "", fmt.Errorf("failed to execute template %s: %w", name, err) } + return buf.String(), nil } diff --git a/walk.go b/walk.go index 0596496..7a8dded 100644 --- a/walk.go +++ b/walk.go @@ -11,7 +11,8 @@ import ( // If walkFunc is provided, it's called for each file and directory without writing the file to disk. func (e *Executor) WalkAndProcessDir(inputDir, outputDir string, data interface{}) error { // Create output directory - if err := os.MkdirAll(outputDir, 0755); err != nil { + const perms = os.FileMode(0o755) + if err := os.MkdirAll(outputDir, perms); err != nil { return fmt.Errorf("failed to create output directory: %w", err) } @@ -80,7 +81,6 @@ func (e *Executor) WalkAndProcessDir(inputDir, outputDir string, data interface{ return nil }) - if err != nil { return fmt.Errorf("error walking through directory: %w", err) } diff --git a/walk_test.go b/walk_test.go index 33a11c4..b863be3 100644 --- a/walk_test.go +++ b/walk_test.go @@ -12,7 +12,7 @@ import ( // TestExecutor_RenderTemplate tests the StringRender function. func TestExecutor_StringRender(t *testing.T) { - var tests = []struct { + tests := []struct { name string tmpl string data interface{} @@ -80,7 +80,7 @@ func TestWalkAndProcessDir(t *testing.T) { "Description": "This is a test project.", "Detail": "more info here.", }, - funcMap: templit.DefaultFuncMap, + funcMap: templit.DefaultFuncMap(), expectedOutput: "test_data/outputs/basic_test/", }, // ... (other test cases)