diff --git a/interpreter/custom_js_functions_test.go b/interpreter/custom_js_functions_test.go new file mode 100644 index 0000000..08bf6f1 --- /dev/null +++ b/interpreter/custom_js_functions_test.go @@ -0,0 +1,455 @@ +package interpreter + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestCustomJavaScriptFunctions(t *testing.T) { + // Create a temporary directory for test files + tempDir := t.TempDir() + + // Create a test Masonry file + masonryContent := `entity User { + name: string required + email: string required unique +} + +entity Post { + title: string required + slug: string required + authorId: uuid required +}` + + masonryFile := filepath.Join(tempDir, "test.masonry") + err := os.WriteFile(masonryFile, []byte(masonryContent), 0644) + if err != nil { + t.Fatalf("Failed to write test Masonry file: %v", err) + } + + // Create templates directory + templatesDir := filepath.Join(tempDir, "templates") + err = os.MkdirAll(templatesDir, 0755) + if err != nil { + t.Fatalf("Failed to create templates directory: %v", err) + } + + // Create a test template that uses custom functions + templateContent := `package models + +// {{formatComment "Generated model"}} +type {{.Entity.Name | title}} struct { + Name string +} + +// Table name: {{pluralize (.Entity.Name | lower)}} +func ({{.Entity.Name | title}}) TableName() string { + return "{{pluralize (.Entity.Name | lower)}}" +} + +// Custom function result: {{customUpper (.Entity.Name)}}` + + templateFile := filepath.Join(templatesDir, "model.tmpl") + err = os.WriteFile(templateFile, []byte(templateContent), 0644) + if err != nil { + t.Fatalf("Failed to write test template file: %v", err) + } + + // Create manifest with custom JavaScript functions + manifestContent := `name: "Test Generator" +description: "Test custom JavaScript functions" + +functions: + # Format a comment block + formatComment: | + function main() { + var text = args[0] || ""; + return "// " + text; + } + + # Convert singular words to plural (simple rules) + pluralize: | + function main() { + var word = args[0] || ""; + if (word.slice(-1) === 'y') { + return word.slice(0, -1) + 'ies'; + } else if (word.slice(-1) === 's') { + return word + 'es'; + } else { + return word + 's'; + } + } + + # Convert to uppercase + customUpper: | + function main() { + var text = args[0] || ""; + return text.toUpperCase(); + } + +outputs: + - path: "models/{{.Entity.Name | lower}}.go" + template: "model" + iterator: "entities" + item_context: "Entity"` + + manifestFile := filepath.Join(templatesDir, "manifest.yaml") + err = os.WriteFile(manifestFile, []byte(manifestContent), 0644) + if err != nil { + t.Fatalf("Failed to write test manifest file: %v", err) + } + + // Create template interpreter and test + interpreter := NewTemplateInterpreter() + + // Test InterpretToFiles with custom functions + outputs, err := interpreter.InterpretToFiles(masonryFile, templatesDir, "manifest.yaml") + if err != nil { + t.Fatalf("InterpretToFiles failed: %v", err) + } + + // Should generate 2 files (User and Post) + expectedFiles := 2 + if len(outputs) != expectedFiles { + t.Errorf("Expected %d output files, got %d", expectedFiles, len(outputs)) + } + + // Check User model output + userFile := "models/user.go" + userContent, exists := outputs[userFile] + if !exists { + t.Errorf("Expected output file %s not found", userFile) + } else { + // Test formatComment function + if !strings.Contains(userContent, "// Generated model") { + t.Errorf("formatComment function not working: expected '// Generated model' in output") + } + + // Test pluralize function + if !strings.Contains(userContent, "Table name: users") { + t.Errorf("pluralize function not working: expected 'Table name: users' in output") + } + if !strings.Contains(userContent, `return "users"`) { + t.Errorf("pluralize function not working in template: expected 'return \"users\"' in output") + } + + // Test customUpper function + if !strings.Contains(userContent, "Custom function result: USER") { + t.Errorf("customUpper function not working: expected 'Custom function result: USER' in output") + } + } + + // Check Post model output + postFile := "models/post.go" + postContent, exists := outputs[postFile] + if !exists { + t.Errorf("Expected output file %s not found", postFile) + } else { + // Test pluralize function with 's' ending + if !strings.Contains(postContent, "Table name: posts") { + t.Errorf("pluralize function not working for 's' ending: expected 'Table name: posts' in output") + } + if !strings.Contains(postContent, `return "posts"`) { + t.Errorf("pluralize function not working in template for 's' ending: expected 'return \"posts\"' in output") + } + + // Test customUpper function + if !strings.Contains(postContent, "Custom function result: POST") { + t.Errorf("customUpper function not working: expected 'Custom function result: POST' in output") + } + } +} + +func TestCustomFunctionErrorHandling(t *testing.T) { + interpreter := NewTemplateInterpreter() + + tests := []struct { + name string + functionName string + jsCode string + expectError bool + errorContains string + }{ + { + name: "Invalid function name", + functionName: "123invalid", + jsCode: "function main() { return 'test'; }", + expectError: true, + errorContains: "invalid function name", + }, + { + name: "Reserved function name", + functionName: "and", + jsCode: "function main() { return 'test'; }", + expectError: true, + errorContains: "invalid function name", + }, + { + name: "Missing main function", + functionName: "testFunc", + jsCode: "function notMain() { return 'test'; }", + expectError: false, // Error will occur during execution, not loading + }, + { + name: "Valid function", + functionName: "validFunc", + jsCode: "function main() { return 'test'; }", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + functions := map[string]string{ + tt.functionName: tt.jsCode, + } + + err := interpreter.loadCustomFunctions(functions) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("Expected error to contain '%s', got: %v", tt.errorContains, err) + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + }) + } +} + +func TestJavaScriptFunctionExecution(t *testing.T) { + interpreter := NewTemplateInterpreter() + + // Load a test function + functions := map[string]string{ + "testConcat": ` + function main() { + var result = ""; + for (var i = 0; i < args.length; i++) { + result += args[i]; + if (i < args.length - 1) result += "-"; + } + return result; + } + `, + "testMath": ` + function main() { + var a = args[0] || 0; + var b = args[1] || 0; + return a + b; + } + `, + "testError": ` + function main() { + throw new Error("Test error"); + } + `, + "testNoMain": ` + function notMain() { + return "should not work"; + } + `, + } + + err := interpreter.loadCustomFunctions(functions) + if err != nil { + t.Fatalf("Failed to load custom functions: %v", err) + } + + // Test function execution through template + testCases := []struct { + name string + template string + data interface{} + expectedOutput string + expectError bool + errorContains string + }{ + { + name: "Concat function with multiple args", + template: `{{testConcat "hello" "world" "test"}}`, + data: struct{}{}, + expectedOutput: "hello-world-test", + expectError: false, + }, + { + name: "Math function", + template: `{{testMath 5 3}}`, + data: struct{}{}, + expectedOutput: "8", + expectError: false, + }, + { + name: "Function with error", + template: `{{testError}}`, + data: struct{}{}, + expectError: true, + errorContains: "Test error", + }, + { + name: "Function without main", + template: `{{testNoMain}}`, + data: struct{}{}, + expectError: true, + errorContains: "must define a main() function", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := interpreter.Interpret("", tc.template) + + if tc.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if tc.errorContains != "" && !strings.Contains(err.Error(), tc.errorContains) { + t.Errorf("Expected error to contain '%s', got: %v", tc.errorContains, err) + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } else if result != tc.expectedOutput { + t.Errorf("Expected output '%s', got '%s'", tc.expectedOutput, result) + } + } + }) + } +} + +func TestCustomFunctionArgumentHandling(t *testing.T) { + interpreter := NewTemplateInterpreter() + + // Load test functions that handle different argument types + functions := map[string]string{ + "argCount": ` + function main() { + return args.length; + } + `, + "argTypes": ` + function main() { + var types = []; + for (var i = 0; i < args.length; i++) { + types.push(typeof args[i]); + } + return types.join(","); + } + `, + "argAccess": ` + function main() { + // Test both args array and individual arg variables + var fromArray = args[0] || "empty"; + var fromVar = arg0 || "empty"; + return fromArray + ":" + fromVar; + } + `, + } + + err := interpreter.loadCustomFunctions(functions) + if err != nil { + t.Fatalf("Failed to load custom functions: %v", err) + } + + testCases := []struct { + name string + template string + expectedOutput string + }{ + { + name: "No arguments", + template: `{{argCount}}`, + expectedOutput: "0", + }, + { + name: "Multiple arguments", + template: `{{argCount "a" "b" "c"}}`, + expectedOutput: "3", + }, + { + name: "Argument types", + template: `{{argTypes "string" 42}}`, + expectedOutput: "string,number", + }, + { + name: "Argument access methods", + template: `{{argAccess "test"}}`, + expectedOutput: "test:test", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := interpreter.Interpret("", tc.template) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } else if result != tc.expectedOutput { + t.Errorf("Expected output '%s', got '%s'", tc.expectedOutput, result) + } + }) + } +} + +func TestFunctionLoadingOrder(t *testing.T) { + // This test ensures that custom functions are loaded before templates + // to prevent the "function not defined" error that was fixed + + tempDir := t.TempDir() + templatesDir := filepath.Join(tempDir, "templates") + err := os.MkdirAll(templatesDir, 0755) + if err != nil { + t.Fatalf("Failed to create templates directory: %v", err) + } + + // Create a template that uses a custom function + templateContent := `{{customFunc "test"}}` + templateFile := filepath.Join(templatesDir, "test.tmpl") + err = os.WriteFile(templateFile, []byte(templateContent), 0644) + if err != nil { + t.Fatalf("Failed to write template file: %v", err) + } + + // Create manifest with the custom function + manifestContent := `name: "Order Test" +functions: + customFunc: | + function main() { + return "custom:" + args[0]; + } +outputs: + - path: "test.txt" + template: "test"` + + manifestFile := filepath.Join(templatesDir, "manifest.yaml") + err = os.WriteFile(manifestFile, []byte(manifestContent), 0644) + if err != nil { + t.Fatalf("Failed to write manifest file: %v", err) + } + + // Create empty Masonry file + masonryFile := filepath.Join(tempDir, "empty.masonry") + err = os.WriteFile(masonryFile, []byte(""), 0644) + if err != nil { + t.Fatalf("Failed to write Masonry file: %v", err) + } + + // Test that InterpretToFiles works (this would fail if functions aren't loaded first) + interpreter := NewTemplateInterpreter() + outputs, err := interpreter.InterpretToFiles(masonryFile, templatesDir, "manifest.yaml") + if err != nil { + t.Fatalf("InterpretToFiles failed: %v", err) + } + + // Check that the custom function was executed + testContent, exists := outputs["test.txt"] + if !exists { + t.Errorf("Expected output file test.txt not found") + } else if testContent != "custom:test" { + t.Errorf("Expected 'custom:test', got '%s'", testContent) + } +} diff --git a/interpreter/template_iterator_test.go b/interpreter/template_iterator_test.go index 40203f1..ec158b5 100644 --- a/interpreter/template_iterator_test.go +++ b/interpreter/template_iterator_test.go @@ -185,45 +185,55 @@ func createTestAST() lang.AST { Name: "HomePage", Path: "/", Layout: "public", - Components: []lang.Component{ - {Type: "header"}, - {Type: "footer"}, - }, - Sections: []lang.Section{ + Elements: []lang.PageElement{ { - Name: "hero", - Type: strPtr("container"), - Elements: []lang.SectionElement{ - { - Component: &lang.Component{ - Type: "carousel", + Component: &lang.Component{ + Type: "header", + }, + }, + { + Component: &lang.Component{ + Type: "footer", + }, + }, + { + Section: &lang.Section{ + Name: "hero", + Type: strPtr("container"), + Elements: []lang.SectionElement{ + { + Component: &lang.Component{ + Type: "carousel", + }, }, - }, - { - Section: &lang.Section{ - Name: "banner", - Type: strPtr("panel"), + { + Section: &lang.Section{ + Name: "banner", + Type: strPtr("panel"), + }, }, }, }, }, { - Name: "content", - Type: strPtr("container"), - Elements: []lang.SectionElement{ - { - Component: &lang.Component{ - Type: "article", + Section: &lang.Section{ + Name: "content", + Type: strPtr("container"), + Elements: []lang.SectionElement{ + { + Component: &lang.Component{ + Type: "article", + }, }, - }, - { - Section: &lang.Section{ - Name: "sidebar", - Type: strPtr("panel"), - Elements: []lang.SectionElement{ - { - Component: &lang.Component{ - Type: "widget", + { + Section: &lang.Section{ + Name: "sidebar", + Type: strPtr("panel"), + Elements: []lang.SectionElement{ + { + Component: &lang.Component{ + Type: "widget", + }, }, }, }, @@ -240,10 +250,12 @@ func createTestAST() lang.AST { Name: "AdminPage", Path: "/admin", Layout: "admin", - Sections: []lang.Section{ + Elements: []lang.PageElement{ { - Name: "dashboard", - Type: strPtr("container"), + Section: &lang.Section{ + Name: "dashboard", + Type: strPtr("container"), + }, }, }, },