diff --git a/README.md b/README.md index b4a39864..f1372c7c 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,61 @@ make install ----- +## 🧠 Editor Integration (LSP) + +Unlike other API clients, **yapi** ships with a **full LSP implementation** out of the box. Your editor becomes an intelligent API development environment with real-time validation, autocompletion, and inline execution. + +### VS Code & Cursor + +Install the official extension from [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=yapi.yapi-extension) or [Open VSX](https://open-vsx.org/extension/yapi/yapi-extension): + +**Features:** +- **Run with `Cmd+Enter`** (Mac) or `Ctrl+Enter` (Windows/Linux) - execute requests without leaving your editor +- **Inline results panel** - see responses, headers, and timing right in VS Code +- **Real-time validation** - errors and warnings as you type +- **Intelligent autocompletion** - context-aware suggestions for keys, methods, and variables +- **Hover info** - hover over `${VAR}` to see environment variable status + +The extension automatically detects `.yapi.yml` files and activates the language server. No configuration needed. + +### Neovim (Native Plugin) + +**yapi** was built with Neovim in mind. First-class support via `lua/yapi_nvim`: + +```lua +-- lazy.nvim +{ + dir = "~/path/to/yapi/lua/yapi_nvim", + config = function() + require("yapi_nvim").setup({ + lsp = true, -- Enables the yapi Language Server + pretty = true, -- Uses the TUI renderer in the popup + }) + end +} +``` + +Commands: +- `:YapiRun` - Execute the current buffer +- `:YapiWatch` - Open a split with live reload + +### Other Editors + +The LSP communicates over stdio and works with any editor that supports the Language Server Protocol: + +```bash +yapi lsp +``` + +| Feature | Description | +|---------|-------------| +| **Real-time Validation** | Errors and warnings as you type, with precise line/column positions | +| **Intelligent Autocompletion** | Context-aware suggestions for keys, HTTP methods, content types | +| **Hover Info** | Hover over `${VAR}` to see environment variable status | +| **Go to Definition** | Jump to referenced chain steps and variables | + +----- + ## 🚀 Quick Start 1. **Create a request file** (e.g., `get-user.yapi.yml`): @@ -161,6 +216,7 @@ chain: **Key features:** - Reference previous step data with `${step_name.field}` syntax - Access nested JSON properties: `${login.data.token}` +- Use chain variables in assertions: `.id == ${previous_step.expected_id}` - Assertions use JQ expressions that must evaluate to true - Chains stop on first failure (fail-fast) @@ -231,6 +287,29 @@ expect: - .[] | .active == true # all items are active ``` +**Chain variable assertions** - compare values across steps: + +```yaml +yapi: v1 +chain: + - name: create_item + url: https://api.example.com/items + method: POST + body: + name: "Test Item" + expect: + status: 201 + + - name: get_item + url: https://api.example.com/items/${create_item.id} + method: GET + expect: + status: 200 + assert: + - .id == ${create_item.id} # verify same ID + - .name == "Test Item" +``` + ### 5\. JQ Filtering (Built-in\!) Don't grep output. Filter it right in the config. @@ -492,61 +571,6 @@ yapi test ./tests --verbose # See server output ----- -## 🧠 Editor Integration (LSP) - -Unlike other API clients, **yapi** ships with a **full LSP implementation** out of the box. Your editor becomes an intelligent API development environment with real-time validation, autocompletion, and inline execution. - -### VS Code & Cursor - -Install the official extension from [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=yapi.yapi-extension) or [Open VSX](https://open-vsx.org/extension/yapi/yapi-extension): - -**Features:** -- **Run with `Cmd+Enter`** (Mac) or `Ctrl+Enter` (Windows/Linux) - execute requests without leaving your editor -- **Inline results panel** - see responses, headers, and timing right in VS Code -- **Real-time validation** - errors and warnings as you type -- **Intelligent autocompletion** - context-aware suggestions for keys, methods, and variables -- **Hover info** - hover over `${VAR}` to see environment variable status - -The extension automatically detects `.yapi.yml` files and activates the language server. No configuration needed. - -### Neovim (Native Plugin) - -**yapi** was built with Neovim in mind. First-class support via `lua/yapi_nvim`: - -```lua --- lazy.nvim -{ - dir = "~/path/to/yapi/lua/yapi_nvim", - config = function() - require("yapi_nvim").setup({ - lsp = true, -- Enables the yapi Language Server - pretty = true, -- Uses the TUI renderer in the popup - }) - end -} -``` - -Commands: -- `:YapiRun` - Execute the current buffer -- `:YapiWatch` - Open a split with live reload - -### Other Editors - -The LSP communicates over stdio and works with any editor that supports the Language Server Protocol: - -```bash -yapi lsp -``` - -| Feature | Description | -|---------|-------------| -| **Real-time Validation** | Errors and warnings as you type, with precise line/column positions | -| **Intelligent Autocompletion** | Context-aware suggestions for keys, HTTP methods, content types | -| **Hover Info** | Hover over `${VAR}` to see environment variable status | -| **Go to Definition** | Jump to referenced chain steps and variables | - ------ - ## 🌍 Environment Management Create a `yapi.config.yml` file in your project root to manage multiple environments: diff --git a/apps/web/app/components/Landing.tsx b/apps/web/app/components/Landing.tsx index fde34196..cbeeaa24 100644 --- a/apps/web/app/components/Landing.tsx +++ b/apps/web/app/components/Landing.tsx @@ -330,6 +330,7 @@ export default async function Landing() {
diff --git a/cli/internal/runner/runner.go b/cli/internal/runner/runner.go index b911b2af..cde5a115 100644 --- a/cli/internal/runner/runner.go +++ b/cli/internal/runner/runner.go @@ -407,8 +407,12 @@ func RunChain(ctx context.Context, factory ExecutorFactory, base *config.ConfigV logStepResponse(i+1, step.Name, result) } - // 7. Assert Expectations - expectRes := CheckExpectationsWithEnv(step.Expect, result, opts.EnvOverrides) + // 7. Assert Expectations (with chain variable interpolation) + interpolatedExpect, err := interpolateExpectations(chainCtx, step.Expect) + if err != nil { + return nil, fmt.Errorf("step '%s': %w", step.Name, err) + } + expectRes := CheckExpectationsWithEnv(interpolatedExpect, result, opts.EnvOverrides) // 8. Store Result (including expectation result even if failed) chainCtx.AddResult(step.Name, result) @@ -501,6 +505,38 @@ func warnBareChainRefsInConfig(stepName string, cfg *config.ConfigV1) { } } +// interpolateExpectations expands chain variables in assertion expressions. +// This allows assertions like: .result.track_index == ${create_track.result.index} +func interpolateExpectations(chainCtx *ChainContext, expect config.Expectation) (config.Expectation, error) { + result := expect + + // Interpolate body assertions + if len(expect.Assert.Body) > 0 { + result.Assert.Body = make([]string, len(expect.Assert.Body)) + for i, assertion := range expect.Assert.Body { + expanded, err := chainCtx.ExpandVariables(assertion) + if err != nil { + return result, fmt.Errorf("assertion '%s': %w", assertion, err) + } + result.Assert.Body[i] = expanded + } + } + + // Interpolate header assertions + if len(expect.Assert.Headers) > 0 { + result.Assert.Headers = make([]string, len(expect.Assert.Headers)) + for i, assertion := range expect.Assert.Headers { + expanded, err := chainCtx.ExpandVariables(assertion) + if err != nil { + return result, fmt.Errorf("header assertion '%s': %w", assertion, err) + } + result.Assert.Headers[i] = expanded + } + } + + return result, nil +} + // interpolateConfig expands chain variables in a config func interpolateConfig(chainCtx *ChainContext, cfg *config.ConfigV1) (*config.ConfigV1, error) { result := *cfg // Copy diff --git a/cli/internal/utils/fn.go b/cli/internal/utils/fn.go index e8767d08..6cb54063 100644 --- a/cli/internal/utils/fn.go +++ b/cli/internal/utils/fn.go @@ -1,5 +1,5 @@ // Package utils provides generic utility functions. -package utils +package utils //nolint:revive // grab-bag utility package; no better name exists import ( "io" diff --git a/cli/internal/utils/fn_test.go b/cli/internal/utils/fn_test.go index 7eee8e81..55696651 100644 --- a/cli/internal/utils/fn_test.go +++ b/cli/internal/utils/fn_test.go @@ -1,4 +1,4 @@ -package utils +package utils //nolint:revive // matches package declaration in fn.go import "testing" diff --git a/cli/internal/validation/graphql_jq.go b/cli/internal/validation/graphql_jq.go index 634be6db..2634da45 100644 --- a/cli/internal/validation/graphql_jq.go +++ b/cli/internal/validation/graphql_jq.go @@ -9,6 +9,7 @@ import ( "github.com/itchyny/gojq" "gopkg.in/yaml.v3" "yapi.run/cli/internal/domain" + "yapi.run/cli/internal/vars" ) // ValidateGraphQLSyntax validates the GraphQL query syntax if present. @@ -130,11 +131,17 @@ func findFieldInNode(node *yaml.Node, field string) int { } // ValidateChainAssertions validates JQ syntax for all assertions in chain steps. +// Chain variable references like ${step.field} are replaced with null before +// parsing, since they'll be interpolated at runtime. func ValidateChainAssertions(text string, assertions []string, stepName string) []Diagnostic { var diags []Diagnostic for _, assertion := range assertions { - _, err := gojq.Parse(assertion) + // Replace ${...} chain variable refs with null for syntax validation + // These will be expanded at runtime before JQ evaluation + sanitized := vars.Expansion.ReplaceAllString(assertion, "null") + + _, err := gojq.Parse(sanitized) if err != nil { // Find the line where this assertion appears line := findValueInTextForAssertion(text, assertion) diff --git a/examples/debugging/chain-var-in-assertion.yapi.yml b/examples/debugging/chain-var-in-assertion.yapi.yml new file mode 100644 index 00000000..f4932639 --- /dev/null +++ b/examples/debugging/chain-var-in-assertion.yapi.yml @@ -0,0 +1,27 @@ +# Demonstrates using chain variables in assertions. +# +# The second step's assertion compares a response value against +# a value from the first step using ${step.field} syntax. +# +# Run with: +# yapi run chain-var-in-assertion.yapi.yml +# +# The assertion `.userId == ${get_todo.userId}` expands to `.userId == 1` +# before being evaluated by JQ. + +yapi: v1 +chain: + - name: get_todo + url: https://jsonplaceholder.typicode.com/todos/1 + method: GET + expect: + status: 200 + + - name: get_another_todo + url: https://jsonplaceholder.typicode.com/todos/2 + method: GET + expect: + status: 200 + assert: + # This todo also belongs to userId 1, so this should pass + - .userId == ${get_todo.userId} diff --git a/integrations/nvim/lua/yapi_nvim/init.lua b/integrations/nvim/lua/yapi_nvim/init.lua index 7e68e1e5..55beff68 100644 --- a/integrations/nvim/lua/yapi_nvim/init.lua +++ b/integrations/nvim/lua/yapi_nvim/init.lua @@ -175,12 +175,20 @@ function M.setup(opts) -- Setup LSP for yapi files if M._opts.lsp then - vim.lsp.config.yapi = { - cmd = { "yapi", "lsp" }, - filetypes = { "yaml.yapi" }, - root_markers = { "yapi.config.yml", "yapi.config.yaml", ".git" }, - } - vim.lsp.enable("yapi") + local ok, lspconfig = pcall(require, "lspconfig") + if ok then + local configs = require("lspconfig.configs") + if not configs.yapi then + configs.yapi = { + default_config = { + cmd = { "yapi", "lsp" }, + filetypes = { "yaml.yapi" }, + root_dir = lspconfig.util.root_pattern("yapi.config.yml", "yapi.config.yaml", ".git"), + }, + } + end + lspconfig.yapi.setup({}) + end -- Set filetype to yaml.yapi for yapi config files vim.api.nvim_create_autocmd({ "BufReadPost", "BufNewFile" }, {