Repository navigation
v0.8.0
Three review passes over the package in one day, closing eighteen findings.
Nothing in the public API changed shape — bc-check against v0.7.0 is clean —
but a great deal changed in what the package says about a document or an
exchange, which is why this is a minor rather than a patch. Read the entries
below before upgrading: several of them turn a document that used to compile
into one that does not, and one of them turns a request that used to pass into
a violation.
-
Changed. The refusal of a document without
pathssays why: a 3.1
document may legally declare onlywebhooksorcomponents, and it is this
package that has nothing to validate, not the document that is malformed.
The README now also draws the line the package draws — it checks what a
verdict about a message depends on, and leaves what only affects
documentation to a linter. -
Fixed. The same parameter declared twice inside one
parameterslist is
rejected. A parameter is unique by name and location, and reading the last
declaration silently dropped whichever was stricter — a document could
narrow a parameter and have the narrowing disappear. Across the two lists
the Operation's declaration still replaces the Path Item's, which is the
specification's own rule; header names are compared case-insensitively, so
X-Traceandx-traceare one parameter. -
Fixed. A header parameter named
Accept,Content-Typeor
Authorizationis ignored, as both specifications say in the same words.
Enforcing it turned a conforming request into three violations, and a
document declaringAuthorizationas a header parameter — a common habit —
into one that demanded the header on every request. -
Fixed. Under OAS 3.0,
requestBodyonGET,HEADandDELETEis
ignored, which is what that dialect says consumers SHALL do. OAS 3.1 permits
it instead and only advises against it, so a 3.1 document is validated as
before. -
Fixed.
$refis resolved only where it is a keyword. The resolver walked
every array and read any map carrying a$refmember as a reference, so a
document that merely showed such a payload — in anexample, in an Example
Object'svalue, in a Schema Object'sdefault, or in a specification
extension — was refused; and where the pointer did resolve, the example the
document declared was silently replaced by a piece of the document. Those
positions carry the author's data and are now left exactly as written. An
Example Object reached through$refis still a reference and still
resolves. -
Fixed. A response header declaring the boolean schema
falsefails
closed when the header arrives, as a body declaring it always has. It used to
read as "no schema here" and assert presence only. -
Fixed. The request path comes from the URI rather than from
parse_url()on the rendered URI, which reads a scheme where a relative
path's first segment carries a colon. -
Added. The README says what the package does not do: declared
security
is compiled but never enforced — a request missing its credential validates
clean — andallowReservedis an annotation validation does not read, like
example/examples. -
Fixed. A trailing slash is part of the path.
/petsand/pets/are
different resources to RFC 3986 and to every router an application sits
behind; matching trimmed both ends of both sides, so a document declaring
both compiled and then answered both requests with the same operation,
leaving the other unreachable. -
Fixed. The diagnostic for an unmatched request carries the path, not the
whole URI. The query string is where an API key travels, and the rendered
message is whatContractViolationcarries into an application's log — the
redaction that guardsactualused to find the credential inside
instancePathand redact the line below while printing this one. -
Fixed. A Schema Object is checked all the way down where the document is
compiled, not on the first message that happens to reach a subschema. An
itemsorpropertiesmember the package cannot read used to survive
compilation and then surface asresponse.header.serializationor
request.body.decode— the document's defect blamed on the traffic, and
answered differently depending on which of four places read it first. The
three validators that still catch a deserialization failure now re-throw
InvalidContractbefore turning it into a violation, for the one path left:
anOperationbuilt by hand rather than compiled. -
Fixed. A parameter name that occurs more than once, where its style
admits a single value, isrequest.parameter.duplicateinstead of the first
occurrence.?n=5&n=999is a well-formed query whose meaning depends on the
runtime — PHP keeps the last occurrence inparse_str(),$_GETand every
PSR-7getQueryParams(), Go keeps the first, Node keeps both — so reading
the first let a request satisfy the contract with one value and hand the
application another, which is a silent bypass of every scalar assertion.
Exploded lists are untouched: repeating the name is what that style means. -
Internal. The body decoders, the value decoder, the message-reading
trait and the response selection value object are named in#[Covers]at
last. Infection is fed by those attributes, so an unlisted class gets no
mutants at all: roughly 470 of them — every regression the two body decoders
could suffer — were invisible to the gate. The suite gained the cases those
mutants asked for; the measured MSI is back above the gate on the honest
population. -
Internal. The parameter codec's property tests gained the example phase
they were missing: the eighteen style/explode combinations now run
deterministically before the random phase instead of waiting on the
generator to draw them, and the eighteenClassify::covergates sit at a
threshold their own distribution can actually hold. -
Added. The complete set of
Violationcodes is written down — in both
READMEs andllms.txt— with a test that pins it against what the source
actually emits, so a new or renamed code is a deliberate edit rather than a
side effect. The documents also say what is stable: the code is, the
formatter's message text is a diagnostic and may be reworded. -
Added.
Operation::responseFor(), public since 0.2.0, is documented.
The parameter merge (Operation over Path Item, last declaration wins within
one list), the host-agnostic matching of a request that carries no
authority, and thepattern/preg_matchthreat model are stated as
contract rather than left to be read off the tests. -
Fixed. Truncated diagnostics no longer end in a partial
\uXXXXescape.
Every value the formatter bounds isjson_encodeoutput, so the cut cannot
split a UTF-8 sequence — but it could land inside an escape and leave
\u04where a character was meant. -
Internal.
rasuvaeff/understudymoved to the^0.9line. -
Added.
ContractException, the type every exception this package raises
implements. "Handle anything the contract can throw" had to be written as
the current list of classes and rechecked on every upgrade, or widened to
\InvalidArgumentException, which catches this package together with
everything else on the stack. The concrete parents are unchanged. -
Fixed. A status outside 100-599 is a violation
(response.status.invalid), not a bareInvalidArgumentExceptionout of
validateResponse(), andOperation::responseFor()answersnullfor one
instead of raising where it answersnullfor an undeclared 418. -
Fixed. A body stream that reports it is not finished and then reads
nothing is a violation (request.body.unreadable/
response.body.unreadable), like the non-seekable stream and the oversized
body next to it. It used to leave the public validate methods as a bare
\RuntimeException. -
Fixed. A
contentmap is matched by specificity instead of by the order
its keys happen to be written in. A*/*entry above an exact media type
used to decide the body's schema, and moving the two lines changed what the
document said — while OpenAPI gives a map's key order no meaning at all.
Exacttype/subtypenow wins overtype/*+suffix, which wins overtype/*,
which wins over*/*; only equally specific keys fall back to declaration
order. -
Fixed.
$refsiblings are read by the dialect the document declares and
by the position the node sits in. Every reference used to be merged with its
siblings winning, which is not what any of the four cases says: OAS 3.0
ignores added properties on a Reference Object — and a 3.0 Schema Object
is one — while 3.1 keeps onlysummary/descriptionon a Reference
Object and makes a Schema Object's siblings apply in addition to the
reference, as JSON Schema 2020-12 requires of an applicator. A sibling
typenext to a$refused to silently replace the referenced constraint
in a 3.0 document; it is ignored now, and in 3.1{$ref: X, maximum: 10}
compiles to the conjunction of both instead of dropping whichever the merge
overwrote. -
Fixed. A declaration this package cannot read is now rejected where it
is written, instead of being read as "there is nothing to check here". A
requestBodythat is not an object used to compile to no body at all while
the response side rejected the same shape;parametersthat were not a list
vanished; a Media Type Object,encoding,headersor Schema Object of an
unreadable shape was skipped at request time, and the very same document
raised a bareInvalidArgumentExceptionout ofvalidateRequest()while
validateResponse()called the body valid. All of them areInvalidContract
fromfromArray()/fromJson()/fromFile()now, and the two directions read
one document the same way. A schema this package cannot hand to the backend
at all — a value JSON cannot encode, which is how YAML's.nanand.inf
arrive — is rejected there too, rather than surfacing as a rawJsonException
on the first request that happened to use it. -
Fixed. A boolean field written as a string (
required: "true",
explode: "yes") is rejected instead of silently reading as its default,
which made a required parameter optional. -
Fixed. A document whose
pathsproduce no operation at all — path items
with no method, or onlyx-keys — is rejected. Such a contract answered
UnknownOperationto every request, which reads as "this request is wrong"
when what is wrong is the document. -
Fixed. A YAML file that does not parse leaves
fromFile()as
InvalidContractnaming the document, with the parser's exception as
previous. symfony/yaml'sParseExceptionused to escape as itself — a
third-party type on a public exit, from an optional dependency. -
Changed. A response
contentmap with nothing in it now means "no media
type is declared", so a body that arrives under one is reported as
undeclared. It used to skip body validation entirely. -
Internal. The generated corpus is re-recorded against
property-testing-openapiat the header decision: 287 cases over 20
operations, up from 274 over 19. The new operation isheaders.get— a
scalar, a list and an exploded object header — which the corpus could not
carry before, because the zoo had no header parameter at all and both
packages percent-encoded, so they agreed with each other and with nobody
else. Oneuploads.createcase changed shape: the generator no longer
builds a multipart text part with whitespace on its edge, since the parsers
in the wild trim it and the value the application receives would not be the
value the case recorded. No case changed its verdict. -
Internal. The generated corpus gains the three cases that close its one
known blind spot: a multipart part sent under a media type its
encoding.contentTypedoes not allow (290 cases, up from 287). Neglecting
that keyword is fail-open — a validator that reads it and ignores it accepts
every valid case unchanged — so replaying the corpus against such a validator
was green, and no amount of valid traffic could have said otherwise.
property-testing-openapican now build the contradiction, and this records
it. Verified by making the check fail open again on the working tree: the
replay turns red onencoded.create/part-content-type/1.