← back to Cli Printing Press
feat(profiler): add API Shape Intelligence Engine
f499c4b57c38df3b0dfc552e99e8353d13142af0 · 2026-03-25 21:12:29 -0700 · Matt Van Horn
Analyze OpenAPI spec structure to predict power-user features.
Detects 11 signals: pagination, search gaps, webhooks, CRUD patterns,
lifecycle states, foreign keys, chronological resources, file ops.
Produces APIProfile with ToVisionaryPlan() for template selection.
Tests: Petstore (simple), Discord (complex), Minimal - all pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Files touched
A internal/profiler/profiler.goA internal/profiler/profiler_test.go
Diff
commit f499c4b57c38df3b0dfc552e99e8353d13142af0
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Wed Mar 25 21:12:29 2026 -0700
feat(profiler): add API Shape Intelligence Engine
Analyze OpenAPI spec structure to predict power-user features.
Detects 11 signals: pagination, search gaps, webhooks, CRUD patterns,
lifecycle states, foreign keys, chronological resources, file ops.
Produces APIProfile with ToVisionaryPlan() for template selection.
Tests: Petstore (simple), Discord (complex), Minimal - all pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
internal/profiler/profiler.go | 499 +++++++++++++++++++++++++++++++++++++
internal/profiler/profiler_test.go | 368 +++++++++++++++++++++++++++
2 files changed, 867 insertions(+)
diff --git a/internal/profiler/profiler.go b/internal/profiler/profiler.go
new file mode 100644
index 00000000..263b44f6
--- /dev/null
+++ b/internal/profiler/profiler.go
@@ -0,0 +1,499 @@
+package profiler
+
+import (
+ "sort"
+ "strings"
+
+ "github.com/mvanhorn/cli-printing-press/internal/spec"
+ "github.com/mvanhorn/cli-printing-press/internal/vision"
+)
+
+// APIProfile describes the shape of an API and what power-user features it warrants.
+type APIProfile struct {
+ HighVolume bool
+ NeedsSearch bool
+ HasRealtime bool
+ OfflineValuable bool
+ ComplexResources bool
+ HasLifecycles bool
+ HasDependencies bool
+ HasChronological bool
+ HasFileOps bool
+ CRUDResources int
+ ListEndpoints int
+ TotalEndpoints int
+ ReadRatio float64
+
+ SyncableResources []string
+ SearchableFields map[string][]string
+}
+
+func Profile(s *spec.APISpec) *APIProfile {
+ if s == nil {
+ return &APIProfile{
+ SearchableFields: make(map[string][]string),
+ }
+ }
+
+ p := &APIProfile{
+ SearchableFields: make(map[string][]string),
+ }
+
+ resourceNames := collectResourceNames(s.Resources)
+ syncable := make(map[string]struct{})
+ searchable := make(map[string]map[string]struct{})
+ listResources := make(map[string]struct{})
+
+ var getEndpoints int
+ var listCapableGETs int
+ var hasSearchEndpoint bool
+
+ var walk func(name string, r spec.Resource)
+ walk = func(name string, r spec.Resource) {
+ resourceName := strings.ToLower(name)
+ resourceHasGet := false
+ resourceHasPost := false
+ resourceHasMutating := false
+
+ if containsAny(resourceName, []string{"webhook", "event", "callback", "notification"}) {
+ p.HasRealtime = true
+ }
+ if containsAny(resourceName, []string{"audit", "log", "event", "history", "activity"}) {
+ p.HasChronological = true
+ }
+
+ for endpointName, endpoint := range r.Endpoints {
+ p.TotalEndpoints++
+
+ method := strings.ToUpper(endpoint.Method)
+ switch method {
+ case "GET":
+ getEndpoints++
+ resourceHasGet = true
+ case "POST":
+ resourceHasPost = true
+ case "PUT", "PATCH", "DELETE":
+ resourceHasMutating = true
+ }
+
+ endpointNameLower := strings.ToLower(endpointName)
+ pathLower := strings.ToLower(endpoint.Path)
+
+ if containsAny(endpointNameLower, []string{"search"}) || containsAny(pathLower, []string{"search"}) {
+ hasSearchEndpoint = true
+ }
+ if containsAny(pathLower, []string{"webhook", "event", "callback", "notification"}) {
+ p.HasRealtime = true
+ }
+ if containsAny(pathLower, []string{"audit", "log", "event", "history", "activity"}) || hasChronologicalParams(endpoint.Params) {
+ p.HasChronological = true
+ }
+
+ if isListEndpoint(endpointName, endpoint) {
+ listCapableGETs++
+ listResources[resourceName] = struct{}{}
+ if endpoint.Pagination != nil {
+ p.ListEndpoints++
+ syncable[resourceName] = struct{}{}
+ }
+ }
+
+ if len(endpoint.Body) > 10 {
+ p.ComplexResources = true
+ }
+ if hasLifecycleField(endpoint.Body) || hasLifecycleField(endpoint.Params) {
+ p.HasLifecycles = true
+ }
+ if hasFileBody(endpoint.Body) {
+ p.HasFileOps = true
+ }
+ if !p.HasDependencies && hasDependency(endpoint.Body, resourceNames) {
+ p.HasDependencies = true
+ }
+
+ for _, field := range collectStringFields(endpoint.Body) {
+ if searchable[resourceName] == nil {
+ searchable[resourceName] = make(map[string]struct{})
+ }
+ searchable[resourceName][field] = struct{}{}
+ }
+ }
+
+ if resourceHasGet && resourceHasPost && resourceHasMutating {
+ p.CRUDResources++
+ }
+
+ for subName, sub := range r.SubResources {
+ walk(subName, sub)
+ }
+ }
+
+ for name, resource := range s.Resources {
+ walk(name, resource)
+ }
+
+ if p.TotalEndpoints > 0 {
+ p.ReadRatio = float64(getEndpoints) / float64(p.TotalEndpoints)
+ p.OfflineValuable = p.ReadRatio > 0.6
+ }
+ if listCapableGETs > 0 {
+ p.HighVolume = float64(p.ListEndpoints)/float64(listCapableGETs) > 0.5
+ }
+ p.NeedsSearch = len(listResources) >= 3 && !hasSearchEndpoint
+
+ p.SyncableResources = sortedKeys(syncable)
+ for resource, fields := range searchable {
+ p.SearchableFields[resource] = sortedKeys(fields)
+ }
+
+ return p
+}
+
+func (p *APIProfile) ToVisionaryPlan(apiName string) *vision.VisionaryPlan {
+ if p == nil {
+ p = &APIProfile{}
+ }
+
+ plan := &vision.VisionaryPlan{
+ APIName: apiName,
+ Identity: vision.APIIdentity{
+ CoreEntities: p.SyncableResources,
+ DataProfile: vision.DataProfile{
+ Volume: lowHigh(p.HighVolume),
+ SearchNeed: lowHigh(p.NeedsSearch),
+ Realtime: p.HasRealtime,
+ },
+ },
+ }
+
+ plan.Architecture = append(plan.Architecture,
+ vision.ArchitectureDecision{
+ Area: "persistence",
+ NeedLevel: lowHigh(p.HighVolume || p.OfflineValuable),
+ Decision: "local store",
+ Rationale: "Read-heavy or high-volume APIs benefit from local persistence for repeat access and offline workflows.",
+ ImplementationHint: "Use SQLite-backed storage and cache frequently accessed resources.",
+ },
+ vision.ArchitectureDecision{
+ Area: "search",
+ NeedLevel: lowHigh(p.NeedsSearch),
+ Decision: "full-text indexing",
+ Rationale: "Multi-resource list-heavy APIs need a fast local search surface when no dedicated endpoint exists.",
+ ImplementationHint: "Index string fields in FTS5 tables keyed by resource type.",
+ },
+ vision.ArchitectureDecision{
+ Area: "realtime",
+ NeedLevel: lowHigh(p.HasRealtime),
+ Decision: "streaming event tail",
+ Rationale: "Webhook and event-heavy APIs warrant live inspection workflows.",
+ ImplementationHint: "Offer tail-style commands that poll or stream event resources.",
+ },
+ )
+
+ for _, featureName := range p.RecommendedFeatures() {
+ feature := featureIdeaFor(featureName, p)
+ feature.TotalScore = feature.ComputeScore()
+ plan.Features = append(plan.Features, feature)
+ }
+
+ return plan
+}
+
+func (p *APIProfile) RecommendedFeatures() []string {
+ if p == nil {
+ return []string{"export", "import"}
+ }
+
+ var features []string
+ if p.HighVolume {
+ features = append(features, "sync")
+ }
+ if p.NeedsSearch {
+ features = append(features, "search")
+ }
+ if p.HighVolume || p.NeedsSearch || p.HasDependencies {
+ features = append(features, "store")
+ }
+
+ features = append(features, "export", "import")
+
+ if p.HasRealtime || p.HasChronological {
+ features = append(features, "tail")
+ }
+ if p.HighVolume || p.HasChronological {
+ features = append(features, "analytics")
+ }
+
+ return features
+}
+
+func featureIdeaFor(name string, p *APIProfile) vision.FeatureIdea {
+ switch name {
+ case "sync":
+ return scoredFeature(
+ "sync",
+ "Continuously mirror paginated resources into a local cache for fast bulk access.",
+ []string{"sync.go.tmpl"},
+ 2, 3, 2, 1, 2, 3, 2, 1,
+ )
+ case "search":
+ return scoredFeature(
+ "search",
+ "Search across locally indexed records when the upstream API lacks a dedicated search endpoint.",
+ []string{"search.go.tmpl"},
+ 2, 3, 2, 1, 2, 3, 2, 1,
+ )
+ case "store":
+ return scoredFeature(
+ "store",
+ "Persist fetched records locally to support repeat access, joins, and offline work.",
+ []string{"store.go.tmpl"},
+ 2, 2, 3, 1, 2, 2, 2, 1,
+ )
+ case "export":
+ return scoredFeature(
+ "export",
+ "Export API records into shell-friendly formats for scripting and archival.",
+ []string{"export.go.tmpl"},
+ 1, 2, 3, 1, 2, 1, 3, 1,
+ )
+ case "import":
+ return scoredFeature(
+ "import",
+ "Import records from files or stdin to support bootstrap and migration workflows.",
+ []string{"import.go.tmpl"},
+ 1, 2, 3, 1, 2, 1, 3, 1,
+ )
+ case "tail":
+ return scoredFeature(
+ "tail",
+ "Tail event-like resources to inspect API activity as it happens.",
+ []string{"tail.go.tmpl"},
+ 2, 3, 2, 1, 1, dataFit(p.HasRealtime || p.HasChronological), 2, 1,
+ )
+ case "analytics":
+ return scoredFeature(
+ "analytics",
+ "Run local analytics over synced records to summarize high-volume or historical activity.",
+ []string{"analytics.go.tmpl"},
+ 2, 2, 2, 1, 2, dataFit(p.HighVolume || p.HasChronological), 2, 1,
+ )
+ default:
+ return vision.FeatureIdea{Name: name}
+ }
+}
+
+func scoredFeature(name, description string, templates []string, evidence, impact, feasibility, uniqueness, composability, fit, maintainability, moat int) vision.FeatureIdea {
+ return vision.FeatureIdea{
+ Name: name,
+ Description: description,
+ EvidenceStrength: evidence,
+ UserImpact: impact,
+ ImplementationFeasibility: feasibility,
+ Uniqueness: uniqueness,
+ Composability: composability,
+ DataProfileFit: fit,
+ Maintainability: maintainability,
+ CompetitiveMoat: moat,
+ TemplateNames: templates,
+ }
+}
+
+func lowHigh(v bool) string {
+ if v {
+ return "high"
+ }
+ return "low"
+}
+
+func dataFit(v bool) int {
+ if v {
+ return 3
+ }
+ return 1
+}
+
+func isListEndpoint(name string, endpoint spec.Endpoint) bool {
+ if strings.ToUpper(endpoint.Method) != "GET" {
+ return false
+ }
+ if endpoint.Pagination != nil {
+ return true
+ }
+ if endpoint.Response.Type == "array" {
+ return true
+ }
+
+ name = strings.ToLower(name)
+ return containsAny(name, []string{"list", "all"})
+}
+
+func hasLifecycleField(params []spec.Param) bool {
+ for _, param := range params {
+ if isLifecycleParam(param) {
+ return true
+ }
+ if hasLifecycleField(param.Fields) {
+ return true
+ }
+ }
+ return false
+}
+
+func isLifecycleParam(param spec.Param) bool {
+ name := strings.ToLower(param.Name)
+ return (name == "status" || name == "state") && len(param.Enum) >= 3
+}
+
+func hasFileBody(params []spec.Param) bool {
+ for _, param := range params {
+ if strings.EqualFold(param.Type, "file") || strings.EqualFold(param.Format, "binary") {
+ return true
+ }
+ if hasFileBody(param.Fields) {
+ return true
+ }
+ }
+ return false
+}
+
+func hasDependency(params []spec.Param, resourceNames map[string]struct{}) bool {
+ for _, param := range params {
+ name := strings.ToLower(param.Name)
+ if strings.HasSuffix(name, "_id") && strings.EqualFold(param.Type, "string") {
+ prefix := strings.TrimSuffix(name, "_id")
+ if matchesResource(prefix, resourceNames) {
+ return true
+ }
+ }
+ if hasDependency(param.Fields, resourceNames) {
+ return true
+ }
+ }
+ return false
+}
+
+func matchesResource(name string, resourceNames map[string]struct{}) bool {
+ for _, variant := range nameVariants(name) {
+ if _, ok := resourceNames[variant]; ok {
+ return true
+ }
+ }
+ return false
+}
+
+func collectResourceNames(resources map[string]spec.Resource) map[string]struct{} {
+ names := make(map[string]struct{})
+
+ var walk func(name string, r spec.Resource)
+ walk = func(name string, r spec.Resource) {
+ for _, variant := range nameVariants(name) {
+ names[variant] = struct{}{}
+ }
+ for subName, sub := range r.SubResources {
+ walk(subName, sub)
+ }
+ }
+
+ for name, resource := range resources {
+ walk(name, resource)
+ }
+
+ return names
+}
+
+func nameVariants(name string) []string {
+ normalized := normalizeName(name)
+ if normalized == "" {
+ return nil
+ }
+
+ seen := map[string]struct{}{normalized: {}}
+ var variants []string
+ variants = append(variants, normalized)
+
+ if strings.HasSuffix(normalized, "ies") {
+ addVariant(normalized[:len(normalized)-3]+"y", seen, &variants)
+ }
+ if strings.HasSuffix(normalized, "es") {
+ addVariant(strings.TrimSuffix(normalized, "es"), seen, &variants)
+ }
+ if strings.HasSuffix(normalized, "s") {
+ addVariant(strings.TrimSuffix(normalized, "s"), seen, &variants)
+ }
+
+ return variants
+}
+
+func addVariant(variant string, seen map[string]struct{}, variants *[]string) {
+ if variant == "" {
+ return
+ }
+ if _, ok := seen[variant]; ok {
+ return
+ }
+ seen[variant] = struct{}{}
+ *variants = append(*variants, variant)
+}
+
+func normalizeName(name string) string {
+ replacer := strings.NewReplacer("-", "_", " ", "_")
+ return strings.Trim(replacer.Replace(strings.ToLower(name)), "_")
+}
+
+func collectStringFields(params []spec.Param) []string {
+ fields := make(map[string]struct{})
+ var walk func(items []spec.Param)
+ walk = func(items []spec.Param) {
+ for _, param := range items {
+ if strings.EqualFold(param.Type, "string") {
+ fields[param.Name] = struct{}{}
+ }
+ if len(param.Fields) > 0 {
+ walk(param.Fields)
+ }
+ }
+ }
+ walk(params)
+ return sortedKeys(fields)
+}
+
+func hasChronologicalParams(params []spec.Param) bool {
+ for _, param := range params {
+ name := strings.ToLower(param.Name)
+ desc := strings.ToLower(param.Description)
+
+ if name == "since" || name == "until" || name == "before" || name == "after" {
+ return true
+ }
+ if strings.Contains(name, "timestamp") || strings.Contains(name, "created_at") || strings.Contains(name, "updated_at") {
+ return true
+ }
+ if (strings.Contains(name, "sort") || strings.Contains(name, "order")) &&
+ (strings.Contains(desc, "time") || strings.Contains(desc, "date") || strings.Contains(desc, "timestamp") || strings.Contains(desc, "created")) {
+ return true
+ }
+ if hasChronologicalParams(param.Fields) {
+ return true
+ }
+ }
+ return false
+}
+
+func containsAny(s string, needles []string) bool {
+ for _, needle := range needles {
+ if strings.Contains(s, needle) {
+ return true
+ }
+ }
+ return false
+}
+
+func sortedKeys[V any](m map[string]V) []string {
+ keys := make([]string, 0, len(m))
+ for k := range m {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ return keys
+}
diff --git a/internal/profiler/profiler_test.go b/internal/profiler/profiler_test.go
new file mode 100644
index 00000000..680b9756
--- /dev/null
+++ b/internal/profiler/profiler_test.go
@@ -0,0 +1,368 @@
+package profiler
+
+import (
+ "testing"
+
+ "github.com/mvanhorn/cli-printing-press/internal/spec"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestProfilePetstore(t *testing.T) {
+ profile := Profile(petstoreSpec())
+
+ assert.False(t, profile.HighVolume)
+ assert.False(t, profile.NeedsSearch)
+ assert.False(t, profile.HasRealtime)
+ assert.Equal(t, []string{"export", "import"}, profile.RecommendedFeatures())
+}
+
+func TestProfileDiscord(t *testing.T) {
+ profile := Profile(discordSpec())
+
+ assert.True(t, profile.HighVolume)
+ assert.True(t, profile.NeedsSearch)
+ assert.True(t, profile.HasRealtime)
+ assert.True(t, profile.HasChronological)
+ assert.True(t, profile.HasDependencies)
+ assert.ElementsMatch(t, []string{"sync", "search", "store", "export", "import", "tail", "analytics"}, profile.RecommendedFeatures())
+ assert.Contains(t, profile.SyncableResources, "messages")
+ assert.Contains(t, profile.SearchableFields["messages"], "content")
+}
+
+func TestProfileMinimal(t *testing.T) {
+ profile := Profile(minimalSpec())
+
+ assert.False(t, profile.HighVolume)
+ assert.False(t, profile.NeedsSearch)
+ assert.False(t, profile.HasRealtime)
+ assert.False(t, profile.HasChronological)
+ assert.False(t, profile.HasDependencies)
+ assert.Zero(t, profile.CRUDResources)
+ assert.Equal(t, []string{"export", "import"}, profile.RecommendedFeatures())
+}
+
+func TestToVisionaryPlan(t *testing.T) {
+ profile := Profile(discordSpec())
+ plan := profile.ToVisionaryPlan("discord")
+
+ require.NotNil(t, plan)
+ assert.Equal(t, "discord", plan.APIName)
+ assert.Equal(t, "high", plan.Identity.DataProfile.Volume)
+ assert.Equal(t, "high", plan.Identity.DataProfile.SearchNeed)
+ assert.True(t, plan.Identity.DataProfile.Realtime)
+
+ areas := make(map[string]string)
+ for _, decision := range plan.Architecture {
+ areas[decision.Area] = decision.NeedLevel
+ }
+ assert.Equal(t, "high", areas["persistence"])
+ assert.Equal(t, "high", areas["search"])
+ assert.Equal(t, "high", areas["realtime"])
+
+ featureTemplates := make(map[string][]string)
+ for _, feature := range plan.Features {
+ featureTemplates[feature.Name] = feature.TemplateNames
+ assert.GreaterOrEqual(t, feature.TotalScore, 8)
+ }
+ assert.Equal(t, []string{"sync.go.tmpl"}, featureTemplates["sync"])
+ assert.Equal(t, []string{"search.go.tmpl"}, featureTemplates["search"])
+ assert.Equal(t, []string{"store.go.tmpl"}, featureTemplates["store"])
+ assert.Equal(t, []string{"tail.go.tmpl"}, featureTemplates["tail"])
+ assert.Equal(t, []string{"analytics.go.tmpl"}, featureTemplates["analytics"])
+}
+
+func petstoreSpec() *spec.APISpec {
+ return &spec.APISpec{
+ Name: "petstore",
+ Resources: map[string]spec.Resource{
+ "pets": {
+ Endpoints: map[string]spec.Endpoint{
+ "list": {
+ Method: "GET",
+ Path: "/pets",
+ Response: spec.ResponseDef{Type: "array"},
+ },
+ "get": {
+ Method: "GET",
+ Path: "/pets/{petId}",
+ Response: spec.ResponseDef{Type: "object"},
+ },
+ "create": {
+ Method: "POST",
+ Path: "/pets",
+ Body: []spec.Param{
+ {Name: "name", Type: "string"},
+ {Name: "status", Type: "string", Enum: []string{"available", "pending", "sold"}},
+ },
+ },
+ "update": {
+ Method: "PUT",
+ Path: "/pets/{petId}",
+ Body: []spec.Param{
+ {Name: "name", Type: "string"},
+ },
+ },
+ "delete": {
+ Method: "DELETE",
+ Path: "/pets/{petId}",
+ },
+ "findByStatus": {
+ Method: "GET",
+ Path: "/pets/findByStatus",
+ Response: spec.ResponseDef{Type: "array"},
+ Params: []spec.Param{
+ {Name: "status", Type: "string"},
+ },
+ },
+ },
+ },
+ "store": {
+ Endpoints: map[string]spec.Endpoint{
+ "inventory": {
+ Method: "GET",
+ Path: "/store/inventory",
+ Response: spec.ResponseDef{Type: "object"},
+ },
+ "order": {
+ Method: "POST",
+ Path: "/store/order",
+ Body: []spec.Param{
+ {Name: "pet_id", Type: "integer"},
+ },
+ },
+ },
+ },
+ "user": {
+ Endpoints: map[string]spec.Endpoint{
+ "list": {
+ Method: "GET",
+ Path: "/users",
+ Response: spec.ResponseDef{Type: "array"},
+ },
+ "get": {
+ Method: "GET",
+ Path: "/users/{username}",
+ Response: spec.ResponseDef{Type: "object"},
+ },
+ "create": {
+ Method: "POST",
+ Path: "/users",
+ Body: []spec.Param{
+ {Name: "username", Type: "string"},
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func minimalSpec() *spec.APISpec {
+ return &spec.APISpec{
+ Name: "minimal",
+ Resources: map[string]spec.Resource{
+ "widgets": {
+ Endpoints: map[string]spec.Endpoint{
+ "list": {
+ Method: "GET",
+ Path: "/widgets",
+ Response: spec.ResponseDef{Type: "array"},
+ },
+ "get": {
+ Method: "GET",
+ Path: "/widgets/{widgetId}",
+ Response: spec.ResponseDef{Type: "object"},
+ },
+ },
+ },
+ },
+ }
+}
+
+func discordSpec() *spec.APISpec {
+ paginatedList := func(path string) spec.Endpoint {
+ return spec.Endpoint{
+ Method: "GET",
+ Path: path,
+ Response: spec.ResponseDef{Type: "array"},
+ Pagination: &spec.Pagination{Type: "cursor", LimitParam: "limit", CursorParam: "before"},
+ }
+ }
+
+ return &spec.APISpec{
+ Name: "discord",
+ Resources: map[string]spec.Resource{
+ "guilds": {
+ Endpoints: map[string]spec.Endpoint{
+ "list": paginatedList("/guilds"),
+ "get": {
+ Method: "GET",
+ Path: "/guilds/{guild_id}",
+ Response: spec.ResponseDef{Type: "object"},
+ },
+ "create": {
+ Method: "POST",
+ Path: "/guilds",
+ Body: []spec.Param{
+ {Name: "name", Type: "string"},
+ {Name: "region", Type: "string"},
+ {Name: "status", Type: "string", Enum: []string{"active", "archived", "deleted"}},
+ },
+ },
+ "update": {
+ Method: "PATCH",
+ Path: "/guilds/{guild_id}",
+ Body: []spec.Param{
+ {Name: "name", Type: "string"},
+ {Name: "state", Type: "string", Enum: []string{"draft", "active", "paused"}},
+ },
+ },
+ "delete": {
+ Method: "DELETE",
+ Path: "/guilds/{guild_id}",
+ },
+ },
+ },
+ "channels": {
+ Endpoints: map[string]spec.Endpoint{
+ "list": paginatedList("/channels"),
+ "create": {
+ Method: "POST",
+ Path: "/channels",
+ Body: []spec.Param{
+ {Name: "guild_id", Type: "string"},
+ {Name: "name", Type: "string"},
+ {Name: "topic", Type: "string"},
+ },
+ },
+ },
+ },
+ "messages": {
+ Endpoints: map[string]spec.Endpoint{
+ "list": paginatedList("/channels/{channel_id}/messages"),
+ "create": {
+ Method: "POST",
+ Path: "/channels/{channel_id}/messages",
+ Body: []spec.Param{
+ {Name: "channel_id", Type: "string"},
+ {Name: "author_id", Type: "string"},
+ {Name: "content", Type: "string"},
+ {Name: "title", Type: "string"},
+ {Name: "summary", Type: "string"},
+ {Name: "content_type", Type: "string"},
+ {Name: "visibility", Type: "string"},
+ {Name: "status", Type: "string", Enum: []string{"draft", "queued", "sent"}},
+ {Name: "thread_id", Type: "string"},
+ {Name: "reply_to_id", Type: "string"},
+ {Name: "embed_title", Type: "string"},
+ {Name: "embed_description", Type: "string"},
+ },
+ },
+ "upload": {
+ Method: "POST",
+ Path: "/channels/{channel_id}/attachments",
+ Body: []spec.Param{
+ {Name: "channel_id", Type: "string"},
+ {Name: "file", Type: "file", Format: "binary"},
+ },
+ },
+ },
+ },
+ "members": {
+ Endpoints: map[string]spec.Endpoint{
+ "list": paginatedList("/guilds/{guild_id}/members"),
+ "create": {
+ Method: "POST",
+ Path: "/guilds/{guild_id}/members",
+ Body: []spec.Param{
+ {Name: "guild_id", Type: "string"},
+ {Name: "user_id", Type: "string"},
+ {Name: "nick", Type: "string"},
+ },
+ },
+ },
+ },
+ "roles": {
+ Endpoints: map[string]spec.Endpoint{
+ "list": paginatedList("/guilds/{guild_id}/roles"),
+ "create": {
+ Method: "POST",
+ Path: "/guilds/{guild_id}/roles",
+ Body: []spec.Param{
+ {Name: "guild_id", Type: "string"},
+ {Name: "name", Type: "string"},
+ },
+ },
+ },
+ },
+ "threads": {
+ Endpoints: map[string]spec.Endpoint{
+ "list": paginatedList("/channels/{channel_id}/threads"),
+ "create": {
+ Method: "POST",
+ Path: "/channels/{channel_id}/threads",
+ Body: []spec.Param{
+ {Name: "channel_id", Type: "string"},
+ {Name: "name", Type: "string"},
+ },
+ },
+ },
+ },
+ "reactions": {
+ Endpoints: map[string]spec.Endpoint{
+ "list": paginatedList("/channels/{channel_id}/messages/{message_id}/reactions"),
+ "create": {
+ Method: "POST",
+ Path: "/channels/{channel_id}/messages/{message_id}/reactions",
+ Body: []spec.Param{
+ {Name: "channel_id", Type: "string"},
+ {Name: "message_id", Type: "string"},
+ {Name: "emoji", Type: "string"},
+ },
+ },
+ },
+ },
+ "webhooks": {
+ Endpoints: map[string]spec.Endpoint{
+ "list": paginatedList("/webhooks"),
+ "create": {
+ Method: "POST",
+ Path: "/webhooks",
+ Body: []spec.Param{
+ {Name: "channel_id", Type: "string"},
+ {Name: "callback_url", Type: "string"},
+ },
+ },
+ },
+ },
+ "audit-logs": {
+ Endpoints: map[string]spec.Endpoint{
+ "list": {
+ Method: "GET",
+ Path: "/guilds/{guild_id}/audit-logs",
+ Response: spec.ResponseDef{Type: "array"},
+ Pagination: &spec.Pagination{Type: "cursor", LimitParam: "limit", CursorParam: "before"},
+ Params: []spec.Param{
+ {Name: "before", Type: "string", Description: "Return entries before this timestamp"},
+ {Name: "sort", Type: "string", Description: "Sort by created timestamp descending"},
+ },
+ },
+ },
+ },
+ "notifications": {
+ Endpoints: map[string]spec.Endpoint{
+ "list": paginatedList("/users/{user_id}/notifications"),
+ "create": {
+ Method: "POST",
+ Path: "/users/{user_id}/notifications",
+ Body: []spec.Param{
+ {Name: "user_id", Type: "string"},
+ {Name: "message", Type: "string"},
+ },
+ },
+ },
+ },
+ },
+ }
+}
← 5c6840d5 fix(scorecard): replace presence checks with quality-based s
·
back to Cli Printing Press
·
feat(generator): wire vision templates into Generate() bba88f65 →