Skip to content

Commit cedcb28

Browse files
committed
Merge branch 'next'
2 parents 3417a25 + 4ed219a commit cedcb28

44 files changed

Lines changed: 2096 additions & 285 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/cli.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ on:
55
branches: [ main, next ]
66
paths:
77
- 'cli/**'
8+
- 'docs/**'
89
- 'examples/**'
910
- '.github/workflows/cli.yml'
1011
pull_request:
1112
branches: [ main, next ]
1213
paths:
1314
- 'cli/**'
15+
- 'docs/**'
1416
- 'examples/**'
1517
- '.github/workflows/cli.yml'
1618
workflow_dispatch:
@@ -28,6 +30,9 @@ jobs:
2830
go-version: '1.25'
2931
cache-dependency-path: cli/go.sum
3032

33+
- name: Sync topic docs
34+
run: make sync-docs
35+
3136
- name: Install linters
3237
run: make lint-install
3338

@@ -50,6 +55,9 @@ jobs:
5055
go-version: '1.25'
5156
cache-dependency-path: cli/go.sum
5257

58+
- name: Sync topic docs
59+
run: make sync-docs
60+
5361
- name: Download dependencies
5462
run: cd cli && go mod download
5563

.github/workflows/codecov.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ jobs:
2020
go-version: '1.24'
2121
cache-dependency-path: cli/go.sum
2222

23+
- name: Sync topic docs
24+
run: make sync-docs
25+
2326
- name: Generate coverage
2427
working-directory: cli
2528
run: go test -coverprofile=coverage.txt -covermode=atomic ./... || true

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ dist
5454
apps/web/icons
5555
coverage.out
5656
cli/bin/yapi
57+
cli/internal/docs/topics
58+
docs/commands
5759
specs
5860
.specify
5961
.claude/commands/specify.*

Makefile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: build run run-print-analytics test fuzz fmt fmt-check clean install docker web web-run bump-patch bump-minor bump-major release build-all lint lint-install install-lint lint-quick lint-full gen-docs gh-action fuzz-cover local-release
1+
.PHONY: build run run-print-analytics test fuzz fmt fmt-check clean install docker web web-run bump-patch bump-minor bump-major release build-all lint lint-install install-lint lint-quick lint-full gen-docs gh-action fuzz-cover local-release sync-docs
22

33
NAME := yapi
44
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
@@ -9,6 +9,7 @@ LDFLAGS := -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DA
99

1010
install: build
1111
@echo "Installing yapi to $$(go env GOPATH)/bin..."
12+
@mkdir -p $$(go env GOPATH)/bin
1213
@cp ./cli/bin/yapi $$(go env GOPATH)/bin/yapi
1314
@codesign --sign - --force $$(go env GOPATH)/bin/yapi 2>/dev/null || true
1415
@echo "Done! Ensure $$(go env GOPATH)/bin is in your PATH."
@@ -44,7 +45,12 @@ lint-full: lint
4445
@echo "Running govulncheck..."
4546
@cd cli && govulncheck ./...
4647

47-
build:
48+
sync-docs:
49+
@mkdir -p cli/internal/docs/topics
50+
@rm -rf cli/internal/docs/topics/*
51+
@cp docs/topics/*.md cli/internal/docs/topics/
52+
53+
build: sync-docs
4854
@echo "Building yapi CLI..."
4955
@cd cli && go build -ldflags "$(LDFLAGS)" -o ./bin/yapi ./cmd/yapi
5056
@codesign --sign - --force ./cli/bin/yapi 2>/dev/null || true

PLAN.md

Lines changed: 49 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -1,262 +1,81 @@
1-
# Plan: `wait_for` Feature - DX Design
1+
# Plan: Better Error Messages & Debuggability
22

3-
## Overview
4-
5-
A new `wait_for` block that repeatedly polls an endpoint until a condition is satisfied. Designed for async server operations that require time to complete (job processing, webhooks, eventual consistency, etc.).
6-
7-
## DX Design
8-
9-
### Basic Syntax
10-
11-
```yaml
12-
yapi: v1
13-
url: ${url}/jobs/${job_id}
14-
method: GET
15-
16-
wait_for:
17-
until:
18-
- .status == "completed"
19-
period: 2s
20-
timeout: 60s
21-
```
22-
23-
### Fixed Period (Simple)
24-
25-
```yaml
26-
wait_for:
27-
until:
28-
- .status == "completed"
29-
period: 2s # Fixed time between attempts
30-
timeout: 60s # Total time limit
31-
```
32-
33-
### Exponential Backoff
34-
35-
```yaml
36-
wait_for:
37-
until:
38-
- .status == "completed"
39-
backoff:
40-
seed: 1s # Initial wait time
41-
multiplier: 2 # Each attempt waits multiplier * previous
42-
timeout: 60s # Total time limit
43-
```
44-
45-
Backoff example with `seed: 1s, multiplier: 2`:
46-
- Attempt 1 → wait 1s
47-
- Attempt 2 → wait 2s
48-
- Attempt 3 → wait 4s
49-
- Attempt 4 → wait 8s
50-
- ...continues until timeout
51-
52-
### Behavior
53-
54-
1. Execute the request
55-
2. If `until` conditions pass → success, stop polling
56-
3. If `until` conditions fail OR request errors (5xx, network) → wait (period or backoff), retry
57-
4. If `timeout` exceeded → fail with timeout error
58-
59-
**Error handling**: Intermediate failures (5xx, network errors, 4xx) are treated as "not ready yet" and polling continues. Only timeout causes failure.
60-
61-
**Timing**: Either `period` OR `backoff` must be specified (mutually exclusive).
3+
## Context
4+
Implementing WISHLIST.md items #1, #2, and #4 (removing #3 since `yapi send` already exists).
625

636
---
647

65-
## Use Cases
66-
67-
### 1. Single Request - Job Completion
68-
69-
```yaml
70-
yapi: v1
71-
url: ${url}/jobs/${job_id}
72-
method: GET
8+
## 1. WISHLIST.md cleanup
9+
Remove item #3 (yapi send) since it's already shipped.
7310

74-
wait_for:
75-
until:
76-
- .status == "completed" or .status == "failed"
77-
period: 2s
78-
timeout: 120s
79-
80-
expect:
81-
status: 200
82-
assert:
83-
- .status == "completed" # Final assertion after wait_for succeeds
84-
```
85-
86-
### 2. Chain Step - Async Workflow
87-
88-
```yaml
89-
yapi: v1
90-
chain:
91-
- name: create_job
92-
url: ${url}/jobs
93-
method: POST
94-
body:
95-
type: "data_export"
96-
expect:
97-
status: 202
98-
assert:
99-
- .job_id != null
100-
101-
- name: wait_for_job
102-
url: ${url}/jobs/${create_job.job_id}
103-
method: GET
104-
wait_for:
105-
until:
106-
- .status == "completed"
107-
backoff:
108-
seed: 1s
109-
multiplier: 2
110-
timeout: 300s
111-
expect:
112-
status: 200
113-
assert:
114-
- .download_url != null
115-
116-
- name: download_result
117-
url: ${wait_for_job.download_url}
118-
method: GET
119-
output_file: ./export.csv
120-
```
121-
122-
### 3. Webhook/Callback Waiting
11+
---
12312

124-
```yaml
125-
yapi: v1
126-
chain:
127-
- name: trigger_webhook
128-
url: ${url}/webhooks/trigger
129-
method: POST
13+
## 2. Warn on bare `$word.word` variable syntax (WISHLIST #1)
13014

131-
- name: check_received
132-
url: ${url}/webhooks/received
133-
method: GET
134-
wait_for:
135-
until:
136-
- . | length > 0
137-
- .[0].payload.event == "user.created"
138-
period: 1s
139-
timeout: 30s
140-
```
15+
The problem: `$step.field` (no braces) silently passes as a literal string instead of being substituted. Only `${step.field}` works.
14116

142-
### 4. Database Eventual Consistency
17+
**Changes:**
14318

144-
```yaml
145-
yapi: v1
146-
chain:
147-
- name: create_user
148-
url: ${url}/users
149-
method: POST
150-
body:
151-
email: "test@example.com"
152-
expect:
153-
status: 201
19+
- **`cli/internal/vars/vars.go`**: Add a `BareChainRef` regex that matches `$word.word` patterns that are NOT inside `${...}`.
20+
- **`cli/internal/vars/vars.go`**: Add `FindBareRefs(s string) []string` that returns the bare refs found.
21+
- **`cli/internal/validation/analyzer.go`**: In `analyzeParsed()`, call a new `warnBareChainRefs(text)` validation function that scans the raw YAML text for bare `$word.word` patterns and emits `SeverityWarning` diagnostics with line numbers and an actionable message like:
22+
`"possible bare variable reference '$step.field' -- did you mean '${step.field}'? Only the ${...} form is substituted."`
15423

155-
- name: verify_searchable
156-
url: ${url}/users/search?email=test@example.com
157-
method: GET
158-
wait_for:
159-
until:
160-
- . | length == 1
161-
period: 500ms
162-
timeout: 10s
163-
```
24+
This catches the problem at config analysis time (before execution), so users see the warning immediately -- even in `yapi validate`.
16425

16526
---
16627

167-
## Interaction with Existing Features
168-
169-
### With `expect`
28+
## 3. Show resolved request details in verbose chain execution (WISHLIST #2)
17029

171-
`wait_for` runs first. Once `until` conditions pass, `expect` runs on the final response:
30+
The problem: When a chain step fails, you can't see what values were actually sent because variable substitution is invisible.
17231

173-
```yaml
174-
wait_for:
175-
until:
176-
- .status != "pending" # Wait until not pending
177-
period: 1s
178-
timeout: 30s
32+
**Changes:**
17933

180-
expect:
181-
status: 200
182-
assert:
183-
- .status == "completed" # Then verify it's completed (not failed)
184-
```
34+
- **`cli/internal/runner/runner.go`**: Add `Verbose bool` field to `runner.Options`.
35+
- **`cli/internal/runner/runner.go`**: In `RunChain()`, after `interpolateConfig()` succeeds and before executing, if `opts.Verbose` is true, print the resolved config to stderr:
36+
- Resolved URL (with method)
37+
- Resolved headers
38+
- Resolved body (JSON-serialized if map, or raw if string)
39+
- Uses `fmt.Fprintf(os.Stderr, ...)` with `[VERBOSE]` prefix, consistent with the existing Logger pattern.
40+
- **`cli/cmd/yapi/run.go`**: Set `opts.Verbose = ctx.verbose` when building runner.Options in `executeRunE()`.
18541

186-
### With `timeout`
187-
188-
The existing `timeout` field is per-request. `wait_for.timeout` is total polling time:
189-
190-
```yaml
191-
timeout: 5s # Each poll attempt times out after 5s
42+
---
19243

193-
wait_for:
194-
until:
195-
- .ready == true
196-
period: 2s
197-
timeout: 60s # Total polling time limit
198-
```
44+
## 4. Print step responses in verbose chain mode (WISHLIST #4)
19945

200-
### With `delay`
46+
The problem: In chain execution, you only see the final failing step's output, not intermediate step responses.
20147

202-
`delay` happens before `wait_for` starts:
48+
**Changes:**
20349

204-
```yaml
205-
delay: 5s # Wait 5s before starting to poll
50+
- **`cli/internal/runner/runner.go`**: In `RunChain()`, after each step executes, if `opts.Verbose` is true, print the step's response details to stderr:
51+
- Status code
52+
- Response body (truncated at 1000 chars for readability)
53+
- Duration
20654

207-
wait_for:
208-
until:
209-
- .status == "done"
210-
period: 2s
211-
timeout: 30s
212-
```
55+
This replaces the need for a per-step `debug: true` field -- verbose mode shows everything, which is simpler and avoids new config surface area.
21356

21457
---
21558

216-
## Output During Polling
59+
## 5. Example files: `examples/debugging/`
21760

218-
When running with verbose/default output:
61+
Create example `.yapi.yml` files that demonstrate the improved debugging experience:
21962

220-
```
221-
[POLL] Attempt 1 - conditions not met, retrying in 2s...
222-
[POLL] Attempt 2 - conditions not met, retrying in 2s...
223-
[POLL] Attempt 3 - request failed (503), retrying in 2s...
224-
[POLL] Attempt 4 - conditions met!
225-
```
63+
- **`bare-variable-warning.yapi.yml`**: A chain that uses `$step.field` (bare) to trigger the new warning.
64+
- **`chain-verbose-demo.yapi.yml`**: A multi-step chain against jsonplaceholder with variables that shows how `--verbose` reveals resolved values.
65+
- **`assertion-failure-demo.yapi.yml`**: A request with an `expect:` block that will fail, showing the detailed assertion error output.
66+
- **`missing-key-demo.yapi.yml`**: A chain that references a nonexistent JSON key, showing the precise error path.
22667

22768
---
22869

229-
## Config Schema
230-
231-
```go
232-
type Backoff struct {
233-
Seed string `yaml:"seed"` // Initial wait, e.g., "1s"
234-
Multiplier float64 `yaml:"multiplier"` // e.g., 2
235-
}
236-
237-
type WaitFor struct {
238-
Until []string `yaml:"until"` // Required: JQ assertions
239-
Period string `yaml:"period,omitempty"` // Fixed interval, e.g., "2s"
240-
Backoff *Backoff `yaml:"backoff,omitempty"` // Exponential backoff
241-
Timeout string `yaml:"timeout"` // Required: total time limit
242-
}
243-
```
244-
245-
Added to `ConfigV1`:
246-
```go
247-
type ConfigV1 struct {
248-
// ... existing fields ...
249-
WaitFor *WaitFor `yaml:"wait_for,omitempty"`
250-
}
251-
```
252-
253-
---
70+
## Files changed
25471

255-
## Validation Rules
72+
| File | Change |
73+
|------|--------|
74+
| `WISHLIST.md` | Remove item #3 |
75+
| `cli/internal/vars/vars.go` | Add `BareChainRef` regex + `FindBareRefs()` function |
76+
| `cli/internal/validation/analyzer.go` | Add `warnBareChainRefs()`, call from `analyzeParsed()` |
77+
| `cli/internal/runner/runner.go` | Add `Verbose` to `Options`, add verbose logging in `RunChain()` |
78+
| `cli/cmd/yapi/run.go` | Thread `verbose` into `runner.Options` |
79+
| `examples/debugging/*.yapi.yml` | 4 new example files |
25680

257-
1. `until` is required and must have at least one assertion
258-
2. `timeout` is required and must be valid Go duration
259-
3. Exactly one of `period` OR `backoff` must be specified (mutually exclusive)
260-
4. If `period`: must be valid Go duration
261-
5. If `backoff`: `seed` must be valid Go duration, `multiplier` must be > 1
262-
6. All `until` expressions must be valid JQ
81+
**No new dependencies. No config schema changes. No breaking changes.**

0 commit comments

Comments
 (0)