fix(compilers/openapi): keep a path item's undeclared keys - #382
Open
OmarAlJarrah wants to merge 3 commits into
Open
fix(compilers/openapi): keep a path item's undeclared keys#382OmarAlJarrah wants to merge 3 commits into
OmarAlJarrah wants to merge 3 commits into
Conversation
Every OpenAPI object the compiler reads records the keys the specification does not define for it, kept verbatim under Unmodeled and announced at warning. A Path Item Object was the one exception, because the library takes no census for it: the unmarshaller folds a key it does not recognize into the item's embedded operations map rather than recording it as undeclared, so GetUnknownProperties is empty however much the document wrote. The value written under such a key reached the IR in no form. For a scalar the key was at least named, by the type mismatch the fold produces when something that is not an operation object is unmarshalled as one; for a well-formed operation under an undefined key there was no finding at all, and two documents differing only in it compiled to the same IR. Read what is left of the operations map once the HTTP methods are taken out. Everything else a path item may write — summary, description, servers, parameters, additionalOperations, x-* — is a field of the library's model, and a $ref is consumed by the reference wrapper, so nothing else reaches that map. The predicate is the library's own IsStandardMethod rather than this compiler's httpMethods: the two answer different questions, and reading the map against httpMethods would report a valid OpenAPI 3.2 `query` operation as an undeclared key, since the compiler does not lower that method yet. Whether it should is a separate question and stays open. Entries land on every operation of the item, like its servers and its x-*, under the same key spelling, reason and severity the census already uses. All three routes that lower a path item — a path, a webhook, a callback expression — reach it through the one entry point that already applies the other two.
applyPathItem runs once per operation an item produces, so an item that produces none reached it through nothing. Its servers, its extensions and its undeclared keys were dropped whole, and no diagnostic named the loss -- the census added above included, since it had no carrier to write to. An item produces no operation when it declares none, when every method it declares is one this compiler does not lower yet (#293), or when the only keys it holds are ones the Path Item Object does not define. They go on the service, which is where the Paths Object's own extensions already go for the same reason: a path item lowers to no node, so the nearest node holding an Unmodeled map is what holds them. The key carries the item's own pointer, because one service holds every such item and a bare prefix would let two collide -- the survivor decided by iteration order. A warning says why they are there rather than on an operation. Both loops that mount operations off a path item are swept, paths and webhooks. What is kept is what applyPathItem keeps anywhere; a path item's summary and description are dropped here as they are dropped on a mounted item, and the message does not claim otherwise.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked. Base is
feat/openapi-census-remaining-carriers(#378), which is itselfbased on
fix/openapi-extensions-everywhere(#345) and carries a merge offix/openapi-unknown-key-census(#356). The undeclared-key census this extendslives on #356/#378, and the deferral note this rewrites lives on #378, so merge
order is #345 → #356 → #378 → this. The diff here is the one commit on top of
#378; nothing else in it is mine.
Summary
Every OpenAPI object the compiler reads records the keys the specification does not
define for it, kept verbatim under
Unmodeledand announced at warning. A Path ItemObject was the one exception, and the reason was upstream: the unmarshaller folds a
key it does not recognize into the item's embedded operations map rather than
recording it as undeclared, so
GetUnknownPropertiesis empty however much thedocument wrote (speakeasy-api/openapi v1.24.0).
The value written under such a key reached the IR in no form:
Before, that compiled to one diagnostic — the type mismatch the fold produces when a
scalar is unmarshalled as an Operation — and no
Unmodeledentry, so the1wasgone. The quieter half is worse: give the key a well-formed operation as its value
and the fold succeeds, so there was no finding at all and two documents differing
only in that key compiled to the same IR.
This reads what is left of the operations map once the HTTP methods are taken out.
Everything else a path item may write —
summary,description,servers,parameters,additionalOperations,x-*— is a field of the library's model, anda
$refis consumed by the reference wrapper around it, so nothing else reaches thatmap. The value comes off the raw node, the shape
dependentRequiredalready uses.The predicate is the library's own
IsStandardMethod, not this compiler'shttpMethods. The two answer different questions:httpMethodssays what thecompiler lowers, while this asks only whether a key names a method at all. Reading
the map against
httpMethodswould report a valid OpenAPI 3.2queryoperation as anundeclared key, because the compiler does not lower that method yet — whether it
should is a separate question, and this leaves it exactly where it stands, along with
additionalOperations, which is a declared field and so never censused.Entries land on every operation of the item, like its
serversand itsx-*, underthe key spelling (
openapi:pathItem/<key>), reason (out_of_scope) and severity(warning,
openapi/unknown-object-key) the census already uses. All three routes thatlower a path item — a path, a webhook, a callback expression — reach it through the
one entry point that already applies the other two.
Refactoring, for the record:
censusnow takes the key list and the mapping nodeinstead of a model, so a caller that has its own keys can share the grading rather
than restate it; the sort moved with it, since neither source of keys hands over
document order.
UnknownKeysUnderis unchanged from the outside.Test plan
gofmt,go vet,golangci-lint run(0 issues),go build ./...and./scripts/check-coverage.shall pass.Compiled and inspected rather than read:
openapi:pathItem/bogusPathItem=1ongetX,reason
out_of_scope, pointer/paths/~1x/bogusPathItem, with a warningopenapi/unknown-object-keyat that pointer. The pre-existing type-mismatch erroris untouched: the diagnostic list goes from one entry to two.
bogusPathItem: {operationId: nope, …}plus an uppercaseGET:) went from zero diagnostics and zero entries to both keys kept wholewith a warning each.
getandqueryside by side compiles with zerodiagnostics and no
openapi:pathItem/entry —queryis not reported.$ref,summary,description,servers,parametersandadditionalOperationsreports no undeclared key either, and the$ref'd item'soperation still lowers, so the census demonstrably ran on it.
Tests, each confirmed to fail when the behaviour is removed:
TestUnknownKeys_PathItemKeyKeepsItsValue— the reproducer verbatim.TestUnknownKeys_PathItemDeclaredFieldsAreNotUndeclared— the control, includingquery. SwappingIsStandardMethodfor a scan ofhttpMethodsturns it red withkey "query" is not defined by the OpenAPI object it is written on, which is thewhole reason the library's vocabulary is the one being read.
TestOperations_PathItemUnknownKeyKeptOnEveryRoute— path, webhook and callback,each with its own marker value. Restricting the census to the
pathswalk reddensthe other two rows.
testdata/openapi/unknown_keys.yamlgains a path-item row (an uppercaseGET, thesilent case), so it runs through the corpus oracles — no-panic, no error diagnostic,
irverify, round-trip, determinism and the two-order diff. Deleting the line fromthe fixture reddens
TestUnknownKeys_KeptAtEveryObject, and disabling the census atthe path item reddens it too.
Closes #377
Follow-up in this branch: an item that mounts no operation
Reviewing the census's reach turned up the one path item shape it could not
cover.
applyPathItemruns once per operation an item produces, so an itemproducing none reached it through nothing — and lost its servers, its
extensions and its undeclared keys whole, with no diagnostic naming the loss.
Before: no
Unmodeledentry anywhere, and the only diagnostic was the library'stype mismatch about the fold. After: all three kept on the service, plus a
warning saying why they are not on an operation.
An item mounts no operation when it declares none, when every method it declares
is one this compiler does not lower yet (#293), or when the only keys it holds
are ones the Path Item Object does not define.
Where they go. The service, which is where the Paths Object's own extensions
already go, for exactly the reason stated in
LowerService: a path item lowersto no node, so the nearest node holding an
Unmodeledmap is what holds them.The key carries the item's own pointer (
openapi:pathItem/paths/~1no-op/x-vendor)because one service holds every such item and a bare prefix would let two collide,
with the survivor decided by iteration order. The operation carrier's keys are
unchanged,
openapi:serversincluded.Both loops that mount operations off a path item are swept — paths and webhooks.
Stated limit: a path item's
summaryanddescriptionare dropped here, asthey are dropped on a mounted item too (verified: neither survives anywhere
today). That is a separate, pre-existing gap; the warning names servers,
extensions and undeclared keys only, and does not claim otherwise.
Bite proof: removing both orphan branches reddens
TestUnknownKeys_PathItemWithNoOperationKeepsWhatItWroteon all four keys andthe announcement count; removing only the webhook branch reddens exactly the
webhook key and the count.
TestPathItem_WithNoOperationAndNothingToKeepIsSilentholds the other half — an item with nothing beside its operations announces
nothing.