Repository navigation
feat(ir)!: scope servers to operations by ID, home wire facts - #1144
Conversation
An OpenAPI operation could write four things the IR held only as no_ir_home Unmodeled entries: a path item's and an operation's own servers, a header's style and explode, a parameter's allowEmptyValue and an encoding's allowReserved. Each now has a typed home. Servers become named entities. ir.ServerID is derived from the pointer of the Server Object that declares the server, and Server gains ID and Provenance. Document.Servers is a registry keyed by ServerID holding every server the source declares, wherever it declares it. Service.Servers and Channel.Servers name servers by ID instead of indexing a list, and Operation.Servers is new: the servers an operation is sent to in place of its service's. An empty service or channel list now names no server rather than every server, since the registry also holds servers only one operation uses. The OpenAPI compiler lowers servers through a new servers package that both walks reach. The service names the document's servers. An operation names its own, else its path item's, else none, and a path item two paths mount declares its servers once. A document whose servers field is absent or empty is served at the "/" OpenAPI supplies, minted as server/default with no pointer, so it is no longer indistinguishable from a declared server. HTTPParamBinding gains AllowEmptyValue, PartEncoding gains AllowReserved, and Property gains Style and Explode, which only a header carries; pass/validate reports either on any other property (ir/header-serialization-outside-header). An allowEmptyValue written under a style that cannot write an empty value is now reported as having no effect. BREAKING CHANGE: the IR shape changes, so IRVersion is bumped. Document.Servers is a map, Service.Servers and Channel.Servers are []ServerID with new empty semantics, and the openapi:servers, openapi:operationServers, openapi:allowEmptyValue, openapi:style, openapi:explode and openapi:<scope>/allowReserved entries are no longer written. The diagnostic code ir/server-index-out-of-range, which pass/validate and irverify both reported, gives way to the general ir/dangling-server-ref.
Main took IRVersion 0.20.0 for the flattened-properties change, so this branch's shape change moves to 0.21.0: the constant, its generations row and its ir-design history entry. The goldens are regenerated over the merged code.
The split-spec golden gains a path item and an operation in paths.yaml that write servers. Each is registered in paths.yaml's namespace at the pointer it is declared at there, and the operation names its own, which the test asserts beside the golden so a server minted in the source's namespace fails it rather than only moving a golden line.
Main took IRVersion 0.21.0 for the typed inferred-marker list, so this branch's shape change moves to 0.22.0. The operation walk's signatures keep both changes: the walk accumulator this branch threads, and the ir.Inferred marker main typed. The test of the finding an operation's own servers drew stays deleted, as this branch removes that finding.
A path item's and an operation's Server Objects now lower as the document's do, so a key the Server Object does not define is kept on the server and reported as an unknown-object-key warning, where before it rode silently inside a verbatim copy of the list. The census fixture gains one such key at each level and the census test a row for each, so the rule that every object is judged covers them.
Document.Servers was described as holding every server the source declares. Two declared lists are not lowered: one a nearer layer of a path item reference overrides, and one inside a component path item no reference reaches, which is kept whole. The IR and compiler docs now say the registry holds the servers whichever object declares them, and the path-item-servers rule names both exceptions. ir-spec-matrix gains a per-op-servers row, the capability Operation.Servers homes, witnessed by four OpenAPI conformance specs, and irverify's comments name servers among the keyed registries.
A path item that mounts no operation drew one warning claiming both that its servers were registered and that its extensions were kept, whichever it wrote. The warning now names only what landed. The server registry's Root no longer dereferences a context holding no document, and Servers hands back a copy rather than the map the registry keeps writing to. The documents stop stating counts that drift: the 0.22.0 history entry and the server reference sites no longer number what they list, and the plan view's ParamBind is described by the facts it carries rather than as the whole of a binding's serialization. ir-design §10 names the version servers stopped being indexed in.
An IR change meets things ir-design does not spell out: another open PR claiming the same IRVersion, which merges silently on the constant; classification tests that a new field, ID class or sum kind must pass, and the one gap among them, a stale entry that passes; guarantees a map held that a list does not; and predicates that name a struct's fields one by one. morphic-changing-the-ir loads for work under ir/ or pass/, the IR and emitter design documents, and IRVersion. It links ir-design §2.1 and architecture §2.5 for the bump and landing rules rather than restating them, and names the bump no test sees: a change of meaning with no change of shape. The four checker rules about the IR's own verifier and codecs move here from verifying-changes. irversion_peers.py lists the open PRs that claim an IRVersion past main's and reports a collision or a claim that trails main; with the working tree set to 0.22.0 it reports #1144's claim of the same.
|
This PR and #1147 meet in three places, and none of them shows as a merge conflict:
With those three on the merged tree, |
main's allocation test holds every validate family to costing nothing per sound value, so the header rule spells a location only for a diagnostic, and its fixture names servers in the registry's shape.
Property.Style and Property.Explode meant something at three of the six positions holding a Property and nothing at the others, so a validate rule kept them off a model's and a resource's properties, over a hand-named list of carriers and through ir.ForEachGroup, a traversal nothing else called. A headers list is a list of its own, so the type can hold the rule. Response, ErrorCase and PartEncoding now hold []Header, a Property with Style and Explode beside it. It encodes flat, so a headers entry keeps the members it had. The rule, its diagnostic code and ForEachGroup go, and a model's properties no longer pay for two fields they cannot use. Breaking: Response.Headers, ErrorCase.Headers and PartEncoding.Headers are []Header, not []Property, and Property has no Style or Explode.
A path item's list was read once per operation it mounts, through a registry and a list threaded on the carrier that holds what a path item keeps. Servers are not kept, they are named, and a fact belongs to the level that owns it. mountPathItem now reads the list once, through itemServers, and an operation naming none of its own takes a copy of the IDs, so the override sits in one place and carrier is back to what it holds. Root drops a check for a server it could only have registered itself: it runs once a compile. Tests pin what the mount now owns: that two operations do not share a list, that each layer of an unmounted item says what it landed, and that a finding on an operation's own server reaches the caller, which nothing had held.
The order oracle compared the type registry alone, on the reasoning that an ID-keyed registry is unchanged by a permutation unless a lowering collided. Document.Servers is such a registry now, and a server is lowered at whichever mount reaches it first, so it answers to the same question. A server's variables follow the mapping that declares them, so they are compared as a set, as a model's properties are.
TestRegistryChecks_ACleanEntryCostsNothing holds every check to what its entries cost, and its fixture held every entry a check ranges over but the servers checkIDs and checkRegistryKeys now range over, so spelling a path per server drew nothing.
Each limit the change leaves open is named where a reader of that code or section reaches it: an empty servers list at Registry.Declared (#1081), the unmounted warning's site at preserveUnmountedPathItem (#1164), and a success response's headers in emitter-design (#1163). applyPathItemDocs cited #285 for a path item's summary and description, which that issue never covered and this change closes; #383 is where they are open. ir-design said a binding carries every serialization fact beside a list that left out Prefix and PathPattern, so it no longer claims to list them.
Summary
An OpenAPI operation can write four things the IR held only as
no_ir_homeUnmodeledentries, so an emitter had no typed field to read for them:serversopenapi:servers,openapi:operationServerson each operationDocument.Serversentries named by the newOperation.Serversstyle,explodeopenapi:style,openapi:explodeon the header'sPropertyHeader.Style,Header.ExplodeallowEmptyValueopenapi:allowEmptyValueon theParameterHTTPParamBinding.AllowEmptyValueallowReservedopenapi:encoding/<part>/allowReservedon theContentPartEncoding.AllowReservedThe issue's fifth row, an error response's media types, needed no change:
ErrorCase.Payloadalready carries oneContentper media type.Servers are entities now
Servers were the one named entity referenced by list index, and that model could not hold an operation's own servers. An index stops naming its server once a pass removes or reorders an entry. And "an empty
Service.Serversmeans every server" would have sent a service to servers only one operation uses. So:ir.ServerID(kind prefixserver) is derived from the pointer of the Server Object that declares the server, in the declaring document's namespace.ServergainsIDandProvenance.Document.Serversis a registry,map[ServerID]Server, likeTypes,Channels,MessagesandAuth. It holds the servers the source declares, whichever object declares them. Two OpenAPI lists stay out of it, as their objects' rules already have it: a list a nearer layer of a path item$refoverrides (reported as a collision), and one inside a component path item no reference reaches (kept whole with the component).Service.ServersandChannel.Serversare[]ServerID, most preferred first.Operation.Serversis new: the servers the operation is sent to in place of its service's. Empty on an operation means its service's apply. Empty on a service or channel means no declared server. A format that reads a channel naming none as served by every server, as AsyncAPI does, lists them all./for a document whoseserversis absent or empty is minted asserver/defaultand records no pointer. This settles ir: Server has no provenance, and the default server the parser injects is lowered as if declared #672's second half: the injected default is no longer indistinguishable from a declared server.In the OpenAPI compiler, a new
internal/serverspackage owns the lowering, and both walks reach it, as they dointernal/auth. The service walk names the document's servers. Each operation names its own, else its path item's, else none. A path item's list is read once per mount, whatever number of operations that mounts, and each operation holds its own copy of the IDs. A path item mounted from two paths declares its servers once, and both mounts name the same IDs. A path item's list is registered even where every operation overrides it, so the override loses no declaration, and so is the list of a path item that mounts no operation. The warning such an item draws now names only what landed: its servers registered, what else it writes kept beside it, or both. Server variables now keep the order the document writes them in; the verbatim copy had sorted them by key. A key a path item's or an operation's Server Object does not define is now kept on the server and reported as anopenapi/unknown-object-keywarning, as the document's own servers' are; it used to ride silently inside the verbatim list.The operation walk's per-service accumulators (the type registry, the operationId claims and now the server registry) travel as one
walkvalue rather than as separate parameters.Serialization facts
HTTPParamBinding.AllowEmptyValuesits besideAllowReserved. A parameter's binding already held every other serialization fact, so its absence read as an oversight. OpenAPI calls the field NOT RECOMMENDED, but a document that writes it still states it, Swagger 2.0 writes it too, and a request validator or mock server needs it. OpenAPI says it SHALL be ignored under a style whose empty value its style table leaves n/a (spaceDelimited,pipeDelimited,deepObject). That combination now draws anopenapi/inapplicable-location-keywordwarning from the validation table, as anallowReservedwith no effect does. The field is a plain bool besideAllowReserved, since a declaredfalsemeans what an absent one does.Headeris a new type, andResponse,ErrorCaseandPartEncodingholdHeaders []Header. It embedsPropertyand addsStyleandExplode, which hold what a header declares, nil or empty when it declares nothing, so a declaredexplode: falseis distinguishable from none. It is a type rather than two fields onPropertybecause those fields would mean nothing on a model's or a resource's property, and a check over a hand-named list of positions would have to keep them off; here that state cannot be written, so no check is needed.Property.Argstakes the check's route because a field argument can sit at any depth of a model, while a header is a list of its own. AHeaderencodes flat, so a headers entry is a property's object with two more optional members.Styleis a field rather than a constant because Swagger 2.0's response-headercollectionFormatwrites space- and pipe-delimited lists.PartEncoding.AllowReservedcloses the same gap for an encoding.partEncodingEmpty, which leaves an empty part encoding out ofContent.Encoding, lists fields by hand. It now reads the new field too, so a part declaring onlyallowReservedkeeps it, and a new test fails if a laterPartEncodingfield is left out of that list. As on a parameter, a declaredfalselowers as unset.The decisions and their reasoning are written where the next reader reaches them: ir-design §10 (servers), §7.2 (
Header) and §8.1 (allowEmptyValue). emitter-design §8.10 says how a client uses an operation's own servers, and §3.4'sParamBindcarries a binding's escaping, empty-value and media-type facts beside its style.ir-spec-matrixgains aper-op-serversrow for the capability, witnessed by four OpenAPI conformance specs.Left out on purpose
servers: []on a path item or operation still lowers as an absent one. Whether it should override the list above it is openapi: decide whether an explicit empty servers list on a path item or operation overrides the servers above it #1081's decision. The new shape can express either answer.styleorexplodewritten beside itscontent, where they have no effect, now lowers silently onto the property, as a parameter's already does. That is openapi: style, explode and allowReserved beside a parameter's content lower silently #887. The new header site is noted in the code and in the lowering spec. Before this change the keywords drew an info only because the IR had no field for them, not because they had no effect.--explainreports type nodes and diagnostics only, so it still shows nothing at a server's pointer, as it shows nothing at an operation's.StyleandExplodehave no designed consumer. PlannedError has no Headers and no media-type election now that ErrorCase is a response #447 is the error-side half of that gap, and docs/emitter-design: say how a client reads a success response's headers #1163 the success side.Closes #285
Closes #672
Breaking
The IR shape changes, so
IRVersionmoves to 0.22.0, recorded in ir-design §2.1 and thegenerationstable.Document.Serversismap[ServerID]Server, not[]Server.Service.ServersandChannel.Serversare[]ServerID, not[]int, and an empty list now names no server rather than every server.ServergainsIDandProvenance, andOperationgainsServers.HTTPParamBindinggainsAllowEmptyValueandPartEncodinggainsAllowReserved.Response.Headers,ErrorCase.HeadersandPartEncoding.Headersare[]Header, not[]Property. Field reads go through the embeddedPropertyunchanged; a function taking a[]Propertyof headers takes a[]Header.openapi/unknown-object-keywarnings.openapi:servers,openapi:operationServers,openapi:allowEmptyValue,openapi:style,openapi:explode,openapi:encoding/<part>/allowReservedoropenapi:itemEncoding/allowReservedentries. It drops the infoopenapi/degraded-constructfindings that announced them, and the per-itemopenapi:pathItem/<item pointer>/serversentry for a path item that mounts no operation. A declaredfalseforallowEmptyValueor an encoding'sallowReserved, which the verbatim copy kept, is no longer recorded, since it means what an absent one does.ir/server-index-out-of-range, which pass/validate and irverify both reported, is gone. A server reference that names nothing isir/dangling-server-ref, as every other class of ID is.Test plan
make gatepasses on the head, the exactly-100% statement check included.ir/testdata/shape.golden.txtandtestdata/unwitnessed.golden.txtare regenerated. Every golden line the change moves is accounted for: each removedUnmodeledentry pairs with a removeddegraded-constructinfo and a new field. The census losesService.serversand gains nothing: every new member is witnessed by a committed spec.Rootreading through the getter, or a context with no document;Servershanding out the registry itself; variables sorted; a header'sStyleorExplodeleft unread; aHeaderencoded nested rather than flat;AllowEmptyValueor a part'sAllowReservedleft unread;partEncodingEmptyignoringAllowReserved; the n/a-style rule removed; irverify skipping server registry keys or ID agreement, not takingServerIDfor an ID, or spelling a path per server in either registry check; the warning for a path item that mounts no operation naming servers no layer of it landed, or that another layer landed; the order oracle skipping the server registry, or comparing variables by position.degraded-constructfindings,--verifyclean. The operation with its own servers names them; its sibling names the path item's. A path item$ref'd from another file registers its servers once in that file's namespace, provenance naming that source.cloudflare/api-schemasopenapi.jsonatef1d30e): main and this branch both exit 1 on the same 9 errors. This branch's diagnostics are main's less 11 infos (9 path-item servers, 2allowEmptyValue), and nothing is added. The 9 operations that had only anopenapi:serversentry now name their servers, and the service names the document's one server. Commands:morphic compile openapi.json -o out.json, thenjq '[.. | objects | select(has("bindings") and has("servers"))] | length' out.json→ 9. Five alternating runs of each build show no measurable cost: main's median is 108.8B instructions retired (range 103.8B to 109.8B), this branch's 105.9B (104.3B to 108.2B), and peak memory is level.testdata/and a local corpus of real-world specs, 2,165 documents against main: no exit code changes. For all 410 operations main kept servers for, the URLs this branch'sOperation.Serversnames equal the list main kept, in order, the operation's own where it wrote one. For all 2,072 documents with IR,Service.Serversnames what main held inDocument.Servers, in order.morphic-harnessgives the same verdict per spec on main and this branch over the same two trees, 2,162 specs, with its order oracle now comparing the server registry. A separate two-order diff of each operation's and service's servers over the 2,057 documents compiling in both orders, 129 of them with an operation's servers, finds none different.Headerand on a model's property draws the sameirverifyandpass/validatecodes, so embedding narrowed no check; the one difference isArgs, which validate has only ever read on a model. A compiled document with an unknown or repeated member in a header is refused at decode. Server count does not grow the allocations ofVerifyorValidate: 812 and 693 at 500 servers, 853 and 716 at 2,000.