Skip to content

Commit 1538edd

Browse files
jamierpondclaude
andauthored
assert with variables (#106)
* ok better errors * can do vars in jq * wip * great * Add Jamie Pond attribution to footer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Move editor integration section near top of README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use lspconfig for neovim LSP setup instead of vim.lsp.config Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Address review feedback: extract truncateStr helper, add FindBareRefs tests, fix example line number - DRY up duplicated truncation logic into a UTF-8-safe truncateStr helper - Add table-driven tests for FindBareRefs covering bare refs, wrapped refs, edge cases - Fix incorrect line number in bare-variable-warning example comment (18 → 25) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix lint: remove unused maxLen parameter from truncateStr Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * wip --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e1cf043 commit 1538edd

8 files changed

Lines changed: 169 additions & 66 deletions

File tree

README.md

Lines changed: 79 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,61 @@ make install
5959

6060
-----
6161

62+
## 🧠 Editor Integration (LSP)
63+
64+
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.
65+
66+
### VS Code & Cursor
67+
68+
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):
69+
70+
**Features:**
71+
- **Run with `Cmd+Enter`** (Mac) or `Ctrl+Enter` (Windows/Linux) - execute requests without leaving your editor
72+
- **Inline results panel** - see responses, headers, and timing right in VS Code
73+
- **Real-time validation** - errors and warnings as you type
74+
- **Intelligent autocompletion** - context-aware suggestions for keys, methods, and variables
75+
- **Hover info** - hover over `${VAR}` to see environment variable status
76+
77+
The extension automatically detects `.yapi.yml` files and activates the language server. No configuration needed.
78+
79+
### Neovim (Native Plugin)
80+
81+
**yapi** was built with Neovim in mind. First-class support via `lua/yapi_nvim`:
82+
83+
```lua
84+
-- lazy.nvim
85+
{
86+
dir = "~/path/to/yapi/lua/yapi_nvim",
87+
config = function()
88+
require("yapi_nvim").setup({
89+
lsp = true, -- Enables the yapi Language Server
90+
pretty = true, -- Uses the TUI renderer in the popup
91+
})
92+
end
93+
}
94+
```
95+
96+
Commands:
97+
- `:YapiRun` - Execute the current buffer
98+
- `:YapiWatch` - Open a split with live reload
99+
100+
### Other Editors
101+
102+
The LSP communicates over stdio and works with any editor that supports the Language Server Protocol:
103+
104+
```bash
105+
yapi lsp
106+
```
107+
108+
| Feature | Description |
109+
|---------|-------------|
110+
| **Real-time Validation** | Errors and warnings as you type, with precise line/column positions |
111+
| **Intelligent Autocompletion** | Context-aware suggestions for keys, HTTP methods, content types |
112+
| **Hover Info** | Hover over `${VAR}` to see environment variable status |
113+
| **Go to Definition** | Jump to referenced chain steps and variables |
114+
115+
-----
116+
62117
## 🚀 Quick Start
63118

64119
1. **Create a request file** (e.g., `get-user.yapi.yml`):
@@ -161,6 +216,7 @@ chain:
161216
**Key features:**
162217
- Reference previous step data with `${step_name.field}` syntax
163218
- Access nested JSON properties: `${login.data.token}`
219+
- Use chain variables in assertions: `.id == ${previous_step.expected_id}`
164220
- Assertions use JQ expressions that must evaluate to true
165221
- Chains stop on first failure (fail-fast)
166222

@@ -231,6 +287,29 @@ expect:
231287
- .[] | .active == true # all items are active
232288
```
233289

290+
**Chain variable assertions** - compare values across steps:
291+
292+
```yaml
293+
yapi: v1
294+
chain:
295+
- name: create_item
296+
url: https://api.example.com/items
297+
method: POST
298+
body:
299+
name: "Test Item"
300+
expect:
301+
status: 201
302+
303+
- name: get_item
304+
url: https://api.example.com/items/${create_item.id}
305+
method: GET
306+
expect:
307+
status: 200
308+
assert:
309+
- .id == ${create_item.id} # verify same ID
310+
- .name == "Test Item"
311+
```
312+
234313
### 5\. JQ Filtering (Built-in\!)
235314

236315
Don't grep output. Filter it right in the config.
@@ -492,61 +571,6 @@ yapi test ./tests --verbose # See server output
492571

493572
-----
494573

495-
## 🧠 Editor Integration (LSP)
496-
497-
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.
498-
499-
### VS Code & Cursor
500-
501-
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):
502-
503-
**Features:**
504-
- **Run with `Cmd+Enter`** (Mac) or `Ctrl+Enter` (Windows/Linux) - execute requests without leaving your editor
505-
- **Inline results panel** - see responses, headers, and timing right in VS Code
506-
- **Real-time validation** - errors and warnings as you type
507-
- **Intelligent autocompletion** - context-aware suggestions for keys, methods, and variables
508-
- **Hover info** - hover over `${VAR}` to see environment variable status
509-
510-
The extension automatically detects `.yapi.yml` files and activates the language server. No configuration needed.
511-
512-
### Neovim (Native Plugin)
513-
514-
**yapi** was built with Neovim in mind. First-class support via `lua/yapi_nvim`:
515-
516-
```lua
517-
-- lazy.nvim
518-
{
519-
dir = "~/path/to/yapi/lua/yapi_nvim",
520-
config = function()
521-
require("yapi_nvim").setup({
522-
lsp = true, -- Enables the yapi Language Server
523-
pretty = true, -- Uses the TUI renderer in the popup
524-
})
525-
end
526-
}
527-
```
528-
529-
Commands:
530-
- `:YapiRun` - Execute the current buffer
531-
- `:YapiWatch` - Open a split with live reload
532-
533-
### Other Editors
534-
535-
The LSP communicates over stdio and works with any editor that supports the Language Server Protocol:
536-
537-
```bash
538-
yapi lsp
539-
```
540-
541-
| Feature | Description |
542-
|---------|-------------|
543-
| **Real-time Validation** | Errors and warnings as you type, with precise line/column positions |
544-
| **Intelligent Autocompletion** | Context-aware suggestions for keys, HTTP methods, content types |
545-
| **Hover Info** | Hover over `${VAR}` to see environment variable status |
546-
| **Go to Definition** | Jump to referenced chain steps and variables |
547-
548-
-----
549-
550574
## 🌍 Environment Management
551575

552576
Create a `yapi.config.yml` file in your project root to manage multiple environments:

apps/web/app/components/Landing.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ export default async function Landing() {
330330
<div className="flex gap-6">
331331
<a href="https://github.com/jamierpond/yapi" className="text-yapi-fg-subtle hover:text-yapi-accent transition-colors text-sm">Source Code</a>
332332
<a href="/docs" className="text-yapi-fg-subtle hover:text-yapi-accent transition-colors text-sm">Documentation</a>
333+
<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>
333334
</div>
334335
</div>
335336
</footer>

cli/internal/runner/runner.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,8 +407,12 @@ func RunChain(ctx context.Context, factory ExecutorFactory, base *config.ConfigV
407407
logStepResponse(i+1, step.Name, result)
408408
}
409409

410-
// 7. Assert Expectations
411-
expectRes := CheckExpectationsWithEnv(step.Expect, result, opts.EnvOverrides)
410+
// 7. Assert Expectations (with chain variable interpolation)
411+
interpolatedExpect, err := interpolateExpectations(chainCtx, step.Expect)
412+
if err != nil {
413+
return nil, fmt.Errorf("step '%s': %w", step.Name, err)
414+
}
415+
expectRes := CheckExpectationsWithEnv(interpolatedExpect, result, opts.EnvOverrides)
412416

413417
// 8. Store Result (including expectation result even if failed)
414418
chainCtx.AddResult(step.Name, result)
@@ -501,6 +505,38 @@ func warnBareChainRefsInConfig(stepName string, cfg *config.ConfigV1) {
501505
}
502506
}
503507

508+
// interpolateExpectations expands chain variables in assertion expressions.
509+
// This allows assertions like: .result.track_index == ${create_track.result.index}
510+
func interpolateExpectations(chainCtx *ChainContext, expect config.Expectation) (config.Expectation, error) {
511+
result := expect
512+
513+
// Interpolate body assertions
514+
if len(expect.Assert.Body) > 0 {
515+
result.Assert.Body = make([]string, len(expect.Assert.Body))
516+
for i, assertion := range expect.Assert.Body {
517+
expanded, err := chainCtx.ExpandVariables(assertion)
518+
if err != nil {
519+
return result, fmt.Errorf("assertion '%s': %w", assertion, err)
520+
}
521+
result.Assert.Body[i] = expanded
522+
}
523+
}
524+
525+
// Interpolate header assertions
526+
if len(expect.Assert.Headers) > 0 {
527+
result.Assert.Headers = make([]string, len(expect.Assert.Headers))
528+
for i, assertion := range expect.Assert.Headers {
529+
expanded, err := chainCtx.ExpandVariables(assertion)
530+
if err != nil {
531+
return result, fmt.Errorf("header assertion '%s': %w", assertion, err)
532+
}
533+
result.Assert.Headers[i] = expanded
534+
}
535+
}
536+
537+
return result, nil
538+
}
539+
504540
// interpolateConfig expands chain variables in a config
505541
func interpolateConfig(chainCtx *ChainContext, cfg *config.ConfigV1) (*config.ConfigV1, error) {
506542
result := *cfg // Copy

cli/internal/utils/fn.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Package utils provides generic utility functions.
2-
package utils
2+
package utils //nolint:revive // grab-bag utility package; no better name exists
33

44
import (
55
"io"

cli/internal/utils/fn_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package utils
1+
package utils //nolint:revive // matches package declaration in fn.go
22

33
import "testing"
44

cli/internal/validation/graphql_jq.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/itchyny/gojq"
1010
"gopkg.in/yaml.v3"
1111
"yapi.run/cli/internal/domain"
12+
"yapi.run/cli/internal/vars"
1213
)
1314

1415
// ValidateGraphQLSyntax validates the GraphQL query syntax if present.
@@ -130,11 +131,17 @@ func findFieldInNode(node *yaml.Node, field string) int {
130131
}
131132

132133
// ValidateChainAssertions validates JQ syntax for all assertions in chain steps.
134+
// Chain variable references like ${step.field} are replaced with null before
135+
// parsing, since they'll be interpolated at runtime.
133136
func ValidateChainAssertions(text string, assertions []string, stepName string) []Diagnostic {
134137
var diags []Diagnostic
135138

136139
for _, assertion := range assertions {
137-
_, err := gojq.Parse(assertion)
140+
// Replace ${...} chain variable refs with null for syntax validation
141+
// These will be expanded at runtime before JQ evaluation
142+
sanitized := vars.Expansion.ReplaceAllString(assertion, "null")
143+
144+
_, err := gojq.Parse(sanitized)
138145
if err != nil {
139146
// Find the line where this assertion appears
140147
line := findValueInTextForAssertion(text, assertion)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Demonstrates using chain variables in assertions.
2+
#
3+
# The second step's assertion compares a response value against
4+
# a value from the first step using ${step.field} syntax.
5+
#
6+
# Run with:
7+
# yapi run chain-var-in-assertion.yapi.yml
8+
#
9+
# The assertion `.userId == ${get_todo.userId}` expands to `.userId == 1`
10+
# before being evaluated by JQ.
11+
12+
yapi: v1
13+
chain:
14+
- name: get_todo
15+
url: https://jsonplaceholder.typicode.com/todos/1
16+
method: GET
17+
expect:
18+
status: 200
19+
20+
- name: get_another_todo
21+
url: https://jsonplaceholder.typicode.com/todos/2
22+
method: GET
23+
expect:
24+
status: 200
25+
assert:
26+
# This todo also belongs to userId 1, so this should pass
27+
- .userId == ${get_todo.userId}

integrations/nvim/lua/yapi_nvim/init.lua

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,20 @@ function M.setup(opts)
175175

176176
-- Setup LSP for yapi files
177177
if M._opts.lsp then
178-
vim.lsp.config.yapi = {
179-
cmd = { "yapi", "lsp" },
180-
filetypes = { "yaml.yapi" },
181-
root_markers = { "yapi.config.yml", "yapi.config.yaml", ".git" },
182-
}
183-
vim.lsp.enable("yapi")
178+
local ok, lspconfig = pcall(require, "lspconfig")
179+
if ok then
180+
local configs = require("lspconfig.configs")
181+
if not configs.yapi then
182+
configs.yapi = {
183+
default_config = {
184+
cmd = { "yapi", "lsp" },
185+
filetypes = { "yaml.yapi" },
186+
root_dir = lspconfig.util.root_pattern("yapi.config.yml", "yapi.config.yaml", ".git"),
187+
},
188+
}
189+
end
190+
lspconfig.yapi.setup({})
191+
end
184192

185193
-- Set filetype to yaml.yapi for yapi config files
186194
vim.api.nvim_create_autocmd({ "BufReadPost", "BufNewFile" }, {

0 commit comments

Comments
 (0)