[object Object]

← back to Cli Printing Press

feat(cli): support static config headers (#769)

838f5a497975aad11e7c938f982f7a64d96d9a15 · 2026-05-08 19:38:47 -0700 · Trevin Chow

Files touched

Diff

commit 838f5a497975aad11e7c938f982f7a64d96d9a15
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Fri May 8 19:38:47 2026 -0700

    feat(cli): support static config headers (#769)
---
 internal/generator/config_headers_test.go          | 70 ++++++++++++++++++++++
 internal/generator/templates/client.go.tmpl        |  5 ++
 internal/generator/templates/config.go.tmpl        |  1 +
 internal/generator/templates/readme.md.tmpl        |  2 +
 .../internal/client/client.go                      |  5 ++
 .../internal/config/config.go                      |  1 +
 .../printing-press-rich-auth/README.md             |  2 +
 .../internal/config/config.go                      |  1 +
 .../printing-press-golden/README.md                |  2 +
 .../internal/client/client.go                      |  5 ++
 .../internal/config/config.go                      |  1 +
 .../public-param-golden/README.md                  |  2 +
 .../tier-routing-golden/internal/client/client.go  |  5 ++
 13 files changed, 102 insertions(+)

diff --git a/internal/generator/config_headers_test.go b/internal/generator/config_headers_test.go
new file mode 100644
index 00000000..a8a8fc71
--- /dev/null
+++ b/internal/generator/config_headers_test.go
@@ -0,0 +1,70 @@
+package generator
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/mvanhorn/cli-printing-press/v4/internal/naming"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGeneratedClientAppliesConfigHeadersBeforeRequestOverrides(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("static-headers")
+	outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+	require.NoError(t, New(apiSpec, outputDir).Generate())
+
+	const behaviorTest = `package client
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"time"
+
+	"static-headers-pp-cli/internal/config"
+)
+
+func TestConfigHeadersAreAppliedBeforeRequestOverrides(t *testing.T) {
+	var seen int
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		seen++
+		if got := r.Header.Get("Authorization"); got != "Bearer config-token" {
+			t.Fatalf("Authorization header = %q, want %q", got, "Bearer config-token")
+		}
+		if got := r.Header.Get("apikey"); got != "config-key" {
+			t.Fatalf("apikey header = %q, want %q", got, "config-key")
+		}
+		if got := r.Header.Get("x-api-version"); got != "override-version" {
+			t.Fatalf("x-api-version header = %q, want request override to win", got)
+		}
+		w.Header().Set("Content-Type", "application/json")
+		_, _ = w.Write([]byte(` + "`" + `{"ok":true}` + "`" + `))
+	}))
+	defer server.Close()
+
+	cfg := &config.Config{
+		BaseURL:       server.URL,
+		AuthHeaderVal: "Bearer config-token",
+		Headers: map[string]string{
+			"apikey":        "config-key",
+			"x-api-version": "config-version",
+		},
+	}
+	c := New(cfg, time.Second, 0)
+	c.NoCache = true
+
+	if _, err := c.GetWithHeaders("/items", nil, map[string]string{"x-api-version": "override-version"}); err != nil {
+		t.Fatalf("GetWithHeaders returned error: %v", err)
+	}
+	if seen != 1 {
+		t.Fatalf("server saw %d requests, want 1", seen)
+	}
+}
+`
+	require.NoError(t, os.WriteFile(filepath.Join(outputDir, "internal", "client", "config_headers_test.go"), []byte(behaviorTest), 0o644))
+
+	runGoCommand(t, outputDir, "test", "./internal/client")
+}
diff --git a/internal/generator/templates/client.go.tmpl b/internal/generator/templates/client.go.tmpl
index dc2fd59b..0140677c 100644
--- a/internal/generator/templates/client.go.tmpl
+++ b/internal/generator/templates/client.go.tmpl
@@ -670,6 +670,11 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
 {{- range .RequiredHeaders}}
 		req.Header.Set("{{.Name}}", "{{.Value}}")
 {{- end}}
+		if c.Config != nil {
+			for k, v := range c.Config.Headers {
+				req.Header.Set(k, v)
+			}
+		}
 		// Per-endpoint header overrides (e.g., different API version per resource)
 		for k, v := range headerOverrides {
 			req.Header.Set(k, v)
diff --git a/internal/generator/templates/config.go.tmpl b/internal/generator/templates/config.go.tmpl
index 1e72b242..ccff6502 100644
--- a/internal/generator/templates/config.go.tmpl
+++ b/internal/generator/templates/config.go.tmpl
@@ -23,6 +23,7 @@ import (
 type Config struct {
 	BaseURL        string `{{configTag .Config.Format}}:"base_url"`
 	AuthHeaderVal  string `{{configTag .Config.Format}}:"auth_header"`
+	Headers        map[string]string `{{configTag .Config.Format}}:"headers,omitempty"`
 	AuthSource     string `{{configTag .Config.Format}}:"-"`
 {{- if .HasAuthCommand}}
 	AccessToken    string `{{configTag .Config.Format}}:"access_token"`
diff --git a/internal/generator/templates/readme.md.tmpl b/internal/generator/templates/readme.md.tmpl
index f8f1f62e..fdfbf688 100644
--- a/internal/generator/templates/readme.md.tmpl
+++ b/internal/generator/templates/readme.md.tmpl
@@ -490,6 +490,8 @@ Verifies configuration{{if and (ne .Auth.Type "") (ne .Auth.Type "none")}}, cred
 ## Configuration
 
 Config file: `{{.Config.Path}}`
+
+Static request headers can be configured under `headers`; per-command header overrides take precedence.
 {{- if or .Auth.EnvVarSpecs .Auth.EnvVars}}
 
 Environment variables:
diff --git a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go
index 6966ce01..901bd1fc 100644
--- a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go
+++ b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go
@@ -227,6 +227,11 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
 		if authHeader != "" {
 			req.Header.Set("Authorization", authHeader)
 		}
+		if c.Config != nil {
+			for k, v := range c.Config.Headers {
+				req.Header.Set(k, v)
+			}
+		}
 		// Per-endpoint header overrides (e.g., different API version per resource)
 		for k, v := range headerOverrides {
 			req.Header.Set(k, v)
diff --git a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/config/config.go b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/config/config.go
index d5bac5f0..9d50f491 100644
--- a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/config/config.go
+++ b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/config/config.go
@@ -16,6 +16,7 @@ import (
 type Config struct {
 	BaseURL        string `toml:"base_url"`
 	AuthHeaderVal  string `toml:"auth_header"`
+	Headers        map[string]string `toml:"headers,omitempty"`
 	AuthSource     string `toml:"-"`
 	AccessToken    string `toml:"access_token"`
 	RefreshToken   string `toml:"refresh_token"`
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.md b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.md
index 6ef17e40..0ddca815 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.md
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/README.md
@@ -201,6 +201,8 @@ Verifies configuration, credentials, and connectivity to the API.
 
 Config file: `~/.config/printing-press-rich-pp-cli/config.toml`
 
+Static request headers can be configured under `headers`; per-command header overrides take precedence.
+
 Environment variables:
 
 | Name | Kind | Required | Description |
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/config/config.go b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/config/config.go
index ec271074..33fb8aaa 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/config/config.go
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/config/config.go
@@ -16,6 +16,7 @@ import (
 type Config struct {
 	BaseURL        string `toml:"base_url"`
 	AuthHeaderVal  string `toml:"auth_header"`
+	Headers        map[string]string `toml:"headers,omitempty"`
 	AuthSource     string `toml:"-"`
 	AccessToken    string `toml:"access_token"`
 	RefreshToken   string `toml:"refresh_token"`
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md b/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
index 5c5d2154..982dea47 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
@@ -222,6 +222,8 @@ Verifies configuration, credentials, and connectivity to the API.
 
 Config file: `~/.config/printing-press-golden-pp-cli/config.toml`
 
+Static request headers can be configured under `headers`; per-command header overrides take precedence.
+
 Environment variables:
 
 | Name | Kind | Required | Description |
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
index 52c5cced..80eff7fb 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
@@ -222,6 +222,11 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
 			req.Header.Set("X-API-Key", authHeader)
 		}
 		req.Header.Set("X-Api-Version", "2026-04-01")
+		if c.Config != nil {
+			for k, v := range c.Config.Headers {
+				req.Header.Set(k, v)
+			}
+		}
 		// Per-endpoint header overrides (e.g., different API version per resource)
 		for k, v := range headerOverrides {
 			req.Header.Set(k, v)
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/config/config.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/config/config.go
index a4d51dcb..cb12c836 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/config/config.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/config/config.go
@@ -16,6 +16,7 @@ import (
 type Config struct {
 	BaseURL        string `toml:"base_url"`
 	AuthHeaderVal  string `toml:"auth_header"`
+	Headers        map[string]string `toml:"headers,omitempty"`
 	AuthSource     string `toml:"-"`
 	AccessToken    string `toml:"access_token"`
 	RefreshToken   string `toml:"refresh_token"`
diff --git a/testdata/golden/expected/generate-public-param-names/public-param-golden/README.md b/testdata/golden/expected/generate-public-param-names/public-param-golden/README.md
index 2a6280ec..84ff2e89 100644
--- a/testdata/golden/expected/generate-public-param-names/public-param-golden/README.md
+++ b/testdata/golden/expected/generate-public-param-names/public-param-golden/README.md
@@ -189,6 +189,8 @@ Verifies configuration and connectivity to the API.
 
 Config file: ``
 
+Static request headers can be configured under `headers`; per-command header overrides take precedence.
+
 ## Troubleshooting
 **Not found errors (exit code 3)**
 - Check the resource ID is correct
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
index 4f13cb33..09787659 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
@@ -325,6 +325,11 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
 				req.Header.Set(authInfo.Name, authHeader)
 			}
 		}
+		if c.Config != nil {
+			for k, v := range c.Config.Headers {
+				req.Header.Set(k, v)
+			}
+		}
 		// Per-endpoint header overrides (e.g., different API version per resource)
 		for k, v := range headerOverrides {
 			req.Header.Set(k, v)

← a5b57248 chore(main): release 4.1.0 (#729)  ·  back to Cli Printing Press  ·  fix(cli): avoid duplicate token auth map keys (#775) de34ec0e →