Skip to content

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 15:33
· 36 commits to master since this release
2c08da7

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 paths says why: a 3.1
    document may legally declare only webhooks or components, 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 parameters list 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-Trace and x-trace are one parameter.

  • Fixed. A header parameter named Accept, Content-Type or
    Authorization is ignored, as both specifications say in the same words.
    Enforcing it turned a conforming request into three violations, and a
    document declaring Authorization as a header parameter — a common habit —
    into one that demanded the header on every request.

  • Fixed. Under OAS 3.0, requestBody on GET, HEAD and DELETE is
    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. $ref is resolved only where it is a keyword. The resolver walked
    every array and read any map carrying a $ref member as a reference, so a
    document that merely showed such a payload — in an example, in an Example
    Object's value, in a Schema Object's default, 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 $ref is still a reference and still
    resolves.

  • Fixed. A response header declaring the boolean schema false fails
    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 — and allowReserved is an annotation validation does not read, like
    example/examples.

  • Fixed. A trailing slash is part of the path. /pets and /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 what ContractViolation carries into an application's log — the
    redaction that guards actual used to find the credential inside
    instancePath and 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
    items or properties member the package cannot read used to survive
    compilation and then surface as response.header.serialization or
    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
    InvalidContract before turning it into a violation, for the one path left:
    an Operation built by hand rather than compiled.

  • Fixed. A parameter name that occurs more than once, where its style
    admits a single value, is request.parameter.duplicate instead of the first
    occurrence. ?n=5&n=999 is a well-formed query whose meaning depends on the
    runtime — PHP keeps the last occurrence in parse_str(), $_GET and every
    PSR-7 getQueryParams(), 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 eighteen Classify::cover gates sit at a
    threshold their own distribution can actually hold.

  • Added. The complete set of Violation codes is written down — in both
    READMEs and llms.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 the pattern/preg_match threat model are stated as
    contract rather than left to be read off the tests.

  • Fixed. Truncated diagnostics no longer end in a partial \uXXXX escape.
    Every value the formatter bounds is json_encode output, so the cut cannot
    split a UTF-8 sequence — but it could land inside an escape and leave
    \u04 where a character was meant.

  • Internal. rasuvaeff/understudy moved to the ^0.9 line.

  • 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 bare InvalidArgumentException out of
    validateResponse(), and Operation::responseFor() answers null for one
    instead of raising where it answers null for 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 content map 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.
    Exact type/subtype now wins over type/*+suffix, which wins over type/*,
    which wins over */*; only equally specific keys fall back to declaration
    order.

  • Fixed. $ref siblings 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 only summary/description on 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
    type next to a $ref used 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
    requestBody that is not an object used to compile to no body at all while
    the response side rejected the same shape; parameters that were not a list
    vanished; a Media Type Object, encoding, headers or Schema Object of an
    unreadable shape was skipped at request time, and the very same document
    raised a bare InvalidArgumentException out of validateRequest() while
    validateResponse() called the body valid. All of them are InvalidContract
    from fromArray()/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 .nan and .inf
    arrive — is rejected there too, rather than surfacing as a raw JsonException
    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 paths produce no operation at all — path items
    with no method, or only x- keys — is rejected. Such a contract answered
    UnknownOperation to 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
    InvalidContract naming the document, with the parser's exception as
    previous. symfony/yaml's ParseException used to escape as itself — a
    third-party type on a public exit, from an optional dependency.

  • Changed. A response content map 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-openapi at the header decision: 287 cases over 20
    operations, up from 274 over 19. The new operation is headers.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. One uploads.create case 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.contentType does 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-openapi can now build the contradiction, and this records
    it. Verified by making the check fail open again on the working tree: the
    replay turns red on encoded.create/part-content-type/1.