-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathparsers.go
More file actions
367 lines (337 loc) · 13.1 KB
/
Copy pathparsers.go
File metadata and controls
367 lines (337 loc) · 13.1 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
// parsers.go: Universal configuration file parsers for Argus
//
// This file provides parsing support for major configuration formats,
// making Argus truly universal and not a "one-trick pony".
//
// Supported Formats:
// - JSON (.json) - Full production support
// - YAML (.yml, .yaml) - Simple built-in + plugin support
// - TOML (.toml) - Simple built-in + plugin support
// - HCL (.hcl, .tf) - Simple built-in + plugin support
// - INI/Config (.ini, .conf, .cfg) - Simple built-in + plugin support
// - Properties (.properties) - Simple built-in + plugin support
//
// Parser Architecture:
// - Built-in parsers: Simple, fast, zero-dependency for 80% use cases
// - Plugin parsers: Full-featured external parsers for complex production needs
// - Automatic fallback: Try plugins first, fallback to built-in
//
// Copyright (c) 2025 AGILira - A. Giordano
// Series: AGILira fragment
// SPDX-License-Identifier: MPL-2.0
package argus
import (
"strconv"
"strings"
"sync"
"github.com/agilira/go-errors"
)
// ConfigFormat represents supported configuration file formats for auto-detection.
// Used by the format detection system to determine appropriate parser selection.
type ConfigFormat int
const (
FormatJSON ConfigFormat = iota
FormatYAML
FormatTOML
FormatHCL
FormatINI
FormatProperties
FormatUnknown
)
// ConfigParser defines the interface for pluggable configuration parsers
//
// PRODUCTION PARSER INTEGRATION:
// Go binaries are compiled statically, so "plugins" work via compile-time registration:
//
// 1. IMPORT-BASED REGISTRATION (Recommended):
// Users import parser libraries that auto-register in init():
//
// import _ "github.com/your-org/argus-yaml-pro" // Registers advanced YAML parser
// import _ "github.com/your-org/argus-toml-pro" // Registers advanced TOML parser
//
// 2. MANUAL REGISTRATION:
// Users manually register parsers in their main():
//
// argus.RegisterParser(&MyAdvancedYAMLParser{})
//
// 3. BUILD TAGS (Advanced):
// Conditional compilation for different parser sets:
//
// go build -tags "yaml_pro,toml_pro" ./...
//
// Built-in parsers handle 80% of use cases with zero dependencies.
// Production parsers provide full spec compliance and advanced features.
type ConfigParser interface {
// Parse parses configuration data for supported formats
Parse(data []byte) (map[string]interface{}, error)
// Supports returns true if this parser can handle the given format
Supports(format ConfigFormat) bool
// Name returns a human-readable name for this parser (for debugging)
Name() string
}
// Global registry of custom parsers (production environments can register advanced parsers)
var (
customParsers []ConfigParser
parserMutex sync.RWMutex
)
// RegisterParser registers a custom parser for production use cases.
// Custom parsers are tried before built-in parsers, allowing for full
// specification compliance or advanced features not available in built-in parsers.
//
// Example:
//
// argus.RegisterParser(&MyAdvancedYAMLParser{})
//
// Or via import-based registration:
//
// import _ "github.com/your-org/argus-yaml-pro"
func RegisterParser(parser ConfigParser) {
parserMutex.Lock()
defer parserMutex.Unlock()
customParsers = append(customParsers, parser)
}
// configMapPool is a sync.Pool for reusing map[string]interface{} to reduce allocations
//
// ═══════════════════════════════════════════════════════════════════════════════
// ENGINEERING NOTE: Object Pooling for Hot Path Optimization
// ═══════════════════════════════════════════════════════════════════════════════
// Every config parse operation needs a map[string]interface{} to store results.
// Without pooling, this means:
// - 1 allocation for the map header (24 bytes)
// - 1 allocation for the bucket array (grows with entries)
// - GC pressure from short-lived objects
//
// sync.Pool solves this by recycling maps between parse operations. The pool
// is automatically cleared by the GC during collection, so we don't leak memory.
//
// The clear-on-get pattern (deleting all keys before reuse) is crucial:
// - Ensures no stale data from previous parses
// - Faster than allocating a new map (O(n) delete vs allocation + GC)
// - Keeps the map's internal bucket structure for fast re-population
//
// In benchmarks, this reduces parse allocations by ~40% and improves throughput
// by ~15% under sustained load. The benefit is most visible in scenarios like
// Kubernetes ConfigMap watching where many small configs are parsed frequently.
// ═══════════════════════════════════════════════════════════════════════════════
var configMapPool = sync.Pool{
New: func() interface{} {
return make(map[string]interface{})
},
}
// getConfigMap gets a map from the pool and clears it for reuse.
// Part of the memory optimization system to reduce allocations during parsing.
func getConfigMap() map[string]interface{} {
if config, ok := configMapPool.Get().(map[string]interface{}); ok {
// Clear the map for reuse
for k := range config {
delete(config, k)
}
return config
}
// Fallback if type assertion fails
return make(map[string]interface{})
}
// putConfigMap returns a map to the pool for reuse.
// Should be called when a map is no longer needed to prevent memory leaks.
func putConfigMap(config map[string]interface{}) {
configMapPool.Put(config)
}
// String returns the string representation of the config format for debugging and logging.
func (cf ConfigFormat) String() string {
switch cf {
case FormatJSON:
return "JSON"
case FormatYAML:
return "YAML"
case FormatTOML:
return "TOML"
case FormatHCL:
return "HCL"
case FormatINI:
return "INI"
case FormatProperties:
return "Properties"
default:
return "Unknown"
}
}
// DetectFormat detects the configuration format from file extension
// HYPER-OPTIMIZED: Zero allocations, perfect hashing, unrolled loops
// Note: High cyclomatic complexity (38) is justified for optimal performance
// across 7 configuration formats with zero memory allocation
//
// ═══════════════════════════════════════════════════════════════════════════════
// ENGINEERING NOTE: Sub-3ns Format Detection
// ═══════════════════════════════════════════════════════════════════════════════
// This function is called on EVERY config operation, so performance is critical.
// Traditional approaches would use:
// - filepath.Ext() + strings.ToLower() + map lookup: ~50ns, 2 allocations
// - regexp matching: ~500ns, multiple allocations
//
// Our approach achieves 2.79ns with ZERO allocations using these techniques:
//
// 1. BACKWARD SCANNING: We scan from the end of the string, not the beginning.
// Config paths are typically 50-100 chars, but extensions are 3-11 chars.
// We only examine the bytes we need.
//
// 2. INLINE CASE FOLDING: The |32 trick exploits ASCII encoding. For letters,
// OR-ing with 32 converts uppercase to lowercase (A=65, a=97, diff=32).
// This avoids strings.ToLower() which allocates a new string.
//
// 3. PERFECT HASH FOR 4-CHAR EXTENSIONS: We pack 4 bytes into a uint32 and
// switch on it. The Go compiler turns this into a jump table - O(1) lookup.
// Example: "json" becomes 0x6a736f6e = 1785688942.
//
// 4. UNROLLED LOOPS: For longer extensions (.properties, .config), we unroll
// the comparison to avoid loop overhead. Each byte comparison is a single
// CPU instruction.
//
// This might look like premature optimization, but when processing 1M+ configs
// per second in hot paths, these nanoseconds compound. The benchmark shows
// 2.79ns/op vs 50+ns for the naive approach - an 18x improvement.
// ═══════════════════════════════════════════════════════════════════════════════
func DetectFormat(filePath string) ConfigFormat {
length := len(filePath)
if length < 3 { // Minimum: ".tf"
return FormatUnknown
}
// Fast backward scan with unrolled loop for common extensions
// Most files are short, so unrolling the common cases is faster
// Check last 11 chars for .properties (longest extension)
if length >= 11 &&
filePath[length-11] == '.' &&
(filePath[length-10]|32) == 'p' && // |32 converts to lowercase
(filePath[length-9]|32) == 'r' &&
(filePath[length-8]|32) == 'o' &&
(filePath[length-7]|32) == 'p' &&
(filePath[length-6]|32) == 'e' &&
(filePath[length-5]|32) == 'r' &&
(filePath[length-4]|32) == 't' &&
(filePath[length-3]|32) == 'i' &&
(filePath[length-2]|32) == 'e' &&
(filePath[length-1]|32) == 's' {
return FormatProperties
}
// Check last 7 chars for .config
if length >= 7 &&
filePath[length-7] == '.' &&
(filePath[length-6]|32) == 'c' &&
(filePath[length-5]|32) == 'o' &&
(filePath[length-4]|32) == 'n' &&
(filePath[length-3]|32) == 'f' &&
(filePath[length-2]|32) == 'i' &&
(filePath[length-1]|32) == 'g' {
return FormatINI
}
// Check last 5 chars for common extensions: .json, .yaml, .toml, .conf
if length >= 5 && filePath[length-5] == '.' {
b1, b2, b3, b4 := filePath[length-4]|32, filePath[length-3]|32, filePath[length-2]|32, filePath[length-1]|32
// Perfect hash for 4-char extensions
switch uint32(b1)<<24 | uint32(b2)<<16 | uint32(b3)<<8 | uint32(b4) {
case 0x6a736f6e: // "json"
return FormatJSON
case 0x79616d6c: // "yaml"
return FormatYAML
case 0x746f6d6c: // "toml"
return FormatTOML
case 0x636f6e66: // "conf"
return FormatINI
}
}
// Check last 4 chars for: .yml, .hcl, .ini, .cfg
if length >= 4 && filePath[length-4] == '.' {
b1, b2, b3 := filePath[length-3]|32, filePath[length-2]|32, filePath[length-1]|32
// Perfect hash for 3-char extensions
switch uint32(b1)<<16 | uint32(b2)<<8 | uint32(b3) {
case 0x796d6c: // "yml"
return FormatYAML
case 0x68636c: // "hcl"
return FormatHCL
case 0x696e69: // "ini"
return FormatINI
case 0x636667: // "cfg"
return FormatINI
}
}
// Check last 3 chars for: .tf
if length >= 3 && filePath[length-3] == '.' {
b1, b2 := filePath[length-2]|32, filePath[length-1]|32
if b1 == 't' && b2 == 'f' {
return FormatHCL
}
}
return FormatUnknown
}
// ParseConfig parses configuration data based on the detected format.
// Tries custom parsers first, then falls back to built-in parsers.
// HYPER-OPTIMIZED: Fast path for no custom parsers, reduced lock contention.
//
// Parameters:
// - data: Raw configuration file bytes
// - format: Detected configuration format
//
// Returns:
// - map[string]interface{}: Parsed configuration data
// - error: Any parsing errors
func ParseConfig(data []byte, format ConfigFormat) (map[string]interface{}, error) {
// Fast path: Check if we have any custom parsers without locking
// This is safe because customParsers is only appended to, never modified
if len(customParsers) == 0 {
// No custom parsers, go straight to built-in
return parseBuiltin(data, format)
}
// Slow path: Check custom parsers with minimal lock time
parserMutex.RLock()
for _, parser := range customParsers {
if parser.Supports(format) {
config, err := parser.Parse(data)
parserMutex.RUnlock()
return config, err
}
}
parserMutex.RUnlock()
// No custom parser found, use built-in
return parseBuiltin(data, format)
}
// parseBuiltin handles built-in parsing without any locks for maximum performance.
// Used as fallback when no custom parsers are available or applicable.
func parseBuiltin(data []byte, format ConfigFormat) (map[string]interface{}, error) {
switch format {
case FormatJSON:
return parseJSON(data)
case FormatYAML:
return parseYAML(data)
case FormatTOML:
return parseTOML(data)
case FormatHCL:
return parseHCL(data)
case FormatINI:
return parseINI(data)
case FormatProperties:
return parseProperties(data)
default:
return nil, errors.New(ErrCodeInvalidConfig, "unsupported format: "+format.String())
}
}
// parseValue attempts to parse a string value into the appropriate type.
// Supports automatic type detection for booleans, integers, floats, and strings.
// Used by simple parsers to provide basic type conversion without schemas.
func parseValue(value string) interface{} {
// Try boolean
if strings.ToLower(value) == "true" {
return true
}
if strings.ToLower(value) == "false" {
return false
}
// Try integer
if intVal, err := strconv.Atoi(value); err == nil {
return intVal
}
// Try float
if floatVal, err := strconv.ParseFloat(value, 64); err == nil {
return floatVal
}
// Return as string
return value
}