-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathparser_impl.go
More file actions
401 lines (346 loc) · 9.78 KB
/
parser_impl.go
File metadata and controls
401 lines (346 loc) · 9.78 KB
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
package parser
import (
"bytes"
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"strconv"
"strings"
"sync"
"text/template"
)
// templateParser implements the Parser interface
type templateParser struct {
config Config
cache *TemplateCache
ctx context.Context
cancel context.CancelFunc
mu sync.RWMutex
closed bool
}
// genericParser implements the GenericParser interface
type genericParser[T any] struct {
*templateParser
}
// NewParser creates a new template parser with the given configuration
func NewParser(config Config) (Parser, error) {
parser, err := newTemplateParser(config)
if err != nil {
return nil, err
}
return parser, nil
}
// NewGenericParser creates a new generic template parser with the given configuration
func NewGenericParser[T any](config Config) (GenericParser[T], error) {
parser, err := newTemplateParser(config)
if err != nil {
return nil, err
}
return &genericParser[T]{templateParser: parser}, nil
}
// newTemplateParser creates the underlying template parser
func newTemplateParser(config Config) (*templateParser, error) {
// If no TemplateLoader is specified, use MemoryLoader by default
if config.TemplateLoader == nil {
config.TemplateLoader = NewMemoryLoader()
}
// Create context for file watching
ctx, cancel := context.WithCancel(context.Background())
// Using default function map if not specified
if config.FuncMap == nil {
config.FuncMap = DefaultFuncMap()
}
// Create template cache
cache := NewTemplateCache(config.MaxCacheSize, config.FuncMap)
parser := &templateParser{
config: config,
cache: cache,
ctx: ctx,
cancel: cancel,
}
// Start file watching if enabled
if config.WatchFiles {
err := config.TemplateLoader.Watch(ctx, parser.onTemplateChanged)
if err != nil {
cancel()
return nil, err
}
}
return parser, nil
}
// Parse implements GenericParser - executes template and returns result as type T
func (g *genericParser[T]) Parse(templateName string, request *http.Request) (T, *RequestData, error) {
return g.ParseWith(templateName, request, nil)
}
// ParseWith implements GenericParser - executes template with custom data and returns result as type T
func (g *genericParser[T]) ParseWith(templateName string, request *http.Request, data interface{}) (T, *RequestData, error) {
var zero T
// Parse template to string buffer first
var buf bytes.Buffer
requestData, err := g.templateParser.ParseWith(templateName, request, data, &buf)
if err != nil {
return zero, nil, err
}
// Convert string result to target type T
result, err := convertToType[T](buf.String())
if err != nil {
return zero, requestData, err
}
return result, requestData, nil
}
// Extract extracts RequestData from the request without parsing any template
func (g *genericParser[T]) Extract(request *http.Request, body ...[]byte) (*RequestData, error) {
return g.templateParser.Extract(request, body...)
}
// convertToType converts a string to the target type T
func convertToType[T any](s string) (T, error) {
var zero T
var result interface{}
// Use type assertion to determine the target type
switch any(zero).(type) {
case string:
result = s
case []byte:
result = []byte(s)
case int:
val, err := strconv.Atoi(strings.TrimSpace(s))
if err != nil {
return zero, fmt.Errorf("cannot convert '%s' to int: %w", s, err)
}
result = val
case int64:
val, err := strconv.ParseInt(strings.TrimSpace(s), 10, 64)
if err != nil {
return zero, fmt.Errorf("cannot convert '%s' to int64: %w", s, err)
}
result = val
case float64:
val, err := strconv.ParseFloat(strings.TrimSpace(s), 64)
if err != nil {
return zero, fmt.Errorf("cannot convert '%s' to float64: %w", s, err)
}
result = val
case bool:
val, err := strconv.ParseBool(strings.TrimSpace(s))
if err != nil {
return zero, fmt.Errorf("cannot convert '%s' to bool: %w", s, err)
}
result = val
default:
// For complex types, try JSON unmarshaling
var target T
err := json.Unmarshal([]byte(s), &target)
if err != nil {
return zero, fmt.Errorf("cannot unmarshal '%s' to type %T: %w", s, zero, err)
}
result = target
}
return result.(T), nil
}
// Parse implements Parser
func (p *templateParser) Parse(templateName string, request *http.Request, output io.Writer) (*RequestData, error) {
return p.ParseWith(templateName, request, nil, output)
}
// ParseWith implements Parser
func (p *templateParser) ParseWith(templateName string, request *http.Request, data interface{}, output io.Writer) (*RequestData, error) {
p.mu.RLock()
if p.closed {
p.mu.RUnlock()
return nil, ErrParserClosed
}
p.mu.RUnlock()
// Create re-readable request
req, err := NewRereadableRequest(request)
if err != nil {
return nil, err
}
// Extract request data
requestData, err := req.Extract()
if err != nil {
return nil, err
}
// Set custom data for ParseWith
requestData.Custom = data
// Get template from cache
tmpl, err := p.cache.Get(templateName, p.config.TemplateLoader)
if err != nil {
return requestData, err
}
// Execute template
err = tmpl.Execute(output, requestData)
// Reset request body for potential reuse
req.Reset()
return requestData, err
}
// Extract extracts RequestData from the request without parsing any template
func (p *templateParser) Extract(req *http.Request, body ...[]byte) (*RequestData, error) {
p.mu.RLock()
if p.closed {
p.mu.RUnlock()
return nil, ErrParserClosed
}
p.mu.RUnlock()
// Create re-readable request with optional body
rereadable, err := NewRereadableRequest(req, body...)
if err != nil {
return nil, err
}
// Extract request data (no longer pass customData, it's removed from this method)
requestData, err := rereadable.Extract()
if err != nil {
return nil, err
}
// Reset request body for potential reuse
rereadable.Reset()
return requestData, nil
}
// UpdateTemplate implements Parser
func (p *templateParser) UpdateTemplate(name string, content string) error {
p.mu.RLock()
if p.closed {
p.mu.RUnlock()
return ErrParserClosed
}
p.mu.RUnlock()
// Calculate MD5 hash of the content
hash := md5.Sum([]byte(content))
hashString := hex.EncodeToString(hash[:])
// Check if template exists and has the same hash
existingHash := p.cache.GetHash(name)
if existingHash != "" && existingHash == hashString {
// Template exists and hasn't changed, no need to update
return nil
}
// Parse the template content
tmpl, err := template.New(name).Funcs(p.config.FuncMap).Parse(content)
if err != nil {
return err
}
slog.Info("Updated template", "name", name, "hash", hashString)
// Update the cache directly with the parsed template
p.cache.Set(name, tmpl, hashString)
return nil
}
// Close implements Parser
func (p *templateParser) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return nil
}
p.closed = true
// Cancel file watching
p.cancel()
// Clear cache
p.cache.Clear()
return nil
}
// onTemplateChanged handles template file changes
func (p *templateParser) onTemplateChanged(name string) {
p.mu.RLock()
if p.closed {
p.mu.RUnlock()
return
}
p.mu.RUnlock()
// Remove from cache to force reload on next access
p.cache.Remove(name)
}
// GetCacheStats returns cache statistics
func (p *templateParser) GetCacheStats() CacheStats {
return p.cache.Stats()
}
// Helper function to create default function map with useful template functions
func DefaultFuncMap() template.FuncMap {
xmlHelper := XMLHelper{}
return template.FuncMap{
// String functions
"upper": func(s string) string {
return strings.ToUpper(s)
},
"lower": func(s string) string {
return strings.ToLower(s)
},
"title": func(s string) string {
return strings.Title(s)
},
"trim": func(s string) string {
return strings.TrimSpace(s)
},
"hasPrefix": func(s, prefix string) bool {
return strings.HasPrefix(s, prefix)
},
"hasSuffix": func(s, suffix string) bool {
return strings.HasSuffix(s, suffix)
},
"contains": func(s, substr string) bool {
return strings.Contains(s, substr)
},
"replace": func(s, old, new string) string {
return strings.ReplaceAll(s, old, new)
},
"split": func(s, sep string) []string {
return strings.Split(s, sep)
},
"join": func(elems []string, sep string) string {
return strings.Join(elems, sep)
},
"trimPrefix": func(s, prefix string) string {
return strings.TrimPrefix(s, prefix)
},
"trimSuffix": func(s, suffix string) string {
return strings.TrimSuffix(s, suffix)
},
"repeat": func(s string, count int) string {
return strings.Repeat(s, count)
},
"substr": func(s string, start, length int) string {
if start < 0 || start >= len(s) {
return ""
}
end := start + length
if end > len(s) {
end = len(s)
}
return s[start:end]
},
// Utility functions
"default": func(defaultValue, value interface{}) interface{} {
if value == nil {
return defaultValue
}
if s, ok := value.(string); ok && s == "" {
return defaultValue
}
return value
},
// Request-specific functions
"header": func(req *http.Request, name string) string {
return req.Header.Get(name)
},
"query": func(req *http.Request, name string) string {
return req.URL.Query().Get(name)
},
"form": func(req *http.Request, name string) string {
return req.FormValue(name)
},
// XML helper functions
"xmlAttr": xmlHelper.GetXMLAttribute,
"xmlAttrArray": xmlHelper.GetXMLAttributeArray,
"xmlValue": xmlHelper.GetXMLValue,
"xmlValueArray": xmlHelper.GetXMLValueArray,
"xmlText": xmlHelper.GetXMLText,
"xmlTextArray": xmlHelper.GetXMLTextArray,
"hasXMLAttr": xmlHelper.HasXMLAttribute,
"hasXMLElement": xmlHelper.HasXMLElement,
"isXMLArray": xmlHelper.IsXMLArray,
"xmlArrayLen": xmlHelper.XMLArrayLength,
"xmlAttrs": xmlHelper.ListXMLAttributes,
"xmlElements": xmlHelper.ListXMLElements,
}
}