This is an automated email from the git hooks/post-receive script.

git pushed a commit to branch main
in repository ego.

View the commit online.

commit 71d20c218f536ca6fb5b552f50b6ad7eb27aa754
Author: [email protected] <[email protected]>
AuthorDate: Mon Mar 9 09:27:16 2026 -0600

    feat: add template-based code generator for EFL bindings
    
    ego-gen now uses templates to generate Go wrapper code for EFL classes
    and enums, eliminating manual boilerplate and ensuring consistent binding
    patterns across the codebase. The Generator supports class constructors,
    property getters/setters, method wrappers, and event registration
    stubs, with automatic go/format output validation.
    
    Includes:
      - Generator struct with template loading and file writing
      - class.go.tmpl: generates structs, constructors, properties, methods
      - enum.go.tmpl: generates typed constants from C enum values
      - Comprehensive test suite covering class/enum generation and edge cases
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 cmd/ego-gen/generator.go            | 182 ++++++++++++++++++++++++++++++
 cmd/ego-gen/generator_test.go       | 219 ++++++++++++++++++++++++++++++++++++
 cmd/ego-gen/templates/class.go.tmpl | 119 ++++++++++++++++++++
 cmd/ego-gen/templates/enum.go.tmpl  |  19 ++++
 4 files changed, 539 insertions(+)

diff --git a/cmd/ego-gen/generator.go b/cmd/ego-gen/generator.go
new file mode 100644
index 0000000..d29522f
--- /dev/null
+++ b/cmd/ego-gen/generator.go
@@ -0,0 +1,182 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"go/format"
+	"os"
+	"path/filepath"
+	"strings"
+	"text/template"
+	"unicode"
+)
+
+// ClassData holds all the information needed to render a Go file for one EFL class.
+type ClassData struct {
+	PackageName  string
+	GoName       string
+	EolianName   string
+	CClassName   string // e.g. "efl_ui_button_class_get()"
+	CPrefix      string // e.g. "efl_ui_button"
+	ParentGoType string
+	ParentPkg    string
+	IsAbstract   bool
+	IsMixin      bool
+	IsInterface  bool
+	Methods      []MethodData
+	Properties   []PropertyData
+	Events       []EventData
+	Interfaces   []string
+}
+
+// MethodData describes a single method on an EFL class.
+type MethodData struct {
+	GoName      string
+	CName       string
+	Params      []ParamData
+	ReturnType  string // Go type, empty if void
+	ReturnCType string
+}
+
+// ParamData describes a single parameter of a method.
+type ParamData struct {
+	GoName    string
+	GoType    string
+	CType     string
+	Direction string // "in", "out", "inout"
+}
+
+// PropertyData describes an EFL property with optional getter and setter.
+type PropertyData struct {
+	GoGetter string
+	GoSetter string
+	CGetName string
+	CSetName string
+	GoType   string
+	CType    string
+	HasGet   bool
+	HasSet   bool
+}
+
+// EventData describes a single event on an EFL class.
+type EventData struct {
+	GoName        string
+	CEventName    string
+	PayloadGoType string
+	PayloadCType  string
+}
+
+// EnumData holds the information needed to render a Go file for one EFL enum.
+type EnumData struct {
+	PackageName string
+	GoTypeName  string
+	Values      []EnumValue
+}
+
+// EnumValue is a single constant in an EFL enum.
+type EnumValue struct {
+	GoName string
+	CName  string
+}
+
+// Generator loads templates and writes generated Go files to an output directory.
+type Generator struct {
+	tmpl      *template.Template
+	outputDir string
+}
+
+// NewGenerator creates a Generator that reads templates from templateDir and
+// writes output under outputDir. All templates matching "*.tmpl" in templateDir
+// are loaded. Template functions cTypeToGo, needsStringConversion, and
+// isPointerType are registered before parsing.
+func NewGenerator(templateDir, outputDir string) (*Generator, error) {
+	funcs := template.FuncMap{
+		"cTypeToGo":            CTypeToGo,
+		"needsStringConversion": NeedsStringConversion,
+		"isPointerType":        IsPointerType,
+	}
+
+	pattern := filepath.Join(templateDir, "*.tmpl")
+	tmpl, err := template.New("ego-gen").Funcs(funcs).ParseGlob(pattern)
+	if err != nil {
+		return nil, fmt.Errorf("generator: parse templates from %s: %w", templateDir, err)
+	}
+
+	return &Generator{tmpl: tmpl, outputDir: outputDir}, nil
+}
+
+// GenerateClass renders the class template for data and writes the result to
+// outputDir/<packagename>/<snake_case_name>.go.
+func (g *Generator) GenerateClass(data ClassData) error {
+	return g.generate("class.go.tmpl", data.PackageName, toSnake(data.GoName), data)
+}
+
+// GenerateEnum renders the enum template for data and writes the result to
+// outputDir/<packagename>/<snake_case_name>.go.
+func (g *Generator) GenerateEnum(data EnumData) error {
+	return g.generate("enum.go.tmpl", data.PackageName, toSnake(data.GoTypeName), data)
+}
+
+// generate executes the named template with the given data, formats the
+// output with go/format, and writes it to outputDir/pkg/filename.go.
+// If go/format fails, the raw output is written so the file can be inspected.
+func (g *Generator) generate(tmplName, pkg, baseName string, data any) error {
+	pkgDir := filepath.Join(g.outputDir, pkg)
+	if err := os.MkdirAll(pkgDir, 0o755); err != nil {
+		return fmt.Errorf("generator: mkdir %s: %w", pkgDir, err)
+	}
+
+	var buf bytes.Buffer
+	if err := g.tmpl.ExecuteTemplate(&buf, tmplName, data); err != nil {
+		return fmt.Errorf("generator: execute template %s: %w", tmplName, err)
+	}
+
+	src := buf.Bytes()
+	formatted, err := format.Source(src)
+	if err != nil {
+		// Write unformatted output for debugging; do not treat as fatal.
+		formatted = src
+	}
+
+	outPath := filepath.Join(pkgDir, baseName+".go")
+	if err := os.WriteFile(outPath, formatted, 0o644); err != nil {
+		return fmt.Errorf("generator: write %s: %w", outPath, err)
+	}
+	return nil
+}
+
+// toSnake converts a CamelCase identifier to snake_case for use as a filename.
+// Consecutive uppercase letters (acronyms) are kept together so that "UIButton"
+// becomes "ui_button" rather than "u_i_button".
+//
+// Examples:
+//
+//	"Button"    → "button"
+//	"SpinButton" → "spin_button"
+//	"UIButton"  → "ui_button"
+func toSnake(s string) string {
+	if s == "" {
+		return ""
+	}
+	runes := []rune(s)
+	var b strings.Builder
+	for i, r := range runes {
+		if unicode.IsUpper(r) {
+			// Insert underscore before an uppercase letter when:
+			//   - it is not the first character, AND
+			//   - the previous character is lowercase, OR
+			//   - the next character is lowercase (start of a new word after an acronym)
+			if i > 0 {
+				prev := runes[i-1]
+				nextIsLower := i+1 < len(runes) && unicode.IsLower(runes[i+1])
+				if unicode.IsLower(prev) || (unicode.IsUpper(prev) && nextIsLower) {
+					b.WriteRune('_')
+				}
+			}
+			b.WriteRune(unicode.ToLower(r))
+		} else {
+			b.WriteRune(r)
+		}
+	}
+	return b.String()
+}
diff --git a/cmd/ego-gen/generator_test.go b/cmd/ego-gen/generator_test.go
new file mode 100644
index 0000000..d3c9969
--- /dev/null
+++ b/cmd/ego-gen/generator_test.go
@@ -0,0 +1,219 @@
+package main
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+// templateDir returns the absolute path to the templates embedded in the
+// source tree. Tests must find templates relative to the source file because
+// the working directory of `go test` is the package directory.
+func templateDir(t *testing.T) string {
+	t.Helper()
+	// The test binary runs with cwd set to the package directory
+	// (cmd/ego-gen), so "templates" is a sibling directory.
+	dir, err := filepath.Abs("templates")
+	if err != nil {
+		t.Fatalf("templateDir: %v", err)
+	}
+	return dir
+}
+
+func newTestGenerator(t *testing.T) (*Generator, string) {
+	t.Helper()
+	outDir := t.TempDir()
+	g, err := NewGenerator(templateDir(t), outDir)
+	if err != nil {
+		t.Fatalf("NewGenerator: %v", err)
+	}
+	return g, outDir
+}
+
+func TestGenerateClass(t *testing.T) {
+	g, outDir := newTestGenerator(t)
+
+	data := ClassData{
+		PackageName:  "ui",
+		GoName:       "Button",
+		EolianName:   "Efl.Ui.Button",
+		CClassName:   "efl_ui_button_class_get()",
+		CPrefix:      "efl_ui_button",
+		ParentGoType: "Widget",
+		ParentPkg:    "ui",
+		IsAbstract:   false,
+		IsMixin:      false,
+		IsInterface:  false,
+		Properties: []PropertyData{
+			{
+				GoGetter: "Text",
+				GoSetter: "SetText",
+				CGetName: "efl_ui_button_text_get",
+				CSetName: "efl_ui_button_text_set",
+				GoType:   "string",
+				CType:    "const char *",
+				HasGet:   true,
+				HasSet:   true,
+			},
+		},
+		Methods: []MethodData{
+			{
+				GoName: "Click",
+				CName:  "efl_ui_button_click",
+				Params: []ParamData{},
+			},
+		},
+		Events: []EventData{
+			{
+				GoName:     "OnClicked",
+				CEventName: "clicked",
+			},
+		},
+	}
+
+	if err := g.GenerateClass(data); err != nil {
+		t.Fatalf("GenerateClass: %v", err)
+	}
+
+	outFile := filepath.Join(outDir, "ui", "button.go")
+	content, err := os.ReadFile(outFile)
+	if err != nil {
+		t.Fatalf("output file not found at %s: %v", outFile, err)
+	}
+
+	src := string(content)
+
+	mustContain := []string{
+		"DO NOT EDIT",
+		"package ui",
+		"#cgo pkg-config: elementary",
+		"#include <Elementary.h>",
+		"_ego_efl_ui_button_class_get",
+		"efl_ui_button_class_get()",
+		"type Button struct",
+		"func wrapButton(",
+		"func NewButton(",
+		"func (o *Button) Text()",
+		"func (o *Button) SetText(",
+		"func (o *Button) Click()",
+		"func (o *Button) OnClicked(",
+	}
+
+	for _, want := range mustContain {
+		if !strings.Contains(src, want) {
+			t.Errorf("output missing expected pattern %q\nfile content:\n%s", want, src)
+		}
+	}
+}
+
+func TestGenerateClass_Abstract(t *testing.T) {
+	g, outDir := newTestGenerator(t)
+
+	data := ClassData{
+		PackageName: "efl",
+		GoName:      "Loop",
+		EolianName:  "Efl.Loop",
+		CClassName:  "efl_loop_class_get()",
+		CPrefix:     "efl_loop",
+		IsAbstract:  true,
+	}
+
+	if err := g.GenerateClass(data); err != nil {
+		t.Fatalf("GenerateClass abstract: %v", err)
+	}
+
+	outFile := filepath.Join(outDir, "efl", "loop.go")
+	content, err := os.ReadFile(outFile)
+	if err != nil {
+		t.Fatalf("output file not found at %s: %v", outFile, err)
+	}
+
+	src := string(content)
+
+	// Abstract classes must not have a constructor.
+	if strings.Contains(src, "func NewLoop(") {
+		t.Error("abstract class should not have a constructor, but NewLoop was found")
+	}
+
+	if !strings.Contains(src, "type Loop struct") {
+		t.Error("expected type Loop struct in output")
+	}
+}
+
+func TestGenerateEnum(t *testing.T) {
+	g, outDir := newTestGenerator(t)
+
+	data := EnumData{
+		PackageName: "ui",
+		GoTypeName:  "Dir",
+		Values: []EnumValue{
+			{GoName: "DirDefault", CName: "EFL_UI_DIR_DEFAULT"},
+			{GoName: "DirHorizontal", CName: "EFL_UI_DIR_HORIZONTAL"},
+			{GoName: "DirVertical", CName: "EFL_UI_DIR_VERTICAL"},
+		},
+	}
+
+	if err := g.GenerateEnum(data); err != nil {
+		t.Fatalf("GenerateEnum: %v", err)
+	}
+
+	outFile := filepath.Join(outDir, "ui", "dir.go")
+	content, err := os.ReadFile(outFile)
+	if err != nil {
+		t.Fatalf("output file not found at %s: %v", outFile, err)
+	}
+
+	src := string(content)
+
+	mustContain := []string{
+		"DO NOT EDIT",
+		"package ui",
+		"#cgo pkg-config: elementary",
+		"type Dir int",
+		"DirDefault",
+		"EFL_UI_DIR_DEFAULT",
+		"DirHorizontal",
+		"EFL_UI_DIR_HORIZONTAL",
+		"DirVertical",
+		"EFL_UI_DIR_VERTICAL",
+	}
+
+	for _, want := range mustContain {
+		if !strings.Contains(src, want) {
+			t.Errorf("enum output missing expected pattern %q\nfile content:\n%s", want, src)
+		}
+	}
+}
+
+func TestToSnake(t *testing.T) {
+	tests := []struct {
+		input string
+		want  string
+	}{
+		{"Button", "button"},
+		{"SpinButton", "spin_button"},
+		{"UIButton", "ui_button"},
+		{"HTTPSHandler", "https_handler"},
+		{"", ""},
+		{"A", "a"},
+		{"AB", "ab"},
+		{"ABc", "a_bc"},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.input, func(t *testing.T) {
+			got := toSnake(tc.input)
+			if got != tc.want {
+				t.Errorf("toSnake(%q) = %q, want %q", tc.input, got, tc.want)
+			}
+		})
+	}
+}
+
+func TestNewGenerator_MissingTemplateDir(t *testing.T) {
+	_, err := NewGenerator("/nonexistent/path/to/templates", t.TempDir())
+	if err == nil {
+		t.Fatal("expected error for missing template directory, got nil")
+	}
+}
diff --git a/cmd/ego-gen/templates/class.go.tmpl b/cmd/ego-gen/templates/class.go.tmpl
new file mode 100644
index 0000000..e5b6980
--- /dev/null
+++ b/cmd/ego-gen/templates/class.go.tmpl
@@ -0,0 +1,119 @@
+// Code generated by ego-gen. DO NOT EDIT.
+
+package {{.PackageName}}
+
+/*
+#cgo pkg-config: elementary
+#include <Elementary.h>
+
+// _ego_{{.CPrefix}}_class_get wraps the {{.CClassName}} macro so it can be
+// called as a regular function from Go via cgo.
+static const Efl_Class *_ego_{{.CPrefix}}_class_get(void) {
+    return {{.CClassName}};
+}
+*/
+import "C"
+
+import (
+    "unsafe"
+
+    "git.enlightenment.org/cedric/ego/efl"
+)
+
+// {{.GoName}} wraps the EFL class {{.EolianName}}.
+type {{.GoName}} struct {
+{{- if .ParentGoType}}
+    {{.ParentPkg}}.{{.ParentGoType}}
+{{- else}}
+    efl.Object
+{{- end}}
+}
+
+// wrap{{.GoName}} wraps an existing Eo pointer as a {{.GoName}}.
+func wrap{{.GoName}}(obj *C.Eo) *{{.GoName}} {
+    o := &{{.GoName}}{}
+    o.SetEo(unsafe.Pointer(obj))
+    return o
+}
+
+{{if not .IsAbstract}}{{if not .IsMixin}}{{if not .IsInterface}}
+// New{{.GoName}} creates a new {{.GoName}} with the given parent.
+func New{{.GoName}}(parent efl.Objecter) *{{.GoName}} {
+    var parentEo *C.Eo
+    if parent != nil {
+        parentEo = (*C.Eo)(parent.Eo())
+    }
+    obj := C.efl_add(_ego_{{.CPrefix}}_class_get(), parentEo)
+    if obj == nil {
+        return nil
+    }
+    return wrap{{.GoName}}(obj)
+}
+{{end}}{{end}}{{end}}
+
+{{range .Properties}}
+{{if .HasGet}}
+// {{.GoGetter}} returns the {{.GoGetter}} property value.
+func (o *{{$.GoName}}) {{.GoGetter}}() {{.GoType}} {
+    eo := (*C.Eo)(o.Eo())
+{{- if needsStringConversion .CType}}
+    cs := C.{{.CGetName}}(eo)
+    return C.GoString(cs)
+{{- else if isPointerType .GoType}}
+    return unsafe.Pointer(C.{{.CGetName}}(eo))
+{{- else}}
+    return {{.GoType}}(C.{{.CGetName}}(eo))
+{{- end}}
+}
+{{end}}
+{{if .HasSet}}
+// {{.GoSetter}} sets the {{.GoGetter}} property value.
+func (o *{{$.GoName}}) {{.GoSetter}}(v {{.GoType}}) {
+    eo := (*C.Eo)(o.Eo())
+{{- if needsStringConversion .CType}}
+    cs := C.CString(v)
+    defer C.free(unsafe.Pointer(cs))
+    C.{{.CSetName}}(eo, cs)
+{{- else if isPointerType .GoType}}
+    C.{{.CSetName}}(eo, (*C.Eo)(v))
+{{- else}}
+    C.{{.CSetName}}(eo, C.{{.CType}}(v))
+{{- end}}
+}
+{{end}}
+{{end}}
+
+{{range .Methods}}
+// {{.GoName}} calls the {{.CName}} EFL method.
+{{- $hasReturn := .ReturnType}}
+func (o *{{$.GoName}}) {{.GoName}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.GoName}} {{$p.GoType}}{{end}}) {{if $hasReturn}}{{.ReturnType}}{{end}} {
+    eo := (*C.Eo)(o.Eo())
+    _ = eo
+{{- range .Params}}
+{{- if needsStringConversion .CType}}
+    _c{{.GoName}} := C.CString({{.GoName}})
+    defer C.free(unsafe.Pointer(_c{{.GoName}}))
+{{- end}}
+{{- end}}
+{{- if $hasReturn}}
+    ret := C.{{.CName}}(eo{{range .Params}}, {{if needsStringConversion .CType}}_c{{.GoName}}{{else if isPointerType .GoType}}(*C.Eo)({{.GoName}}){{else}}C.{{.CType}}({{.GoName}}){{end}}{{end}})
+{{- if needsStringConversion .ReturnCType}}
+    return C.GoString(ret)
+{{- else if isPointerType .ReturnType}}
+    return unsafe.Pointer(ret)
+{{- else}}
+    return {{.ReturnType}}(ret)
+{{- end}}
+{{- else}}
+    C.{{.CName}}(eo{{range .Params}}, {{if needsStringConversion .CType}}_c{{.GoName}}{{else if isPointerType .GoType}}(*C.Eo)({{.GoName}}){{else}}C.{{.CType}}({{.GoName}}){{end}}{{end}})
+{{- end}}
+}
+{{end}}
+
+{{range .Events}}
+// {{.GoName}} registers a callback for the {{.CEventName}} event.
+func (o *{{$.GoName}}) {{.GoName}}(cb func({{if .PayloadGoType}}{{.PayloadGoType}}{{end}})) {
+    _ = cb
+    // TODO: wire up Efl_Event_Cb and efl_event_callback_add
+}
+{{end}}
diff --git a/cmd/ego-gen/templates/enum.go.tmpl b/cmd/ego-gen/templates/enum.go.tmpl
new file mode 100644
index 0000000..bf56b34
--- /dev/null
+++ b/cmd/ego-gen/templates/enum.go.tmpl
@@ -0,0 +1,19 @@
+// Code generated by ego-gen. DO NOT EDIT.
+
+package {{.PackageName}}
+
+/*
+#cgo pkg-config: elementary
+#include <Elementary.h>
+*/
+import "C"
+
+// {{.GoTypeName}} represents the EFL enum type mapped to this Go type.
+type {{.GoTypeName}} int
+
+const (
+{{- range .Values}}
+    // {{.GoName}} corresponds to the C constant {{.CName}}.
+    {{.GoName}} {{$.GoTypeName}} = {{$.GoTypeName}}(C.{{.CName}})
+{{- end}}
+)

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to