Repository navigation
feat(client): add the API client for the postcode gateway - #3
Merged
Merged
Conversation
Spec 0.2.0 adds the client contract, the fixtures and the contract scenarios that the PHP client must pass. Its grammar and vectors are the same as before, so the core passes them unchanged and only its SPEC_VERSION moves. CONTEXT.md takes the new terms of the glossary. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The client must give the same errors and the same retries as every other Gatepost client, so the status map and the retry rules follow the spec's client contract. PSR-18 has no timeout, so the client times each attempt and reports a failure at the timeout as a timeout. A call stops at its total deadline, and a Retry-After of 10 seconds or less on a 429, 502, 503 or 504 sets the wait. A fake clock drives the tests, so none of them sleeps. The package now requires the PSR interfaces for HTTP and caching, which ADR 0003 records. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A lookup is the call that every integrator needs first. The client checks the code with the core, so a typo never costs a request, and it reads the level from the fields of the response, because the gateway does not state it. A postcode that the gateway does not know is a result, not an error. A status stays text, so a new gateway word is not lost, and a body that the client cannot read is an unexpected_response. Levels 2 to 5 follow NIPOST's docs, since the test keys hold level 1 only. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The spec calls a Retry-After date in the past invalid, so a 429 with one now fails at once with rate_limited, and a 502, 503 or 504 uses the client's own wait. A date that does not exist is invalid too, and a whole number of up to 12 digits is valid. A test now pins the 250 ms limit of the random part of a wait. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A level 2 or 3 body can lack one address name, or hold a recent address or a building use of another type. The gateway has charged for such a call, so failing it would lose a paid answer. Only valid and status can now fail a lookup, as the contract lists. The tests also check that the client keeps the caller's postcode when the body echoes another one. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A checkout field needs the postcode at a GPS fix and suggestions while the user types. The gateway can find an area with no unit in the radius, so a reverse result allows found with no unit. The client refuses a coordinate or a radius outside its range, because the gateway cuts a larger radius without a sign, and it writes each number in its shortest decimal form, so every client sends the same query. Autocomplete sends no empty text, because the gateway never answers it, and it builds the postcode of each suggestion, because the gateway sends the value of one segment only. It waits 15 s by default, and it does not retry a timeout, because the next keystroke replaces the call. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The cache stays off unless the caller sets cacheTtlMs and passes a PSR-16 cache, as the spec asks. Each key of the caller's cache holds a generation, so clearCache() hides every kept result without wiping the caller's other entries. A call takes its key before it sends the request, so a request that started before clearCache() keeps no result. Each API key keeps its own results, because two keys can hold different lookup levels, and the client keeps no error. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Every client must pass the contract scenarios of the spec against the mock server, so the PHP client gives the errors, waits and outcomes of the TypeScript client. The contract job runs the mock server from the js repo with this repo's own spec, on PHP 8.1 and the newest PHP. The synchronous client never has two requests in flight, so the runner leaves out each scenario whose order is parallel. A unit test in the normal check reads the scenario files and checks that rule. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The client reads a failed attempt as a timeout when it took timeoutMs or
more, and as a network error when it took less. With two defaults, 8 s
and 15 s, a caller who set the documented 8 s on the transport got a
network error from autocomplete and three attempts. The docs now say
that the transport timeout must be the longest in use, and tests pin
the limits at 14999 and 15000 ms for autocomplete and 7999 and 8000 ms
for a lookup. An optional clock lets the tests run with no sleep and no
reflection.
A JSON object that holds no field is now kept apart from an empty list,
so suggestions with the value {} give an unexpected_response, as in the
other clients. Text over the input limit and text that is not UTF-8 now
get their own message, because the old one named a rule that the text
had not broken.
Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A cache is an extra, so a store that throws on a read or a write now costs a request and nothing more, and the caller still gets the result. A kept entry that the client cannot read counts as a miss, and the new result replaces it, so an entry from an older version or a damaged entry cannot fail every call until its TTL ends. The cache key now uses an HMAC of the request with the API key as the HMAC key, so a reader of the cache cannot test guesses of the key with a plain hash. clearCache() still passes a failure of the store to the caller, because a clear that did not happen must not look as if it did. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
DOC-3 asks that CI runs each code example of a README. The new examples call the gateway, so the contract job runs them against the mock server with a mock key, and the unit suite runs the examples that need no network. The README says that levels 2 to 5 follow NIPOST's docs, that the client is synchronous, and that a caller sets the timeout on the PSR-18 client, which has none, to the longest timeout in use. It tells callers to use a cache store with eviction, because the client sets no size bound of its own. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A PSR-18 transport holds one timeout, so the client uses one timeoutMs for every call. The README said to set the transport timeout to 15 s and pass no timeoutMs. A hanging gateway then made each lookup attempt last 15 s, which the client read as a timeout, and a call lasted about 31 s. The README, the transport doc and the decision record now say to pass the same value as the transport timeout. The README examples do so. Two tests pin the setup: one reads the examples, and one runs a hanging gateway against the fake clock. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
http_build_query() reads the ini value arg_separator.output when it gets no separator. A host that sets it to & made every reverse call lose its second parameter, and every lookup send a parameter named amp;level. The client now passes the separator and the encoding itself. The client also sends Accept: application/json, as the TypeScript client does. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The dump left out a symbol when its doc comment held the text @internal anywhere. The doc of the timer parameter held it, so the dump lost the whole constructor of PostcodeClient, and a rename of an option did not change the dump. The dump now reads only a tag at the start of a doc line. The timer parameter doc no longer holds the word, and the dump shows the constructor again. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The decimal writer stripped trailing zeros that its own search never leaves, so the call could not change any result. Infection's console reporter crashed on the mutant that removed it, because it put a style tag right after the backslash of the native call name. The command then exited 1, and the weekly mutation workflow would fail on its first run. The dead call is gone. The mutation script also passes --show-mutations=0, so that a later mutant of that kind cannot crash the reporter. The text log still lists each escaped mutant. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
RFC 9110 says that a recipient must accept IMF-fixdate, RFC 850 and asctime dates. The client read only the first, so a 429 with an RFC 850 or asctime date gave no wait. The format that PHP uses to read a weekday also moves the date to that weekday, so a date with a wrong weekday gave a wait of days. The client now reads the three forms, with the same shape rules as the TypeScript client, and it counts a date whose weekday does not match as no header. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The reader collapsed spaces with preg_replace() and kept the original text for a null result. The pattern is valid, so the result is never null, and the fallback could not run. Only the asctime form can hold two spaces in a row, so a plain replace of two spaces is enough. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A reverse call that times out must retry. A client with no maxRetries must retry twice. A lookup response with the fields of levels 3 and 4 must read as level 4. A store that fails only when the client reads an entry must cost a request and nothing more. No test failed when one of these rules changed. The tests now fail for each of those changes. They also pin the 429 wait message at its rounding edges, the two autocomplete input messages and the decimal form of 1. The 429 messages are made inside the test body, because the mutation tool changed the code only after a data provider had made them. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
retryAfterMs() holds the wait of a 502, 503 or 504 too, and the README named only the 429. It also left out that a 5xx uses a valid Retry-After in place of its own wait, that the cache hash covers the base URL, and that the Node range of the mock server skips Node 23. The exception of a failed connection holds the failure of the transport, which can hold the request with the X-API-Key header. The README now says not to log it with its objects. Two doc comments lose a broken wrap and a note that a caller of the constructor cannot use. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
RFC 9110 does not require a recipient to check the day name of an HTTP date, and the TypeScript client ignores it. Commit 8806436 made the PHP client count a date with a wrong day name as no header. The two clients then gave different waits for the same header. The client still reads the day name in each of the three date forms, but it does not compare it. A date with a wrong day name is valid and gives the wait of its date. The tests that expected no wait now expect the wait of the date. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
MobileMage
marked this pull request as ready for review
October 3, 2026 04:08
The spec and the mock server are merged. The spec submodule now names the spec release v0.2.0, and the contract job takes the mock server from the js commit on main, which holds the same server. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
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.
gatepost/postcode now also calls NIPOST's postcode gateway: lookup,
reverse and autocomplete, with the results, errors and limits that the
spec's client contract sets. It takes PSR-18, PSR-17 and PSR-16 objects,
retries what can succeed, honours Retry-After, and keeps an optional
cache whose keys are hashed with the API key, so no store holds the key.
A cache that fails never fails a call.
The client is synchronous, so it runs every contract scenario except the
four that need parallel calls. CI plays them on PHP 8.1 and 8.5 against
the mock gateway server from the js repo. The README examples run there
too.
This change is larger than the 400 lines that GIT-5 suggests, because it
adds the client. Most of it is tests.
Signed-off-by: MobileMage aidelojejoshua@gmail.com