← 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
M internal/generator/generator.goM internal/generator/templates/command.go.tmplM internal/generator/templates/root.go.tmplM internal/openapi/parser_test.go
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 →