[object Object]

← back to Cli Printing Press

feat(pipeline): spec discovery registry and overlay merge types

5d456c67fceca750ed074f2642891e73fbd26431 · 2026-03-24 07:15:25 -0700 · Matt Van Horn

17 known API specs (Google, Stripe, Discord, etc.) with apis-guru fallback.
SpecOverlay type for enrichment phase - non-nil fields override originals.
MergeOverlay applies param patches (e.g., userId default=me) to APISpec.

Files touched

Diff

commit 5d456c67fceca750ed074f2642891e73fbd26431
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date:   Tue Mar 24 07:15:25 2026 -0700

    feat(pipeline): spec discovery registry and overlay merge types
    
    17 known API specs (Google, Stripe, Discord, etc.) with apis-guru fallback.
    SpecOverlay type for enrichment phase - non-nil fields override originals.
    MergeOverlay applies param patches (e.g., userId default=me) to APISpec.
---
 internal/pipeline/discover.go   | 51 ++++++++++++++++++++++++++++++++
 internal/pipeline/merge.go      | 64 +++++++++++++++++++++++++++++++++++++++++
 internal/pipeline/merge_test.go | 60 ++++++++++++++++++++++++++++++++++++++
 internal/pipeline/overlay.go    | 25 ++++++++++++++++
 4 files changed, 200 insertions(+)

diff --git a/internal/pipeline/discover.go b/internal/pipeline/discover.go
new file mode 100644
index 00000000..907703f7
--- /dev/null
+++ b/internal/pipeline/discover.go
@@ -0,0 +1,51 @@
+package pipeline
+
+import (
+	"fmt"
+	"strings"
+)
+
+// KnownSpecs maps common API names to their OpenAPI spec URLs.
+var KnownSpecs = map[string]string{
+	"petstore":     "https://petstore3.swagger.io/api/v3/openapi.json",
+	"gmail":        "https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/googleapis.com/gmail/v1/openapi.yaml",
+	"calendar":     "https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/googleapis.com/calendar/v3/openapi.yaml",
+	"drive":        "https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/googleapis.com/drive/v3/openapi.yaml",
+	"sheets":       "https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/googleapis.com/sheets/v4/openapi.yaml",
+	"youtube":      "https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/googleapis.com/youtube/v3/openapi.yaml",
+	"stripe":       "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json",
+	"twilio":       "https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json",
+	"sendgrid":     "https://raw.githubusercontent.com/sendgrid/sendgrid-oai/main/oai_stoplight.json",
+	"github":       "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json",
+	"discord":      "https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json",
+	"digitalocean": "https://api-engineering.nyc3.cdn.digitaloceanspaces.com/spec-ci/DigitalOcean-public.v2.yaml",
+	"slack":        "https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/slack.com/1.7.0/openapi.yaml",
+	"asana":        "https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/asana.com/1.0/openapi.yaml",
+	"hubspot":      "https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/hubspot.com/crm/v3/openapi.yaml",
+	"openai":       "https://raw.githubusercontent.com/openai/openai-openapi/master/openapi.yaml",
+	"anthropic":    "https://raw.githubusercontent.com/anthropics/anthropic-cookbook/main/misc/anthropic.openapi.yaml",
+}
+
+// ApisGuruPattern builds an apis-guru URL for a provider and version.
+func ApisGuruPattern(provider, version string) string {
+	return fmt.Sprintf("https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/%s/%s/openapi.yaml", provider, version)
+}
+
+// DiscoverSpec finds the OpenAPI spec URL for a given API name.
+// Returns the URL and a source description.
+func DiscoverSpec(apiName string) (string, string, error) {
+	normalized := strings.ToLower(strings.TrimSpace(apiName))
+
+	// Check known specs first
+	if url, ok := KnownSpecs[normalized]; ok {
+		return url, "known-specs registry", nil
+	}
+
+	// Try apis-guru with common version patterns
+	for _, version := range []string{"v1", "v2", "v3", "1.0", "2.0"} {
+		url := ApisGuruPattern(normalized+".com", version)
+		return url, "apis-guru (unverified, needs fetch validation)", nil
+	}
+
+	return "", "", fmt.Errorf("could not find OpenAPI spec for %q - try providing a URL with --spec", apiName)
+}
diff --git a/internal/pipeline/merge.go b/internal/pipeline/merge.go
new file mode 100644
index 00000000..cac4c76a
--- /dev/null
+++ b/internal/pipeline/merge.go
@@ -0,0 +1,64 @@
+package pipeline
+
+import (
+	"github.com/mvanhorn/cli-printing-press/internal/spec"
+)
+
+// MergeOverlay applies an overlay onto an APISpec, modifying it in place.
+// Non-nil overlay fields override the original spec values.
+func MergeOverlay(s *spec.APISpec, overlay *SpecOverlay) {
+	if overlay == nil || s == nil {
+		return
+	}
+
+	for rName, rOverlay := range overlay.Resources {
+		resource, ok := s.Resources[rName]
+		if !ok {
+			continue
+		}
+
+		if rOverlay.Description != nil {
+			resource.Description = *rOverlay.Description
+		}
+
+		for eName, eOverlay := range rOverlay.Endpoints {
+			endpoint, ok := resource.Endpoints[eName]
+			if !ok {
+				// Check sub-resources
+				for subName, sub := range resource.SubResources {
+					if ep, ok := sub.Endpoints[eName]; ok {
+						if eOverlay.Description != nil {
+							ep.Description = *eOverlay.Description
+						}
+						applyParamPatches(&ep, eOverlay.Params)
+						sub.Endpoints[eName] = ep
+						resource.SubResources[subName] = sub
+						break
+					}
+				}
+				continue
+			}
+
+			if eOverlay.Description != nil {
+				endpoint.Description = *eOverlay.Description
+			}
+			applyParamPatches(&endpoint, eOverlay.Params)
+			resource.Endpoints[eName] = endpoint
+		}
+
+		s.Resources[rName] = resource
+	}
+}
+
+func applyParamPatches(endpoint *spec.Endpoint, patches []ParamPatch) {
+	for _, patch := range patches {
+		for i, param := range endpoint.Params {
+			if param.Name == patch.Name {
+				if patch.Default != nil {
+					endpoint.Params[i].Default = *patch.Default
+				}
+				break
+			}
+		}
+	}
+}
diff --git a/internal/pipeline/merge_test.go b/internal/pipeline/merge_test.go
new file mode 100644
index 00000000..82c62d91
--- /dev/null
+++ b/internal/pipeline/merge_test.go
@@ -0,0 +1,60 @@
+package pipeline
+
+import (
+	"testing"
+
+	"github.com/mvanhorn/cli-printing-press/internal/spec"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMergeOverlay(t *testing.T) {
+	apiSpec := &spec.APISpec{
+		Name: "test",
+		Resources: map[string]spec.Resource{
+			"messages": {
+				Description: "old description",
+				Endpoints: map[string]spec.Endpoint{
+					"list": {
+						Method:      "GET",
+						Path:        "/messages",
+						Description: "List messages",
+						Params: []spec.Param{
+							{Name: "userId", Type: "string", Required: true, Positional: true},
+							{Name: "maxResults", Type: "integer"},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	newDesc := "Manage email messages"
+	defaultUser := "me"
+
+	overlay := &SpecOverlay{
+		Resources: map[string]ResourceOverlay{
+			"messages": {
+				Description: &newDesc,
+				Endpoints: map[string]EndpointOverlay{
+					"list": {
+						Params: []ParamPatch{
+							{Name: "userId", Default: &defaultUser},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	MergeOverlay(apiSpec, overlay)
+
+	assert.Equal(t, "Manage email messages", apiSpec.Resources["messages"].Description)
+	assert.Equal(t, "me", apiSpec.Resources["messages"].Endpoints["list"].Params[0].Default)
+	assert.Nil(t, apiSpec.Resources["messages"].Endpoints["list"].Params[1].Default)
+}
+
+func TestMergeOverlayNilSafe(t *testing.T) {
+	MergeOverlay(nil, nil)
+	MergeOverlay(&spec.APISpec{}, nil)
+	MergeOverlay(nil, &SpecOverlay{})
+}
diff --git a/internal/pipeline/overlay.go b/internal/pipeline/overlay.go
new file mode 100644
index 00000000..77835b91
--- /dev/null
+++ b/internal/pipeline/overlay.go
@@ -0,0 +1,25 @@
+package pipeline
+
+// SpecOverlay represents enrichments to apply on top of an original spec.
+// Non-nil fields override the original when merged.
+type SpecOverlay struct {
+	Resources map[string]ResourceOverlay `yaml:"resources,omitempty"`
+}
+
+// ResourceOverlay enriches a single resource.
+type ResourceOverlay struct {
+	Description *string                    `yaml:"description,omitempty"`
+	Endpoints   map[string]EndpointOverlay `yaml:"endpoints,omitempty"`
+}
+
+// EndpointOverlay enriches a single endpoint.
+type EndpointOverlay struct {
+	Description *string      `yaml:"description,omitempty"`
+	Params      []ParamPatch `yaml:"params,omitempty"`
+}
+
+// ParamPatch modifies a single parameter.
+type ParamPatch struct {
+	Name    string  `yaml:"name"`
+	Default *string `yaml:"default,omitempty"`
+}

← e2560ea7 feat(pipeline): add pipeline state manager with phase tracki  ·  back to Cli Printing Press  ·  feat(cli): add 'printing-press print' command with plan-per- 6a76cb26 →