A go template renderer based on Perl's Template Toolkit

test: add virtual methods test file

- Add TestVirtualMethodGet for .get(key) functionality
- Add TestVirtualMethodGetErrors for error cases
- Add TestCustomVirtualMethodsWithArgs for custom vmethods
- Add TestExistingVirtualMethods for built-in vmethods (.exists, .defined, .length, .size, .first, .last)
- Add TestVirtualMethodPrecedence to verify vmethods take precedence over functions
- All 88 tests pass

+319
+319
vmethod_test.go
··· 1 + package gott 2 + 3 + import ( 4 + "fmt" 5 + "testing" 6 + ) 7 + 8 + func TestVirtualMethodGet(t *testing.T) { 9 + renderer, err := New(nil) 10 + if err != nil { 11 + t.Fatalf("Failed to create renderer: %v", err) 12 + } 13 + 14 + tests := []struct { 15 + name string 16 + template string 17 + vars map[string]any 18 + want string 19 + }{ 20 + { 21 + name: "get method - simple string key", 22 + template: "[% map.get('key1') %]", 23 + vars: map[string]any{ 24 + "map": map[string]any{ 25 + "key1": "value1", 26 + "key2": "value2", 27 + }, 28 + }, 29 + want: "value1", 30 + }, 31 + { 32 + name: "get method - dynamic key from variable", 33 + template: "[% map.get(key) %]", 34 + vars: map[string]any{ 35 + "map": map[string]any{ 36 + "key1": "value1", 37 + "key2": "value2", 38 + }, 39 + "key": "key2", 40 + }, 41 + want: "value2", 42 + }, 43 + { 44 + name: "get method - key not found returns nil (not false)", 45 + template: "[% IF map.get('missing') %]yes[% ELSE %]no[% END %]", 46 + vars: map[string]any{ 47 + "map": map[string]any{ 48 + "key1": "value1", 49 + }, 50 + }, 51 + want: "no", 52 + }, 53 + { 54 + name: "get method - in FOREACH loop", 55 + template: "[% FOREACH item IN items %][% server_types.get(item.type) %]-[% END %]", 56 + vars: map[string]any{ 57 + "items": []map[string]any{ 58 + {"type": "web", "name": "server1"}, 59 + {"type": "db", "name": "db1"}, 60 + }, 61 + "server_types": map[string]any{ 62 + "web": []string{"web1", "web2"}, 63 + "db": []string{"db1", "db2"}, 64 + }, 65 + }, 66 + want: "[web1 web2]-[db1 db2]-", 67 + }, 68 + } 69 + 70 + for _, tt := range tests { 71 + t.Run(tt.name, func(t *testing.T) { 72 + got, err := renderer.Process(tt.template, tt.vars) 73 + if err != nil { 74 + t.Fatalf("Process error: %v", err) 75 + } 76 + if got != tt.want { 77 + t.Errorf("got %q, want %q", got, tt.want) 78 + } 79 + }) 80 + } 81 + } 82 + 83 + func TestVirtualMethodGetErrors(t *testing.T) { 84 + renderer, err := New(nil) 85 + if err != nil { 86 + t.Fatalf("Failed to create renderer: %v", err) 87 + } 88 + 89 + tests := []struct { 90 + name string 91 + template string 92 + vars map[string]any 93 + wantErr string 94 + }{ 95 + { 96 + name: "get method on non-map type", 97 + template: "[% number.get('key') %]", 98 + vars: map[string]any{ 99 + "number": 42, 100 + }, 101 + wantErr: "line 0, column 0: get() not supported for int", 102 + }, 103 + { 104 + name: "get method on nil value", 105 + template: "[% nil_map.get('key') %]", 106 + vars: map[string]any{ 107 + "nil_map": nil, 108 + }, 109 + wantErr: "line 0, column 0: get() not supported for <nil>", 110 + }, 111 + { 112 + name: "get method on slice", 113 + template: "[% items.get('key') %]", 114 + vars: map[string]any{ 115 + "items": []string{"a", "b", "c"}, 116 + }, 117 + wantErr: "line 0, column 0: get() not supported for []string", 118 + }, 119 + { 120 + name: "get method with too many args", 121 + template: "[% map.get('key', 'extra') %]", 122 + vars: map[string]any{ 123 + "map": map[string]any{"key": "value"}, 124 + }, 125 + wantErr: "line 1, column 13: get() expects 1 argument, got 2", 126 + }, 127 + } 128 + 129 + for _, tt := range tests { 130 + t.Run(tt.name, func(t *testing.T) { 131 + _, err := renderer.Process(tt.template, tt.vars) 132 + if err == nil { 133 + t.Fatalf("expected error, got nil") 134 + } 135 + if err.Error() != tt.wantErr { 136 + t.Errorf("got error %q, want error %q", err.Error(), tt.wantErr) 137 + } 138 + }) 139 + } 140 + } 141 + 142 + func TestCustomVirtualMethodsWithArgs(t *testing.T) { 143 + renderer, err := New(nil) 144 + if err != nil { 145 + t.Fatalf("Failed to create renderer: %v", err) 146 + } 147 + 148 + vars := map[string]any{ 149 + "items": []int{1, 2, 3}, 150 + "text": "hello", 151 + } 152 + 153 + renderer.AddVirtualMethod("double", func(obj any, args ...any) (any, bool) { 154 + if slice, ok := obj.([]int); ok { 155 + if len(args) < 1 { 156 + return nil, false 157 + } 158 + multiplier := int(toFloat(args[0])) 159 + result := make([]int, len(slice)) 160 + for i, v := range slice { 161 + result[i] = v * multiplier 162 + } 163 + return result, true 164 + } 165 + return nil, false 166 + }) 167 + 168 + renderer.AddVirtualMethod("repeat", func(obj any, args ...any) (any, bool) { 169 + if str, ok := obj.(string); ok && len(args) > 0 { 170 + count := int(toFloat(args[0])) 171 + result := "" 172 + for i := 0; i < count; i++ { 173 + result += str 174 + } 175 + return result, true 176 + } 177 + return nil, false 178 + }) 179 + 180 + got1, err := renderer.Process("[% items.double(5) %]", vars) 181 + if err != nil { 182 + t.Fatalf("Process error: %v", err) 183 + } 184 + if got1 != "[5 10 15]" { 185 + t.Errorf("got %q, want %q", got1, "[5 10 15]") 186 + } 187 + 188 + got2, err := renderer.Process("[% text.repeat(3) %]", vars) 189 + if err != nil { 190 + t.Fatalf("Process error: %v", err) 191 + } 192 + if got2 != "hellohellohello" { 193 + t.Errorf("got %q, want %q", got2, "hellohellohello") 194 + } 195 + } 196 + 197 + func TestExistingVirtualMethods(t *testing.T) { 198 + renderer, err := New(nil) 199 + if err != nil { 200 + t.Fatalf("Failed to create renderer: %v", err) 201 + } 202 + 203 + vars := map[string]any{ 204 + "items": []string{"a", "b", "c"}, 205 + "text": "hello", 206 + "map": map[string]any{"key": "value"}, 207 + } 208 + 209 + tests := []struct { 210 + name string 211 + template string 212 + want string 213 + }{ 214 + { 215 + name: "length method without args", 216 + template: "[% items.length %]", 217 + want: "3", 218 + }, 219 + { 220 + name: "size method without args", 221 + template: "[% text.size %]", 222 + want: "5", 223 + }, 224 + { 225 + name: "first method without args", 226 + template: "[% items.first %]", 227 + want: "a", 228 + }, 229 + { 230 + name: "last method without args", 231 + template: "[% items.last %]", 232 + want: "c", 233 + }, 234 + { 235 + name: "exists method without args - key exists", 236 + template: "[% IF map.key.exists %]yes[% END %]", 237 + want: "yes", 238 + }, 239 + { 240 + name: "exists method without args - key missing", 241 + template: "[% IF map.missing.exists %]yes[% END %]", 242 + want: "", 243 + }, 244 + { 245 + name: "defined method without args - key defined", 246 + template: "[% IF map.key.defined %]yes[% END %]", 247 + want: "yes", 248 + }, 249 + { 250 + name: "defined method without args - key undefined", 251 + template: "[% IF map.missing.defined %]yes[% END %]", 252 + want: "", 253 + }, 254 + } 255 + 256 + for _, tt := range tests { 257 + t.Run(tt.name, func(t *testing.T) { 258 + got, err := renderer.Process(tt.template, vars) 259 + if err != nil { 260 + t.Fatalf("Process error: %v", err) 261 + } 262 + if got != tt.want { 263 + t.Errorf("got %q, want %q", got, tt.want) 264 + } 265 + }) 266 + } 267 + } 268 + 269 + func TestVirtualMethodPrecedence(t *testing.T) { 270 + renderer, err := New(nil) 271 + if err != nil { 272 + t.Fatalf("Failed to create renderer: %v", err) 273 + } 274 + 275 + renderer.AddVirtualMethod("customMethod", func(obj any, args ...any) (any, bool) { 276 + if str, ok := obj.(string); ok { 277 + return "virtual:" + str, true 278 + } 279 + return nil, false 280 + }) 281 + 282 + vars := map[string]any{ 283 + "text": "hello", 284 + "customMethod": func() string { 285 + return "functionResult" 286 + }, 287 + } 288 + 289 + got, err := renderer.Process("[% text.customMethod() %]", vars) 290 + if err != nil { 291 + t.Fatalf("Process error: %v", err) 292 + } 293 + if got != "virtual:hello" { 294 + t.Errorf("got %q, want %q (virtual method should take precedence)", got, "virtual:hello") 295 + } 296 + } 297 + 298 + func toFloat(v any) float64 { 299 + switch val := v.(type) { 300 + case float64: 301 + return val 302 + case int: 303 + return float64(val) 304 + case int64: 305 + return float64(val) 306 + case string: 307 + if val == "" { 308 + return 0 309 + } 310 + var result float64 311 + _, err := fmt.Sscanf(val, "%f", &result) 312 + if err == nil { 313 + return result 314 + } 315 + return 0 316 + default: 317 + return 0 318 + } 319 + }