Skip to content

Commit 2abbc1a

Browse files
davidmovasclaude
andcommitted
scaffold: OpenAPI generator plugin skeleton
Generator plugin (not executor) — invoked via `qube generate --from <spec>`. - contract.go, plugin.go, host.go — shared template - info.go — generator plugin metadata (no protocols, no fields) - generator.go — GenerateInput/Output types and generate() skeleton - exports.go — custom exports: plugin_info, plugin_init, generate, plugin_destroy (no execute/validate — this plugin is not a test executor) Detects CRUD patterns (POST + GET/{id} + PUT/{id} + DELETE/{id}) and generates scenario files with save/extract chains. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f004ee0 commit 2abbc1a

11 files changed

Lines changed: 320 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
vet:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-go@v5
19+
with:
20+
go-version: '1.26.x'
21+
- run: go vet ./...
22+
23+
build-wasm:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
- uses: actions/setup-go@v5
28+
with:
29+
go-version: '1.26.x'
30+
- name: Install TinyGo
31+
run: |
32+
wget https://github.com/tinygo-org/tinygo/releases/download/v0.33.0/tinygo_0.33.0_amd64.deb
33+
sudo dpkg -i tinygo_0.33.0_amd64.deb
34+
- name: Build WASM
35+
run: tinygo build -o plugin-openapi.wasm -target=wasi ./
36+
- name: Check WASM size
37+
run: ls -lh plugin-openapi.wasm
38+
- uses: actions/upload-artifact@v4
39+
with:
40+
name: plugin-openapi.wasm
41+
path: plugin-openapi.wasm

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
*.wasm
2+
*.exe
3+
*.test
4+
*.out
5+
vendor/
6+
.idea/
7+
.vscode/
8+
.DS_Store

.golangci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
version: "2"
2+
3+
linters:
4+
default: standard
5+
enable:
6+
- govet
7+
- errcheck
8+
- staticcheck
9+
- gocritic
10+
- ineffassign
11+
- misspell

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# plugin-openapi
2+
3+
> OpenAPI / Swagger test generator for [ApiQube](https://github.com/apiqube).
4+
5+
[![License](https://img.shields.io/badge/License-MIT-blue?style=flat-square)](LICENSE)
6+
[![Status](https://img.shields.io/badge/Status-Scaffold-yellow?style=flat-square)]()
7+
8+
Generator plugin (not an executor). Invoked via `qube generate --from openapi.json`
9+
to produce qube test files from an existing API specification.
10+
11+
Detects CRUD patterns and emits scenario files with proper save/extract chains.
12+
13+
## Usage
14+
15+
```bash
16+
# From a local file
17+
qube generate --from ./swagger.json --output ./tests/
18+
19+
# From a URL
20+
qube generate --from http://localhost:8081/swagger/doc.json --output ./tests/
21+
```
22+
23+
## What it generates
24+
25+
For each `paths` entry in the spec:
26+
27+
- CRUD groups (POST + GET/{id} + PUT/{id} + DELETE/{id}) → scenario file
28+
- Schema fields → `fake.*` template values
29+
- Response codes → `expect.status` assertions
30+
- Required fields → test bodies
31+
32+
## Build
33+
34+
```bash
35+
tinygo build -o plugin-openapi.wasm -target=wasi ./
36+
```
37+
38+
## License
39+
40+
[MIT](LICENSE)

contract.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package main
2+
3+
// These types mirror github.com/apiqube/engine/internal/plugin.
4+
// They are duplicated here (not imported) because plugins are compiled
5+
// to WASM independently and communicate with the host via JSON bytes.
6+
7+
// PluginInfo is the metadata returned by plugin_info().
8+
type PluginInfo struct {
9+
Name string `json:"name"`
10+
Version string `json:"version"`
11+
Description string `json:"description"`
12+
Protocols []string `json:"protocols"`
13+
Fields map[string]FieldSpec `json:"fields"`
14+
}
15+
16+
// FieldSpec declares one manifest field this plugin adds.
17+
type FieldSpec struct {
18+
Type string `json:"type"` // string, number, bool, object, array, map, any
19+
Required bool `json:"required"`
20+
Description string `json:"description"`
21+
}
22+
23+
// TestInput is what the host passes to execute().
24+
type TestInput struct {
25+
Target string `json:"target"`
26+
Headers map[string]string `json:"headers,omitempty"`
27+
Timeout string `json:"timeout,omitempty"`
28+
Fields map[string]any `json:"fields"`
29+
}
30+
31+
// TestOutput is what execute() returns to the host.
32+
type TestOutput struct {
33+
Status any `json:"status"`
34+
Headers map[string]string `json:"headers,omitempty"`
35+
Body any `json:"body,omitempty"`
36+
DurationMs int64 `json:"duration_ms"`
37+
Error string `json:"error,omitempty"`
38+
Metadata map[string]any `json:"metadata,omitempty"`
39+
}
40+
41+
// FieldError reports a validation problem on a specific field.
42+
type FieldError struct {
43+
Field string `json:"field"`
44+
Message string `json:"message"`
45+
}

exports.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package main
2+
3+
import "encoding/json"
4+
5+
// plugin_info returns metadata as JSON.
6+
//
7+
//export plugin_info
8+
func pluginInfo() uint64 {
9+
data, _ := json.Marshal(info())
10+
return toPointer(data)
11+
}
12+
13+
// plugin_init runs one-time initialization.
14+
//
15+
//export plugin_init
16+
func pluginInit(cfgPtr uint64) uint64 {
17+
// TODO: read cfgPtr, apply any generator settings
18+
_ = cfgPtr
19+
return 0
20+
}
21+
22+
// generate produces test files from an OpenAPI spec.
23+
//
24+
//export generate
25+
func generateFromSpec(inputPtr uint64) uint64 {
26+
// TODO:
27+
// 1. readBytes(inputPtr) → GenerateInput
28+
// 2. Call generate(in)
29+
// 3. Marshal GenerateOutput to JSON
30+
// 4. Return toPointer(data)
31+
_ = inputPtr
32+
return 0
33+
}
34+
35+
// plugin_destroy releases generator resources.
36+
//
37+
//export plugin_destroy
38+
func pluginDestroy() {}
39+
40+
// toPointer packs a byte slice into a (ptr << 32) | len uint64.
41+
func toPointer(data []byte) uint64 {
42+
// TODO: WASM implementation via unsafe
43+
return uint64(len(data))
44+
}
45+
46+
// readBytes reads bytes at the given packed location.
47+
func readBytes(packed uint64) []byte {
48+
// TODO: WASM implementation
49+
_ = packed
50+
return nil
51+
}

generator.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package main
2+
3+
// GenerateInput is what the host passes to the exported `generate` function.
4+
type GenerateInput struct {
5+
SourceURL string `json:"source_url,omitempty"` // http://localhost/swagger.json
6+
SourcePath string `json:"source_path,omitempty"` // local file path
7+
OutputDir string `json:"output_dir"` // where to write generated test files
8+
Format string `json:"format,omitempty"` // full, compact, oneliner
9+
}
10+
11+
// GenerateOutput is what `generate` returns.
12+
type GenerateOutput struct {
13+
FilesCreated []string `json:"files_created"`
14+
TestsCount int `json:"tests_count"`
15+
Warnings []string `json:"warnings,omitempty"`
16+
Error string `json:"error,omitempty"`
17+
}
18+
19+
// generate reads an OpenAPI spec and produces qube test files.
20+
// This is the main function of the generator plugin.
21+
func generate(in *GenerateInput) *GenerateOutput {
22+
// TODO: implementation
23+
//
24+
// 1. Fetch spec:
25+
// - From URL via host_http_request
26+
// - Or from file via host_read_file
27+
// 2. Parse OpenAPI JSON/YAML (use kin-openapi compatible structure)
28+
// 3. Walk paths, group by tag (or by resource prefix)
29+
// 4. Detect CRUD patterns per resource:
30+
// POST /users → creates, extract id from response
31+
// GET /users/{id} → uses saved id
32+
// PUT /users/{id} → uses saved id, body from schema
33+
// DELETE /users/{id} → uses saved id
34+
// → emit as a scenario file
35+
// 5. For standalone endpoints: emit as independent tests
36+
// 6. For each schema property: use fake.* generator
37+
// 7. For each response code: emit expect.status assertion
38+
// 8. Write files to OutputDir
39+
// 9. Return GenerateOutput with file list and counts
40+
return &GenerateOutput{Error: "not implemented"}
41+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/apiqube/plugin-openapi
2+
3+
go 1.26.0

host.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package main
2+
3+
// Host functions provided by the ApiQube engine to this plugin.
4+
//
5+
// When built as a WASM module, these are linked at load time by wazero.
6+
// The //go:wasmimport directive only takes effect under TinyGo WASI build,
7+
// so a regular `go build` compiles the stubs below cleanly.
8+
9+
// httpResponse mirrors what the host returns from host_http_request.
10+
type httpResponse struct {
11+
Status int `json:"status"`
12+
Headers map[string]string `json:"headers"`
13+
Body []byte `json:"body"`
14+
DurationMs int64 `json:"duration_ms"`
15+
Error string `json:"error,omitempty"`
16+
}
17+
18+
// httpRequest is the Go-friendly wrapper around host_http_request.
19+
func httpRequest(method, url string, headers map[string]string, body []byte) (*httpResponse, error) {
20+
// TODO: implementation
21+
// 1. Marshal headers to JSON
22+
// 2. Pack method/url/headers/body as ptr+len pairs
23+
// 3. Call hostHTTPRequestImport (see below)
24+
// 4. Read response bytes, unmarshal into httpResponse
25+
_ = method
26+
_ = url
27+
_ = headers
28+
_ = body
29+
return nil, nil
30+
}
31+
32+
// logMessage sends a log line to the host for display/storage.
33+
func logMessage(level, msg string) {
34+
// TODO: implementation via host_log import
35+
_ = level
36+
_ = msg
37+
}
38+
39+
// now returns the current Unix timestamp in milliseconds from the host.
40+
// (Plugins don't have direct access to system clock in WASI.)
41+
func now() int64 {
42+
// TODO: implementation via host_now import
43+
return 0
44+
}

info.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package main
2+
3+
// plugin-openapi is a Generator plugin, not an Executor.
4+
// It has no protocols and no manifest fields — it is invoked via
5+
// `qube generate --from openapi.json` and produces YAML test files.
6+
//
7+
// Generator plugins use a different subset of the plugin contract:
8+
// plugin_info() + generate(source) instead of plugin_info() + execute(test).
9+
func info() PluginInfo {
10+
return PluginInfo{
11+
Name: "openapi",
12+
Version: "0.1.0",
13+
Description: "Generates qube test files from OpenAPI/Swagger specifications.",
14+
Protocols: nil, // generator plugins have no protocols
15+
Fields: nil, // generator plugins do not add manifest fields
16+
}
17+
}

0 commit comments

Comments
 (0)