[object Object]

← back to Cli Printing Press

feat(openapi): integration tests + oneline template fix for multiline descriptions

6011234c08aea8cf331caa29227ebc25df502d37 · 2026-03-23 10:39:37 -0700 · Matt Van Horn

Add end-to-end tests that parse real OpenAPI specs (Petstore, Stytch),
generate CLI projects, and verify they compile. Fix multiline descriptions
breaking generated Go string literals by adding oneline template function.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files touched

Diff

commit 6011234c08aea8cf331caa29227ebc25df502d37
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date:   Mon Mar 23 10:39:37 2026 -0700

    feat(openapi): integration tests + oneline template fix for multiline descriptions
    
    Add end-to-end tests that parse real OpenAPI specs (Petstore, Stytch),
    generate CLI projects, and verify they compile. Fix multiline descriptions
    breaking generated Go string literals by adding oneline template function.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 internal/generator/generator.go              | 17 +++++++
 internal/generator/templates/command.go.tmpl |  8 ++--
 internal/generator/templates/root.go.tmpl    |  2 +-
 internal/openapi/parser_test.go              | 70 ++++++++++++++++++++++++++++
 4 files changed, 92 insertions(+), 5 deletions(-)

diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 24a3b592..4ce60df8 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -38,6 +38,7 @@ func New(s *spec.APISpec, outputDir string) *Generator {
 		"envVarField":      envVarField,
 		"envVarPlaceholder": envVarPlaceholder,
 		"add":              func(a, b int) int { return a + b },
+		"oneline":          oneline,
 	}
 	return g
 }
@@ -249,6 +250,22 @@ func envVarField(envVar string) string {
 	return result
 }
 
+func oneline(s string) string {
+	s = strings.ReplaceAll(s, "\r\n", " ")
+	s = strings.ReplaceAll(s, "\n", " ")
+	s = strings.ReplaceAll(s, "\r", " ")
+	s = strings.ReplaceAll(s, `"`, `'`)
+	s = strings.ReplaceAll(s, "\\", "")
+	for strings.Contains(s, "  ") {
+		s = strings.ReplaceAll(s, "  ", " ")
+	}
+	s = strings.TrimSpace(s)
+	if len(s) > 120 {
+		s = s[:117] + "..."
+	}
+	return s
+}
+
 func envVarPlaceholder(envVar string) string {
 	// STYTCH_PROJECT_ID -> project_id (the placeholder in the format string)
 	parts := strings.Split(envVar, "_")
diff --git a/internal/generator/templates/command.go.tmpl b/internal/generator/templates/command.go.tmpl
index 6f33e99d..422c1a42 100644
--- a/internal/generator/templates/command.go.tmpl
+++ b/internal/generator/templates/command.go.tmpl
@@ -13,7 +13,7 @@ var _ = strings.ReplaceAll // ensure import
 func new{{title .ResourceName}}Cmd(flags *rootFlags) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "{{.ResourceName}}",
-		Short: "{{.Resource.Description}}",
+		Short: "{{oneline .Resource.Description}}",
 	}
 {{range $eName, $endpoint := .Resource.Endpoints}}
 	cmd.AddCommand(new{{title $.ResourceName}}{{title $eName}}Cmd(flags))
@@ -33,7 +33,7 @@ func new{{title $.ResourceName}}{{title $eName}}Cmd(flags *rootFlags) *cobra.Com
 
 	cmd := &cobra.Command{
 		Use:   "{{$eName}}{{positionalArgs $endpoint}}",
-		Short: "{{$endpoint.Description}}",
+		Short: "{{oneline $endpoint.Description}}",
 		RunE: func(cmd *cobra.Command, args []string) error {
 			c, err := flags.newClient()
 			if err != nil {
@@ -94,14 +94,14 @@ func new{{title $.ResourceName}}{{title $eName}}Cmd(flags *rootFlags) *cobra.Com
 
 {{- range $endpoint.Params}}
 {{- if not .Positional}}
-	cmd.Flags().{{cobraFlagFunc .Type}}(&flag{{camel .Name}}, "{{.Name}}", {{defaultVal .}}, "{{.Description}}")
+	cmd.Flags().{{cobraFlagFunc .Type}}(&flag{{camel .Name}}, "{{.Name}}", {{defaultVal .}}, "{{oneline .Description}}")
 {{- if .Required}}
 	_ = cmd.MarkFlagRequired("{{.Name}}")
 {{- end}}
 {{- end}}
 {{- end}}
 {{- range $endpoint.Body}}
-	cmd.Flags().{{cobraFlagFunc .Type}}(&body{{camel .Name}}, "{{.Name}}", {{defaultVal .}}, "{{.Description}}")
+	cmd.Flags().{{cobraFlagFunc .Type}}(&body{{camel .Name}}, "{{.Name}}", {{defaultVal .}}, "{{oneline .Description}}")
 {{- if .Required}}
 	_ = cmd.MarkFlagRequired("{{.Name}}")
 {{- end}}
diff --git a/internal/generator/templates/root.go.tmpl b/internal/generator/templates/root.go.tmpl
index 20086dea..eb89bed0 100644
--- a/internal/generator/templates/root.go.tmpl
+++ b/internal/generator/templates/root.go.tmpl
@@ -26,7 +26,7 @@ func Execute() error {
 
 	rootCmd := &cobra.Command{
 		Use:           "{{.Name}}-cli",
-		Short:         "{{.Description}}",
+		Short:         "{{oneline .Description}}",
 		SilenceUsage:  true,
 		SilenceErrors: true,
 		Version:       version,
diff --git a/internal/openapi/parser_test.go b/internal/openapi/parser_test.go
index 93e03728..d83610fd 100644
--- a/internal/openapi/parser_test.go
+++ b/internal/openapi/parser_test.go
@@ -2,9 +2,11 @@ package openapi
 
 import (
 	"os"
+	"os/exec"
 	"path/filepath"
 	"testing"
 
+	"github.com/mvanhorn/cli-printing-press/internal/generator"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -35,6 +37,27 @@ func TestParsePetstore(t *testing.T) {
 	assert.Contains(t, parsed.Types, "Pet")
 }
 
+func TestParseStytchOpenAPI(t *testing.T) {
+	t.Parallel()
+
+	data, err := os.ReadFile(filepath.Join("..", "..", "testdata", "openapi", "stytch.yaml"))
+	require.NoError(t, err)
+
+	parsed, err := Parse(data)
+	require.NoError(t, err)
+
+	assert.Equal(t, "stytch-api", parsed.Name)
+	assert.NotEmpty(t, parsed.BaseURL)
+	assert.NotEmpty(t, parsed.Resources)
+	assert.NotEmpty(t, parsed.Types)
+
+	totalEndpoints := 0
+	for _, resource := range parsed.Resources {
+		totalEndpoints += len(resource.Endpoints)
+	}
+	assert.Greater(t, totalEndpoints, 10)
+}
+
 func TestIsOpenAPI(t *testing.T) {
 	t.Parallel()
 
@@ -61,3 +84,50 @@ resources:
 	assert.True(t, IsOpenAPI(openAPIJSON))
 	assert.False(t, IsOpenAPI(internalYAML))
 }
+
+func TestGenerateFromOpenAPICompiles(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name     string
+		specFile string
+	}{
+		{name: "petstore", specFile: "petstore.yaml"},
+		{name: "stytch", specFile: "stytch.yaml"},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			data, err := os.ReadFile(filepath.Join("..", "..", "testdata", "openapi", tt.specFile))
+			require.NoError(t, err)
+
+			parsed, err := Parse(data)
+			require.NoError(t, err)
+
+			outputDir := filepath.Join(t.TempDir(), parsed.Name+"-cli")
+			gen := generator.New(parsed, outputDir)
+			require.NoError(t, gen.Generate())
+
+			runGo(t, outputDir, "mod", "tidy")
+			runGo(t, outputDir, "build", "./...")
+
+			binaryPath := filepath.Join(outputDir, parsed.Name+"-cli")
+			runGo(t, outputDir, "build", "-o", binaryPath, "./cmd/"+parsed.Name+"-cli")
+
+			info, err := os.Stat(binaryPath)
+			require.NoError(t, err)
+			require.NotZero(t, info.Size())
+		})
+	}
+}
+
+func runGo(t *testing.T, dir string, args ...string) {
+	t.Helper()
+	cmd := exec.Command("go", args...)
+	cmd.Dir = dir
+	cmd.Env = append(os.Environ(), "GOCACHE="+filepath.Join(dir, ".cache", "go-build"))
+	output, err := cmd.CombinedOutput()
+	require.NoError(t, err, string(output))
+}

← 60009103 feat(cli): auto-detect OpenAPI vs internal spec format  ·  back to Cli Printing Press  ·  fix(openapi): Swagger 2.0 detection + resource name sanitiza 4934eb50 →