Generates an OpenAPI 3.x YAML spec by statically analysing a provider codebase with ripwire. The spec may be incomplete (stubs for unresolvable schemas), but it will always be valid OAS and immediately usable for path-coverage testing.
Prerequisites: ripwire on PATH. If absent, check pact-coverage/references/install-ripwire.md.
Step 1 — Identify the framework
Run ripwire to get oriented, then infer the framework from the ranked output and config files:
ripwire <provider-root> --for="HTTP route definitions and request handlers"
Also check for framework config files in the project root:
Gemfilewithsinatraorrails→ Rubypackage.jsonwithexpress,fastify,koa, orhapi→ Node.jspyproject.toml/requirements.txtwithflaskorfastapi→ Python
If the framework is still ambiguous after these checks, ask the user.
Step 2 — Extract all routes
Use framework-specific patterns to find every route registration. Run all applicable
patterns and deduplicate results. Use --regex= for these patterns: --grep= is a literal
match, so alternation (a|b) silently returns zero hits.
Ruby — Rails
ripwire <root> --regex="resources|get '|post '|put '|patch '|delete '|namespace"
Also expand config/routes.rb to get the full route DSL, then trace each action to its
controller method to find what each path actually maps to.
Ruby — Sinatra
ripwire <root> --regex="get '|post '|put '|patch '|delete '"
Each match is a route; the enclosing block is the handler. Note the in= attribute from grep
hits to get the enclosing symbol name, then --expand it for the full handler body.
Node.js — Express / Fastify / Koa
ripwire <root> --regex='\.get\(|\.post\(|\.put\(|\.patch\(|\.delete\(|\.route\('
Also check for router objects (e.g. router.get() and app.use( with sub-routers.
Python — Flask
ripwire <root> --regex='@app\.route|@blueprint\.|@bp\.'
Python — FastAPI
ripwire <root> --regex='@router\.|@app\.get|@app\.post|@app\.put|@app\.patch|@app\.delete'
Fallback — any language
ripwire <root> --for="HTTP route handler path method registration"
Take the top-ranked symbols. For each one, check if it looks like a route definition (contains
a path string starting with /). Expand promising symbols for confirmation.
Step 3 — Build the route inventory
For each route found, record:
- Path template — normalise concrete values to
{param}placeholders (e.g./users/:id→/users/{id},/users/<int:user_id>→/users/{user_id}) - HTTP method —
GET,POST,PUT,PATCH,DELETE - Handler symbol — the enclosing function/method name (for schema extraction in Step 4)
Deduplicate: same method + path = one entry, keep the first handler found.
Step 4 — Extract schemas per route
For each route, --expand its handler symbol and look for:
Request body: fields set from the incoming request
- Ruby:
params[:field],request.body.read,JSON.parse(request.body) - Express:
req.body.field,req.params.field,req.query.field - Flask:
request.json.get('field'),request.form.get('field') - FastAPI: function parameter with a Pydantic model type hint → extract model fields
Response body: what the handler returns
- Ruby:
render json: { field: value },.to_json - Express:
res.json({ field: value }),res.send() - Flask:
jsonify({ 'field': value }) - FastAPI:
returnstatement orresponse_model=annotation
Status codes: explicit codes set in the handler
- Look for
status:,.status(N),HTTPException(status_code=N),abort(N) - Default: 200 for GET, 201 for POST, 204 for DELETE/PUT/PATCH when no body returned
When inference fails: emit an empty schema stub — do not skip the route. A path with no
schema is still useful for path-coverage testing. Mark it with x-schema-source: stub.
Step 5 — Assemble the OpenAPI YAML
Build a valid OAS 3.x document. Use this structure:
openapi: "3.0.3"
info:
title: "<ServiceName> API"
version: "0.0.0-generated"
description: "Auto-generated from source by oas-generator. Review and refine before production use."
paths:
/resource/{id}:
get:
summary: "GET /resource/{id}"
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: "Success"
content:
application/json:
schema:
type: object
properties: {} # stub — expand if fields found
"404":
description: "Not found"
content:
application/json:
schema:
type: object
properties:
message:
type: string
post:
summary: "POST /resource"
requestBody:
required: true
content:
application/json:
schema:
type: object
properties: {} # stub — expand if fields found
responses:
"201":
description: "Created"
content:
application/json:
schema:
type: object
properties: {}
Schema hints:
- If you found field names from Step 4, list them under
propertieswithtype: string(or a more specific type if the code makes it obvious — integer literals, boolean flags, array literals). - Mark inferred fields with
x-schema-source: inferred; mark stubs withx-schema-source: stub. - Use
required: [field1, field2]only when the handler clearly fails/errors without a field. - Resolve path parameters: every
{param}in the path gets aparametersentry within: path.
Step 6 — Write and report
Save the spec to a file the user can use immediately:
- Default location:
<provider-root>/openapi-generated.yaml - Or wherever the user specifies
After writing, print a summary:
Generated OAS written to: <path>
Routes: N
Inferred: N (schemas extracted from handler code)
Stubs: N (no schema found — empty schema emitted)
Status codes: inferred / defaulted breakdown
Remind the user that the generated spec is a starting point: stub schemas and missing required fields will create false gaps in coverage checks. They should review and fill in schemas before using it for production BDCT.
Tips for accuracy
- Rails routes are indirect —
resources :usersgenerates 7 standard actions; trace each to the controller to find what's actually implemented. Use--expand=UsersControllerand look fordef index,def show, etc. - Nested routes multiply paths —
namespace :api { resources :users }→/api/users,/api/users/{id}. - Middleware / concerns can add routes — grep for
include,concern,prepend_before_action. - Versioned APIs often use a prefix (
/v1/,/api/v2/) — preserve it in the path. - ripwire
--callers=handler_symhelps when a router delegates to a class method — find the registration site to confirm the path and method. - If ripwire returns
over_ceiling=1, narrow the root path to just the controllers/routes directory and re-run.