Skip to content

Commit edbc73e

Browse files
OpenAPI improvements, website docs update, and watcher fixes (#9)
## Summary - Improved OpenAPI generator and types in Napper.Core - Updated website documentation (installation, quick-start, OpenAPI import guide) - Fixed watcher logic in VS Code extension - Added E2E tests for OpenAPI scenarios ## Test plan - [ ] Verify OpenAPI import guide renders correctly on website - [ ] Run F# test suite: `make test-fsharp` - [ ] Run VS Code extension tests: `npm test` in `src/Napper.VsCode` - [ ] Confirm CI passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 352fd19 commit edbc73e

18 files changed

Lines changed: 744 additions & 74 deletions

README.md

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,60 @@ Everything you need for API testing. Nothing you don't.
3838
- **OpenAPI Import** (`openapi-generate`) &mdash; Generate test files from any OpenAPI spec. Point it at a file, and Napper creates `.nap` files with requests, headers, bodies, and assertions. Optionally enhance with AI via GitHub Copilot (`vscode-openapi-ai`).
3939
- **Plain Text, Git Friendly** (`nap-file`) &mdash; Every request is a `.nap` file. Every environment is a `.napenv` file (`env-file`). Version control everything. No binary blobs, no lock-in.
4040

41-
## Quick Start
41+
## Installation
42+
43+
### VS Code Extension
4244

43-
### Install the VS Code Extension
45+
Install from the marketplace in one command:
4446

4547
```sh
4648
code --install-extension nimblesite.napper
4749
```
4850

49-
### Or grab the CLI binary
51+
Or search **"Napper"** in the VS Code Extensions panel (`Ctrl+Shift+X` / `Cmd+Shift+X`) and click Install.
52+
53+
To install a specific `.vsix` manually: open the Extensions panel → `...` menu → **Install from VSIX...**.
54+
55+
> **Requirements:** VS Code 1.95.0 or later. The extension shells out to the CLI, so install the CLI binary too.
56+
57+
### CLI Binary
58+
59+
The CLI is a self-contained binary with **no runtime dependencies**.
60+
61+
| Platform | Download |
62+
|----------|----------|
63+
| macOS (Apple Silicon) | [`napper-osx-arm64`](https://github.com/MelbourneDeveloper/napper/releases/latest/download/napper-osx-arm64) |
64+
| macOS (Intel) | [`napper-osx-x64`](https://github.com/MelbourneDeveloper/napper/releases/latest/download/napper-osx-x64) |
65+
| Linux (x64) | [`napper-linux-x64`](https://github.com/MelbourneDeveloper/napper/releases/latest/download/napper-linux-x64) |
66+
| Windows (x64) | [`napper-win-x64.exe`](https://github.com/MelbourneDeveloper/napper/releases/latest/download/napper-win-x64.exe) |
67+
68+
**macOS / Linux:**
69+
```sh
70+
chmod +x napper-osx-arm64
71+
mv napper-osx-arm64 /usr/local/bin/napper
72+
napper --version
73+
```
74+
75+
**Install script (macOS / Linux):**
76+
```sh
77+
curl -fsSL https://raw.githubusercontent.com/MelbourneDeveloper/napper/main/scripts/install.sh | bash
78+
```
5079

51-
Download from the [latest release](https://github.com/MelbourneDeveloper/napper/releases).
80+
**Install script (Windows PowerShell):**
81+
```powershell
82+
irm https://raw.githubusercontent.com/MelbourneDeveloper/napper/main/scripts/install.ps1 | iex
83+
```
84+
85+
**Build from source** (requires .NET SDK + `make`):
86+
```sh
87+
git clone https://github.com/MelbourneDeveloper/napper.git && cd napper && make install-binaries
88+
```
89+
90+
> **Note:** F# (`.fsx`) and C# (`.csx`) script hooks require the [.NET 10 SDK](https://dotnet.microsoft.com/download). Plain `.nap` and `.naplist` files need nothing extra.
91+
92+
See the [full installation guide](https://napperapi.dev/docs/installation/) for VSIX manual install, troubleshooting, and macOS Gatekeeper notes.
93+
94+
## Quick Start
5295

5396
## How do you use Napper?
5497

@@ -188,44 +231,58 @@ Variable priority (highest wins):
188231

189232
## OpenAPI Import
190233

191-
Generate `.nap` test files automatically from any OpenAPI specification. Available from the CLI and the VS Code extension.
234+
Generate `.nap` test files automatically from any OpenAPI or Swagger spec. Napper creates one file per operation, a `.naplist` playlist, and a `.napenv` environment file — giving you a working test suite in seconds.
235+
236+
**Supported formats:** OpenAPI 3.0.x, OpenAPI 3.1.x, Swagger 2.0 (JSON input).
192237

193238
### From the CLI
194239

195240
```sh
196241
# Generate from a local spec file
197242
napper generate openapi ./petstore.json --output-dir ./tests
198243

199-
# Output as JSON (for programmatic use)
200-
napper generate openapi ./spec.yaml --output-dir ./tests --output json
244+
# Output a JSON summary for scripting
245+
napper generate openapi ./spec.json --output-dir ./tests --output json
201246
```
202247

203248
### From VS Code
204249

205-
The extension provides two commands (accessible via the Command Palette):
250+
Open the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and choose:
206251

207-
- **Napper: Import OpenAPI from URL** &mdash; Enter a URL to an OpenAPI spec (e.g. `https://petstore3.swagger.io/api/v3/openapi.json`). The extension downloads the spec, generates `.nap` files, and creates a `.naplist` playlist.
208-
- **Napper: Import OpenAPI from File** &mdash; Select a local OpenAPI spec file (JSON or YAML) and an output folder.
252+
- **Napper: Import OpenAPI from URL** &mdash; paste a URL (e.g. `https://petstore3.swagger.io/api/v3/openapi.json`). Napper downloads the spec and generates files.
253+
- **Napper: Import OpenAPI from File** &mdash; browse to a local `.json` spec file.
209254

210-
Both commands prompt you to choose between basic generation or AI-enhanced generation (requires GitHub Copilot). AI enhancement adds smarter assertions, realistic test data, and reorders the playlist for logical test flow.
255+
Both commands prompt for an output folder and offer basic or AI-enhanced generation.
211256

212257
### What gets generated
213258

214-
| File | Purpose |
215-
|------|---------|
216-
| `01_get-users.nap`, `02_post-users.nap`, ... | One `.nap` file per API endpoint with request, headers, body, and assertions |
217-
| `api-name.naplist` | Playlist referencing all generated files in order |
218-
| `.napenv` | Environment file with the API base URL |
259+
Endpoints are grouped into subdirectories by API tag:
260+
261+
```
262+
tests/
263+
├── pets/
264+
│ ├── get-pets.nap
265+
│ ├── post-pets.nap
266+
│ └── get-pets-petId.nap
267+
├── store/
268+
│ └── get-store-inventory.nap
269+
├── petstore.naplist
270+
└── .napenv
271+
```
272+
273+
Each `.nap` file includes the method, URL (with path params as `{{variables}}`), auth headers, request body (from schema), and status code assertions. The `.napenv` file contains the base URL from the spec's `servers` field and variable placeholders for auth tokens.
274+
275+
### AI Enhancement (optional)
219276

220-
### AI Enhancement (Optional)
277+
With GitHub Copilot available, choose AI-enhanced generation to get:
221278

222-
When GitHub Copilot is available, you can opt for AI-enhanced generation which:
279+
- Semantic assertions beyond status codes (e.g. `body.email contains @`)
280+
- Realistic test data in request bodies instead of placeholder values
281+
- Logical playlist ordering (auth first, then CRUD in dependency order)
223282

224-
- Adds semantic assertions beyond basic status checks (e.g. `body.email contains @`)
225-
- Generates realistic test data for request bodies
226-
- Reorders the playlist for logical flow (auth first, then CRUD operations)
283+
Falls back to basic generation automatically if Copilot is unavailable.
227284

228-
If Copilot is not available, a warning is shown and basic generation proceeds normally.
285+
See the [full OpenAPI import guide](https://napperapi.dev/docs/openapi-import/) for authentication handling, `$ref` resolution, customisation tips, and troubleshooting.
229286

230287
## CLI Reference
231288

@@ -241,6 +298,7 @@ Options:
241298
--var <key=value> Variable override (repeatable) (cli-var)
242299
--output <format> Output: pretty, junit, json, ndjson (cli-output)
243300
--output-dir <dir> Output directory for generate command (cli-output-dir)
301+
--version Print the installed CLI version
244302
--verbose Enable debug-level logging (cli-verbose)
245303
```
246304

src/Napper.Core.Tests/OpenApiE2eTests.fs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ let ``Petstore POST endpoints have request body`` () =
184184
allNaps
185185
|> Array.filter (fun f ->
186186
let content = File.ReadAllText(f)
187-
content.Contains("POST {{baseUrl}}"))
187+
content.Contains("method = POST"))
188188

189189
Assert.True(postFiles.Length >= 1, "Must have at least one POST endpoint")
190190

@@ -397,7 +397,7 @@ let ``Beeceptor POST endpoints have request body and headers`` () =
397397
allNaps
398398
|> Array.filter (fun f ->
399399
let content = File.ReadAllText(f)
400-
content.Contains "POST {{baseUrl}}")
400+
content.Contains "method = POST")
401401
// auth/register, auth/login, cart/items, checkout, addresses POST = 5
402402
Assert.True(postFiles.Length >= 5, $"Must have at least 5 POST endpoints, got {postFiles.Length}")
403403

@@ -483,7 +483,9 @@ let ``Beeceptor checkout endpoint asserts 201 status`` () =
483483
allNaps
484484
|> Array.filter (fun f ->
485485
let content = File.ReadAllText(f)
486-
content.Contains("POST {{baseUrl}}/checkout"))
486+
487+
content.Contains("method = POST")
488+
&& content.Contains("url = {{baseUrl}}/checkout"))
487489

488490
Assert.True(checkoutFiles.Length >= 1, "Must have checkout endpoint")
489491
let content = File.ReadAllText(checkoutFiles[0])
@@ -525,7 +527,7 @@ let ``Petstore POST endpoints include actual JSON body content`` () =
525527
allNaps
526528
|> Array.filter (fun f ->
527529
let content = File.ReadAllText(f)
528-
content.Contains("POST {{baseUrl}}") && content.Contains("[request.body]"))
530+
content.Contains("method = POST") && content.Contains("[request.body]"))
529531

530532
Assert.True(postFilesWithBody.Length >= 1, "Must have POST endpoints with body")
531533

src/Napper.Core.Tests/OpenApiGeneratorTests.fs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module OpenApiGeneratorTests
55
// openapi-meta-flag, nap-meta, nap-request, nap-headers, nap-body, nap-vars, nap-assert
66

77
open Xunit
8+
open Napper.Core
89
open Napper.Core.OpenApiGenerator
910

1011
// --- Helpers ---
@@ -153,7 +154,8 @@ let ``OAS3 nap file contains meta section with name`` () =
153154
let ``OAS3 nap file contains request section`` () =
154155
let content = (unwrap minimalOas3 |> firstFile).Content
155156
Assert.Contains("[request]", content)
156-
Assert.Contains("GET {{baseUrl}}/users", content)
157+
Assert.Contains("method = GET", content)
158+
Assert.Contains("url = {{baseUrl}}/users", content)
157159

158160
[<Fact>]
159161
let ``OAS3 nap file contains assert section`` () =
@@ -200,7 +202,8 @@ let ``Swagger2 generates nap file`` () =
200202
let gen = unwrap minimalSwagger2
201203
Assert.Equal(1, gen.NapFiles.Length)
202204
let content = (firstFile gen).Content
203-
Assert.Contains("GET {{baseUrl}}/items", content)
205+
Assert.Contains("method = GET", content)
206+
Assert.Contains("url = {{baseUrl}}/items", content)
204207

205208
// --- Multiple endpoints --- Spec: openapi-nap-gen, openapi-params, openapi-assert-gen
206209

@@ -716,6 +719,42 @@ let ``Environment file has baseUrl key-value pair`` () =
716719

717720
// --- Base URL fallback --- Spec: openapi-baseurl
718721

722+
// --- Generated files must be parseable --- Spec: openapi-nap-gen, nap-file
723+
724+
[<Fact>]
725+
let ``Generated nap files are parseable by the nap parser`` () =
726+
let gen = unwrap minimalOas3
727+
728+
for f in gen.NapFiles do
729+
match Napper.Core.Parser.parseNapFile f.Content with
730+
| Ok parsed ->
731+
Assert.Equal(GET, parsed.Request.Method)
732+
Assert.Contains("{{baseUrl}}/users", parsed.Request.Url)
733+
| Error e -> failwith $"Generated file '{f.FileName}' failed to parse: {e}"
734+
735+
[<Fact>]
736+
let ``Generated POST nap file is parseable with correct method and body`` () =
737+
let gen = unwrap multiMethodSpec
738+
let postFile = gen.NapFiles |> List.find (fun f -> f.Content.Contains("Create pet"))
739+
740+
match Napper.Core.Parser.parseNapFile postFile.Content with
741+
| Ok parsed ->
742+
Assert.Equal(POST, parsed.Request.Method)
743+
Assert.Contains("{{baseUrl}}/pets", parsed.Request.Url)
744+
Assert.True(parsed.Request.Body.IsSome, "POST must have a request body")
745+
| Error e -> failwith $"Generated POST file failed to parse: {e}"
746+
747+
[<Fact>]
748+
let ``Generated nap file with path params is parseable`` () =
749+
let gen = unwrap multiMethodSpec
750+
let petFile = gen.NapFiles |> List.find (fun f -> f.Content.Contains("getPetById"))
751+
752+
match Napper.Core.Parser.parseNapFile petFile.Content with
753+
| Ok parsed ->
754+
Assert.Contains("{{petId}}", parsed.Request.Url)
755+
Assert.True(parsed.Vars.ContainsKey("petId"), "Must have petId var")
756+
| Error e -> failwith $"Generated file with path params failed to parse: {e}"
757+
719758
[<Fact>]
720759
let ``Falls back to default URL when no servers or host`` () =
721760
let spec =

src/Napper.Core/OpenApiGenerator.fs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,10 @@ let private buildRequest (ep: EndpointInfo) : string list =
344344
let url =
345345
sprintf "%s%s%s" BaseUrlVar (convertPathParams ep.UrlPath) (buildQuery ep.QueryParams)
346346

347-
[ SectionRequest; sprintf "%s %s" (ep.Method.ToUpperInvariant()) url; "" ]
347+
[ SectionRequest
348+
sprintf "%s = %s" KeyMethod (ep.Method.ToUpperInvariant())
349+
sprintf "%s = %s" KeyUrl url
350+
"" ]
348351

349352
let private buildHeaders (ep: EndpointInfo) : string list =
350353
let hasBody = methodHasBody ep.Method

src/Napper.Core/OpenApiTypes.fs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ let KeyName = "name"
6060
[<Literal>]
6161
let KeyDescription = "description"
6262

63+
[<Literal>]
64+
let KeyMethod = "method"
65+
66+
[<Literal>]
67+
let KeyUrl = "url"
68+
6369
[<Literal>]
6470
let KeyGenerated = "generated"
6571

src/Napper.VsCode/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const CSX_EXTENSION = '.csx';
1212
export const NAP_GLOB = '**/*.nap';
1313
export const NAPLIST_GLOB = '**/*.naplist';
1414
export const NAPENV_GLOB = '**/.napenv*';
15+
export const DIRECTORY_GLOB = '**/';
1516

1617
// View IDs
1718
export const VIEW_EXPLORER = 'napperExplorer';

src/Napper.VsCode/src/test/e2e/explorer.e2e.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as fs from 'fs';
55
import {
66
activateExtension,
77
closeAllEditors,
8+
deleteFixtureDir,
89
deleteFixtureFile,
910
getFixturePath,
1011
openDocument,
@@ -157,6 +158,41 @@ suite('Explorer Tree View', () => {
157158
);
158159
});
159160

161+
test('folder deletion triggers tree refresh via watcher', async function () {
162+
this.timeout(20000);
163+
const testDir = 'watcher-folder-test',
164+
testFile = `${testDir}/probe.nap`,
165+
napContent = '[request]\nmethod = "GET"\nurl = "https://httpbin.org/get"\n';
166+
167+
// Create a folder with a .nap file so it appears in the tree
168+
writeFixtureFile(testFile, napContent);
169+
await sleep(3000);
170+
171+
const provider = getExplorerProvider(),
172+
nodesAfterCreate = provider.getChildren(),
173+
folderNode = findNodeByLabel(nodesAfterCreate, testDir);
174+
assert.ok(folderNode, `Folder "${testDir}" must appear in explorer after creation`);
175+
176+
// Listen for onDidChangeTreeData — this is what VS Code uses to know
177+
// it should call getChildren() again and repaint the tree
178+
let refreshFired = false;
179+
const disposable = provider.onDidChangeTreeData(() => {
180+
refreshFired = true;
181+
});
182+
183+
// Delete the entire folder from disk (not individual files)
184+
deleteFixtureDir(testDir);
185+
186+
// Wait for the watcher to fire a refresh
187+
await sleep(5000);
188+
disposable.dispose();
189+
190+
assert.ok(
191+
refreshFired,
192+
'onDidChangeTreeData must fire after a folder is deleted — the tree must react to folder removal',
193+
);
194+
});
195+
160196
test('nested playlist in file tree also expands with children', function () {
161197
this.timeout(10000);
162198
const provider = getExplorerProvider(),

src/Napper.VsCode/src/test/helpers/helpers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ export const deleteFixtureFile = (relativePath: string): void => {
8282
}
8383
};
8484

85+
export const deleteFixtureDir = (relativePath: string): void => {
86+
const fullPath = getFixturePath(relativePath);
87+
if (fs.existsSync(fullPath)) {
88+
fs.rmSync(fullPath, { recursive: true });
89+
}
90+
};
91+
8592
export const waitForCondition = async (
8693
condition: () => boolean | Promise<boolean>,
8794
timeout = 10000,

0 commit comments

Comments
 (0)