-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtemplator.go
141 lines (120 loc) · 3.81 KB
/
templator.go
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
139
140
141
// Package templator provides a type-safe template rendering system for Go applications.
// It offers a simple and concurrent-safe way to manage HTML templates with compile-time
// type checking for template data.
package templator
//go:generate go run ./cmd/generate/generate_methods.go
import (
"context"
"html/template"
"io"
"io/fs"
"sync"
)
const (
// DefaultTemplateDir is the default directory where templates are stored.
DefaultTemplateDir = "templates"
// DefaultTemplateExt is the default file extension for templates.
DefaultTemplateExt = "html"
// ExtensionHTML defines the standard HTML template file extension.
ExtensionHTML Extension = ".html"
)
// Extension represents a template file extension type.
type Extension string
// Option configures a Registry instance.
type Option[T any] func(*Registry[T])
// WithTemplatesPath returns an Option that sets a custom template directory path.
// If an empty path is provided, the default path will be used.
func WithTemplatesPath[T any](path string) Option[T] {
return func(r *Registry[T]) {
if path != "" {
r.config.path = path
}
}
}
// WithFieldValidation enables template field validation against the provided model
func WithFieldValidation[T any](model T) Option[T] {
return func(r *Registry[T]) {
r.config.validateFields = true
r.config.validationModel = model
}
}
func WithTemplateFuncs[T any](funcMap template.FuncMap) Option[T] {
return func(r *Registry[T]) {
r.config.funcMap = funcMap
}
}
type config[T any] struct {
path string
validateFields bool
validationModel T
funcMap template.FuncMap
}
// Registry manages template handlers in a concurrent-safe manner.
type Registry[T any] struct {
fs fs.FS
config config[T]
mu sync.RWMutex
templates map[string]*Handler[T]
}
// Handler manages a specific template instance with type-safe data handling.
// It provides methods for template execution and customization.
type Handler[T any] struct {
tmpl *template.Template
reg *Registry[T]
}
// NewRegistry creates a new template registry with the provided filesystem and options.
// It accepts a filesystem interface and variadic options for customization.
func NewRegistry[T any](fsys fs.FS, opts ...Option[T]) (*Registry[T], error) {
reg := &Registry[T]{
fs: fsys,
config: config[T]{
path: DefaultTemplateDir,
},
templates: make(map[string]*Handler[T]),
}
for _, opt := range opts {
opt(reg)
}
return reg, nil
}
// Get retrieves or creates a type-safe handler for a specific template.
// It automatically appends the .html extension to the template name.
// Returns an error if the template cannot be parsed.
func (r *Registry[T]) Get(name string) (*Handler[T], error) {
r.mu.RLock()
if h, ok := r.templates[name]; ok {
r.mu.RUnlock()
return h, nil
}
r.mu.RUnlock()
r.mu.Lock()
defer r.mu.Unlock()
// Check again in case another goroutine has loaded the template
if h, ok := r.templates[name]; ok {
return h, nil
}
// Read template content first
content, err := fs.ReadFile(r.fs, r.config.path+"/"+name+".html")
if err != nil {
return nil, err
}
// Validate fields if enabled - validate content before parsing
if r.config.validateFields {
if err := validateTemplateFields(name, string(content), r.config.validationModel); err != nil {
return nil, err
}
}
// Parse template after validation
tmpl, err := template.New(name + ".html").Funcs(r.config.funcMap).Parse(string(content))
if err != nil {
return nil, err
}
handler := &Handler[T]{tmpl: tmpl, reg: r}
r.templates[name] = handler
return handler, nil
}
// Execute renders the template with the provided data and writes the output to the writer.
// The context parameter can be used for cancellation and deadline control.
func (h *Handler[T]) Execute(ctx context.Context, w io.Writer, data T) error {
return h.tmpl.Execute(w, data)
}