diff --git a/cli/cmd/yapi/docs.go b/cli/cmd/yapi/docs.go new file mode 100644 index 00000000..e49f99f8 --- /dev/null +++ b/cli/cmd/yapi/docs.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + "yapi.run/cli/internal/docs" +) + +// TODO: Consolidate cobra-generated command docs (--help) with manual topic docs +// in internal/docs/topics/ so there's a single source of truth for all documentation. +func docsE(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return printTopicIndex() + } + return printTopic(args[0]) +} + +func printTopicIndex() error { + fmt.Println("Available documentation topics:") + fmt.Println() + for _, t := range docs.List() { + fmt.Printf(" yapi docs %-15s %s\n", t.Name, t.Summary) + } + fmt.Println() + fmt.Println("Run 'yapi docs ' to read a topic.") + return nil +} + +func printTopic(name string) error { + content, err := docs.Get(name) + if err != nil { + // Try fuzzy suggestion + if suggestion := docs.Suggest(name); suggestion != "" { + fmt.Fprintf(os.Stderr, "Unknown topic %q. Did you mean %q?\n\n", name, suggestion) + } else { + fmt.Fprintf(os.Stderr, "Unknown topic %q.\n\n", name) + } + fmt.Fprintf(os.Stderr, "Available topics: %s\n", strings.Join(docs.TopicNames(), ", ")) + return fmt.Errorf("unknown topic %q", name) + } + rendered, err := docs.Render(content) + if err != nil { + return fmt.Errorf("rendering docs: %w", err) + } + fmt.Print(rendered) + return nil +} diff --git a/cli/cmd/yapi/main.go b/cli/cmd/yapi/main.go index bb935e2f..7f8e0c31 100644 --- a/cli/cmd/yapi/main.go +++ b/cli/cmd/yapi/main.go @@ -96,6 +96,7 @@ func main() { About: aboutE, Import: importE, Send: app.sendE, + Docs: docsE, } rootCmd := commands.BuildRoot(cfg, handlers) @@ -111,7 +112,7 @@ func main() { rootCmd.PersistentPostRun = func(cmd *cobra.Command, args []string) { // Log command to history (skip meta commands) switch cmd.Name() { - case "history", "version", "lsp", "help", "yapi", "about": + case "history", "version", "lsp", "help", "yapi", "about", "docs": return } logHistoryCmd(reconstructCommand(cmd, args)) diff --git a/cli/go.mod b/cli/go.mod index f1811253..c174dbc1 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -7,7 +7,7 @@ require ( github.com/alecthomas/chroma/v2 v2.20.0 github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/lipgloss v1.1.0 + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 github.com/fullstorydev/grpcurl v1.9.3 github.com/go-git/go-git/v5 v5.16.4 github.com/golang/protobuf v1.5.4 @@ -32,10 +32,13 @@ require ( github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/bufbuild/protocompile v0.14.1 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/glamour v0.10.0 // indirect github.com/charmbracelet/x/ansi v0.10.1 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect @@ -50,6 +53,7 @@ require ( github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -59,8 +63,10 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect @@ -77,6 +83,8 @@ require ( github.com/tliron/go-kutil v0.4.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.7.8 // indirect + github.com/yuin/goldmark-emoji v1.0.5 // indirect golang.org/x/crypto v0.43.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.38.0 // indirect diff --git a/cli/go.sum b/cli/go.sum index 8423647e..86f9b4b6 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -21,6 +21,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= @@ -29,12 +31,20 @@ github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlv github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= +github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= @@ -87,6 +97,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= @@ -125,12 +137,17 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= @@ -146,6 +163,7 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -188,6 +206,11 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= +github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= diff --git a/cli/internal/cli/commands/commands.go b/cli/internal/cli/commands/commands.go index b2239911..43c4df18 100644 --- a/cli/internal/cli/commands/commands.go +++ b/cli/internal/cli/commands/commands.go @@ -30,6 +30,7 @@ type Handlers struct { About func(cmd *cobra.Command, args []string) error Import func(cmd *cobra.Command, args []string) error Send func(cmd *cobra.Command, args []string) error + Docs func(cmd *cobra.Command, args []string) error } // BuildRoot builds the root command tree with optional handlers. @@ -42,6 +43,7 @@ func BuildRoot(cfg *Config, handlers *Handlers) *cobra.Command { rootCmd := &cobra.Command{ Use: "yapi", Short: "yapi is a unified API client for HTTP, gRPC, and TCP", + Long: "yapi is a unified API client for HTTP, gRPC, GraphQL, and TCP.\n\nRun 'yapi docs' to browse topic-based documentation.", SilenceUsage: true, SilenceErrors: true, Run: func(cmd *cobra.Command, args []string) {}, @@ -70,6 +72,7 @@ var cmdManifest = []CommandSpec{ { Use: "run [file]", Short: "Run a request defined in a yapi config file (reads from stdin if no file specified)", + Long: "Run a request defined in a yapi config file (reads from stdin if no file specified).\n\nRelated: yapi docs assert, yapi docs chain, yapi docs variables", Args: cobra.MaximumNArgs(1), Flags: []FlagSpec{ {Name: "env", Shorthand: "e", Type: "string", Default: "", Usage: "Target environment from yapi.config.yml"}, @@ -125,6 +128,7 @@ var cmdManifest = []CommandSpec{ { Use: "test [directory]", Short: "Run all *.test.yapi, *.test.yapi.yml, *.test.yapi.yaml files in the current directory or specified directory", + Long: "Run all *.test.yapi, *.test.yapi.yml, *.test.yapi.yaml files in the current directory or specified directory.\n\nRelated: yapi docs testing, yapi docs assert", Args: cobra.MaximumNArgs(1), Flags: []FlagSpec{ {Name: "all", Shorthand: "a", Type: "bool", Default: false, Usage: "Run all *.yapi, *.yapi.yml, *.yapi.yaml files (not just test files)"}, @@ -168,7 +172,7 @@ var cmdManifest = []CommandSpec{ { Use: "send [body]", Short: "Send a quick request without a config file", - Long: "Send a one-off HTTP or TCP request directly from the command line.\nThe transport is auto-detected from the URL scheme (tcp://, grpc://, or HTTP by default).\n\nExamples:\n yapi send https://httpbin.org/get\n yapi send -X POST https://httpbin.org/post '{\"hello\":\"world\"}'\n yapi send tcp://localhost:9877 '{\"type\":\"health\",\"params\":{}}'", + Long: "Send a one-off HTTP or TCP request directly from the command line.\nThe transport is auto-detected from the URL scheme (tcp://, grpc://, or HTTP by default).\n\nExamples:\n yapi send https://httpbin.org/get\n yapi send -X POST https://httpbin.org/post '{\"hello\":\"world\"}'\n yapi send tcp://localhost:9877 '{\"type\":\"health\",\"params\":{}}'\n\nRelated: yapi docs send, yapi docs protocols", Args: cobra.RangeArgs(1, 2), Flags: []FlagSpec{ {Name: "method", Shorthand: "X", Type: "string", Default: "", Usage: "HTTP method (default: GET, or POST if body is provided)"}, @@ -178,6 +182,12 @@ var cmdManifest = []CommandSpec{ {Name: "jq", Type: "string", Default: "", Usage: "JQ filter to apply to the response"}, }, }, + { + Use: "docs [topic]", + Short: "Browse topic-based documentation", + Long: "Browse topic-based documentation for yapi features.\nRun 'yapi docs' to see all topics, or 'yapi docs ' to read one.", + Args: cobra.MaximumNArgs(1), + }, { Use: "import [file]", Short: "Import an external collection (Postman) to yapi format", @@ -231,6 +241,8 @@ func getHandler(h *Handlers, use string) func(*cobra.Command, []string) error { return h.About case "send": return h.Send + case "docs": + return h.Docs case "import": return h.Import default: diff --git a/cli/internal/docs/docs.go b/cli/internal/docs/docs.go new file mode 100644 index 00000000..d81d0b1d --- /dev/null +++ b/cli/internal/docs/docs.go @@ -0,0 +1,100 @@ +// Package docs provides embedded topic-based documentation for yapi. +package docs + +import ( + "embed" + "fmt" + "sort" + "strings" + + "github.com/charmbracelet/glamour" +) + +//go:embed topics +var topicsFS embed.FS + +// Topic represents a documentation topic. +type Topic struct { + Name string + Summary string +} + +// topics defines all available documentation topics. +var topics = []Topic{ + {Name: "assert", Summary: "Assertions on status, body, and headers"}, + {Name: "chain", Summary: "Multi-step request chaining"}, + {Name: "config", Summary: "YAML config field reference"}, + {Name: "environments", Summary: "Multi-environment configuration"}, + {Name: "jq", Summary: "JQ filtering and expressions"}, + {Name: "polling", Summary: "Polling with wait_for"}, + {Name: "protocols", Summary: "HTTP, gRPC, GraphQL, TCP"}, + {Name: "send", Summary: "Quick one-off requests"}, + {Name: "testing", Summary: "Test runner and CI/CD"}, + {Name: "variables", Summary: "Variable interpolation and resolution"}, +} + +// Get returns the content of a topic by name. +func Get(name string) (string, error) { + name = strings.ToLower(strings.TrimSpace(name)) + data, err := topicsFS.ReadFile("topics/" + name + ".md") + if err != nil { + return "", fmt.Errorf("unknown topic %q. Run 'yapi docs' to see available topics", name) + } + return string(data), nil +} + +// List returns all available topics. +func List() []Topic { + return topics +} + +// Suggest returns the closest topic name if one is similar enough, or empty string. +func Suggest(input string) string { + input = strings.ToLower(strings.TrimSpace(input)) + // Check prefix matches + var matches []string + for _, t := range topics { + if strings.HasPrefix(t.Name, input) { + matches = append(matches, t.Name) + } + } + if len(matches) == 1 { + return matches[0] + } + // Check substring matches + matches = matches[:0] + for _, t := range topics { + if strings.Contains(t.Name, input) || strings.Contains(input, t.Name) { + matches = append(matches, t.Name) + } + } + if len(matches) == 1 { + return matches[0] + } + // Check summary substring + matches = matches[:0] + for _, t := range topics { + if strings.Contains(strings.ToLower(t.Summary), input) { + matches = append(matches, t.Name) + } + } + if len(matches) == 1 { + return matches[0] + } + return "" +} + +// TopicNames returns a sorted list of topic names. +func TopicNames() []string { + names := make([]string, len(topics)) + for i, t := range topics { + names[i] = t.Name + } + sort.Strings(names) + return names +} + +// Render renders markdown content for terminal display using glamour. +func Render(markdown string) (string, error) { + return glamour.Render(markdown, "auto") +} diff --git a/cli/internal/docs/topics/assert.md b/cli/internal/docs/topics/assert.md new file mode 100644 index 00000000..98ee788f --- /dev/null +++ b/cli/internal/docs/topics/assert.md @@ -0,0 +1,130 @@ +# Assertions + +Assertions let you validate HTTP responses — status codes, body content, and headers. +They're the core of yapi's testing capability. + +## Status Expectations + +```yaml +expect: + status: 200 # Exact match + status: [200, 201, 204] # Any of these +``` + +## Body Assertions + +Body assertions are JQ expressions that must evaluate to `true`: + +```yaml +expect: + status: 200 + assert: + - .id != null + - .email != null + - .active == true +``` + +### Operators + +All JQ comparison operators work: `==`, `!=`, `>`, `>=`, `<`, `<=` + +```yaml +assert: + - . | length > 0 # Array has items + - .count >= 10 # Numeric comparison + - .name != "" # Not empty string +``` + +### Array Operations + +```yaml +assert: + - . | type == "array" + - . | length > 0 + - .[0].name != null # First element + - .[] | .status == "active" # All items match +``` + +### Type Checks + +```yaml +assert: + - . | type == "array" + - .data | type == "object" + - .count | type == "number" +``` + +## Environment Variable References + +Compare response values against environment variables using `env.VAR_NAME`: + +```yaml +assert: + - .owner.login == env.GITHUB_USER + - .email == env.EXPECTED_EMAIL +``` + +## Header Assertions + +Use the grouped syntax to assert on response headers: + +```yaml +expect: + status: 200 + assert: + headers: + - .["content-type"] | startswith("application/json") + - .["x-request-id"] != null + body: + - .id != null +``` + +When using the grouped syntax, body assertions go under `body:`. + +## Flat vs Grouped Syntax + +**Flat** (all assertions are body assertions): +```yaml +assert: + - .id != null +``` + +**Grouped** (separate body and header assertions): +```yaml +assert: + headers: + - .["content-type"] | contains("json") + body: + - .id != null +``` + +## Assertions in Chains + +Each chain step can have its own `expect` block: + +```yaml +chain: + - name: create + url: /api/items + method: POST + body: { title: "test" } + expect: + status: 201 + assert: + - .id != null + + - name: verify + url: /api/items/${create.id} + method: GET + expect: + status: 200 + assert: + - .title == "test" +``` + +## See Also + +- `yapi docs chain` — Multi-step request chaining +- `yapi docs jq` — JQ filtering and expressions +- `yapi docs variables` — Variable interpolation +- `yapi docs testing` — Test runner and CI/CD diff --git a/cli/internal/docs/topics/chain.md b/cli/internal/docs/topics/chain.md new file mode 100644 index 00000000..0ba67324 --- /dev/null +++ b/cli/internal/docs/topics/chain.md @@ -0,0 +1,112 @@ +# Request Chaining + +Chains let you execute multiple requests sequentially, passing data between steps. +Login, use the token, verify the result — all in one file. + +## Basic Chain + +```yaml +yapi: v1 +chain: + - name: login + url: https://api.example.com/auth/login + method: POST + body: + username: ${USERNAME} + password: ${PASSWORD} + expect: + status: 200 + assert: + - .token != null + + - name: get_profile + url: https://api.example.com/me + method: GET + headers: + Authorization: Bearer ${login.token} + expect: + status: 200 +``` + +## Step References + +Reference data from previous steps with `${step_name.field}`: + +```yaml +- name: create_user + url: /api/users + method: POST + body: { name: "Alice" } + expect: + assert: + - .id != null + +- name: get_user + url: /api/users/${create_user.id} + method: GET +``` + +### Nested Paths + +Access nested JSON fields with dot notation: + +```yaml +${step_name.data.user.email} # Nested object +${step_name.items[0].id} # Array indexing +``` + +### Where References Work + +Step references work in any string field: URLs, headers, body values, assertions. + +```yaml +- name: verify + url: /api/users/${create.id} + headers: + Authorization: Bearer ${login.token} + expect: + assert: + - .email == env.EXPECTED_EMAIL +``` + +## Type Preservation + +When a `${ref}` is the entire value (not part of a larger string), its type is preserved: + +```yaml +body: + user_id: ${get_user.id} # Stays an integer + name: "User ${get_user.id}" # String interpolation +``` + +## Fail-Fast Behavior + +Chains stop on the first failure: +- If a request fails (network error, timeout), the chain stops +- If an assertion fails, the chain stops +- Subsequent steps are skipped + +## Step Config Inheritance + +Each step can override any base-level config field. Steps inherit from the +top-level config (URL, headers, timeout, etc.): + +```yaml +yapi: v1 +timeout: 10s # Default for all steps + +chain: + - name: fast_check + url: /health + timeout: 2s # Override for this step + + - name: slow_op + url: /process + timeout: 30s # Override for this step +``` + +## See Also + +- `yapi docs assert` — Assertions on status, body, and headers +- `yapi docs variables` — Variable interpolation and resolution +- `yapi docs polling` — Polling with wait_for diff --git a/cli/internal/docs/topics/config.md b/cli/internal/docs/topics/config.md new file mode 100644 index 00000000..1b769786 --- /dev/null +++ b/cli/internal/docs/topics/config.md @@ -0,0 +1,140 @@ +# Config Reference + +Every yapi request file starts with `yapi: v1`. This page lists all available fields. + +## Project Layout + +Place a `yapi.config.yml` at your project root to define environments and base URLs. Request files (`.yapi.yml`) can live anywhere beneath it — yapi walks up the directory tree to find the nearest config. + +``` +my-project/ +├── yapi.config.yml # project config (environments, base URLs) +└── yapi/ + ├── homepage.yapi.yml # GET / + ├── sitemap.yapi.yml # GET /sitemap.xml + └── health.yapi.yml # GET /healthz +``` + +### Example: `yapi.config.yml` + +```yaml +yapi: v1 + +default_environment: local + +environments: + local: + url: http://localhost:3000 + prod: + url: https://api.example.com +``` + +### Example: `yapi/homepage.yapi.yml` + +```yaml +yapi: v1 +path: / +method: GET + +headers: + User-Agent: yapi-cli + +expect: + status: 200 +``` + +Because the request file uses `path: /` instead of a full `url`, yapi resolves it against the active environment's base URL. Running `yapi run yapi/homepage.yapi.yml` hits `http://localhost:3000/` by default, or `https://api.example.com/` with `-e prod`. + +## Request Fields + +| Field | Type | Description | +|---|---|---| +| `yapi` | string | **Required.** Version tag. Always `v1`. | +| `url` | string | Full request URL | +| `path` | string | Path appended to environment base URL | +| `method` | string | HTTP method: GET, POST, PUT, PATCH, DELETE | +| `headers` | map | Request headers | +| `query` | map | Query parameters | +| `timeout` | string | Request timeout (e.g., `"4s"`, `"100ms"`) | +| `delay` | string | Wait before executing (e.g., `"5s"`) | +| `insecure` | bool | Skip TLS verification | + +`url` and `path` are mutually exclusive. Use `path` when `yapi.config.yml` provides a base URL. + +## Body Fields + +These are mutually exclusive — use only one: + +| Field | Type | Description | +|---|---|---| +| `body` | map | JSON object body | +| `json` | string | Raw JSON string body | +| `form` | map | Form-encoded body | + +## Response Processing + +| Field | Type | Description | +|---|---|---| +| `jq_filter` | string | JQ expression to transform response | +| `output_file` | string | Save response body to file | +| `content_type` | string | Override content type | + +## GraphQL Fields + +| Field | Type | Description | +|---|---|---| +| `graphql` | string | GraphQL query or mutation | +| `variables` | map | GraphQL variables | + +## gRPC Fields + +| Field | Type | Description | +|---|---|---| +| `service` | string | gRPC service name | +| `rpc` | string | RPC method name | +| `proto` | string | Path to .proto file | +| `proto_path` | string | Proto import path | +| `plaintext` | bool | No TLS for gRPC | + +## TCP Fields + +| Field | Type | Description | +|---|---|---| +| `data` | string | Raw data to send | +| `encoding` | string | `text` (default), `hex`, `base64` | +| `read_timeout` | int | Seconds to wait for response | +| `idle_timeout` | int | Milliseconds before response is considered complete | +| `close_after_send` | bool | Close connection after sending | + +## Testing Fields + +| Field | Type | Description | +|---|---|---| +| `expect` | object | Status and assertion expectations | +| `expect.status` | int/[]int | Expected status code(s) | +| `expect.assert` | list/map | Body and header assertions | +| `wait_for` | object | Polling configuration | +| `chain` | list | Multi-step request chain | + +## Environment Fields + +| Field | Type | Description | +|---|---|---| +| `env_files` | []string | Paths to .env files to load | + +## Project Config (`yapi.config.yml`) + +| Field | Type | Description | +|---|---|---| +| `default_environment` | string | Environment used without `-e` | +| `defaults.vars` | map | Variables for all environments | +| `environments` | map | Environment definitions | +| `environments.{name}.url` | string | Base URL | +| `environments.{name}.vars` | map | Environment-specific variables | +| `environments.{name}.env_file` | string | .env file path | + +## See Also + +- `yapi docs protocols` — Protocol-specific details and examples +- `yapi docs variables` — Variable interpolation +- `yapi docs assert` — Assertion syntax diff --git a/cli/internal/docs/topics/environments.md b/cli/internal/docs/topics/environments.md new file mode 100644 index 00000000..162aa5db --- /dev/null +++ b/cli/internal/docs/topics/environments.md @@ -0,0 +1,101 @@ +# Environments + +Environments let you switch between local, staging, and production configs +without changing your request files. + +## Project Config File + +Create `yapi.config.yml` in your project root: + +```yaml +yapi: v1 + +default_environment: local + +defaults: + vars: + API_VERSION: v1 + SHARED_VAR: shared_value + +environments: + local: + url: http://localhost:3000 + vars: + API_KEY: dev_key + DEBUG: "true" + + staging: + url: https://staging.api.example.com + vars: + API_KEY: ${STAGING_API_KEY} + + prod: + url: https://api.example.com + vars: + API_KEY: ${PROD_API_KEY} + env_file: .env.prod +``` + +## Key Fields + +- **`default_environment`**: Used when `-e` flag is not specified +- **`defaults`**: Variables available in ALL environments +- **`environments`**: Environment-specific settings + - **`url`**: Base URL for requests using `path:` instead of `url:` + - **`vars`**: Environment-specific variables + - **`env_file`**: Path to a `.env` file to load + +## Selecting an Environment + +```bash +yapi run request.yapi.yml -e staging +yapi test ./tests -e prod +``` + +Without `-e`, the `default_environment` is used. + +## How URL Resolution Works + +Request files can use `path:` instead of `url:`: + +```yaml +# request.yapi.yml +yapi: v1 +path: /api/users +method: GET +``` + +The `path` is appended to the environment's `url`. So with `-e local`, +the full URL becomes `http://localhost:3000/api/users`. + +## Variable Precedence + +When the same variable is defined in multiple places: + +1. Chain step references (highest priority) +2. Environment-specific `vars` +3. `defaults.vars` +4. Shell environment variables +5. Default values (`${VAR:-default}`) + +## Env Files + +Load secrets from `.env` files per environment: + +```yaml +environments: + prod: + env_file: .env.prod +``` + +``` +# .env.prod +API_KEY=sk-prod-abc123 +DB_URL=postgres://prod-host/db +``` + +## See Also + +- `yapi docs variables` — Variable interpolation and resolution +- `yapi docs config` — Full YAML config field reference +- `yapi docs testing` — Running tests across environments diff --git a/cli/internal/docs/topics/jq.md b/cli/internal/docs/topics/jq.md new file mode 100644 index 00000000..7abee7b7 --- /dev/null +++ b/cli/internal/docs/topics/jq.md @@ -0,0 +1,86 @@ +# JQ Filtering + +JQ lets you filter and transform JSON responses inline — +both for display (`jq_filter`) and for validation (`assert`). + +## Response Filtering + +Use `jq_filter` to transform what gets displayed: + +```yaml +yapi: v1 +url: https://api.example.com/users +method: GET +jq_filter: '[.[] | {name, email}] | sort_by(.name)' +``` + +This filters the response before printing, so you only see what matters. + +## JQ in Assertions + +Assertions are JQ expressions that must evaluate to `true`: + +```yaml +expect: + assert: + - .id != null + - . | length > 0 + - .data | type == "object" + - .[0].name | startswith("A") +``` + +## Common Patterns + +### Check array length +```yaml +assert: + - . | length > 0 + - .items | length == 10 +``` + +### Check type +```yaml +assert: + - . | type == "array" + - .data | type == "object" + - .count | type == "number" +``` + +### String operations +```yaml +assert: + - .name | startswith("test_") + - .email | endswith("@example.com") + - .url | contains("api") +``` + +### Check all items in array +```yaml +assert: + - .[] | .status == "active" # Every item matches + - [.[] | .score > 0] | all # All scores positive +``` + +### Select specific fields +```yaml +jq_filter: '{id, name, email}' # Single object +jq_filter: '[.[] | {id, name}]' # Array of objects +``` + +### Sort and limit +```yaml +jq_filter: 'sort_by(.created_at) | reverse | .[:5]' +``` + +## JQ with yapi send + +Apply JQ filters on the command line: + +```bash +yapi send https://api.example.com/users --jq '.[0].name' +``` + +## See Also + +- `yapi docs assert` — Assertions on status, body, and headers +- `yapi docs send` — Quick one-off requests with --jq flag diff --git a/cli/internal/docs/topics/polling.md b/cli/internal/docs/topics/polling.md new file mode 100644 index 00000000..6e770862 --- /dev/null +++ b/cli/internal/docs/topics/polling.md @@ -0,0 +1,110 @@ +# Polling + +The `wait_for` block lets you poll an endpoint until conditions are met. +Useful for async operations, health checks, and eventual consistency. + +## Basic Polling + +```yaml +yapi: v1 +url: https://api.example.com/jobs/123 +method: GET + +wait_for: + until: + - .status == "completed" + period: 2s + timeout: 30s +``` + +This sends GET requests every 2 seconds until `.status == "completed"` or 30 seconds elapse. + +## Fields + +- **`until`**: Array of JQ assertions. ALL must pass to stop polling. +- **`period`**: Fixed delay between attempts (e.g., `"1s"`, `"500ms"`). +- **`backoff`**: Exponential backoff (mutually exclusive with `period`). +- **`timeout`**: Maximum wait time. Default: `30s`. + +## Fixed Period + +Retry at a constant interval: + +```yaml +wait_for: + until: + - .ready == true + period: 1s + timeout: 60s +``` + +## Exponential Backoff + +Start with a short delay, grow exponentially: + +```yaml +wait_for: + until: + - .status == "done" + backoff: + seed: 1s # First wait: 1s + multiplier: 2 # Then 2s, 4s, 8s, 16s... + timeout: 60s +``` + +The delay doubles each attempt: seed, seed*multiplier, seed*multiplier^2, etc. +`multiplier` must be greater than 1. + +## Multiple Conditions + +All `until` conditions must pass simultaneously: + +```yaml +wait_for: + until: + - .status == "completed" + - .result != null + - .errors | length == 0 + period: 2s + timeout: 30s +``` + +## Polling in Chains + +Each chain step can have its own `wait_for`: + +```yaml +chain: + - name: start_job + url: /api/jobs + method: POST + body: { task: "process" } + expect: + status: 202 + assert: + - .job_id != null + + - name: wait_done + url: /api/jobs/${start_job.job_id} + method: GET + wait_for: + until: + - .status == "completed" + period: 2s + timeout: 60s + expect: + assert: + - .result != null +``` + +## Behavior + +- If the request itself fails (network error), polling continues until timeout +- If assertions don't pass, polling retries +- On timeout, the test fails with an error +- On success, execution continues normally (chain proceeds to next step) + +## See Also + +- `yapi docs assert` — Assertions used in until conditions +- `yapi docs chain` — Multi-step request chaining diff --git a/cli/internal/docs/topics/protocols.md b/cli/internal/docs/topics/protocols.md new file mode 100644 index 00000000..5f28c22c --- /dev/null +++ b/cli/internal/docs/topics/protocols.md @@ -0,0 +1,119 @@ +# Protocols + +yapi supports HTTP, gRPC, GraphQL, and TCP as first-class protocols. +Transport is detected from the URL scheme and config fields. + +## HTTP / REST + +The default protocol. Standard request/response: + +```yaml +yapi: v1 +url: https://api.example.com/users +method: POST +content_type: application/json +headers: + Authorization: Bearer ${TOKEN} +body: + name: "Alice" + email: "alice@example.com" +expect: + status: 201 +``` + +### Form Data + +```yaml +yapi: v1 +url: https://example.com/login +method: POST +form: + username: admin + password: ${PASSWORD} +``` + +## GraphQL + +Detected by the `graphql:` field: + +```yaml +yapi: v1 +url: https://countries.trevorblades.com/graphql + +graphql: | + query getCountry($code: ID!) { + country(code: $code) { + name + capital + } + } + +variables: + code: "US" +``` + +GraphQL requests are always POST. The `graphql` field contains your query or mutation, +and `variables` holds the GraphQL variables. + +## gRPC + +Detected by `grpc://` URL scheme. Requires server reflection or proto files: + +```yaml +yapi: v1 +url: grpc://localhost:50051 +service: helloworld.Greeter +rpc: SayHello + +body: + name: "World" +``` + +### With Proto Files + +```yaml +yapi: v1 +url: grpc://localhost:50051 +service: helloworld.Greeter +rpc: SayHello +proto: ./proto/helloworld.proto +proto_path: ./proto + +body: + name: "World" +``` + +### Insecure / Plaintext + +```yaml +plaintext: true # No TLS +insecure: true # Skip TLS verification +``` + +## TCP + +Detected by `tcp://` URL scheme. Raw socket communication: + +```yaml +yapi: v1 +url: tcp://localhost:9877 +data: '{"type":"health","params":{}}' +encoding: text # text (default), hex, base64 +read_timeout: 5 # Seconds to wait for response +idle_timeout: 500 # Milliseconds before considering response complete +close_after_send: false # Keep connection open to read response +``` + +## Auto-Detection Summary + +| Indicator | Protocol | +|---|---| +| `grpc://` URL | gRPC | +| `tcp://` URL | TCP | +| `graphql:` field present | GraphQL | +| Everything else | HTTP | + +## See Also + +- `yapi docs send` — Quick one-off requests (auto-detects protocol) +- `yapi docs config` — Full field reference for all protocols diff --git a/cli/internal/docs/topics/send.md b/cli/internal/docs/topics/send.md new file mode 100644 index 00000000..cd17af0c --- /dev/null +++ b/cli/internal/docs/topics/send.md @@ -0,0 +1,84 @@ +# Send + +`yapi send` makes quick one-off requests without a config file. +Think of it as curl with better defaults. + +## Basic Usage + +```bash +yapi send https://httpbin.org/get +yapi send https://httpbin.org/post '{"hello":"world"}' +``` + +## Method Detection + +- **No body**: defaults to GET +- **Body provided**: defaults to POST +- **Override with -X**: `yapi send -X PUT https://api.example.com/users/1 '{"name":"Bob"}'` + +## Headers + +```bash +yapi send -H "Authorization: Bearer token123" https://api.example.com/me +yapi send -H "Content-Type: text/plain" -H "X-Custom: value" https://example.com/data +``` + +## JQ Filtering + +Filter the response with `--jq`: + +```bash +yapi send https://api.example.com/users --jq '.[0].name' +yapi send https://api.example.com/users --jq '[.[] | {name, email}]' +``` + +## JSON Output + +Get structured JSON output with metadata: + +```bash +yapi send https://httpbin.org/get --json +``` + +## Protocol Auto-Detection + +The URL scheme determines the protocol: + +```bash +yapi send https://api.example.com/users # HTTP +yapi send tcp://localhost:9877 '{"type":"ping"}' # TCP +yapi send grpc://localhost:50051 # gRPC +``` + +## Verbose Mode + +See request and response details: + +```bash +yapi send -v https://httpbin.org/get +``` + +## Examples + +```bash +# GET request +yapi send https://jsonplaceholder.typicode.com/posts/1 + +# POST with JSON body +yapi send https://httpbin.org/post '{"key":"value"}' + +# PUT with headers +yapi send -X PUT https://api.example.com/items/1 '{"name":"updated"}' \ + -H "Authorization: Bearer ${TOKEN}" + +# Filter response +yapi send https://jsonplaceholder.typicode.com/users --jq '.[].name' + +# TCP raw message +yapi send tcp://localhost:9877 '{"type":"health","params":{}}' +``` + +## See Also + +- `yapi docs protocols` — Protocol-specific details +- `yapi docs jq` — JQ filtering and expressions diff --git a/cli/internal/docs/topics/testing.md b/cli/internal/docs/topics/testing.md new file mode 100644 index 00000000..827a4063 --- /dev/null +++ b/cli/internal/docs/topics/testing.md @@ -0,0 +1,124 @@ +# Testing + +yapi has a built-in test runner for running assertion-based API tests, +with parallel execution and dev server management. + +## Test Files + +By default, `yapi test` runs files matching `*.test.yapi`, `*.test.yapi.yml`, +or `*.test.yapi.yaml`: + +```bash +yapi test # Current directory +yapi test ./tests # Specific directory +yapi test -a # All .yapi files, not just .test ones +``` + +## Writing Tests + +A test file is a normal yapi request file with `expect` assertions: + +```yaml +# users.test.yapi.yml +yapi: v1 +url: https://api.example.com/users +method: GET +expect: + status: 200 + assert: + - . | type == "array" + - . | length > 0 +``` + +Chain files work as tests too — each step's assertions are checked: + +```yaml +# auth-flow.test.yapi.yml +yapi: v1 +chain: + - name: login + url: /auth/login + method: POST + body: + email: ${TEST_EMAIL} + password: ${TEST_PASSWORD} + expect: + status: 200 + assert: + - .token != null + + - name: me + url: /auth/me + headers: + Authorization: Bearer ${login.token} + expect: + status: 200 +``` + +## Parallel Execution + +Run tests concurrently: + +```bash +yapi test -p 4 # 4 parallel threads +``` + +## Dev Server Management + +yapi can start your dev server and wait for it before running tests. + +### Via yapi.config.yml + +```yaml +# yapi.config.yml +yapi: v1 +default_environment: local + +test: + start: npm run dev + wait_on: + - http://localhost:3000/health + wait_timeout: 30s +``` + +### Via CLI Flags + +```bash +yapi test --start "npm run dev" --wait-on http://localhost:3000/health +yapi test --no-start # Skip server startup +``` + +The server is automatically stopped when tests finish or on Ctrl+C. + +## Verbose Output + +```bash +yapi test -v # Show detailed pass/fail per test +``` + +## Environment Selection + +```bash +yapi test -e staging # Run tests against staging environment +``` + +## GitHub Actions + +```yaml +- uses: jamierpond/yapi/action@0.X.X + with: + start: npm run dev + wait-on: http://localhost:3000/health + command: yapi test ./tests -a +``` + +## Exit Codes + +- `0`: All tests passed +- `1`: One or more tests failed + +## See Also + +- `yapi docs assert` — Assertions on status, body, and headers +- `yapi docs chain` — Multi-step request chaining +- `yapi docs environments` — Running tests across environments diff --git a/cli/internal/docs/topics/variables.md b/cli/internal/docs/topics/variables.md new file mode 100644 index 00000000..311161c1 --- /dev/null +++ b/cli/internal/docs/topics/variables.md @@ -0,0 +1,109 @@ +# Variables + +Variables let you parameterize requests — URLs, headers, bodies, assertions. +Use `${VAR}` syntax anywhere in your YAML files. + +## Syntax + +```yaml +url: ${BASE_URL}/api/users/${USER_ID} +headers: + Authorization: Bearer ${API_KEY} +``` + +## Default Values + +Provide fallbacks with `:-`: + +```yaml +url: ${BASE_URL:-http://localhost:3000}/api/users +headers: + X-Timeout: ${TIMEOUT:-30} +``` + +If `BASE_URL` is not set, `http://localhost:3000` is used. + +## Resolution Order + +Variables resolve in this priority (highest first): + +1. **Chain step references**: `${step_name.field}` — data from previous chain steps +2. **Environment vars from yapi.config.yml** — vars defined in the active environment +3. **Shell environment variables** — from your OS/shell +4. **Default values** — specified with `:-` + +## Type Preservation + +When `${VAR}` is the entire value, the original type is preserved: + +```yaml +body: + count: ${step.count} # Stays an integer if count is int + label: "Count: ${step.count}" # String interpolation +``` + +## Environment Variable References in Assertions + +In assertions, use `env.VAR_NAME` to compare against environment variables: + +```yaml +expect: + assert: + - .owner == env.GITHUB_USER + - .region == env.AWS_REGION +``` + +## Variables from Config + +Define variables in `yapi.config.yml`: + +```yaml +yapi: v1 +default_environment: local + +defaults: + vars: + API_VERSION: v1 + +environments: + local: + url: http://localhost:3000 + vars: + API_KEY: dev_key +``` + +Then reference them in request files: + +```yaml +yapi: v1 +url: ${url}/api/${API_VERSION}/users +headers: + X-Api-Key: ${API_KEY} +``` + +## Env Files + +Load variables from `.env` files: + +```yaml +# yapi.config.yml +environments: + prod: + url: https://api.example.com + env_file: .env.prod +``` + +Or per-request: + +```yaml +yapi: v1 +env_files: + - .env.local +url: ${BASE_URL}/api/users +``` + +## See Also + +- `yapi docs environments` — Multi-environment configuration +- `yapi docs chain` — Chain step references +- `yapi docs assert` — Using env vars in assertions