package transform import ( "fmt" "regexp" "strings" ) // Go type definitions to inject const goTypeDefinitions = ` // WriteKit SDK Types type Post struct { Slug string ` + "`json:\"slug\"`" + ` Title string ` + "`json:\"title\"`" + ` Url string ` + "`json:\"url\"`" + ` Excerpt string ` + "`json:\"excerpt\"`" + ` PublishedAt string ` + "`json:\"publishedAt\"`" + ` UpdatedAt string ` + "`json:\"updatedAt\"`" + ` Tags []string ` + "`json:\"tags\"`" + ` ReadingTime int ` + "`json:\"readingTime\"`" + ` } type Author struct { Name string ` + "`json:\"name\"`" + ` Email string ` + "`json:\"email\"`" + ` Avatar string ` + "`json:\"avatar\"`" + ` } type Blog struct { Name string ` + "`json:\"name\"`" + ` Url string ` + "`json:\"url\"`" + ` } type Comment struct { Id string ` + "`json:\"id\"`" + ` Content string ` + "`json:\"content\"`" + ` AuthorName string ` + "`json:\"authorName\"`" + ` AuthorEmail string ` + "`json:\"authorEmail\"`" + ` PostSlug string ` + "`json:\"postSlug\"`" + ` ParentId string ` + "`json:\"parentId\"`" + ` CreatedAt string ` + "`json:\"createdAt\"`" + ` } type Member struct { Email string ` + "`json:\"email\"`" + ` Name string ` + "`json:\"name\"`" + ` SubscribedAt string ` + "`json:\"subscribedAt\"`" + ` } type Tier struct { Name string ` + "`json:\"name\"`" + ` Price int ` + "`json:\"price\"`" + ` } type TitleChange struct { Old string ` + "`json:\"old\"`" + ` New string ` + "`json:\"new\"`" + ` } type TagChanges struct { Added []string ` + "`json:\"added\"`" + ` Removed []string ` + "`json:\"removed\"`" + ` } type Changes struct { Title TitleChange ` + "`json:\"title\"`" + ` Content bool ` + "`json:\"content\"`" + ` Tags TagChanges ` + "`json:\"tags\"`" + ` } type Period struct { Start string ` + "`json:\"start\"`" + ` End string ` + "`json:\"end\"`" + ` } type PageView struct { Path string ` + "`json:\"path\"`" + ` Views int ` + "`json:\"views\"`" + ` } // Event types type PostPublishedEvent struct { Post Post ` + "`json:\"post\"`" + ` Author Author ` + "`json:\"author\"`" + ` Blog Blog ` + "`json:\"blog\"`" + ` } type PostUpdatedEvent struct { Post Post ` + "`json:\"post\"`" + ` Author Author ` + "`json:\"author\"`" + ` Changes Changes ` + "`json:\"changes\"`" + ` } type CommentCreatedEvent struct { Comment Comment ` + "`json:\"comment\"`" + ` Post Post ` + "`json:\"post\"`" + ` } type CommentInput struct { Content string ` + "`json:\"content\"`" + ` AuthorName string ` + "`json:\"authorName\"`" + ` AuthorEmail string ` + "`json:\"authorEmail\"`" + ` PostSlug string ` + "`json:\"postSlug\"`" + ` } type ValidationResult struct { Allowed bool ` + "`json:\"allowed\"`" + ` Reason string ` + "`json:\"reason,omitempty\"`" + ` } type MemberSubscribedEvent struct { Member Member ` + "`json:\"member\"`" + ` Tier Tier ` + "`json:\"tier\"`" + ` } type ContentRenderInput struct { Html string ` + "`json:\"html\"`" + ` Post Post ` + "`json:\"post\"`" + ` } type ContentRenderOutput struct { Html string ` + "`json:\"html\"`" + ` } type AssetUploadedEvent struct { Id string ` + "`json:\"id\"`" + ` Url string ` + "`json:\"url\"`" + ` ContentType string ` + "`json:\"contentType\"`" + ` Size int ` + "`json:\"size\"`" + ` Width int ` + "`json:\"width\"`" + ` Height int ` + "`json:\"height\"`" + ` } type AnalyticsSyncEvent struct { Period Period ` + "`json:\"period\"`" + ` Pageviews int ` + "`json:\"pageviews\"`" + ` Visitors int ` + "`json:\"visitors\"`" + ` TopPages []PageView ` + "`json:\"topPages\"`" + ` } // Runner SDK wrapper type runnerSDK struct{} var Runner = runnerSDK{} func (r runnerSDK) Log(msg string) { pdk.Log(pdk.LogInfo, msg) } type httpResponse struct { Status int Headers map[string]string Body string } func (r runnerSDK) HttpRequest(url, method string, body []byte) httpResponse { req := pdk.NewHTTPRequest(pdk.MethodPost, url) if method == "GET" { req = pdk.NewHTTPRequest(pdk.MethodGet, url) } if body != nil { req.SetBody(body) } resp := req.Send() return httpResponse{ Status: int(resp.Status()), Body: string(resp.Body()), } } // Secrets accessor (populated at runtime via pdk.GetConfig) type secretsAccessor struct{} var Secrets = secretsAccessor{} ` // Go transforms plugin source code from the clean API to the Extism-compatible format. // // Input (clean API): // // func OnPostPublished(event PostPublishedEvent) error { // Runner.Log("Post published: " + event.Post.Title) // return nil // } // // Output (Extism-compatible): // // //export on_post_published // func onPostPublished() int32 { // var event PostPublishedEvent // if err := pdk.InputJSON(&event); err != nil { // pdk.Log(pdk.LogError, err.Error()) // return 1 // } // Runner.Log("Post published: " + event.Post.Title) // return 0 // } func Go(source string) (string, error) { result := source // Transform Runner.Secrets.X to pdk.GetConfig("X") calls result = transformRunnerSecrets(result) // Map of hook functions: CleanName -> export_name hookMap := map[string]string{ "OnPostPublished": "on_post_published", "OnPostUpdated": "on_post_updated", "OnCommentCreated": "on_comment_created", "OnMemberSubscribed": "on_member_subscribed", "OnAssetUploaded": "on_asset_uploaded", "OnAnalyticsSync": "on_analytics_sync", "ValidateComment": "validate_comment", "RenderContent": "render_content", } // Find all hook functions // Pattern: func FuncName(params) returnType { funcPattern := regexp.MustCompile(`(?m)^func\s+(\w+)\s*\(([^)]*)\)\s*(\([^)]*\)|[\w\*]+)?\s*\{`) matches := funcPattern.FindAllStringSubmatchIndex(result, -1) // Process matches in reverse order to preserve indices for i := len(matches) - 1; i >= 0; i-- { match := matches[i] funcName := result[match[2]:match[3]] // Check if this is a hook function exportName, isHook := hookMap[funcName] if !isHook { continue } params := result[match[4]:match[5]] // Get return type if present returnType := "" if match[6] != -1 && match[7] != -1 { returnType = strings.TrimSpace(result[match[6]:match[7]]) } // Find the closing brace braceStart := match[7] if braceStart == -1 { braceStart = match[5] + 1 } // Find the actual opening brace position for braceStart < len(result) && result[braceStart] != '{' { braceStart++ } braceEnd := findClosingBrace(result, braceStart+1) if braceEnd == -1 { continue } // Extract function body body := result[braceStart+1 : braceEnd] // Parse parameters eventParam, eventType, secretsParam, secretsType := parseGoParams(params) // Build transformed function var transformed strings.Builder // Add export directive transformed.WriteString(fmt.Sprintf("//export %s\n", exportName)) // New function signature (lowercase name, no params, returns int32) lowercaseName := strings.ToLower(funcName[:1]) + funcName[1:] transformed.WriteString(fmt.Sprintf("func %s() int32 {\n", lowercaseName)) // Inject event deserialization if we have an event parameter if eventParam != "" && eventType != "" { transformed.WriteString(fmt.Sprintf(" var %s %s\n", eventParam, eventType)) transformed.WriteString(fmt.Sprintf(" if err := pdk.InputJSON(&%s); err != nil {\n", eventParam)) transformed.WriteString(" pdk.Log(pdk.LogError, err.Error())\n") transformed.WriteString(" return 1\n") transformed.WriteString(" }\n") } // Inject secrets if present if secretsParam != "" && secretsType != "" { secretsCode := generateGoSecretsInjection(source, secretsParam, secretsType) transformed.WriteString(secretsCode) } // Transform the body transformedBody := body // Check if return type includes a result type (tuple return) hasResultReturn := strings.Contains(returnType, ",") || (returnType != "" && returnType != "error" && !strings.HasPrefix(returnType, "(")) if hasResultReturn { // Transform returns that have result values transformedBody = transformGoReturnsWithResult(body) } else { // Transform simple error returns to int32 returns transformedBody = transformGoReturns(body) } transformed.WriteString(transformedBody) // Ensure we have a return 0 at the end if !strings.HasSuffix(strings.TrimSpace(transformedBody), "return 0") { transformed.WriteString("\n return 0\n") } transformed.WriteString("}") // Replace the original function result = result[:match[0]] + transformed.String() + result[braceEnd+1:] } // Inject imports and type definitions result = injectGoImportsAndTypes(result) return result, nil } // transformRunnerSecrets transforms Runner.Secrets.X to pdk.GetConfig("X") calls func transformRunnerSecrets(source string) string { // Transform Runner.Secrets.SomeName to func() string { v, _ := pdk.GetConfig("SOME_NAME"); return v }() pattern := regexp.MustCompile(`Runner\.Secrets\.(\w+)`) return pattern.ReplaceAllStringFunc(source, func(match string) string { m := pattern.FindStringSubmatch(match) if m != nil { fieldName := m[1] configKey := toUpperSnakeCase(fieldName) return fmt.Sprintf(`func() string { v, _ := pdk.GetConfig("%s"); return v }()`, configKey) } return match }) } // injectGoImportsAndTypes injects the necessary imports and type definitions func injectGoImportsAndTypes(source string) string { // Check if source already has package main if !strings.Contains(source, "package main") { source = "package main\n\n" + source } // Replace "package main" with package + imports + types importBlock := `package main import ( "github.com/extism/go-pdk" ) ` + goTypeDefinitions // Remove the original package line and add our block result := regexp.MustCompile(`package\s+main\s*\n*`).ReplaceAllString(source, "") return importBlock + "\n" + result } // parseGoParams extracts parameters from Go function signature func parseGoParams(params string) (eventParam, eventType, secretsParam, secretsType string) { params = strings.TrimSpace(params) if params == "" { return } // Split by comma (handling potential spaces) parts := splitGoParams(params) // First parameter is the event if len(parts) >= 1 { eventPart := strings.TrimSpace(parts[0]) // Match: paramName ParamType re := regexp.MustCompile(`(\w+)\s+(\w+)`) if m := re.FindStringSubmatch(eventPart); m != nil { eventParam = m[1] eventType = m[2] } } // Second parameter (if present) is secrets if len(parts) >= 2 { secretsPart := strings.TrimSpace(parts[1]) re := regexp.MustCompile(`(\w+)\s+(\w+)`) if m := re.FindStringSubmatch(secretsPart); m != nil { secretsParam = m[1] secretsType = m[2] } } return } // splitGoParams splits Go parameters, handling edge cases func splitGoParams(params string) []string { var parts []string var current strings.Builder depth := 0 for _, ch := range params { switch ch { case '(': depth++ current.WriteRune(ch) case ')': depth-- current.WriteRune(ch) case ',': if depth == 0 { parts = append(parts, current.String()) current.Reset() } else { current.WriteRune(ch) } default: current.WriteRune(ch) } } if current.Len() > 0 { parts = append(parts, current.String()) } return parts } // generateGoSecretsInjection creates code to build the secrets struct from pdk.GetConfig() calls func generateGoSecretsInjection(source, secretsParam, secretsType string) string { // Find the struct definition for the secrets type structPattern := regexp.MustCompile(fmt.Sprintf(`(?s)type\s+%s\s+struct\s*\{([^}]*)\}`, regexp.QuoteMeta(secretsType))) match := structPattern.FindStringSubmatch(source) if match == nil { return fmt.Sprintf(" var %s %s\n", secretsParam, secretsType) } // Parse struct body for field names structBody := match[1] fieldPattern := regexp.MustCompile(`(\w+)\s+string`) fields := fieldPattern.FindAllStringSubmatch(structBody, -1) if len(fields) == 0 { return fmt.Sprintf(" var %s %s\n", secretsParam, secretsType) } // Build the secrets struct initialization var builder strings.Builder builder.WriteString(fmt.Sprintf(" %s := %s{\n", secretsParam, secretsType)) for _, field := range fields { fieldName := field[1] // Convert Go field name to config key (e.g., SlackWebhook -> SLACK_WEBHOOK) configKey := toUpperSnakeCase(fieldName) builder.WriteString(fmt.Sprintf(" %s: func() string { v, _ := pdk.GetConfig(\"%s\"); return v }(),\n", fieldName, configKey)) } builder.WriteString(" }\n") return builder.String() } // toUpperSnakeCase converts CamelCase to UPPER_SNAKE_CASE func toUpperSnakeCase(s string) string { var result strings.Builder for i, ch := range s { if i > 0 && ch >= 'A' && ch <= 'Z' { result.WriteRune('_') } result.WriteRune(ch) } return strings.ToUpper(result.String()) } // transformGoReturns transforms return statements from error returns to int32 returns func transformGoReturns(body string) string { // Replace "return nil" with "return 0" result := regexp.MustCompile(`return\s+nil\b`).ReplaceAllString(body, "return 0") // Replace "return err" or "return someErr" with error handling result = regexp.MustCompile(`return\s+(\w+)\s*$`).ReplaceAllStringFunc(result, func(match string) string { re := regexp.MustCompile(`return\s+(\w+)`) m := re.FindStringSubmatch(match) if m != nil && m[1] != "nil" && m[1] != "0" && m[1] != "1" { // It's an error variable return fmt.Sprintf("if %s != nil { pdk.Log(pdk.LogError, %s.Error()); return 1 }; return 0", m[1], m[1]) } return match }) return result } // transformGoReturnsWithResult transforms returns that have a result value func transformGoReturnsWithResult(body string) string { // Match: return result, nil or return result, err returnPattern := regexp.MustCompile(`return\s+(\w+),\s*(\w+)\s*$`) return returnPattern.ReplaceAllStringFunc(body, func(match string) string { m := returnPattern.FindStringSubmatch(match) if m != nil { result := m[1] errVar := m[2] if errVar == "nil" { return fmt.Sprintf("pdk.OutputJSON(%s); return 0", result) } return fmt.Sprintf("if %s != nil { pdk.Log(pdk.LogError, %s.Error()); return 1 }; pdk.OutputJSON(%s); return 0", errVar, errVar, result) } return match }) }