init
This commit is contained in:
commit
91a950e72f
17 changed files with 2724 additions and 0 deletions
497
transform/go.go
Normal file
497
transform/go.go
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
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
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue