497 lines
14 KiB
Go
497 lines
14 KiB
Go
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
|
|
})
|
|
}
|