← 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
A internal/pipeline/discover.goA internal/pipeline/merge.goA internal/pipeline/merge_test.goA internal/pipeline/overlay.go
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 →