Repository navigation
Releases: rasuvaeff/openapi-contract
Releases · rasuvaeff/openapi-contract
Release list
v0.16.1
- Fixed. A closed object (
additionalProperties: false) whose declared
member fails its own subschema no longer receives an extra
additionalPropertiesviolation of the parent over the member's own
failure (#175). The backend marks a member as evaluated only when every
declared member passed, so the failing one still counted as unchecked when
additionalPropertiesran; a member the siblingpropertiesmap declares,
or one of itspatternPropertiespatterns matches, is now what the leaf
filter treats as not additional. An undeclared member, and a member that
fails the object form ofadditionalProperties, keep their leaves; the
verdict never moved.
v0.16.0
- Added. OpenAPI 3.1 schemas now support
prefixItems,containswith
minContains/maxContains,dependentRequired,dependentSchemas, and
unevaluatedProperties/unevaluatedItems(#157). These keywords are
compiled, shape-checked, eagerly parsed, and included in directional
rewrites;contentSchemaremains fail-closed. - Added. OpenAPI 3.1 schemas support the conditional keywords
if,
thenandelse(#157). Each is a subschema the compiler emits, the load
-time check walks, and the backend evaluates under draft 2020-12, at any
depth and nested inside one another; a document using a conditional
required— ordinary OpenAPI today — is loadable instead of
InvalidContract. The directional rewrite reaches all three branches, so a
readOnly/writeOnlyproperty required inside a conditional is not required
of the direction that does not own it, exactly as at the top level. Under
OAS 3.0 a booleanif/then/elseis still refused: that dialect
predates boolean schemas, as it does foritemsandnot. - Added. OpenAPI 3.1 schemas support
patternPropertiesand
propertyNames(#157). Both are compiled and checked at contract load, and
the directional rewrite recurses into them, so a map of^x--prefixed
headers whose members arereadOnlyis read in the direction the exchange
runs in rather than in both at once.
v0.15.1
v0.15.0
- Added.
Contract::validateWebhook(string $name, RequestInterface $request, ?ResponseInterface $response = null)validates an incoming
webhook delivery against the OpenAPI 3.1webhooksmap (#158). Each
entry compiles to oneOperationper method, a Path Item without a
path: no template, no path parameters (in: pathis refused), no server
matching — the delivery's URL is the receiver's own. Parameters in the
query, header and cookie, the request body and, when a response is
passed, the receiver's answer go through the ordinary pipeline; an
undeclared name or method is onerequest.operation.unknownviolation.
Contract::webhooks()lists them by name;operations()andmatch()
keep to path operations;operation()andvalidateResponse()answer a
webhook's key — itsoperationId, elseWEBHOOK <METHOD> <name>.
Operationgains a defaultedwebhookparameter (append-only: named
arguments) carrying the map key, and violations point under
/webhooks/<name>. A 3.1 document declaring onlywebhooksloads; the
refusal now applies to a document with neitherpathsnorwebhooks
operations, and a 3.0 document carryingwebhooksis refused as such. - Changed. A body that fails its schema is reported per leaf failure
(#160): eachrequest.body.schema/response.body.schemaviolation
names the failing member ininstancePath($.age,
$.children[2].name), the assertion in the newViolation::$keyword
(minimum,type,required, ...;nullon every other violation),
and carries that one assertion asexpected—{"minimum": 0}rather
than the media type's schema. Arequiredmember the value lacks is
reported at the path it would have had withnullasactual; the
list is bounded at twenty leaves; a leaf two union branches report
alike is reported once; a failure of the value itself keeps$. The
codes are unchanged — this is a diagnostic change — but a consumer that
asserted exactly one body violation, orinstancePath === '$', now
sees more.ValidationResultFormatterprints akeywordline where
one is set and renders a body member'sactualas it renders a
parameter's — the credential name pattern applies to the member path
and to the names inside it, so$.ageprints-1and$.password
prints[redacted]; a whole-body violation at$stays redacted
wholesale. - Changed. For a
oneOf/anyOfthat declares adiscriminator, the
body diagnostics follow the branch thepropertyNamevalue names —
throughmapping, as a$refor a component name, else by the
component name the value spells — instead of reporting every branch's
errors; a value naming no branch is one violation at the discriminator
member with the keyworddiscriminator. The verdict is untouched: every
branch is still evaluated. So that a branch keeps its name, a$ref
branch of a discriminated union is now compiled as the local
{$ref: '#/$defs/…'}a shared component takes rather than inlined; a
consumer that read such a branch's members offOperationfollows the
reference into the schema's$defs, as it already does for a cycle.
v0.14.0
- Fixed. A document whose
componentsform a shared-component DAG —
Stripe's publishedspec3.jsonis the case that refused — loads. Every
$refwas inlined at every use, so a component copied once per path that
reached it multiplied along the depth: resolving
#/components/schemas/chargealone visited more than 20 000 000 nodes.
The first resolution of a component is now kept for the whole document
and every further use at least one level below the members the wire
decoders read becomes the same local{$ref: '#/$defs/…'}a cycle's
back-reference already takes, with the defs the first resolution
collected registered alongside; uses the decoders readproperties/
itemsmaps off stay inlined, so no wire decoding changes. A deferred
node now carries the target'sformatbeside itstype. Measured:
resolvingcharge20 000 000+ nodes → 64 415; GitHub's REST description
1.09 s / 199 MiB → 0.82 s / 161 MiB; Stripe'sspec3.jsonloads — 594
operations, 70 s, 7.5 GiB peak — with raised limits (documentBytespast
8 MB,resolvedNodesin the tens of millions); the compile-phase cost of
a document with thousands of schema positions is tracked separately. - Changed.
rasuvaeff/property-testing-testois required at^1.0
(the strict engine); no test in this package needed a change for it.
v0.13.0
- Fixed. A schema that refers to itself — a tree whose
childrenare
trees, a comment thread, a nested error — can be loaded. Every$refwas
inlined, so a self-reference expanded until the depth guard tripped and the
document was refused as$ref chain is too deep (possible circular reference). The members of a reference cycle are now kept as the Schema
Object's$defs, named after their JSON Pointer
(#/components/schemas/Node→components.schemas.Node,a.json:Nodefrom
another file), and the reference back to one becomes a local
{$ref: '#/$defs/…'}carrying the target'stype— the form the validation
backend evaluates natively; the schema the cycle starts from is inlined
where it is first met and kept as a def as well. The directional
readOnly/writeOnlyrewrite (SchemaCheck::effective()) reaches into
$defs, so areadOnlymember of a tree node is not required at any depth
of a request. A schema without a cycle compiles exactly as before, with no
$defs; a cycle outside a schema (a Path Item or Response reaching itself)
and a cycle with no schema in it (A: {$ref: B},B: {$ref: A}) stay
refused, now by name. The cycle is found before the depth is charged, so a
cycle at the end of a long chain is a cycle, not a deep chain, and the
depth message drops its "(possible circular reference)". (#159) - Fixed.
{}in a subschema position —items: {},properties: {x: {}},additionalProperties: {}— was refused as a list where a schema was
expected: the empty object decodes to the empty array, which
array_is_list()calls a list. It is the schema with no keywords, admits
everything, and now goes back on the wire as{}; so does a schema that
normalizes down to nothing, such as OAS 3.0's{nullable: true}, which the
backend used to reject as[]. GitHub's published REST description
declares both shapes. - Added.
Limits::$resolvedNodes(default 1 000 000): the
reference-resolution budget was a constant of 100 000 inside the resolver,
absent fromLimitswhile the README listed it among the budgets, and a
large flat document tripped it with no way to raise it. A shared component
is visited once per use, so a description costs more resolution than it
has nodes — GitHub's REST API (1 239 operations, 13 MB) needs about 360 000,
and loads withdocumentBytesraised. Stripe'sspec3.jsonstill does
not: its components form a DAG that inlining expands past twenty million
nodes for a single schema, which is a different limitation, tracked
separately. - Changed. A 3.1 schema reference that is an alias of another
(a: {$ref: b}) liftstype,itemsandpropertiesto the top of the
conjunction it forms with its siblings, as a direct reference always did;
the chain is resolved to its end before the merge.
v0.12.2
- Added.
SchemaCheck::isMultipleOf(int|float $value, int|float $divisor): bool
— themultipleOfverdict of 0.12.1, static and exported, so a consumer
that has to predict it asks this rather than keeps a second copy of the
rule.rasuvaeff/property-testing-openapidecided whether thenumber
branch of aoneOfadmits an integer by the float rule the contract left
in 0.12.1, and disagreed on 4670 of the integers in ±100000 for
multipleOf: 0.7(#154, property-testing-openapi#132). - Fixed. The decimal predicate took
abs()of an integer, which turns
PHP_INT_MINinto a float and loses its low digits; the sign is trimmed
from the spelling instead (#154).
v0.12.1
- Fixed. The
multipleOfverdict depended on whetherext-bcmathwas
loaded, and neither verdict was the specification's. The backend judged
the keyword on the parsed doubles: in floating point with a1e-14
tolerance, which from about64upward is less than one ulp, so64.1
was rejected formultipleOf: 0.1(thecomposer:2image, for one); or,
with the extension, on the double's expansion to fourteen decimals, which
is123.40000000000001for123.4and was rejected the same way (the
GitHub runners, for one). Same document, same bytes, two verdicts. The
keyword is now judged on the decimals the document and the message spell
— both numbers are taken back to their shortest round-trip spelling,
scaled to integers over a common power of ten and divided exactly, with no
optional extension involved — so64.1and521642427059.686are the
multiples they read as, and64.10000000000001is not (#151). A generator
that emitted the float product where the old float path demanded it
(rasuvaeff/property-testing-openapi0.15.0) needs its 0.15.1.
v0.12.0
- Changed (breaking).
Operation::$serverBasesis gone. It was documented
as the v0.1 base-path projection ofOperation::$serversand duplicated it
entry for entry; read$servers[$i]['base']. (#146) - Changed. A
readOnlyproperty on a request — and awriteOnlyone on a
response — loses only itsrequiredentry: it stays declared and typed, as
both specifications have it ("the required will take effect on the response
only"). Dropping the subschema made the verdict depend on
additionalProperties: an open object accepted{"id": "x"}against an
integerid, and a closed one rejected the{"id": 1}the document
declared.SchemaCheck,SchemaDirectionand the README say so. (#141) - Added.
SchemaCheck::effective(array $schema, SchemaDirection $direction)
exports the directional rewrite the validators apply before a value is
judged — a fixed point of itself, dialect-independent — so a consumer that
builds values for one direction builds them against the schema they will be
checked by instead of a copy of the rule. (#147) - Added.
format: int32andformat: int64are asserted as ranges of the
integer type; every other non-string format stays an annotation, and README
now tables which formats are asserted and which are not. (#148) - Added.
InvalidLimits: a budget below 1 is refused with an
\InvalidArgumentExceptionthat implementsContractException, which
README had promised of every exception andLimitsalone did not keep.
(#145) - Changed.
Operation::__constructis@apiand append-only, constructed
with named arguments — consumers build operations by hand in their tests,
so it was frozen in practice.Operation::$requestBodyand
Operation::$responsescarry declared shapes,CompiledRequestBodyand
CompiledResponses, that a consumer may import. (#146) - Fixed. Every Schema Object the validators read — each parameter's, each
request and response media type's, each response header's and multipart
part header's — is compiled while the contract is built, in its own
direction, so a schema this package cannot evaluate (patternProperties,
propertyNames,if/then, a$schemanaming another dialect, an OAS
3.0exclusiveMinimumwritten as a number, apatternorminimumthe
backend cannot parse, at any depth) isInvalidContractfrom
fromArray()/fromJson()/fromFile(). It used to load and raise from the
firstvalidate*()call that reached it — for a middleware, a 500 on live
traffic instead of an error at boot.SchemaCheck::accepts()refuses such
a schema on the first call too, where the backend used to parse a nested
member only when a value reached it. (#139) - Fixed. The empty Schema Object
{}on a media type, a header or a
parameter is read as the unconstrained schema it is; it loaded and then
raisedInvalidContract("Schema must be an object")from the first message
that reached it. (#149) - Fixed. A cookie string is split into pairs on
;by the validator
itself; it was rewritten to&and read with the query grammar, so
sid=abc&def— one cookie with a legal&in its value — validated as
abc. (#140) - Fixed. A wire string is coerced to
integer/numberonly when it
spells one by the JSON number grammar, anchored:5\nwas an integer,
.5,5.and5were numbers, and an integer past PHP's range saturated
toPHP_INT_MAXinstead of keeping its magnitude. (#142) - Fixed. A percent-encoded
/or\inside a path parameter no longer
makes the whole requestrequest.operation.unknown: the path is matched on
raw segments, each decoded on its own, so/pets/a%2Fbmatches
/pets/{name}withname = "a/b", the value the application receives.
(#143) - Fixed.
ValidationResultFormatterredacts theactualvalue of a
violation atlocation: cookiethe way it redacts a body: a cookie is a
credential carrier whatever the document named it. (#144) - Changed.
Contract::operation()looks the key up in an index built at
construction instead of scanning the operation list. - Documentation. README and
llms.txtlist the keywords that are
accepted and never read (allowEmptyValue,discriminator,xml,
externalDocs,deprecated), and note thatmultipleOfis evaluated in
decimal arithmetic withext-bcmathand in floating point without it.
v0.11.1
- Fixed. A
readOnlyproperty is now dropped from a request schema — and a
writeOnlyone from a response schema — underadditionalPropertiesas it
already was underproperties,itemsand the composition keywords. A
document that keyed its items by name instead of listing them kept the
property required, so a legal request was reported as
request.body.schema.notis deliberately left alone: neither
specification says what areadOnlyproperty means under a negation.
(#130) - Fixed.
Contract::requireMatch()names the request path in its
UnknownOperationmessage, not the whole URI: a query string and userinfo
are where a credential travels, and the unmatched violation had printed
the path alone for exactly that reason. Therequest.server.mismatch
diagnostic renders scheme, host and port instead of the PSR-7 authority,
which carries userinfo the comparison never reads. (#131) - Fixed. A Media Type Object declaring the boolean schema
falseon a
non-JSON response body now producesresponse.body.schema, as the request
side and the JSON branch already did; the opaque branch had readfalseas
"no schema declared" and passed the body. (#132) - Fixed.
Contract::accepts()andSchemaCheck::accepts()raise
InvalidContractfor a schema they cannot read — one that cannot be
JSON-encoded (NAN/INF, malformed UTF-8, more than 512 levels), or a list
in place of an object — where the first leaked a rawJsonExceptionand
the second was read as the empty schema and accepted every value. A schema
out of a document meets neither: the compiler settles both at load time. - Documentation. Four boundaries are now written down: a JSON body deeper
than 64 levels is reported as*.body.json, which the decoder cannot tell
from malformed JSON; the schema compilation cache never evicts, so a
SchemaCheckis one per document; a hand-passed schema may$refonly
inside itself; andsecuritySchemes()checksflowsand the URL fields
for shape, not for content.llms.txtno longer names the
Contract::MAX_*constants that 0.9.0 replaced withLimits.