|
| 1 | +# October 28, named and validated query arguments |
| 2 | + |
| 3 | +- **Before.** For a declared query with a parameter contract, native |
| 4 | + differed from published PHP in four ways: |
| 5 | + - it checked positional arguments only; |
| 6 | + - it refused a named-argument map as "must decode to an array"; |
| 7 | + - it answered every violation with a bare `invalid_query_arguments`; |
| 8 | + - it refused some contracts as `unsupported_query_contract` (defaults, |
| 9 | + and declared types other than a few scalars). |
| 10 | + |
| 11 | + The worker then received the arguments exactly as the client sent them. |
| 12 | +- **Contract** (published 2.11.5, Core 2.15.1; |
| 13 | + `WorkflowQueryTaskBroker::validateContractedQueryArguments`, |
| 14 | + `WorkflowQueryContract::validatedArgumentsForRun`, |
| 15 | + `normalizePositionalArguments`, `normalizeNamedArguments`, |
| 16 | + `valueMatchesDeclaredType`): |
| 17 | + - **Arguments.** An Avro array is a positional list. A non-empty map |
| 18 | + with no key PHP casts to an integer is a PHP array of named arguments. |
| 19 | + `Avro::fromDatum` returns any other map (empty, or with a canonical |
| 20 | + decimal integer key) as an `AvroMapValue` object, which is not an array. |
| 21 | + At the top level it is refused before normalization with "must decode to |
| 22 | + an array" (found in review). |
| 23 | + - **Positional.** Values fill the parameters in order. A variadic |
| 24 | + parameter takes the rest. A missing argument takes its default or is |
| 25 | + reported if required. Extra values are reported under `arguments`. |
| 26 | + - **Named.** Each parameter takes its name's value. A variadic parameter |
| 27 | + spreads a list or a map's values. Defaults and required arguments are |
| 28 | + handled as for positional ones. Each unknown name is reported under |
| 29 | + itself. |
| 30 | + - **Values.** A null is refused unless the parameter allows it. A value |
| 31 | + must match the declared type: nullable, union, intersection, `int`, |
| 32 | + `float` (an int too), `string`, `bool`, `array`, `iterable`, `scalar`, |
| 33 | + `true`, `false`, `null`, `mixed`. An adapter map is not an `array`. A |
| 34 | + class or `object` type matches only an object: decoded bytes or an |
| 35 | + adapter map. Neither object is `callable`. A named variadic spreads a |
| 36 | + PHP array's values in wire order and keeps any other value, an adapter |
| 37 | + map included, as one argument (found in review). |
| 38 | + - **Refusal.** Any violation answers `422 invalid_query_arguments`, with |
| 39 | + the message "Workflow query [<name>] argument validation failed." and |
| 40 | + `validation_errors` by argument. |
| 41 | + - **Worker.** Otherwise the worker receives the normalized positional |
| 42 | + list, encoded again with the request's codec. |
| 43 | + - **No parameter contract.** A declared query without one takes a list as |
| 44 | + given and refuses named arguments: "Named arguments require a durable |
| 45 | + or loadable workflow query contract." (a native server never loads a |
| 46 | + workflow class). |
| 47 | +- **Fixture.** `query-contract-arguments`, raw HTTP on protocol 1.20 in a |
| 48 | + fresh namespace. The worker declares `state(string $prefix)`, |
| 49 | + `greet(string $prefix = 'dflt')`, `plain`, `arr(array $value)`, |
| 50 | + `obj(object $value)` and `rest(mixed ...$value)`. There are nineteen |
| 51 | + cases: |
| 52 | + - named, positional, default and named-default queries are routed, and |
| 53 | + the worker's blob is checked; |
| 54 | + - unknown, missing, wrong-type, null, too-many and named-without-contract |
| 55 | + queries are refused, with their messages checked; |
| 56 | + - an empty map, a `{"0": "ok"}` map and a mixed-key map are refused as not |
| 57 | + arrays; |
| 58 | + - a nested empty map is refused by `array`, but routed whole by `object` |
| 59 | + and by the variadic `mixed`; |
| 60 | + - `{value: {z: 20, a: 10}}` reaches the variadic `rest` as `[20, 10]`. |
| 61 | +- **Native.** `query_contract.rs` ports the normalization and type rules |
| 62 | + over `codec::OrderedValue`. The new `codec/wire_order.rs` decodes the |
| 63 | + validated bytes again with map entries in wire order, because Apache's |
| 64 | + reader keeps maps unordered. It also writes the normalized arguments back |
| 65 | + in that order (the same bytes as PHP for every fixture blob). |
| 66 | + `check_query_contract` returns the normalized list for a declared query, |
| 67 | + or the refusal. `query_attempt` enqueues the re-encoded blob. |
| 68 | +- **Kept.** |
| 69 | + - `callable` matches nothing here: PHP's `is_callable` also accepts |
| 70 | + strings naming functions, which a native server cannot resolve. |
| 71 | + - A class name that a published PHP server can load (a real class) would |
| 72 | + refuse other objects. Native, loading no classes, accepts any object. |
| 73 | +- **Tests.** `query_contract_arguments.rs` (routed normalization; refusals |
| 74 | + with messages) fails on the previous source. `query_contract.rs` unit |
| 75 | + tests cover the type rules and integer-keyed maps. |
0 commit comments