diff --git a/codegen/generate_test.go b/codegen/generate_test.go new file mode 100644 index 00000000..d9d63fbf --- /dev/null +++ b/codegen/generate_test.go @@ -0,0 +1,106 @@ +package main + +import ( + "flag" + "os" + "path/filepath" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/heygen-com/heygen-cli/internal/command" +) + +var update = flag.Bool("update", false, "update golden files") + +func loadTestSpec(t *testing.T, path string) *openapi3.T { + t.Helper() + doc, err := openapi3.NewLoader().LoadFromFile(path) + if err != nil { + t.Fatalf("failed to load spec %s: %v", path, err) + } + return doc +} + +func TestGoldenFiles(t *testing.T) { + doc := loadTestSpec(t, "testdata/mini_spec.json") + + examples, err := LoadExamples("testdata/test_examples.yaml") + if err != nil { + t.Fatalf("LoadExamples failed: %v", err) + } + + groups, err := GroupEndpoints(doc, examples) + if err != nil { + t.Fatalf("GroupEndpoints: %v", err) + } + + if len(groups) == 0 { + t.Fatal("expected at least one command group") + } + + outDir := t.TempDir() + if err := Generate(groups, "templates", outDir); err != nil { + t.Fatalf("Generate failed: %v", err) + } + + goldenDir := "testdata/golden" + + if *update { + if err := os.MkdirAll(goldenDir, 0755); err != nil { + t.Fatalf("creating golden dir: %v", err) + } + entries, err := os.ReadDir(outDir) + if err != nil { + t.Fatalf("reading output dir: %v", err) + } + for _, e := range entries { + if e.IsDir() { + continue + } + content, err := os.ReadFile(filepath.Join(outDir, e.Name())) + if err != nil { + t.Fatalf("reading %s: %v", e.Name(), err) + } + if err := os.WriteFile(filepath.Join(goldenDir, e.Name()), content, 0644); err != nil { + t.Fatalf("writing golden %s: %v", e.Name(), err) + } + } + t.Log("Golden files updated") + return + } + + goldenEntries, err := os.ReadDir(goldenDir) + if err != nil { + t.Fatalf("reading golden dir: %v", err) + } + + for _, e := range goldenEntries { + if e.IsDir() { + continue + } + t.Run(e.Name(), func(t *testing.T) { + golden, err := os.ReadFile(filepath.Join(goldenDir, e.Name())) + if err != nil { + t.Fatalf("reading golden file: %v", err) + } + actual, err := os.ReadFile(filepath.Join(outDir, e.Name())) + if err != nil { + t.Fatalf("reading generated file %s: %v", e.Name(), err) + } + if string(golden) != string(actual) { + t.Errorf("output mismatch for %s\nRun with -update to refresh golden files", e.Name()) + } + }) + } +} + +func TestValidateExamplesMissing(t *testing.T) { + groups := command.Groups{ + "test": {{Method: "GET", Endpoint: "/v3/test"}}, + } + + err := validateExamples(groups) + if err == nil { + t.Error("expected error for missing examples") + } +} diff --git a/codegen/testdata/golden/registry.go b/codegen/testdata/golden/registry.go new file mode 100644 index 00000000..13e83646 --- /dev/null +++ b/codegen/testdata/golden/registry.go @@ -0,0 +1,22 @@ +// 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{ + "upload": { + UploadCreate, + }, + "widget": { + WidgetActivateCreate, + WidgetCreate, + WidgetDelete, + WidgetGet, + WidgetList, + }, +} diff --git a/codegen/testdata/golden/upload.go b/codegen/testdata/golden/upload.go new file mode 100644 index 00000000..dbe64fc8 --- /dev/null +++ b/codegen/testdata/golden/upload.go @@ -0,0 +1,30 @@ +// Code generated by heygen-cli/codegen. DO NOT EDIT. + +package gen + +import "github.com/heygen-com/heygen-cli/internal/command" + +var UploadCreate = &command.Spec{ + Group: "upload", + Name: "create", + Summary: "Upload a file", + Description: "", + Endpoint: "/v3/uploads", + Method: "POST", + BodyEncoding: "multipart", + DataField: "data", + Flags: []command.FlagSpec{ + { + Name: "file", + Type: "string", + Default: "", + Help: "File to upload", + Required: true, + Enum: nil, + Min: nil, + Max: nil, + Source: "file", + JSONName: "file", + }, + }, +} diff --git a/codegen/testdata/golden/widget.go b/codegen/testdata/golden/widget.go new file mode 100644 index 00000000..f67cf274 --- /dev/null +++ b/codegen/testdata/golden/widget.go @@ -0,0 +1,160 @@ +// Code generated by heygen-cli/codegen. DO NOT EDIT. + +package gen + +import "github.com/heygen-com/heygen-cli/internal/command" + +var WidgetActivateCreate = &command.Spec{ + Group: "widget", + Name: "activate create", + Summary: "Activate a widget", + Description: "", + Endpoint: "/v3/widgets/{widget_id}/activate", + Method: "POST", + BodyEncoding: "json", + DataField: "data", + Args: []command.ArgSpec{ + {Name: "widget-id", Param: "widget_id", Help: ""}, + }, + Flags: []command.FlagSpec{ + { + Name: "force", + Type: "bool", + Default: "false", + Help: "Force activation", + Required: false, + Enum: nil, + Min: nil, + Max: nil, + Source: "body", + JSONName: "force", + }, + }, +} + +var WidgetCreate = &command.Spec{ + Group: "widget", + Name: "create", + Summary: "Create a widget", + Description: "", + Endpoint: "/v3/widgets", + Method: "POST", + BodyEncoding: "json", + DataField: "data", + Flags: []command.FlagSpec{ + { + Name: "count", + Type: "int", + Default: "", + Help: "Widget count", + Required: false, + Enum: nil, + Min: intPtr(1), + Max: intPtr(100), + Source: "body", + JSONName: "count", + }, + { + Name: "name", + Type: "string", + Default: "", + Help: "Widget name", + Required: true, + Enum: nil, + Min: nil, + Max: nil, + Source: "body", + JSONName: "name", + }, + { + Name: "tags", + Type: "string-slice", + Default: "", + Help: "Widget tags", + Required: false, + Enum: nil, + Min: nil, + Max: nil, + Source: "body", + JSONName: "tags", + }, + }, +} + +var WidgetDelete = &command.Spec{ + Group: "widget", + Name: "delete", + Summary: "Delete a widget", + Description: "", + Endpoint: "/v3/widgets/{widget_id}", + Method: "DELETE", + BodyEncoding: "", + DataField: "data", + Args: []command.ArgSpec{ + {Name: "widget-id", Param: "widget_id", Help: ""}, + }, +} + +var WidgetGet = &command.Spec{ + Group: "widget", + Name: "get", + Summary: "Get widget details", + Description: "", + Endpoint: "/v3/widgets/{widget_id}", + Method: "GET", + BodyEncoding: "", + DataField: "data", + Args: []command.ArgSpec{ + {Name: "widget-id", Param: "widget_id", Help: ""}, + }, +} + +var WidgetList = &command.Spec{ + Group: "widget", + Name: "list", + Summary: "List widgets", + Description: "", + Endpoint: "/v3/widgets", + Method: "GET", + BodyEncoding: "", + TokenField: "next_token", + DataField: "data", + Flags: []command.FlagSpec{ + { + Name: "limit", + Type: "int", + Default: "10", + Help: "Max results per page", + Required: false, + Enum: nil, + Min: intPtr(1), + Max: intPtr(100), + Source: "query", + JSONName: "limit", + }, + { + Name: "token", + Type: "string", + Default: "", + Help: "Pagination token", + Required: false, + Enum: nil, + Min: nil, + Max: nil, + Source: "query", + JSONName: "token", + }, + { + Name: "status", + Type: "string", + Default: "", + Help: "Filter by status", + Required: false, + Enum: []string{"active", "archived"}, + Min: nil, + Max: nil, + Source: "query", + JSONName: "status", + }, + }, +} diff --git a/codegen/testdata/mini_spec.json b/codegen/testdata/mini_spec.json new file mode 100644 index 00000000..64895f26 --- /dev/null +++ b/codegen/testdata/mini_spec.json @@ -0,0 +1,330 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Mini Test API", + "version": "1.0.0" + }, + "paths": { + "/v3/widgets": { + "get": { + "tags": ["Widgets"], + "summary": "List widgets", + "operationId": "listWidgets", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 10 + }, + "description": "Max results per page" + }, + { + "name": "token", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Pagination token" + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": ["active", "archived"] + }, + "description": "Filter by status" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object" + } + }, + "has_more": { + "type": "boolean" + }, + "next_token": { + "type": ["string", "null"] + } + } + } + } + } + } + } + }, + "post": { + "tags": ["Widgets"], + "summary": "Create a widget", + "operationId": "createWidget", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWidgetRequest" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Widget" + } + } + } + } + } + } + } + } + }, + "/v3/widgets/{widget_id}": { + "get": { + "tags": ["Widgets"], + "summary": "Get widget details", + "operationId": "getWidget", + "parameters": [ + { + "name": "widget_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Widget ID" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Widget" + } + } + } + } + } + } + } + }, + "delete": { + "tags": ["Widgets"], + "summary": "Delete a widget", + "operationId": "deleteWidget", + "parameters": [ + { + "name": "widget_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Widget ID" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + } + } + } + } + } + }, + "/v3/widgets/{widget_id}/activate": { + "post": { + "tags": ["Widgets"], + "summary": "Activate a widget", + "operationId": "activateWidget", + "parameters": [ + { + "name": "widget_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Widget ID" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivateWidgetRequest" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + } + } + } + } + } + }, + "/v3/uploads": { + "post": { + "tags": ["Uploads"], + "summary": "Upload a file", + "operationId": "uploadFile", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary", + "description": "File to upload" + } + }, + "required": ["file"] + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + } + } + } + } + } + }, + "/v1/legacy": { + "get": { + "tags": ["Legacy"], + "summary": "Legacy endpoint (should be skipped)", + "operationId": "legacyGet", + "x-cli-visible": false, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Widget": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "name": {"type": "string"}, + "status": {"type": "string"} + } + }, + "CreateWidgetRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Widget name" + }, + "color": { + "anyOf": [ + {"type": "string"}, + {"type": "null"} + ], + "default": null, + "description": "Widget color" + }, + "count": { + "type": "integer", + "description": "Widget count", + "minimum": 1, + "maximum": 100 + }, + "tags": { + "type": "array", + "items": {"type": "string"}, + "description": "Widget tags" + }, + "settings": { + "type": "object", + "description": "Complex settings object", + "properties": { + "nested": {"type": "string"} + } + } + }, + "required": ["name"] + }, + "ActivateWidgetRequest": { + "type": "object", + "properties": { + "force": { + "type": "boolean", + "description": "Force activation", + "default": false + } + } + } + } + } +}