OpenAPI 3.0¶
Parses an OpenAPI 3.0.x specification into the Vayu draft model. OpenAPI is a specification document, not a request log - it describes endpoints, parameters, and schemas but carries no concrete values. The parser therefore emits synthetic request stubs: a {{baseUrl}} from the first server, query and header params carrying whatever value the spec declares for them (usually none, in which case the row imports disabled - see Parameter values & enabled state), and a body sampled from the request schema. Users fill in real values after import.
The document is not thrown away once it has been read. The collection it
produces is bound to it: the spec is stored verbatim (with the URL it was
fetched from, when there was one), and every request records which operation it
is - operationId, method, and the templated path. See
OpenAPI Collections for what the binding is for and what the
collection's Spec tab does with it.
- Source:
engine/src/core/import_document.cpp + openapi_drafts.cpp - Exports:
The parse moved engine-side (issue #877). Every rule on this page is the same rule it always was - the corpus in
engine/tests/fixtures/import-conformance.jsonwas recorded from the parser this replaced and is asserted against on every build - it is simply read byengine/src/core/import_document.cppnow, behindPOST /import/parse, rather than in the renderer. Module names in C++ style below name that file's functions; the app holds no parser.
| Symbol | formatName |
formatKey |
|---|---|---|
OpenApiV3Parser (class, implements ImportParser) |
OpenAPI 3.0 |
openapi-v3 |
schemeToAuth (helper) |
- | - |
OpenApiV3Parser exposes formatName / formatKey as readonly fields and implements detect + parse from ./types.
Detection¶
The top-level openapi field must be a string beginning with "3." (so 3.0.0, 3.0.3, and 3.1.x all match). The 3.1 claim is honoured on the schema side too: Sampler::sample reads the JSON-Schema keywords 3.1 adopted - type arrays (type: ["string", "null"]), const, and the plural examples (see Sampler::sample) - and a {"$ref": ...} path item, which 3.1 made common, is resolved rather than dropped. Swagger 2.0 (swagger: "2.0", no openapi field) is handled by a separate parser - see OpenAPI v2. The factory (core::parse_import) parses the raw text once (JSON, then YAML fallback) and runs each parser's detect in registration order.
Tree structure¶
The spec maps to a single root collection, with operations grouped into child collections by their first tag - or, for an operation that declares no tag, by the first meaningful segment of its path (issue #710).
- Root collection ← the whole spec (named from
info.title). It directly holds: requests: every operation that has neither a tag nor a groupable path.children: one child collection per distinct folder name, in first-encounter order.- Child collections ← created lazily by
OperationFolders(openapi_drafts.cpp) the first time a name is seen, keyed in aMap<string, CollectionDraft>. A tag-named folder takes its description from the matching entry in the top-leveltags[]array (if any); a path-named folder has none, because the document declares none.
Iteration order: parse loops over spec.paths entries, resolves each path item through walk::resolve_single_hop (openapi_drafts.cpp) so a {"$ref": "#/components/pathItems/X"} item is read from its target instead of contributing nothing, and then loops over the fixed HTTP_METHODS list (get, post, put, patch, delete, head, options). A path item that is not an object - or whose $ref does not resolve to one - is counted as a malformed_spec SkippedItem and skipped, so the drop is named in the preview rather than silent. The ref is followed one hop only, like the parameter and requestBody refs. For each present operation parse calls buildOperation(...), then routes by tag:
const tag = op.tags?.[0]; // ONLY the first tag is used
const name = tag ?? pathFolderName(path); // the fallback, per operation
if (name) folders.get(name).requests.push(req);
else rootRequests.push(req);
Multi-tag operations: only op.tags[0] is consulted. An operation with tags: ["a", "b"] lands solely in the a child collection; b is ignored (no duplication, no extra folder).
Untagged operations - the path fallback. A document whose operations carry no tags at all is common and not broken: Stripe's official spec declares root-level tags: that no operation references, so grouping by tag alone imported all 568 of its operations as one flat list. path_folder_name (openapi_drafts.cpp) therefore names a folder from the first path segment that names a resource:
- Leading segments that name no resource are stepped over: a version (
v1,v2.1), theapimount point, and a whole-{template}segment./api/v2/{tenant}/ordersisorders. - The segment is taken exactly as written (
account_links), because that is the name the vendor's own documentation uses. - A path with nothing left -
/,/v1,/{id}- yields no folder, and its request stays on the root rather than being filed under a placeholder.
The fallback applies per operation, so a partly tagged document gets both rules. A path-derived name that a tag also uses is one folder holding both, not two folders with one name; the tag's description is kept. Inferring folders from root tags[] entries that no operation references (Stripe's x-kind: api-group list) is a recorded non-goal - matching those names onto paths is vendor-specific guesswork.
Which rule ran is reported as meta.folderStrategy ("tags", "paths" or "mixed"), and the import preview states it whenever paths were involved: a folder tree the document never spelled out must not read as one it did.
trace operations: OpenAPI 3's Path Item Object also defines trace, which is not in HTTP_METHODS because HttpMethod (types/domain.ts) has no "TRACE" - Vayu cannot execute one. A trace operation is therefore not built, is not counted in requestCount, and is counted as an unsupported_method SkippedItem so the preview says so. UNSUPPORTED_METHODS in import_document.cpp is the list; trace is its only member, since a path item defines exactly the eight methods.
Key internal functions: buildOperation (per-operation RequestDraft), OperationFolders / path_folder_name (openapi_drafts.cpp, folder routing - shared with the v2 parser), buildBody / findJsonMedia (request body), pickPrimaryScheme / schemeToAuth (collection auth), and walk::resolve_ref (openapi_drafts.cpp) for $ref resolution - shared with the v2 parser, which built the identical closure by hand until issue #649.
External $refs¶
The resolver above walks #/-rooted pointers inside the document it was given, and nothing else. A reference naming another file (./schemas/pet.yaml#/Pet, https://acme.dev/common.yaml#/Error) is not such a pointer, so it used to resolve to undefined - which every caller reads as the spec documented nothing here: an empty body stub, a missing parameter, an example that never imported, and no entry in meta.skipped to say so.
Those references are now resolved before the parser runs, by ref-bundler.ts (issue #649): each referenced document is fetched (through the engine's /import/fetch) or read from beside the picked file (through the gated specFile:read IPC), inlined under a root x-vayu-bundled key, and every reference into it rewritten to an in-document pointer. What reaches parse is therefore always self-contained, and the parser needs no notion of files.
What could not be reached is counted as an external_ref SkippedItem - one per reference - and its $ref is left exactly as the document wrote it. See OpenAPI Collections for the user-facing rules, including which intake applies to a fetched, picked or pasted document, and why a multi-file spec is stored as the bundle.
Field mapping¶
Collection (root)¶
Built inline in parse.
| OpenAPI | Vayu CollectionDraft |
Notes |
|---|---|---|
info.title |
name |
fallback "Imported API" |
info.description |
description |
fallback "" |
servers[0].url |
variables.baseUrl |
resolved first (see The base URL); only added when a base URL exists: { baseUrl: { value, enabled: true } }; otherwise variables is {}. Additional servers[] are ignored. |
security / components.securitySchemes |
auth |
via pickPrimaryScheme + schemeToAuth (see Auth). Collections never inherit. |
| (none) | preRequestScript / postRequestScript |
always "" |
op.responses |
examples |
via buildExamples (see Documented responses); absent when nothing was representable |
The base URL¶
servers[0].url is not always an address (issue #719). A Server Object may template its URL, and the URL may be relative, so the field is resolved by resolve_server_url before it becomes {{baseUrl}}:
- Server variables are substituted from the defaults the document declares -
{protocol}://{hostname}/api/v3withvariables: { protocol: { default: "https" }, hostname: { default: "api.acme.dev" } }becomeshttps://api.acme.dev/api/v3. The specification requires a default on every server variable, so a complete document always resolves. Single braces are not Vayu variables - only the path goes throughnormalize_template_vars- so before this the literal{protocol}survived into every request line and failed at connect with nothing said. - A relative URL is resolved against the URL the document was fetched from, which is what OpenAPI says it is relative to.
/api/v3fetched fromhttps://acme.dev/specs/openapi.yamlbecomeshttps://acme.dev/api/v3. The source URL reaches the parser as the fourth argument toparse- the factory has always known it (it is alsospec_documents.source_url) and now hands it over. - Anything still unresolvable is kept exactly as written and counted as an
unresolved_base_urlSkippedItem: a variable with no declared default, or a relative URL in a pasted or file-picked document, which has no location to be relative to. A base URL the user can see is unfinished beats a host Vayu invented.
This substitution is what a Vayu-exported skeleton round-trips through (issue #1441, see the export doc): a collection whose requests all use {{baseUrl}} exports a server of {baseUrl} with variables: { baseUrl: { default: "<the value>" } }, and step 1 above substitutes that default back exactly - never the self-referential value a bare, undeclared {{baseUrl}} used to leave behind.
An absolute URL is passed through untouched - not re-serialized through URL, so a stored document's own spelling survives.
Parameter resolution & merge. buildOperation concatenates path-item-level parameters with operation-level op.parameters, resolving any $ref entries via resolveRef. Each parameter is keyed by `${in}:${name}` in a Map, so an operation-level parameter overrides a path-level one with the same in+name (later writes win). Entries missing in or name after resolution are skipped.
Both lists go through the walk's parameters guard (openapi_drafts.cpp) first. parameters is an array per the spec, but a missing - in hand-written YAML makes it a mapping, and spreading that used to throw is not iterable and abort the whole file. A present-but-non-array parameters is now treated as empty and counted as a malformed_spec SkippedItem (once per offending list); an absent parameters is normal and counted as nothing. Every other path in the file still imports.
Documented responses¶
op.responses was visited by no code path before issue #481 - the parser sampled request bodies and walked straight past the half of the spec that says what comes back, so an imported API description documented no responses at all. Each entry now becomes an ExampleDraft, stored against the request and (once the mock server lands) served from it.
OpenAPI (op.responses[code]) |
Vayu ExampleDraft |
Notes |
|---|---|---|
| the key | status |
must be a three-digit 100-599 code; see below |
description |
name |
"{code} - {description}", or the bare code when the response describes nothing |
the JSON media type's example → first examples[*].value → sampleSchema(schema) |
body |
the same precedence buildBody uses for a request body; a string is stored as-is, anything else is JSON.stringify(value, null, 2) |
| the JSON media type key | contentType, and a single Content-Type header |
application/json, an application/json;… variant or a +json suffix, by the same rule request bodies use |
A response $ref is resolved (single hop, like the parser's other refs). An examples entry is unwrapped from its Example Object - the payload is in value, and storing the wrapper would put a body on disk no server would send; an externalValue names a URL rather than carrying a payload, so it yields nothing (an import must not fetch). The map key the entry was taken from is remembered too (spec_example_key, issue #1457, engine-side only - no surface here displays it): the bound export needs it to write an edited example back into that same entry rather than adding a new one beside it.
A response that documents no body still imports: 204 No Content is a real answer and a mock server has to be able to give it.
A key that is not a numeric status is skipped and counted: default as default_response, anything else (a 2XX wildcard, junk) as example_no_status. Both document a real response, but an example is served under one status line and there is no honest value to pick. They are counted apart because default is conformant and near-universal - every one of Stripe's 568 operations declares one - so the preview names it as information while a malformed key stays a warning (issue #710).
| tag and path groups | children | folders.children() |
| operations with neither | requests | |
Collection (per folder)¶
Built by OperationFolders (openapi_drafts.cpp), shared with the v2 parser.
| OpenAPI | Vayu CollectionDraft |
Notes |
|---|---|---|
tags[0], else the first meaningful path segment |
name |
see Tree structure |
tags[].description where tags[].name === tag |
description |
fallback ""; a path-named folder has none unless a tag of the same name arrives |
| (none) | variables |
always {} (baseUrl lives only on the root) |
| (none) | auth |
always { mode: "none" } - tag collections do not carry security |
| (none) | children |
always [] (tags are flat; no nesting) |
| (none) | preRequestScript / postRequestScript |
always "" |
Request (per operation)¶
Built by buildOperation(method, path, op, resolveRef, pathParams, tally, specOperation). The identity is passed in rather than derived here - see Operation identity.
| OpenAPI | Vayu RequestDraft |
Notes |
|---|---|---|
op.summary → op.operationId → "{METHOD} {path}" |
name |
precedence in that order; final fallback uses upper-cased method + raw path, e.g. "GET /users/{id}" |
op.description |
description |
fallback "" |
| HTTP method | method |
method.toUpperCase() (e.g. get → GET), cast to HttpMethod |
path |
url |
`{{baseUrl}}${normalize_path_templates(path)}` - always prefixed with {{baseUrl}}, even if no server was defined (see URL) |
parameters with in: "query" |
params |
{ key: name, value, enabled, description? } via declared_param_row - description included only when present; value and enabled follow Parameter values & enabled state. Only the enabled rows are joined onto the url by parseImport - see The url/params invariant |
parameters with in: "header" |
headers |
{ key: name, value, enabled } via the same declared_param_row - no description carried (the Headers table has no column for one); authorization and content-type headers are dropped (case-insensitive) since Vayu manages those |
parameters with in: "path" / in: "cookie" |
- | not emitted as params/headers; path params are represented in the URL via normalize_template_vars. Cookie params are dropped. |
op.requestBody |
body |
via buildBody (see Request body) |
| (none) | auth |
always { mode: "inherit" } - auth is configured once at the collection level |
| (none) | preRequestScript / postRequestScript |
always "" |
op.operationId, method, path |
specOperation |
the operation this request is, recorded for sync - see Operation identity. Absent for a paths key that does not start with / |
Operation identity¶
Every request records which operation it is - { operationId?, method, path }, the templated path exactly as the document writes it (/pets/{petId}), never the {{petId}} rewrite that goes into the URL. The walk claims one identity per operation (walk_operations, openapi_walk.hpp) and stamps it on the request draft. The declared-operation index stored beside the document is derived by the engine from the bytes it stores (issue #853), and since #877 the stamp comes off that same walk - walk_operations, which is what decides an operation's identity for the index, the drafts and the import alike. Coverage matching a request's stamp against the index is therefore one answer compared with itself, rather than two readers held together by a fixture.
The request itself is now mirrored engine-side as well (issue #865, core::spec_request_drafts_of): everything this page describes - the URL and its joined query, the parameter rows and their enabled state, the sampled body, the folder an operation is filed under - the engine derives from the stored document too, because the sync diff compares a stored request against one of those and is moving down with it. The import runs on that very builder now (issue #877): core::import_drafts_of is core::spec_request_drafts_of with the tally kept and the operations under a malformed paths key included, so there is one implementation of every rule on this page rather than two held to each other. The cross-language conformance fixture that used to hold them together retired with the renderer parser it pinned; what remains is engine/tests/fixtures/import-conformance.json, which records what that parser produced and is asserted against on every build.
A duplicated operationId is kept on its first declaration only (issue #715). Declaring one id on two operations is invalid OpenAPI and common in generated specs, and stamping it on both requests is what let a later sync resolve the second request to the first operation - reporting it as renamed toward an operation it never was, and rewriting its method, URL and identity on an all-defaults apply. The second operation therefore imports identified by its method and path alone, which is what sync then follows it by, and each repeated declaration is counted as a duplicate_operation_id SkippedItem so the preview names the drop. First declaration wins because document order is stable across re-fetches of the same file. Nothing else is lost: the operation itself imports whole, and op.operationId still supplies the request name when there is no summary. A paths key that does not start with / records no identity at all - the engine refuses one.
Parameter values & enabled state¶
A spec's parameters list declares what an operation accepts, not what every request should send - and an enabled row reaches the wire either way: a query row through the url/params join, a header row as a header the request claims to send. declared_param_row (openapi_drafts.cpp, shared with the Swagger parser and applied to in: "query" and in: "header" alike) decides both fields from the parameter alone:
| Parameter declares | value |
enabled |
|---|---|---|
a scalar example / examples / schema.example / schema.default |
that value as text | true |
required: true, no value |
"" |
true - a query row joins as a bare key (?tenant), a header row is listed with an empty value; either is the cue to fill it in |
| nothing, and not required | "" |
false |
Value precedence is the parameter's own example, then the first entry of its examples map (unwrapped by first_named_example), then the schema's example, then the schema's default - the same "concrete example beats generated stub" order buildBody uses, and a default only describes what the server assumes when the parameter is absent, so it ranks last. The schema is followed one $ref hop (deref).
A Parameter Object declares exactly one of schema or content (the latter for a value that needs a media type to be parsed at all, e.g. a JSON object serialized into a query string) - when schema is absent, declared_param_value_v3 reads content's one media type entry through the same {example, examples, schema.example/schema.default} precedence a schema-based parameter gets, rather than importing value-less.
Only scalars become a value. An array or object is serialized by the parameter's style/explode, which this parser does not read, and one row holds one string - so such a parameter imports value-less, like one declaring nothing. A declared "" is value-less too: an empty-value row writes as a bare key, so ?q= is not a shape the Params table can hold.
Why optional value-less parameters import disabled (issues #622, #658): the row is documentation ("this endpoint accepts verbose"), not intent ("send verbose always"). Enabled, a query row joined the stored URL as ?verbose, which some APIs read as verbose=true, and a header row claimed an X-Request-Id: with nothing in it - both a wire change nobody chose. Disabled, the row is still listed in its table one click from use.
An explicit x-vayu-enabled overrides this heuristic entirely (issue #1441). The heuristic above answers "does this look sent" for a document Vayu did not write; it cannot tell a disabled row that carries a value from an enabled one that carries none - and a Vayu-exported skeleton writes both shapes, since a toggle is not derived from its value there either. So a parameter carrying x-vayu-enabled (boolean) takes its enabled from that flag directly, and only falls back to the required/value-presence table when the extension is absent.
URL & path parameters¶
- The request
urlis always`{{baseUrl}}${normalize_path_templates(path)}`.{{baseUrl}}is a Vayu collection variable resolved fromservers[0].urlat import time (defined on the root collection). If the spec has noservers,baseUrlis absent from the root variables and{{baseUrl}}resolves to empty at runtime. - OpenAPI path templates
{param}are converted to Vayu{{param}}bynormalize_template_vars(path_template.cpp). WithpathTemplates(which only the OpenAPI/Swagger parsers pass) it rewrites single-brace{x}(identifier chars[\w$-]) to{{x}}, while leaving any existing{{...}}pairs intact. So/users/{userId}/posts/{postId}becomes/users/{{userId}}/posts/{{postId}}. Path parameters are not also emitted asparamsentries - they live only in the URL.
Request body generation¶
buildBody(requestBody, resolveRef) resolves a top-level $ref on the requestBody, then inspects content:
content media type |
Vayu RequestBody |
How content is produced |
|---|---|---|
application/json (also any key starting with application/json or ending in +json, via findJsonMedia) |
{ mode: "json", content } |
content = JSON.stringify(media.example ?? sampleSchema(media.schema), null, 2). The media-object example wins over the schema; if neither exists, {}. |
text/plain |
{ mode: "text", content: "" } |
empty string (the schema is not sampled for text bodies) |
application/x-www-form-urlencoded |
{ mode: "x-www-form-urlencoded", fields } |
fields = one { key, value: "", enabled: true } per field from schemaFormFields(schema) |
multipart/form-data |
{ mode: "form-data", fields } |
same as urlencoded, except a format: binary field becomes a file part (see File parts) |
no content, or none of the above |
{ mode: "none" } |
File parts¶
A multipart property declared format: "binary" - OpenAPI 3's only spelling of "a file" - imports as a file part: { key, value: "", enabled: true, type: "file", src: "" }. type: array of binary items (the multi-file field) maps to one file row, since Vayu's model is one file per row; one file the user can attach beats a text row that sends nothing. Until this landed both became ordinary empty-value text rows, so an operation documenting an upload produced a request that looked healthy and sent nothing.
The part carries no path: a spec documents that a field is an upload, never which file it uploads. The user picks the file in the request editor, and the engine refuses the send by field name until they do (Form field 'avatar' is a file part with no file selected). The row is deliberately not marked unresolved - that flag warns that a path came from somewhere else and was never verified here, and there is no path to warn about; the row reads "Choose file", exactly like one a user turned into a file part by hand.
Only under multipart/form-data. application/x-www-form-urlencoded has no file form on the wire, so a format: binary property there is a spec that cannot mean what it says; the field stays a text row rather than becoming a part the body could never carry.
The import preview counts these as N file parts need a file, beside the skip counters, so a spec full of uploads says so before the import rather than one failed send at a time.
JSON is preferred: findJsonMedia is checked first and takes precedence over text/form variants. The x-www-form-urlencoded / multipart/form-data branch reads property names, plus each property's format to tell an upload from a text field - required, defaults and nested structure are not sampled into form fields.
Form field names resolve $ref and allOf, exactly as far as a JSON body does. Sampler::form_fields (openapi_drafts.cpp) samples the schema and returns the stub's own keys, rather than reading schema.properties directly. That key does not exist on a schema written as {"$ref": "#/components/schemas/TokenRequest"} or as an allOf - the shape generators emit - which used to yield the right body mode with an empty field list and no warning anywhere. Consequences of going through the sampler: composition follows the first branch only (the same rule JSON bodies get, not an allOf merge), and a schema that samples to a non-object (a scalar, an array, or an example that is not an object) contributes no field names.
Sampler::sample (schema → stub value)¶
Sampler::sample in openapi_drafts.cpp generates a sample JSON value by walking the schema. It is bounded and recursive - materially more capable than a one-level stub:
- Depth cap.
MAX_DEPTH = 6. Oncedepth > 6, the walker returns{}. Non-object / null nodes also return{}. $refresolution + cycle guard. A node with a string$refis resolved viaresolveRefand walked (depth +1). ASetof already-visited$refstrings is threaded down each branch; re-encountering a$refalready on the current path returns{}(breaks reference cycles). Resolution failures (throwornullresult) also yield{}.- Pinned-value precedence:
const→example→examples[0], all checked after$refand before composition andtype. const(JSON Schema, so OpenAPI 3.1) is returned verbatim and outranksexample: it says the value MUST be exactly this, whereexampleis only an annotation.const: nullsamples asnull.exampleis returned verbatim - this is how authors pin exact sample values.examplesis 3.1's plural replacement forexample; its first entry is used when there is no singularexample. An emptyexamples: []is ignored.allOf/oneOf/anyOf- first branch. If any of these is a non-empty array, the walker recurses intobranch[0]only (precedenceallOf→oneOf→anyOf). It does not mergeallOfmembers; it just samples the first.- Type arrays (3.1). 3.1 writes a nullable field as
type: ["string", "null"]where 3.0 wrotenullable: true. The walker samples the first non-"null"member, so such a field gets the typed stub the user edits rather than the{}an unmatched type used to produce. A type whose only member is"null"(and the scalartype: "null") samples asnull. prefixItems(3.1). 3.1's JSON-Schema-2020-12 tuple form omitsitemsand names each position's own schema inprefixItemsinstead; the walker samples every entry positionally ([sample(prefixItems[0]), sample(prefixItems[1]), ...]) whenitemsis absent, rather than the empty array a schema with neither would otherwise produce.- Type defaults:
schema.type |
Sample value |
|---|---|
string |
enum[0] if a non-empty enum is present, else "" |
integer / number |
0 |
boolean |
false |
null |
null |
array |
[ sample(items) ] if items is present, else [] (one element) |
object (or no/unknown type) |
walks each entry of properties, producing { key: sample }; {} if no properties |
The object/default branch is the same fallback used for untyped schemas - a node with properties but no type is still expanded.
Sampler::form_fields is the form-body wrapper over this: it samples the schema, returns Object.keys() of the result ([] for a non-object sample), and marks each field text or file. The file flag is read off the property schema, not the sampled value: the sampler turns a format: binary string into "", which no longer says anything about the field. See Request body generation.
Older notes claimed sampling was "one level only" and "
oneOf→{}". That is not what the code does: sampling recurses to depth 6, resolves and cycle-guards$refs, honorsexample, and follows the first branch ofoneOf/anyOf/allOf.
Auth / security¶
Collection auth is built the same way it always was, and every request still defaults to { mode: "inherit" } so the common case - one scheme for the whole document - configures credentials once.
pickPrimaryScheme(spec) chooses the collection's scheme:
- If
spec.security[0]exists, take its first key (Object.keys(security[0])[0]) and use the matchingcomponents.securitySchemes[name]. - Otherwise fall back to the first entry of
components.securitySchemes.
schemeToAuth(scheme) maps that scheme to a concrete auth (always with empty secrets - the spec has no real credentials):
securityScheme |
Vayu RequestAuth |
|---|---|
type: "http", scheme: "bearer" |
{ mode: "bearer", token: "" } |
type: "http", scheme: "basic" |
{ mode: "basic", username: "", password: "" } |
type: "apiKey", in: "header" / "query" |
{ mode: "apikey", key: scheme.name ?? "", value: "", in: scheme.in === "query" ? "query" : "header" } |
type: "apiKey", in: "cookie" |
{ mode: "none" }, counted as security_unmapped_apikey_cookie (issue #1444) - Vayu's apikey mode has only header/query placements, and reinterpreting a cookie-placed key as a header would send the credential somewhere the document never named |
type: "oauth2" |
{ mode: "oauth2", config: OAuth2Config } via map_openapi_v3_oauth2 - picks the first usable flow (clientCredentials → authorizationCode+PKCE → password → implicit→auth-code+PKCE), fills its tokenUrl/authorizationUrl/refreshUrl/scope (refreshUrl absent from implicit, which has no token endpoint), and seeds clientId/clientSecret as {{clientId}}/{{clientSecret}} placeholders. A flows object naming none of the four the specification defines is { mode: "none" }, counted the same as any other unmapped scheme - never a fabricated client_credentials config with blank URLs, which would be indistinguishable from a genuinely declared one |
missing / any other type (incl. openIdConnect, http with other schemes) |
{ mode: "none" } |
A per-operation security overrides that default (issue #1444), because inheriting unconditionally sent a collection's bearer token to an endpoint the document declared unauthenticated:
Operation's security |
Imported request's auth |
|---|---|
| absent (the key is not on the operation at all) | { mode: "inherit" } - unchanged |
[], or [{}] (OpenAPI's "security is optional here" shape) |
{ mode: "none" } |
| one requirement naming the same scheme the collection already took | { mode: "inherit" } - identical either way, and inherit keeps following the collection if its auth is edited later |
one requirement naming a different scheme schemeToAuth can map |
that scheme's mode, mapped the same way the collection-level scheme is |
anything else Vayu cannot resolve to one concrete mode - more than one requirement (an OR), a requirement naming more than one scheme (an AND), a requirement naming a scheme the document never declares, or a scheme type schemeToAuth has no mode for (mutualTLS, openIdConnect, any other unmapped type) |
{ mode: "inherit" }, counted under its own security_unmapped_* kind - see the table below |
nonExecutableAuth: always 0 - oauth2 now maps to an executable config, and the other mapped schemes (bearer/basic/apikey) are executable too.
Options & lossy behavior¶
This parser is stub-only: it materializes the shape of each request, and the only values it carries are the ones the spec states outright - a query or header parameter's example/default (above) and a response example. The ImportOptions argument (importEnvironments, importScripts) is ignored - the parameter is _opts and is never read.
Dropped / not represented:
- Scripts: all
preRequestScript/postRequestScriptare""(OpenAPI has no scripts;importScriptshas no effect here). - Environments: none produced (
environments: [],meta.environmentCount: 0). OpenAPI has no environment concept;servers[0]becomes a singlebaseUrlcollection variable. - Additional servers: only
servers[0]is used; other entries and per-operationserversoverrides are dropped and counted asservers_dropped(issue #1444). - 3.1 top-level
webhooks: each entry's own operations are counted (webhook_operations) rather than imported - a webhook describes what the API sends to a callback URL the user registers elsewhere, not a request Vayu can send. - Callbacks, links, security scopes: not consumed. (Response schemas and examples are, since issue #481 - see Documented responses.) An operation's
securityitself is consumed since issue #1444 - see Auth / security - but the scopes a requirement names within it are not: only which scheme is used is mapped. - Response headers (
responses[code].headers): not imported. An example's headers carry only the media type it was stored under. - Cookie parameters and path parameters as params: not emitted (path params live in the URL only).
authorization/content-typeheader parameters: dropped (Vayu manages them).deprecated: an operation's own flag has nowhere to land on Vayu's request model, so it imports identically to a current operation - counted (deprecated_operation) rather than silently ignored.- Multi-tag grouping: only the first tag groups an operation.
- Root
tags[]entries no operation references: not turned into folders - see the path fallback in Tree structure. traceoperations: dropped -HttpMethodhas no"TRACE". Counted asunsupported_method(see Tree structure), not silently omitted.- A path item, or a
parameterslist, whose shape the spec does not allow: stepped over and counted asmalformed_specso the rest of the file still imports. - Form-field property schemas: only field names and whether the field is a file (
format: binary) are imported;required, other types, and nested structure are not. - A whole-body binary (
application/octet-streamand other non-form, non-JSON, non-text media types): no body is produced ({ mode: "none" }) and the operation is counted asunmapped_body(issue #719) - unlike a multipart file part, which imports (see File parts). An operation that declares norequestBodyat all is not counted: it lost nothing, and the two used to be indistinguishable. - Cookie parameters (
in: "cookie"): dropped and counted ascookie_param(issue #719). Vayu has no cookie-parameter row - a request's cookies come from the jar - and mapping them onto aCookieheader is a recorded non-goal: the header is one joined value while a spec declares these one at a time, so building it would mean inventing a merge the document never wrote.in: "path"is neither dropped nor counted; it is already carried, as the{{param}}the URL template holds.
meta population: format = "OpenAPI 3.0" or "OpenAPI 3.1", read off the document's own openapi string rather than assumed (issue #1444 - every 3.x document used to report "OpenAPI 3.0"), requestCount = total operations built (TRACE excluded), folderCount = number of folders (folders.count()), folderStrategy = which rule produced them ("tags" / "paths" / "mixed", absent when there are no folders), environmentCount = 0, exampleCount = example responses imported (read off the finished drafts by count_examples), nonExecutableAuth = 0 (oauth2 is now executable), unattached_file_parts = file parts imported with no file attached (unattached_file_parts, read off the finished drafts), and skipped from the ImportTally:
SkippedItem.kind |
Counted when |
|---|---|
unsupported_method |
a path item carries a trace operation |
malformed_spec |
a path item is not an object / its $ref does not resolve; or a parameters value is present but not an array |
example_no_status |
a response key is neither a three-digit status nor default - a 2XX wildcard, or junk |
default_response |
a response keyed default - conformant, and reported as information rather than as a loss |
duplicate_operation_id |
an operationId another operation in this document already declared (see Operation identity) |
cookie_param |
a parameter declared in: "cookie" - one per distinct cookie parameter per operation |
unmapped_body |
a requestBody declaring only media types with no Vayu mode - one per operation, however many such media types it listed |
unresolved_base_url |
servers[0].url still carries a {variable} with no declared default, or is relative in a document with no source URL (see The base URL) |
servers_dropped |
the document declares more than one servers entry - counted per entry past the first (issue #1444) |
security_unmapped_or |
an operation's security offers more than one alternative requirement (an OR) - see Auth / security (issue #1444) |
security_unmapped_and |
an operation's security requirement names more than one scheme at once (an AND) (issue #1444) |
security_unmapped_scheme |
an operation's security names a scheme the document never declares (issue #1444) |
security_unmapped_mutualtls |
an operation's security names a mutualTLS scheme - schemeToAuth has no mode for it (issue #1444) |
security_unmapped_openidconnect |
an operation's security names an openIdConnect scheme - schemeToAuth has no mode for it (issue #1444) |
security_unmapped_type |
an operation's security names a scheme of any other type schemeToAuth has no mode for - an http scheme other than bearer/basic, or a scheme with no readable type (issue #1444) |
security_unmapped_apikey_cookie |
an apiKey scheme names in: "cookie" - see the auth table above (issue #1444) |
webhook_operations |
a 3.1 top-level webhooks entry's own operation - it describes what the API sends to a callback, not a request Vayu can send, so it is counted rather than imported as zero requests with nothing said (issue #1444) |
deprecated_operation |
an operation declared deprecated: true - Vayu's request model has no deprecated flag, so the operation imports identically to a current one (issue #1444) |
An import with nothing to report still yields skipped: [] - only non-zero kinds are emitted.
Shared helpers used¶
| Helper | Source | Use in this parser |
|---|---|---|
normalize_template_vars |
path_template.cpp |
convert OpenAPI {param} path templates → Vayu {{param}} in request URLs |
Sampler::sample |
openapi_drafts.cpp |
generate a sample JSON body from a request schema (bounded, ref-resolving) |
Sampler::form_fields |
openapi_drafts.cpp |
field names for an urlencoded / multipart body, resolved through the sampler, each flagged text or file |
imported_file_part, unattached_file_parts |
import_document.cpp |
build a file form row; count the rows that still need a file, for meta |
walk::resolve_ref, walk::resolve_single_hop, ImportTally |
openapi_drafts.cpp |
resolve an in-document $ref and a $ref'd path item; guard parameters and tally what was dropped |
bundleExternalRefs |
ref-bundler.ts |
resolve references to other files before parse, and count what it could not reach |
declared_param_row, param_value_text |
openapi_drafts.cpp |
one in: "query" or in: "header" parameter as a table row - the value/enabled rule both OpenAPI parsers apply |
response_example, find_json_media_type, first_named_example, example_body_text, deref |
openapi_drafts.cpp |
map one responses entry to an example draft; the halves the two OpenAPI parsers share |
count_examples |
import_document.cpp |
total the examples across the finished drafts, for meta.exampleCount |
This parser does not use the Postman/Insomnia-shaped helpers in import_document.cpp (as_string, to_var_record, map_key_values, map_postman_auth, raw_body, join_exec); it builds drafts directly. See the index for the full shared-helper reference.