summaryrefslogtreecommitdiff
path: root/gomplate.go
blob: c849e66e7aa0f4d435a532dd05c7fbf2c511425a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// Package gomplate is a template renderer which supports a number of datasources,
// and includes hundreds of built-in functions.
package gomplate

import (
	"bytes"
	"context"
	"fmt"
	"log/slog"
	"path/filepath"
	"strings"
	"text/template"
	"time"

	"github.com/hairyhenderson/gomplate/v4/internal/datafs"
)

// Run all gomplate templates specified by the given configuration
func Run(ctx context.Context, cfg *Config) error {
	Metrics = newMetrics()

	// apply defaults before validation
	cfg.applyDefaults()

	err := cfg.validate()
	if err != nil {
		return fmt.Errorf("failed to validate config: %w\n%+v", err, cfg)
	}

	if cfg.Experimental {
		slog.SetDefault(slog.With("experimental", true))
		slog.InfoContext(ctx, "experimental functions and features enabled!")

		ctx = SetExperimental(ctx)
	}

	// bind plugins from the configuration to the funcMap
	funcMap := template.FuncMap{}
	err = bindPlugins(ctx, cfg, funcMap)
	if err != nil {
		return err
	}

	// if a custom Stdin is set in the config, inject it into the context now
	ctx = datafs.ContextWithStdin(ctx, cfg.Stdin)

	// if a custom FSProvider is set in the context, use it, otherwise inject
	// the default now - one is needed for the calls below to gatherTemplates
	// as well as the rendering itself
	if datafs.FSProviderFromContext(ctx) == nil {
		ctx = datafs.ContextWithFSProvider(ctx, DefaultFSProvider)
	}

	// extract the rendering options from the config
	opts := optionsFromConfig(cfg)
	opts.Funcs = funcMap
	tr := newRenderer(opts)

	start := time.Now()

	// figure out how to name output files (only relevant if we're dealing with an InputDir)
	namer := chooseNamer(cfg, tr)

	// prepare to render templates (read them in, open output writers, etc)
	tmpl, err := gatherTemplates(ctx, cfg, namer)

	Metrics.GatherDuration = time.Since(start)
	if err != nil {
		Metrics.Errors++
		return fmt.Errorf("failed to gather templates for rendering: %w", err)
	}
	Metrics.TemplatesGathered = len(tmpl)

	err = tr.RenderTemplates(ctx, tmpl)
	if err != nil {
		return err
	}

	return nil
}

type outputNamer interface {
	// Name the output file for the given input path
	Name(ctx context.Context, inPath string) (string, error)
}

type outputNamerFunc func(context.Context, string) (string, error)

func (f outputNamerFunc) Name(ctx context.Context, inPath string) (string, error) {
	return f(ctx, inPath)
}

func chooseNamer(cfg *Config, tr *renderer) outputNamer {
	if cfg.OutputMap == "" {
		return simpleNamer(cfg.OutputDir)
	}
	return mappingNamer(cfg.OutputMap, tr)
}

func simpleNamer(outDir string) outputNamer {
	return outputNamerFunc(func(_ context.Context, inPath string) (string, error) {
		outPath := filepath.Join(outDir, inPath)
		return filepath.Clean(outPath), nil
	})
}

func mappingNamer(outMap string, tr *renderer) outputNamer {
	return outputNamerFunc(func(ctx context.Context, inPath string) (string, error) {
		tcontext, err := createTmplContext(ctx, tr.tctxAliases, tr.sr)
		if err != nil {
			return "", err
		}

		// add '.in' to the template context and preserve the original context
		// in '.ctx'
		tctx := &tmplctx{}
		//nolint:gocritic
		switch c := tcontext.(type) {
		case *tmplctx:
			for k, v := range *c {
				if k != "in" && k != "ctx" {
					(*tctx)[k] = v
				}
			}
		}
		(*tctx)["ctx"] = tcontext
		(*tctx)["in"] = inPath

		out := &bytes.Buffer{}
		err = tr.renderTemplatesWithData(ctx,
			[]Template{{Name: "<OutputMap>", Text: outMap, Writer: out}}, tctx)
		if err != nil {
			return "", fmt.Errorf("failed to render outputMap with ctx %+v and inPath %s: %w", tctx, inPath, err)
		}

		return filepath.Clean(strings.TrimSpace(out.String())), nil
	})
}