Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
LDFLAGS := -s -w -X main.version=$(VERSION)

.PHONY: build install test lint clean
.PHONY: build install test lint clean generate

build:
go build -ldflags "$(LDFLAGS)" -o bin/heygen ./cmd/heygen/
Expand All @@ -17,3 +17,8 @@ lint:

clean:
rm -rf bin/

generate:
@find gen/ -name '*.go' -delete 2>/dev/null || true
go run ./codegen/ -spec $(SPEC) -out gen/ -examples codegen/examples/
gofmt -w gen/
132 changes: 132 additions & 0 deletions codegen/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package main

import (
"bytes"
"fmt"
"go/format"
"os"
"path/filepath"
"strings"
"text/template"

"github.com/heygen-com/heygen-cli/internal/command"
"github.com/iancoleman/strcase"
)

// Generate writes Go source files from command.Groups using text/templates.
//
// For each group, it produces one .go file containing command.Spec struct
// literals as exported variables. It also produces a registry.go with a
// Groups map that the runtime builder uses to register commands.
//
// Example: given a group "video" with a Spec{Name: "list", Endpoint: "/v3/videos"},
// Generate writes gen/video.go containing:
//
// var VideoList = &command.Spec{
// Group: "video",
// Name: "list",
// Endpoint: "/v3/videos",
// Method: "GET",
// ...
// }
//
// All output is gofmt'd. Variable names are PascalCase derived from
// group + command name via strcase.ToCamel.
func Generate(groups command.Groups, tmplDir, outDir string) error {
if err := os.MkdirAll(outDir, 0755); err != nil {
return fmt.Errorf("creating output directory: %w", err)
}

cmdTmpl, err := loadTemplate(filepath.Join(tmplDir, "command.go.tmpl"))
if err != nil {
return fmt.Errorf("loading command template: %w", err)
}

regTmpl, err := loadTemplate(filepath.Join(tmplDir, "registry.go.tmpl"))
if err != nil {
return fmt.Errorf("loading registry template: %w", err)
}

// One file per group (sorted for deterministic output)
groupNames := groups.SortedNames()

for _, name := range groupNames {
data := struct {
GroupName string
Specs []*command.Spec
}{name, groups[name]}
filename := filepath.Join(outDir, name+".go")
if err := writeFromTemplate(cmdTmpl, data, filename); err != nil {
return fmt.Errorf("generating %s: %w", filename, err)
}
}

// Registry file
regData := struct {
Groups command.Groups
GroupNames []string
}{groups, groupNames}
regFilename := filepath.Join(outDir, "registry.go")
if err := writeFromTemplate(regTmpl, regData, regFilename); err != nil {
return fmt.Errorf("generating registry: %w", err)
}

return nil
}

func loadTemplate(path string) (*template.Template, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}

funcMap := template.FuncMap{
"quote": quoteString,
"intPtrLiteral": intPtrLiteral,
"stringSlice": stringSliceLiteral,
"pascalCase": strcase.ToCamel,
}

return template.New(filepath.Base(path)).Funcs(funcMap).Parse(string(data))
}

// writeFromTemplate executes a template with data and writes gofmt'd output.
func writeFromTemplate(tmpl *template.Template, data interface{}, filename string) error {
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return err
}

formatted, err := format.Source(buf.Bytes())
if err != nil {
// Write unformatted for debugging
_ = os.WriteFile(filename+".raw", buf.Bytes(), 0644)
return fmt.Errorf("gofmt %s: %w (raw output written to %s.raw)", filename, err, filename)
}

return os.WriteFile(filename, formatted, 0644)
}

// Template helper functions

func quoteString(s string) string {
return fmt.Sprintf("%q", s)
}

func intPtrLiteral(p *int) string {
if p == nil {
return "nil"
}
return fmt.Sprintf("intPtr(%d)", *p)
}

func stringSliceLiteral(ss []string) string {
if len(ss) == 0 {
return "nil"
}
quoted := make([]string, len(ss))
for i, s := range ss {
quoted[i] = fmt.Sprintf("%q", s)
}
return "[]string{" + strings.Join(quoted, ", ") + "}"
}
77 changes: 77 additions & 0 deletions codegen/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Package main implements the codegen pipeline that reads an OpenAPI spec
// and produces command.Spec Go files for the HeyGen CLI.
package main

import (
"flag"
"fmt"
"os"

"github.com/getkin/kin-openapi/openapi3"
"github.com/heygen-com/heygen-cli/internal/command"
)

func main() {
specPath := flag.String("spec", "", "Path to OpenAPI JSON spec")
outDir := flag.String("out", "gen/", "Output directory for generated files")
examplesPath := flag.String("examples", "", "Path to examples YAML file or directory")
flag.Parse()

if *specPath == "" {
fmt.Fprintln(os.Stderr, "error: -spec flag is required")
os.Exit(1)
}

// Step 1: Load examples
var examples Examples
if *examplesPath != "" {
var err error
examples, err = LoadExamples(*examplesPath)
if err != nil {
fmt.Fprintf(os.Stderr, "error loading examples: %v\n", err)
os.Exit(1)
}
} else {
examples = make(Examples)
}

// Step 2: Load the OpenAPI spec
doc, err := openapi3.NewLoader().LoadFromFile(*specPath)
if err != nil {
fmt.Fprintf(os.Stderr, "error loading spec: %v\n", err)
os.Exit(1)
}

// Step 3: Group endpoints into command specs
groups, err := GroupEndpoints(doc, examples)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}

// Step 4: Validate that all commands have examples
if err := validateExamples(groups); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}

// Step 5: Generate Go source files
if err := Generate(groups, "codegen/templates", *outDir); err != nil {
fmt.Fprintf(os.Stderr, "error generating: %v\n", err)
os.Exit(1)
}

fmt.Printf("Generated %d command groups into %s\n", len(groups), *outDir)
}

// validateExamples ensures every command has at least one example.
func validateExamples(groups command.Groups) error {
for _, specs := range groups {
for _, spec := range specs {
if len(spec.Examples) == 0 {
return fmt.Errorf("missing examples for %s %s (add to codegen/examples/)", spec.Method, spec.Endpoint)
}
}
}
return nil
}
56 changes: 56 additions & 0 deletions codegen/templates/command.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Code generated by heygen-cli/codegen. DO NOT EDIT.

package gen

import "github.com/heygen-com/heygen-cli/internal/command"

{{- range .Specs}}

var {{pascalCase .Group}}{{pascalCase .Name}} = &command.Spec{
Group: {{quote .Group}},
Name: {{quote .Name}},
Summary: {{quote .Summary}},
Description: {{quote .Description}},
Endpoint: {{quote .Endpoint}},
Method: {{quote .Method}},
BodyEncoding: {{quote .BodyEncoding}},
{{- if .TokenField}}
TokenField: {{quote .TokenField}},
{{- end}}
{{- if .DataField}}
DataField: {{quote .DataField}},
{{- end}}
{{- if .Examples}}
Examples: []string{
{{- range .Examples}}
{{quote .}},
{{- end}}
},
{{- end}}
{{- if .Args}}
Args: []command.ArgSpec{
{{- range .Args}}
{Name: {{quote .Name}}, Param: {{quote .Param}}, Help: {{quote .Help}}},
{{- end}}
},
{{- end}}
{{- if .Flags}}
Flags: []command.FlagSpec{
{{- range .Flags}}
{
Name: {{quote .Name}},
Type: {{quote .Type}},
Default: {{quote .Default}},
Help: {{quote .Help}},
Required: {{if .Required}}true{{else}}false{{end}},
Enum: {{stringSlice .Enum}},
Min: {{intPtrLiteral .Min}},
Max: {{intPtrLiteral .Max}},
Source: {{quote .Source}},
JSONName: {{quote .JSONName}},
},
{{- end}}
},
{{- end}}
}
{{- end}}
19 changes: 19 additions & 0 deletions codegen/templates/registry.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Code generated by heygen-cli/codegen. DO NOT EDIT.

package gen

import "github.com/heygen-com/heygen-cli/internal/command"

func intPtr(v int) *int { return &v }

// Groups maps group name → slice of command specs.
// Used by the command builder to register generated commands.
var Groups = map[string][]*command.Spec{
{{- range .GroupNames}}
{{quote .}}: {
{{- range index $.Groups .}}
{{pascalCase .Group}}{{pascalCase .Name}},
{{- end}}
},
{{- end}}
}
Loading