Skip to content
134 changes: 79 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`):
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions apps/web/app/components/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ export default async function Landing() {
<div className="flex gap-6">
<a href="https://github.com/jamierpond/yapi" className="text-yapi-fg-subtle hover:text-yapi-accent transition-colors text-sm">Source Code</a>
<a href="/docs" className="text-yapi-fg-subtle hover:text-yapi-accent transition-colors text-sm">Documentation</a>
<a href="https://pond.audio" target="_blank" rel="noopener noreferrer" className="text-yapi-fg-subtle hover:text-yapi-accent transition-colors text-sm">Created by Jamie Pond</a>
</div>
</div>
</footer>
Expand Down
40 changes: 38 additions & 2 deletions cli/internal/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion cli/internal/utils/fn.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion cli/internal/utils/fn_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package utils
package utils //nolint:revive // matches package declaration in fn.go

import "testing"

Expand Down
9 changes: 8 additions & 1 deletion cli/internal/validation/graphql_jq.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
27 changes: 27 additions & 0 deletions examples/debugging/chain-var-in-assertion.yapi.yml
Original file line number Diff line number Diff line change
@@ -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}
20 changes: 14 additions & 6 deletions integrations/nvim/lua/yapi_nvim/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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" }, {
Expand Down
Loading